Add a pre-defined server for the docker version (#413)

* Moved build to docker stage.

* Do not copy node_modules to the docker image

* Optimize Docker builds

* Lock a predefined server with enviroment variables

* Added a example docker compose file

* Removed useless layer

* Fix error with empty server type

* pass process via preload, use file, strict server check

* remove duplicate content-type

* update readme, docker compose

* bugfix: server lock false, not jellyfin

* fix preload type definition

* fix docker, web server lock check

---------

Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
This commit is contained in:
Alberto Rodríguez 2024-02-24 07:55:23 +01:00 committed by GitHub
parent 5caf0d439f
commit 28bb699024
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 348 additions and 261 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
Dockerfile
docker-compose.*

View File

@ -16,7 +16,7 @@ import webpackPaths from './webpack.paths';
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
// at the dev webpack config is not accidentally run in a production environment
if (process.env.NODE_ENV === 'production') {
checkNodeEnv('development');
checkNodeEnv('development');
}
const port = process.env.PORT || 4343;
@ -28,171 +28,174 @@ const requiredByDLLConfig = module.parent!.filename.includes('webpack.config.ren
* Warn if the DLL is not built
*/
if (!requiredByDLLConfig && !(fs.existsSync(webpackPaths.dllPath) && fs.existsSync(manifest))) {
console.log(
chalk.black.bgYellow.bold(
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"',
),
);
execSync('npm run postinstall');
console.log(
chalk.black.bgYellow.bold(
'The DLL files are missing. Sit back while we build them for you with "npm run build-dll"',
),
);
execSync('npm run postinstall');
}
const configuration: webpack.Configuration = {
devtool: 'inline-source-map',
devtool: 'inline-source-map',
mode: 'development',
mode: 'development',
target: ['web', 'electron-renderer'],
target: ['web', 'electron-renderer'],
entry: [
`webpack-dev-server/client?http://localhost:${port}/dist`,
'webpack/hot/only-dev-server',
path.join(webpackPaths.srcRendererPath, 'index.tsx'),
],
output: {
path: webpackPaths.distRendererPath,
publicPath: '/',
filename: 'renderer.dev.js',
library: {
type: 'umd',
},
},
module: {
rules: [
{
test: /\.s?css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?css$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
entry: [
`webpack-dev-server/client?http://localhost:${port}/dist`,
'webpack/hot/only-dev-server',
path.join(webpackPaths.srcRendererPath, 'index.tsx'),
],
},
plugins: [
...(requiredByDLLConfig
? []
: [
new webpack.DllReferencePlugin({
context: webpackPaths.dllPath,
manifest: require(manifest),
sourceType: 'var',
}),
]),
new webpack.NoEmitOnErrorsPlugin(),
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*
* By default, use 'development' as NODE_ENV. This can be overriden with
* 'staging', for example, by changing the ENV variables in the npm scripts
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
}),
new webpack.LoaderOptionsPlugin({
debug: true,
}),
new ReactRefreshWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.join('index.html'),
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
}),
],
node: {
__dirname: false,
__filename: false,
},
devServer: {
port,
compress: true,
hot: true,
headers: { 'Access-Control-Allow-Origin': '*' },
static: {
publicPath: '/',
output: {
path: webpackPaths.distRendererPath,
publicPath: '/',
filename: 'renderer.dev.js',
library: {
type: 'umd',
},
},
historyApiFallback: {
verbose: true,
},
setupMiddlewares(middlewares) {
console.log('Starting preload.js builder...');
const preloadProcess = spawn('npm', ['run', 'start:preload'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting remote.js builder...');
const remoteProcess = spawn('npm', ['run', 'start:remote'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting Main Process...');
spawn('npm', ['run', 'start:main'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => {
preloadProcess.kill();
remoteProcess.kill();
process.exit(code!);
})
.on('error', (spawnError) => console.error(spawnError));
return middlewares;
module: {
rules: [
{
test: /\.s?css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?css$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
],
},
plugins: [
...(requiredByDLLConfig
? []
: [
new webpack.DllReferencePlugin({
context: webpackPaths.dllPath,
manifest: require(manifest),
sourceType: 'var',
}),
]),
new webpack.NoEmitOnErrorsPlugin(),
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*
* By default, use 'development' as NODE_ENV. This can be overriden with
* 'staging', for example, by changing the ENV variables in the npm scripts
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'development',
}),
new webpack.LoaderOptionsPlugin({
debug: true,
}),
new ReactRefreshWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.join('index.html'),
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
templateParameters: {
web: false,
},
}),
],
node: {
__dirname: false,
__filename: false,
},
devServer: {
port,
compress: true,
hot: true,
headers: { 'Access-Control-Allow-Origin': '*' },
static: {
publicPath: '/',
},
historyApiFallback: {
verbose: true,
},
setupMiddlewares(middlewares) {
console.log('Starting preload.js builder...');
const preloadProcess = spawn('npm', ['run', 'start:preload'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting remote.js builder...');
const remoteProcess = spawn('npm', ['run', 'start:remote'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => process.exit(code!))
.on('error', (spawnError) => console.error(spawnError));
console.log('Starting Main Process...');
spawn('npm', ['run', 'start:main'], {
shell: true,
stdio: 'inherit',
})
.on('close', (code: number) => {
preloadProcess.kill();
remoteProcess.kill();
process.exit(code!);
})
.on('error', (spawnError) => console.error(spawnError));
return middlewares;
},
},
},
};
export default merge(baseConfig, configuration);

View File

@ -21,114 +21,117 @@ checkNodeEnv('production');
deleteSourceMaps();
const devtoolsConfig =
process.env.DEBUG_PROD === 'true'
? {
devtool: 'source-map',
}
: {};
process.env.DEBUG_PROD === 'true'
? {
devtool: 'source-map',
}
: {};
const configuration: webpack.Configuration = {
...devtoolsConfig,
...devtoolsConfig,
mode: 'production',
mode: 'production',
target: ['web', 'electron-renderer'],
target: ['web', 'electron-renderer'],
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
entry: [path.join(webpackPaths.srcRendererPath, 'index.tsx')],
output: {
path: webpackPaths.distRendererPath,
publicPath: './',
filename: 'renderer.js',
library: {
type: 'umd',
output: {
path: webpackPaths.distRendererPath,
publicPath: './',
filename: 'renderer.js',
library: {
type: 'umd',
},
},
},
module: {
rules: [
{
test: /\.s?(a|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
module: {
rules: [
{
test: /\.s?(a|c)ss$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
exportLocalsConvention: 'camelCaseOnly',
},
sourceMap: true,
importLoaders: 1,
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?(a|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
},
'sass-loader',
],
include: /\.module\.s?(c|a)ss$/,
},
{
test: /\.s?(a|c)ss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
exclude: /\.module\.s?(c|a)ss$/,
},
// Fonts
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
},
// Images
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: 'asset/resource',
},
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
}),
new CssMinimizerPlugin(),
],
},
plugins: [
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: false,
}),
new MiniCssExtractPlugin({
filename: 'style.css',
}),
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
templateParameters: {
web: false,
},
}),
],
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
}),
new CssMinimizerPlugin(),
],
},
plugins: [
/**
* Create global constants which can be configured at compile time.
*
* Useful for allowing different behaviour between development builds and
* release builds
*
* NODE_ENV should be production so that modules do not perform certain
* development checks
*/
new webpack.EnvironmentPlugin({
NODE_ENV: 'production',
DEBUG_PROD: false,
}),
new MiniCssExtractPlugin({
filename: 'style.css',
}),
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
}),
],
};
export default merge(baseConfig, configuration);

View File

@ -116,6 +116,9 @@ const configuration: webpack.Configuration = {
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
templateParameters: {
web: false, // with hot reload, we don't have NGINX injecting variables
},
}),
],

View File

@ -128,6 +128,9 @@ const configuration: webpack.Configuration = {
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
templateParameters: {
web: true,
},
}),
],
};

View File

@ -1,16 +1,20 @@
# --- Builder stage
FROM node:18-alpine as builder
WORKDIR /app
COPY . /app
#Copy package.json first to cache node_modules
COPY package.json package-lock.json .
# Scripts include electron-specific dependencies, which we don't need
RUN npm install --legacy-peer-deps --ignore-scripts
#Copy code and build with cached modules
COPY . .
RUN npm run build:web
# --- Production stage
FROM nginx:alpine-slim
COPY --chown=nginx:nginx --from=builder /app/release/app/dist/web /usr/share/nginx/html
COPY ./settings.js.template /etc/nginx/templates/settings.js.template
COPY ng.conf.template /etc/nginx/templates/default.conf.template
ENV PUBLIC_PATH="/"

View File

@ -81,6 +81,8 @@ docker run --name feishin -p 9180:9180 feishin
3. _Optional_ - If you want to host Feishin on a subpath (not `/`), then pass in the following environment variable: `PUBLIC_PATH=PATH`. For example, to host on `/feishin`, pass in `PUBLIC_PATH=/feishin`.
4. _Optional_ - To hard code the server url, pass the following environment variables: `SERVER_NAME`, `SERVER_TYPE` (one of `jellyfin` or `navidrome`), `SERVER_URL`. To prevent users from changing these settings, pass `SERVER_LOCK=true`. This can only be set if all three of the previous values are set.
## FAQ
### MPV is either not working or is rapidly switching between pause/play states

13
docker-compose.yaml Normal file
View File

@ -0,0 +1,13 @@
version: '3.5'
services:
feishin:
container_name: feishin
image: jeffvli/feishin
restart: unless-stopped
ports:
- 9180:9180
environment:
- SERVER_NAME=jellyfin # pre defined server name
- SERVER_LOCK=true # When true AND name/type/url are set, only username/password can be toggled
- SERVER_TYPE=jellyfin # navidrome also works
- SERVER_URL= # http://address:port

View File

@ -16,4 +16,12 @@ server {
alias /usr/share/nginx/html/;
try_files $uri $uri/ /index.html =404;
}
location ${PUBLIC_PATH}settings.js {
alias /etc/nginx/conf.d/settings.js;
}
location ${PUBLIC_PATH}/settings.js {
alias /etc/nginx/conf.d/settings.js;
}
}

View File

@ -9,7 +9,7 @@
"build:remote": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.remote.prod.ts",
"build:renderer": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"build:web": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.web.prod.ts",
"build:docker": "npm run build:web && docker build -t jeffvli/feishin .",
"build:docker": "docker build -t jeffvli/feishin .",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",

1
settings.js.template Normal file
View File

@ -0,0 +1 @@
"use strict";window.SERVER_URL="${SERVER_URL}";window.SERVER_NAME="${SERVER_NAME}";window.SERVER_TYPE="${SERVER_TYPE}";window.SERVER_LOCK=${SERVER_LOCK};

View File

@ -1,6 +1,6 @@
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
import Store from 'electron-store';
import type { TitleTheme } from '/@/renderer/types';
import { toServerType, type TitleTheme } from '/@/renderer/types';
const store = new Store();
@ -56,9 +56,20 @@ const themeSet = (theme: TitleTheme): void => {
ipcRenderer.send('theme-set', theme);
};
const SERVER_TYPE = toServerType(process.env.SERVER_TYPE);
const env = {
SERVER_LOCK:
SERVER_TYPE !== null ? process.env.SERVER_LOCK?.toLocaleLowerCase() === 'true' : false,
SERVER_NAME: process.env.SERVER_NAME ?? '',
SERVER_TYPE,
SERVER_URL: process.env.SERVER_URL ?? 'http://',
};
export const localSettings = {
disableMediaKeys,
enableMediaKeys,
env,
fontError,
get,
passwordGet,

View File

@ -8,7 +8,7 @@ import isElectron from 'is-electron';
import { nanoid } from 'nanoid/non-secure';
import { AuthenticationResponse } from '/@/renderer/api/types';
import { useAuthStoreActions } from '/@/renderer/store';
import { ServerType } from '/@/renderer/types';
import { ServerType, toServerType } from '/@/renderer/types';
import { api } from '/@/renderer/api';
import { useTranslation } from 'react-i18next';
@ -33,15 +33,27 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
const form = useForm({
initialValues: {
legacyAuth: false,
name: '',
name: (localSettings ? localSettings.env.SERVER_NAME : window.SERVER_NAME) ?? '',
password: '',
savePassword: false,
type: ServerType.JELLYFIN,
url: 'http://',
type:
(localSettings
? localSettings.env.SERVER_TYPE
: toServerType(window.SERVER_TYPE)) ?? ServerType.JELLYFIN,
url: (localSettings ? localSettings.env.SERVER_URL : window.SERVER_URL) ?? 'https://',
username: '',
},
});
// server lock for web is only true if lock is true *and* all other properties are set
const serverLock =
(localSettings
? !!localSettings.env.SERVER_LOCK
: !!window.SERVER_LOCK &&
window.SERVER_TYPE &&
window.SERVER_NAME &&
window.SERVER_URL) || false;
const isSubmitDisabled = !form.values.name || !form.values.url || !form.values.username;
const handleSubmit = form.onSubmit(async (values) => {
@ -62,7 +74,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
password: values.password,
username: values.username,
},
values.type,
values.type as ServerType,
);
if (!data) {
@ -76,7 +88,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
id: nanoid(),
name: values.name,
ndCredential: data.ndCredential,
type: values.type,
type: values.type as ServerType,
url: values.url.replace(/\/$/, ''),
userId: data.userId,
username: data.username,
@ -117,11 +129,13 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
>
<SegmentedControl
data={SERVER_TYPES}
disabled={serverLock}
{...form.getInputProps('type')}
/>
<Group grow>
<TextInput
data-autofocus
disabled={serverLock}
label={t('form.addServer.input', {
context: 'name',
postProcess: 'titleCase',
@ -129,6 +143,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
{...form.getInputProps('name')}
/>
<TextInput
disabled={serverLock}
label={t('form.addServer.input', {
context: 'url',
postProcess: 'titleCase',

View File

@ -6,6 +6,9 @@
<meta http-equiv="Content-Security-Policy" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Feishin</title>
<% if (web) { %>
<script src="settings.js"></script>
<% } %>
</head>
<body>

View File

@ -13,6 +13,10 @@ import { Browser } from '/@/main/preload/browser';
declare global {
interface Window {
SERVER_LOCK?: boolean;
SERVER_NAME?: string;
SERVER_TYPE?: string;
SERVER_URL?: string;
electron: {
browser: Browser;
discordRpc: DiscordRpc;

View File

@ -60,6 +60,17 @@ export enum ServerType {
SUBSONIC = 'subsonic',
}
export const toServerType = (value?: string): ServerType | null => {
switch (value?.toLowerCase()) {
case ServerType.JELLYFIN:
return ServerType.JELLYFIN;
case ServerType.NAVIDROME:
return ServerType.NAVIDROME;
default:
return null;
}
};
export type ServerListItem = {
credential: string;
features?: Record<string, number[]>;