mirror of
https://github.com/jeffvli/feishin.git
synced 2024-11-20 06:27:09 +01:00
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:
parent
5caf0d439f
commit
28bb699024
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.*
|
@ -143,6 +143,9 @@ const configuration: webpack.Configuration = {
|
|||||||
env: process.env.NODE_ENV,
|
env: process.env.NODE_ENV,
|
||||||
isDevelopment: process.env.NODE_ENV !== 'production',
|
isDevelopment: process.env.NODE_ENV !== 'production',
|
||||||
nodeModules: webpackPaths.appNodeModulesPath,
|
nodeModules: webpackPaths.appNodeModulesPath,
|
||||||
|
templateParameters: {
|
||||||
|
web: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -127,6 +127,9 @@ const configuration: webpack.Configuration = {
|
|||||||
},
|
},
|
||||||
isBrowser: false,
|
isBrowser: false,
|
||||||
isDevelopment: process.env.NODE_ENV !== 'production',
|
isDevelopment: process.env.NODE_ENV !== 'production',
|
||||||
|
templateParameters: {
|
||||||
|
web: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -116,6 +116,9 @@ const configuration: webpack.Configuration = {
|
|||||||
env: process.env.NODE_ENV,
|
env: process.env.NODE_ENV,
|
||||||
isDevelopment: process.env.NODE_ENV !== 'production',
|
isDevelopment: process.env.NODE_ENV !== 'production',
|
||||||
nodeModules: webpackPaths.appNodeModulesPath,
|
nodeModules: webpackPaths.appNodeModulesPath,
|
||||||
|
templateParameters: {
|
||||||
|
web: false, // with hot reload, we don't have NGINX injecting variables
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -128,6 +128,9 @@ const configuration: webpack.Configuration = {
|
|||||||
},
|
},
|
||||||
isBrowser: false,
|
isBrowser: false,
|
||||||
isDevelopment: process.env.NODE_ENV !== 'production',
|
isDevelopment: process.env.NODE_ENV !== 'production',
|
||||||
|
templateParameters: {
|
||||||
|
web: true,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
# --- Builder stage
|
# --- Builder stage
|
||||||
FROM node:18-alpine as builder
|
FROM node:18-alpine as builder
|
||||||
WORKDIR /app
|
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
|
# Scripts include electron-specific dependencies, which we don't need
|
||||||
RUN npm install --legacy-peer-deps --ignore-scripts
|
RUN npm install --legacy-peer-deps --ignore-scripts
|
||||||
|
#Copy code and build with cached modules
|
||||||
|
COPY . .
|
||||||
RUN npm run build:web
|
RUN npm run build:web
|
||||||
|
|
||||||
# --- Production stage
|
# --- Production stage
|
||||||
FROM nginx:alpine-slim
|
FROM nginx:alpine-slim
|
||||||
|
|
||||||
COPY --chown=nginx:nginx --from=builder /app/release/app/dist/web /usr/share/nginx/html
|
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
|
COPY ng.conf.template /etc/nginx/templates/default.conf.template
|
||||||
|
|
||||||
ENV PUBLIC_PATH="/"
|
ENV PUBLIC_PATH="/"
|
||||||
|
@ -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`.
|
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
|
## FAQ
|
||||||
|
|
||||||
### MPV is either not working or is rapidly switching between pause/play states
|
### MPV is either not working or is rapidly switching between pause/play states
|
||||||
|
13
docker-compose.yaml
Normal file
13
docker-compose.yaml
Normal 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
|
@ -16,4 +16,12 @@ server {
|
|||||||
alias /usr/share/nginx/html/;
|
alias /usr/share/nginx/html/;
|
||||||
try_files $uri $uri/ /index.html =404;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -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: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: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: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",
|
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
|
||||||
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
|
"lint": "concurrently \"npm run lint:code\" \"npm run lint:styles\"",
|
||||||
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
"lint:code": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||||
|
1
settings.js.template
Normal file
1
settings.js.template
Normal 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};
|
@ -1,6 +1,6 @@
|
|||||||
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
|
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import type { TitleTheme } from '/@/renderer/types';
|
import { toServerType, type TitleTheme } from '/@/renderer/types';
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
|
||||||
@ -56,9 +56,20 @@ const themeSet = (theme: TitleTheme): void => {
|
|||||||
ipcRenderer.send('theme-set', theme);
|
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 = {
|
export const localSettings = {
|
||||||
disableMediaKeys,
|
disableMediaKeys,
|
||||||
enableMediaKeys,
|
enableMediaKeys,
|
||||||
|
env,
|
||||||
fontError,
|
fontError,
|
||||||
get,
|
get,
|
||||||
passwordGet,
|
passwordGet,
|
||||||
|
@ -8,7 +8,7 @@ import isElectron from 'is-electron';
|
|||||||
import { nanoid } from 'nanoid/non-secure';
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
import { AuthenticationResponse } from '/@/renderer/api/types';
|
import { AuthenticationResponse } from '/@/renderer/api/types';
|
||||||
import { useAuthStoreActions } from '/@/renderer/store';
|
import { useAuthStoreActions } from '/@/renderer/store';
|
||||||
import { ServerType } from '/@/renderer/types';
|
import { ServerType, toServerType } from '/@/renderer/types';
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@ -33,15 +33,27 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
legacyAuth: false,
|
legacyAuth: false,
|
||||||
name: '',
|
name: (localSettings ? localSettings.env.SERVER_NAME : window.SERVER_NAME) ?? '',
|
||||||
password: '',
|
password: '',
|
||||||
savePassword: false,
|
savePassword: false,
|
||||||
type: ServerType.JELLYFIN,
|
type:
|
||||||
url: 'http://',
|
(localSettings
|
||||||
|
? localSettings.env.SERVER_TYPE
|
||||||
|
: toServerType(window.SERVER_TYPE)) ?? ServerType.JELLYFIN,
|
||||||
|
url: (localSettings ? localSettings.env.SERVER_URL : window.SERVER_URL) ?? 'https://',
|
||||||
username: '',
|
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 isSubmitDisabled = !form.values.name || !form.values.url || !form.values.username;
|
||||||
|
|
||||||
const handleSubmit = form.onSubmit(async (values) => {
|
const handleSubmit = form.onSubmit(async (values) => {
|
||||||
@ -62,7 +74,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||||||
password: values.password,
|
password: values.password,
|
||||||
username: values.username,
|
username: values.username,
|
||||||
},
|
},
|
||||||
values.type,
|
values.type as ServerType,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
@ -76,7 +88,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
name: values.name,
|
name: values.name,
|
||||||
ndCredential: data.ndCredential,
|
ndCredential: data.ndCredential,
|
||||||
type: values.type,
|
type: values.type as ServerType,
|
||||||
url: values.url.replace(/\/$/, ''),
|
url: values.url.replace(/\/$/, ''),
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
username: data.username,
|
username: data.username,
|
||||||
@ -117,11 +129,13 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||||||
>
|
>
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
data={SERVER_TYPES}
|
data={SERVER_TYPES}
|
||||||
|
disabled={serverLock}
|
||||||
{...form.getInputProps('type')}
|
{...form.getInputProps('type')}
|
||||||
/>
|
/>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<TextInput
|
<TextInput
|
||||||
data-autofocus
|
data-autofocus
|
||||||
|
disabled={serverLock}
|
||||||
label={t('form.addServer.input', {
|
label={t('form.addServer.input', {
|
||||||
context: 'name',
|
context: 'name',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
@ -129,6 +143,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||||||
{...form.getInputProps('name')}
|
{...form.getInputProps('name')}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
disabled={serverLock}
|
||||||
label={t('form.addServer.input', {
|
label={t('form.addServer.input', {
|
||||||
context: 'url',
|
context: 'url',
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
|
@ -6,6 +6,9 @@
|
|||||||
<meta http-equiv="Content-Security-Policy" />
|
<meta http-equiv="Content-Security-Policy" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Feishin</title>
|
<title>Feishin</title>
|
||||||
|
<% if (web) { %>
|
||||||
|
<script src="settings.js"></script>
|
||||||
|
<% } %>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
4
src/renderer/preload.d.ts
vendored
4
src/renderer/preload.d.ts
vendored
@ -13,6 +13,10 @@ import { Browser } from '/@/main/preload/browser';
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
SERVER_LOCK?: boolean;
|
||||||
|
SERVER_NAME?: string;
|
||||||
|
SERVER_TYPE?: string;
|
||||||
|
SERVER_URL?: string;
|
||||||
electron: {
|
electron: {
|
||||||
browser: Browser;
|
browser: Browser;
|
||||||
discordRpc: DiscordRpc;
|
discordRpc: DiscordRpc;
|
||||||
|
@ -60,6 +60,17 @@ export enum ServerType {
|
|||||||
SUBSONIC = 'subsonic',
|
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 = {
|
export type ServerListItem = {
|
||||||
credential: string;
|
credential: string;
|
||||||
features?: Record<string, number[]>;
|
features?: Record<string, number[]>;
|
||||||
|
Loading…
Reference in New Issue
Block a user