Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
Something went wrong. Try again.
7.5 kB · 190 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190import express from 'express';import type { Express } from 'express';import { childLogger, loggerTest, type LogDataPretty, type Logger } from "@foxxmd/logging";import bodyParser from 'body-parser';import { stripIndents } from "common-tags";import session from 'express-session';import type { PassThrough } from "node:stream";import passport from 'passport';import path from "path";import qs from 'qs';import ViteExpress from "vite-express";import { getRoot } from "../ioc.ts";import { parseBool } from "../utils.ts";import { getAddress } from "../utils/NetworkUtils.ts";import { setupApi } from "./api.ts";import type ScrobbleSources from '../sources/ScrobbleSources.ts';import type ScrobbleClients from '../scrobblers/ScrobbleClients.ts';import { qsOptions } from '../../core/Atomic.ts';import { projectRootDir } from "../common/infrastructure/Atomic.ts";import { createTypedRouter } from "@minisylar/express-typed-router";
export interface ServerArgs { sources: ScrobbleSources clients: ScrobbleClients}export interface ServerOptions { logger?: Logger appLoggerStream?: PassThrough initialOutput?: LogDataPretty[] testMode?: boolean}
export const initServer = async (args: ServerArgs, opts: ServerOptions = {}): Promise<[Express, ReturnType<typeof createTypedRouter>]> => {
const app = express();
app.set('query parser', (str: string) => qs.parse(str, qsOptions));
const { sources, clients } = args; const { logger: parentLogger = loggerTest, } = opts;
const logger = childLogger(parentLogger, 'API');
const router = createTypedRouter().onValidationFailure((info) => { const {req, ...rest} = info; logger.warn(rest, 'Request failed validation'); });
try { const root = getRoot(); // pathname the app is mounted under (e.g. '/myapp', or '/' at root) // decided at runtime from BASE_URL, not baked in at build time. See // vite.config.ts (relative asset base) and src/client/App.tsx (router mode). const local = root.get('localUrl'); const localDefined = root.get('hasDefinedBaseUrl'); const basePath = localDefined && local.pathname !== '/' ? local.pathname : '/';
// mount API routes under the same prefix the frontend's relative // `api/...` fetches resolve against app.use(basePath, router.getRouter()); app.use(bodyParser.json()); app.use( bodyParser.urlencoded({ extended: true, }) );
//app.use(express.static(buildDir));
router.use(session({secret: 'keyboard cat', resave: false, saveUninitialized: false})); app.use(session({secret: 'keyboard cat', resave: false, saveUninitialized: false})); app.use(passport.initialize()); app.use(passport.session());
if(root.get('disableWeb')) { logger.warn('API and Dashboard have been DISABLED. Note that any ingress sources (Webscrobbler, Listenbrainz/Lastfm Endpoint Sources, etc...) will be unusable'); return; }
setupApi({app, router, scrobbleSources: sources, scrobbleClients: clients}, {...opts, logger});
if(opts.testMode === true) { return [app, router]; }
const isProd = root.get('isProd'); const port = root.get('port');
const addy = getAddress(); const addresses: string[] = []; let dockerHint = ''; if (parseBool(process.env.IS_DOCKER) && addy.v4 !== undefined && addy.v4.includes('172')) { dockerHint = stripIndents` --- HINT --- MS is likely being run in a container with BRIDGE networking which means the above addresses are not accessible from outside this container. To ensure the container is accessible make sure you have mapped the *container* port ${port} to a *host* port. https://docs.multi-scrobbler.app/installation/?dockerSetting=networking#recommended-settings The container will then be accessible at http://HOST_MACHINE_IP:HOST_PORT${localDefined ? ` (or ${local} since you defined this!)` : ''} --- HINT --- `; } for (const [k, v] of Object.entries(addy)) { if (v !== undefined) { switch (k) { case 'host': case 'v4': addresses.push(`---> ${k === 'host' ? 'Local'.padEnd(14, ' ') : 'Network'.padEnd(14, ' ')} http://${v}:${port}`); break; case 'v6': addresses.push(`---> Network (IPv6) http://[${v}]:${port}`); } } }
app.use(path.resolve(basePath, 'docs'), express.static(path.resolve(projectRootDir, `./docsite/build`)));
const viteExpressOptions: Parameters<typeof ViteExpress.config>[0] = { mode: isProd ? 'production' : 'development', inlineViteConfig: { base: basePath }, // The build's asset/API references are relative (see vite.config.ts // and the client's api/... calls); a relative URL resolves against // the *current document path* so a nested // client route (e.g. /components/3) would otherwise break them. // <base href> pins resolution to the actual mount root for every // route depth. window.__MS_RUNTIME__ tells react-router the same // mount path so it can set `basename` -- neither can be a // build-time constant since the same build serves every // deployment (root or subpath). transformer: (html) => { const baseHref = basePath === '/' ? '/' : `${basePath}/`; return html.replace( '<head>', `<head><base href="${baseHref}"><script>window.__MS_RUNTIME__=${JSON.stringify({basePath})};</script>` ); } };
if(!isProd) { const conf = await ViteExpress.getViteConfig(); viteExpressOptions.inlineViteConfig = { ...viteExpressOptions.inlineViteConfig, ...conf, // conf.base is the frontend's relative asset base ('./'), not // the Express mount prefix -- keep our runtime-computed one base: basePath }; }
ViteExpress.config(viteExpressOptions); try { ViteExpress.listen(app, port, () => { const start = stripIndents`\n Server started: ${addresses.join('\n')}${dockerHint !== '' ? `\n${dockerHint}` : ''}`
logger.info(start);
if(localDefined) { logger.info(`User-defined base URL for UI and redirect URLs (spotify, deezer, lastfm): ${local}`) } }).on('error', (err) => { throw new Error('Server encountered unrecoverable error', {cause: err}); }); return [app, router]; } catch (e) { throw new Error('Server encountered unrecoverable error', {cause: e}); }
} catch (e) { throw new Error('Server crashed with uncaught exception', {cause: e}); }}
type ViteConfig = { root: string; base: string; build: { outDir: string; }; server?: { hmr?: boolean; };};