Packaging v0: Build and run server in a docker container (#139)
* This is a nightmare * Checkpointing.... getting closer * First cut - packaging the server in a docker container * Remove busted bundles * Minify build * Some common commands for building - we're going to look into proper monorepo solutions soon * Remove dependency on serve-static * Add web serving, full-stack docker target, and Nvidia container support * Remove test action graph for now
@@ -1,10 +1,13 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile
|
||||
*Dockerfile
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
bin
|
||||
dist
|
||||
*/*/dist*
|
||||
*/*/build*
|
||||
|
||||
.pseudotv
|
||||
.dizquetv
|
||||
.dizquetv
|
||||
.tunarr
|
||||
69
build.Dockerfile
Normal file
@@ -0,0 +1,69 @@
|
||||
FROM node:20-alpine3.19 AS base
|
||||
|
||||
# Update
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
FROM base as sources
|
||||
WORKDIR /tunarr
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY server/ ./server
|
||||
COPY shared/ ./shared
|
||||
COPY types ./types
|
||||
COPY web2 ./web2
|
||||
|
||||
FROM sources AS prod-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
### Begin server build ###
|
||||
FROM sources AS build-server
|
||||
# Install deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
# Build common modules
|
||||
RUN pnpm run --filter=types --filter=shared build
|
||||
# Runs tsc --noEmit on the server to ensure the code builds
|
||||
RUN pnpm run --filter=server typecheck
|
||||
# Build ORM metadata cache using source files
|
||||
RUN cd server && pnpm mikro-orm-esm cache:generate --combined --ts
|
||||
# Replace the non-cached metadata config with the cache
|
||||
RUN mv server/mikro-orm.prod.config.ts server/mikro-orm.config.ts
|
||||
# Build and bundle the server
|
||||
RUN pnpm run --filter=server bundle
|
||||
### End server build ###
|
||||
|
||||
### Begin server web ###
|
||||
FROM sources AS build-web
|
||||
# Install deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
# Build common modules
|
||||
RUN pnpm run --filter=types --filter=shared build
|
||||
RUN pnpm run --filter=web build
|
||||
|
||||
### Experimental: Build a SEA
|
||||
FROM build-server AS build-exec
|
||||
COPY --from=build-server /tunarr/server/node_modules /tunarr/server/node_modules
|
||||
COPY --from=build-server /tunarr/server/build /tunarr/server/build
|
||||
RUN pnpm run --filter=server make-exec
|
||||
###
|
||||
|
||||
### Begin server run ###
|
||||
FROM base AS server
|
||||
COPY --from=prod-deps /tunarr/node_modules /tunarr/node_modules
|
||||
COPY --from=prod-deps /tunarr/server/node_modules /tunarr/server/node_modules
|
||||
COPY --from=build-server /tunarr/types /tunarr/types
|
||||
COPY --from=build-server /tunarr/shared /tunarr/shared
|
||||
COPY --from=build-server /tunarr/server/package.json /tunarr/server/package.json
|
||||
COPY --from=build-server /tunarr/server/build /tunarr/server/build
|
||||
ENV TUNARR_BIND_ADDR=0.0.0.0
|
||||
EXPOSE 8000
|
||||
CMD [ "/tunarr/server/build/bundle.js" ]
|
||||
### Begin server run
|
||||
|
||||
### Full stack ###
|
||||
FROM server AS full-stack
|
||||
COPY --from=build-web /tunarr/web2/dist /tunarr/server/build/web
|
||||
77
nvidia.Dockerfile
Normal file
@@ -0,0 +1,77 @@
|
||||
# Setup a node + ffmpeg + nvidia base
|
||||
FROM jrottenberg/ffmpeg:4.4.4-nvidia2204 AS ffmpeg-base
|
||||
ENV NODE_MAJOR=20
|
||||
|
||||
# Install musl for native node bindings (sqlite)
|
||||
RUN apt-get install -y musl-dev
|
||||
RUN ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1
|
||||
|
||||
# Install node
|
||||
RUN <<EOF
|
||||
apt-get update && apt-get install -y ca-certificates curl gnupg
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
apt-get update && apt-get install nodejs -y
|
||||
EOF
|
||||
|
||||
# Install pnpm
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
EXPOSE 8000
|
||||
RUN ln -s /usr/local/bin/ffmpeg /usr/bin/ffmpeg
|
||||
ENTRYPOINT [ "node" ]
|
||||
|
||||
# Add Tunarr sources
|
||||
FROM ffmpeg-base as sources
|
||||
WORKDIR /tunarr
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY server/ ./server
|
||||
COPY shared/ ./shared
|
||||
COPY types ./types
|
||||
COPY web2 ./web2
|
||||
|
||||
FROM sources AS prod-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
### Begin server build ###
|
||||
FROM sources AS build-server
|
||||
# Install deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
# Build common modules
|
||||
RUN pnpm run --filter=types --filter=shared build
|
||||
# Runs tsc --noEmit on the server to ensure the code builds
|
||||
RUN pnpm run --filter=server typecheck
|
||||
# Build ORM metadata cache using source files
|
||||
RUN cd server && pnpm mikro-orm-esm cache:generate --combined --ts
|
||||
# Replace the non-cached metadata config with the cache
|
||||
RUN mv server/mikro-orm.prod.config.ts server/mikro-orm.config.ts
|
||||
# Build and bundle the server
|
||||
RUN pnpm run --filter=server bundle
|
||||
### End server build ###
|
||||
|
||||
### Begin server web ###
|
||||
FROM sources AS build-web
|
||||
# Install deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
# Build common modules
|
||||
RUN pnpm run --filter=types --filter=shared build
|
||||
RUN pnpm run --filter=web build
|
||||
|
||||
### Begin server run ###
|
||||
FROM ffmpeg-base AS server
|
||||
COPY --from=prod-deps /tunarr/node_modules /tunarr/node_modules
|
||||
COPY --from=prod-deps /tunarr/server/node_modules /tunarr/server/node_modules
|
||||
COPY --from=build-server /tunarr/types /tunarr/types
|
||||
COPY --from=build-server /tunarr/shared /tunarr/shared
|
||||
COPY --from=build-server /tunarr/server/package.json /tunarr/server/package.json
|
||||
COPY --from=build-server /tunarr/server/build /tunarr/server/build
|
||||
ENV TUNARR_BIND_ADDR=0.0.0.0
|
||||
EXPOSE 8000
|
||||
CMD [ "/tunarr/server/build/bundle.js" ]
|
||||
### Begin server run
|
||||
|
||||
### Full stack ###
|
||||
FROM server AS full-stack
|
||||
COPY --from=build-web /tunarr/web2/dist /tunarr/server/build/web
|
||||
766
pnpm-lock.yaml
generated
@@ -1,5 +1,5 @@
|
||||
packages:
|
||||
- "server/"
|
||||
- "web2/"
|
||||
- "types/"
|
||||
- "shared/"
|
||||
- server
|
||||
- web2
|
||||
- types
|
||||
- shared
|
||||
@@ -1 +0,0 @@
|
||||
{"items":[{"type":"content","id":"50b50157-3f03-4ca4-870c-5589c8e35993","durationMs":5526279},{"type":"content","id":"d7e04cc1-94fc-4b53-9cae-247937353851","durationMs":6308448},{"type":"content","id":"32084337-f1db-4459-a558-1b2845eb78df","durationMs":7436480},{"type":"content","id":"c1f9bed1-d3b1-4abc-bec4-d51a0587c0a8","durationMs":5053010},{"type":"content","id":"5020e186-f461-43cc-bbc1-345a085c1d05","durationMs":5776799}]}
|
||||
7
server/cjs-shim.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
|
||||
globalThis.require = createRequire(import.meta.url);
|
||||
globalThis.__filename = url.fileURLToPath(import.meta.url);
|
||||
globalThis.__dirname = path.dirname(__filename);
|
||||
57
server/esbuild/native-node-module.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import path from 'path';
|
||||
import { Plugin } from 'esbuild';
|
||||
|
||||
// Copied from https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487
|
||||
export const nativeNodeModulesPlugin = (): Plugin => {
|
||||
return {
|
||||
name: 'native-node-modules',
|
||||
setup(build) {
|
||||
// If a ".node" file is imported within a module in the "file" namespace, resolve
|
||||
// it to an absolute path and put it into the "node-file" virtual namespace.
|
||||
build.onResolve({ filter: /\.node$/, namespace: 'file' }, (args) => {
|
||||
const resolvedId = require.resolve(args.path, {
|
||||
paths: [args.resolveDir],
|
||||
});
|
||||
if (resolvedId.endsWith('.node')) {
|
||||
return {
|
||||
path: resolvedId,
|
||||
namespace: 'node-file',
|
||||
};
|
||||
}
|
||||
return {
|
||||
path: resolvedId,
|
||||
};
|
||||
});
|
||||
|
||||
// Files in the "node-file" virtual namespace call "require()" on the
|
||||
// path from esbuild of the ".node" file in the output directory.
|
||||
build.onLoad({ filter: /.*/, namespace: 'node-file' }, (args) => {
|
||||
return {
|
||||
contents: `
|
||||
import path from ${JSON.stringify(args.path)}
|
||||
try { module.exports = require(path) }
|
||||
catch {}
|
||||
`,
|
||||
resolveDir: path.dirname(args.path),
|
||||
};
|
||||
});
|
||||
|
||||
// If a ".node" file is imported within a module in the "node-file" namespace, put
|
||||
// it in the "file" namespace where esbuild's default loading behavior will handle
|
||||
// it. It is already an absolute path since we resolved it to one above.
|
||||
build.onResolve(
|
||||
{ filter: /\.node$/, namespace: 'node-file' },
|
||||
(args) => ({
|
||||
path: args.path,
|
||||
namespace: 'file',
|
||||
}),
|
||||
);
|
||||
|
||||
// Tell esbuild's default loading behavior to use the "file" loader for
|
||||
// these ".node" files.
|
||||
const opts = build.initialOptions;
|
||||
opts.loader = opts.loader || {};
|
||||
opts.loader['.node'] = 'file';
|
||||
},
|
||||
};
|
||||
};
|
||||
24
server/esbuild/node-protocol.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Plugin } from 'esbuild';
|
||||
|
||||
/**
|
||||
* The node: protocol was added to require in Node v14.18.0
|
||||
* https://nodejs.org/api/esm.html#node-imports
|
||||
*/
|
||||
export const nodeProtocolPlugin = (): Plugin => {
|
||||
const nodeProtocol = 'node:';
|
||||
|
||||
return {
|
||||
name: 'node-protocol-plugin',
|
||||
setup({ onResolve }) {
|
||||
onResolve(
|
||||
{
|
||||
filter: /^node:/,
|
||||
},
|
||||
({ path }) => ({
|
||||
path: path.slice(nodeProtocol.length),
|
||||
external: true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
46
server/mikro-orm.base.config.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
UnderscoreNamingStrategy,
|
||||
defineConfig,
|
||||
} from '@mikro-orm/better-sqlite';
|
||||
import { Migrator } from '@mikro-orm/migrations';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path, { dirname } from 'path';
|
||||
import { CachedImage } from './src/dao/entities/CachedImage.js';
|
||||
import { Channel } from './src/dao/entities/Channel.js';
|
||||
import { ChannelFillerShow } from './src/dao/entities/ChannelFillerShow.js';
|
||||
import { CustomShow } from './src/dao/entities/CustomShow.js';
|
||||
import { CustomShowContent } from './src/dao/entities/CustomShowContent.js';
|
||||
import { FillerListContent } from './src/dao/entities/FillerListContent.js';
|
||||
import { FillerShow } from './src/dao/entities/FillerShow.js';
|
||||
import { PlexServerSettings } from './src/dao/entities/PlexServerSettings.js';
|
||||
import { Program } from './src/dao/entities/Program.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const dbPath = path.join(process.env.DB_PATH ?? '.dizquetv', 'db.db');
|
||||
|
||||
export default defineConfig({
|
||||
dbName: dbPath,
|
||||
baseDir: __dirname,
|
||||
entities: [
|
||||
CachedImage,
|
||||
Channel,
|
||||
ChannelFillerShow,
|
||||
CustomShow,
|
||||
CustomShowContent,
|
||||
FillerListContent,
|
||||
FillerShow,
|
||||
PlexServerSettings,
|
||||
Program,
|
||||
],
|
||||
debug: !!process.env['DATABASE_DEBUG_LOGGING'],
|
||||
namingStrategy: UnderscoreNamingStrategy,
|
||||
forceUndefined: true,
|
||||
dynamicImportProvider: (id) => import(id),
|
||||
migrations: {
|
||||
path: './migrations',
|
||||
pathTs: './src/migrations',
|
||||
},
|
||||
extensions: [Migrator],
|
||||
});
|
||||
@@ -1,30 +1,10 @@
|
||||
import {
|
||||
UnderscoreNamingStrategy,
|
||||
defineConfig,
|
||||
} from '@mikro-orm/better-sqlite';
|
||||
import { Migrator } from '@mikro-orm/migrations';
|
||||
import { defineConfig } from '@mikro-orm/better-sqlite';
|
||||
import { TsMorphMetadataProvider } from '@mikro-orm/reflection';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path, { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const dbPath = path.join(process.env.DB_PATH ?? '.dizquetv', 'db.db');
|
||||
import baseConfig from './mikro-orm.base.config.js';
|
||||
|
||||
export default defineConfig({
|
||||
dbName: dbPath,
|
||||
baseDir: __dirname,
|
||||
entities: ['./build/dao/entities'], // path to our JS entities (dist), relative to `baseDir`
|
||||
entitiesTs: ['./dao/entities'], // path to our TS entities (src), relative to `baseDir`
|
||||
debug: !!process.env['DATABASE_DEBUG_LOGGING'],
|
||||
namingStrategy: UnderscoreNamingStrategy,
|
||||
forceUndefined: true,
|
||||
dynamicImportProvider: (id) => import(id),
|
||||
...baseConfig,
|
||||
entities: ['./dao/entities'], // path to our JS entities (dist), relative to `baseDir`
|
||||
entitiesTs: ['./src/dao/entities'], // path to our TS entities (src), relative to `baseDir`
|
||||
metadataProvider: TsMorphMetadataProvider,
|
||||
migrations: {
|
||||
path: './build/migrations',
|
||||
pathTs: './migrations',
|
||||
},
|
||||
extensions: [Migrator],
|
||||
});
|
||||
|
||||
14
server/mikro-orm.prod.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { GeneratedCacheAdapter, defineConfig } from '@mikro-orm/better-sqlite';
|
||||
import baseConfig from './mikro-orm.base.config.js';
|
||||
import metadataJson from './temp/metadata.json' assert { type: 'json' };
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
metadataCache: {
|
||||
enabled: true,
|
||||
adapter: GeneratedCacheAdapter,
|
||||
options: {
|
||||
data: metadataJson,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -5,17 +5,18 @@
|
||||
"license": "Zlib",
|
||||
"author": "chrisbenincasa",
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"main": "src/index.ts",
|
||||
"bin": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"buildw": "nodemon -e ts --watch . --watch index.ts --ignore build/ -e .json,.ts,.yml,.yaml,.js -x tsc --project tsconfig.build.json",
|
||||
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
||||
"bundle": "node --loader ts-node/esm scripts/bundle.ts",
|
||||
"clean": "rimraf build",
|
||||
"debug": "nodemon --ignore build/ -e .json,.ts,.yml,.yaml,.js --exec \"node --inspect-brk=0.0.0.0:4321 --loader ts-node/esm ./index.ts\"",
|
||||
"dev": "nodemon -e ts --watch . --watch index.ts --ignore build/ --ignore streams/ -e .json,.ts,.yml,.yaml,.js -x ts-node-esm -r tsconfig-paths/register --files --project tsconfig.build.json index.ts --inspect=4321",
|
||||
"dizquetv": "ts-node-esm index.ts",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "nodemon -e ts --watch src/ --ignore build/ --ignore streams/ -e .json,.ts,.yml,.yaml,.js -x ts-node-esm -r tsconfig-paths/register --files --project tsconfig.build.json src/index.ts --inspect=4321",
|
||||
"make-exec": "node --loader ts-node/esm scripts/makeExecutable.ts",
|
||||
"mikro-orm": "NODE_OPTIONS='--loader ts-node/esm --no-warnings' mikro-orm",
|
||||
"package": "sh ./make_dist.sh",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -27,7 +28,6 @@
|
||||
"@mikro-orm/better-sqlite": "^6.0.4",
|
||||
"@mikro-orm/core": "^6.0.4",
|
||||
"@mikro-orm/migrations": "6.0.4",
|
||||
"@mikro-orm/reflection": "^6.0.4",
|
||||
"@tunarr/shared": "workspace:*",
|
||||
"@tunarr/types": "workspace:*",
|
||||
"JSONStream": "1.0.5",
|
||||
@@ -46,6 +46,7 @@
|
||||
"fastify-print-routes": "^2.2.0",
|
||||
"fastify-type-provider-zod": "^1.1.9",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lowdb": "^7.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
@@ -57,7 +58,6 @@
|
||||
"random-js": "2.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"retry": "^0.13.1",
|
||||
"serve-static": "^1.15.0",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.11.0",
|
||||
@@ -68,11 +68,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mikro-orm/cli": "^6.0.4",
|
||||
"@mikro-orm/reflection": "^6.0.4",
|
||||
"@types/async-retry": "^1.4.8",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/express-fileupload": "^1.4.3",
|
||||
"@types/fluent-ffmpeg": "^2.1.23",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/morgan": "^1.9.7",
|
||||
"@types/node": "^20.8.9",
|
||||
@@ -84,17 +86,23 @@
|
||||
"@types/yargs": "^17.0.29",
|
||||
"copyfiles": "^2.2.0",
|
||||
"del-cli": "^3.0.0",
|
||||
"nexe": "^4.0.0-rc.2",
|
||||
"esbuild": "^0.19.5",
|
||||
"fast-glob": "^3.3.2",
|
||||
"nexe": "4.0.0-rc.4",
|
||||
"nodemon": "^3.0.3",
|
||||
"pkg": "^5.8.1",
|
||||
"postject": "1.0.0-alpha.6",
|
||||
"prettier": "^3.0.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"tmp": "^0.2.1",
|
||||
"tmp-promise": "^3.0.3",
|
||||
"ts-essentials": "^9.4.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsify": "^5.0.4",
|
||||
"tsup": "^8.0.2",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.2.0"
|
||||
},
|
||||
"mikro-orm": {
|
||||
|
||||
62
server/scripts/bundle.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import esbuild from 'esbuild';
|
||||
import fg from 'fast-glob';
|
||||
import fs from 'node:fs';
|
||||
import { rimraf } from 'rimraf';
|
||||
import { nativeNodeModulesPlugin } from '../esbuild/native-node-module.js';
|
||||
import { nodeProtocolPlugin } from '../esbuild/node-protocol.js';
|
||||
|
||||
if (fs.existsSync('build')) {
|
||||
console.log('Deleting old build...');
|
||||
await rimraf('build');
|
||||
}
|
||||
|
||||
fs.mkdirSync('build');
|
||||
|
||||
console.log('Copying images...');
|
||||
fs.cpSync('src/resources/images', 'build/resources/images', {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
console.log('Bundling app...');
|
||||
await esbuild.build({
|
||||
entryPoints: ['src/index.ts'],
|
||||
bundle: true,
|
||||
// minify: true,
|
||||
// We can't make this mjs yet because mikro-orm breaks
|
||||
// when using cached metadata w/ not js/ts suffixes:
|
||||
// https://github.com/mikro-orm/mikro-orm/blob/e005cc22ef4e247f9741bdcaf1af012337977b7e/packages/core/src/cache/GeneratedCacheAdapter.ts#L16
|
||||
outfile: 'build/bundle.js',
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
inject: ['cjs-shim.ts'],
|
||||
packages: 'external',
|
||||
tsconfig: './tsconfig.build.json',
|
||||
logLevel: 'verbose',
|
||||
// external: [
|
||||
// 'mysql',
|
||||
// 'mysql2',
|
||||
// 'sqlite3',
|
||||
// 'pg',
|
||||
// 'tedious',
|
||||
// 'pg-query-stream',
|
||||
// 'oracledb',
|
||||
// 'assert',
|
||||
// ],
|
||||
mainFields: ['module', 'main'],
|
||||
plugins: [nativeNodeModulesPlugin(), nodeProtocolPlugin()],
|
||||
});
|
||||
|
||||
console.log('Bundling DB migrations...');
|
||||
await esbuild.build({
|
||||
entryPoints: await fg('src/migrations/*'),
|
||||
outdir: 'build/migrations',
|
||||
bundle: false,
|
||||
packages: 'external',
|
||||
tsconfig: './tsconfig.build.json',
|
||||
});
|
||||
|
||||
fs.cpSync(
|
||||
'src/migrations/.snapshot-db.db.json',
|
||||
'build/migrations/.snapshot-db.db.json',
|
||||
);
|
||||
10
server/scripts/makeExecutable.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { compile } from 'nexe';
|
||||
|
||||
await compile({
|
||||
input: './build/bundle.js',
|
||||
output: './build/server-macos',
|
||||
targets: ['x64-20.11.1'],
|
||||
build: true,
|
||||
resources: ['./build/**/*', 'node_modules/**/*'],
|
||||
python: 'python3',
|
||||
});
|
||||
5
server/scripts/pkg-config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"scripts": "build/bundle.js",
|
||||
"targets": ["node18-linux-arm64"],
|
||||
"outputPath": "dist"
|
||||
}
|
||||
5
server/scripts/sea.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"main": "build/bundle.js",
|
||||
"output": "build/blob.blob",
|
||||
"disableExperimentalSEAWarning": true
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { XmlTvSettings, defaultXmlTvSettings } from '../dao/settings.js';
|
||||
import createLogger from '../logger.js';
|
||||
import { scheduledJobsById } from '../services/scheduler.js';
|
||||
import { firstDefined } from '../util.js';
|
||||
import { serverOptions } from '../globals.js';
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
|
||||
@@ -70,7 +71,7 @@ export const xmlTvSettingsRouter: FastifyPluginCallback = (
|
||||
try {
|
||||
await req.serverCtx.settings.updateSettings(
|
||||
'xmltv',
|
||||
defaultXmlTvSettings,
|
||||
defaultXmlTvSettings(serverOptions().database),
|
||||
);
|
||||
const xmltv = req.serverCtx.settings.xmlTvSettings();
|
||||
await res.send(xmltv);
|
||||
@@ -10,7 +10,7 @@ import path from 'node:path';
|
||||
import 'reflect-metadata';
|
||||
import { globalOptions } from '../globals.js';
|
||||
import createLogger from '../logger.js';
|
||||
import dbConfig from '../mikro-orm.config.js';
|
||||
import dbConfig from '../../mikro-orm.config.js';
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { test, describe, beforeAll, afterAll } from 'vitest';
|
||||
import { MikroORM, RequestContext } from '@mikro-orm/better-sqlite';
|
||||
import dbConfig from '../mikro-orm.config.js';
|
||||
import dbConfig from '../../mikro-orm.prod.config.js';
|
||||
import {
|
||||
migrateChannel,
|
||||
migratePrograms,
|
||||
@@ -141,7 +141,7 @@ async function migrateFromLegacyDbInner(
|
||||
) {
|
||||
const entitiesToMigrate = entities ?? MigratableEntities;
|
||||
// First initialize the default schema:
|
||||
db.data = { ...defaultSchema };
|
||||
db.data = { ...defaultSchema(globalOptions().database) };
|
||||
await db.write();
|
||||
|
||||
let settings: Partial<SettingsSchema> = {};
|
||||
@@ -180,7 +180,7 @@ async function migrateFromLegacyDbInner(
|
||||
settings = {
|
||||
...settings,
|
||||
xmltv: {
|
||||
...defaultXmlTvSettings,
|
||||
...defaultXmlTvSettings(globalOptions().database),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@@ -11,7 +11,6 @@ import { JSONFilePreset } from 'lowdb/node';
|
||||
import path from 'path';
|
||||
import { DeepReadonly } from 'ts-essentials';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import constants from '../constants.js';
|
||||
import { globalOptions } from '../globals.js';
|
||||
|
||||
const CURRENT_VERSION = 1;
|
||||
@@ -55,12 +54,12 @@ export type XmlTvSettings = {
|
||||
enableImageCache: boolean;
|
||||
};
|
||||
|
||||
export const defaultXmlTvSettings: XmlTvSettings = {
|
||||
export const defaultXmlTvSettings = (dbBasePath: string): XmlTvSettings => ({
|
||||
programmingHours: 12,
|
||||
refreshHours: 4,
|
||||
outputPath: path.resolve(constants.DEFAULT_DATA_DIR, 'xmltv.xml'),
|
||||
outputPath: path.resolve(dbBasePath, 'xmltv.xml'),
|
||||
enableImageCache: false,
|
||||
};
|
||||
});
|
||||
|
||||
export type SettingsSchema = {
|
||||
clientId: string;
|
||||
@@ -86,7 +85,7 @@ export type Schema = {
|
||||
settings: SettingsSchema;
|
||||
};
|
||||
|
||||
export const defaultSchema: Schema = {
|
||||
export const defaultSchema = (dbBasePath: string): Schema => ({
|
||||
version: 1,
|
||||
migration: {
|
||||
legacyMigration: false,
|
||||
@@ -94,11 +93,11 @@ export const defaultSchema: Schema = {
|
||||
settings: {
|
||||
clientId: uuidv4(),
|
||||
hdhr: defaultHdhrSettings,
|
||||
xmltv: defaultXmlTvSettings,
|
||||
xmltv: defaultXmlTvSettings(dbBasePath),
|
||||
plexStream: defaultPlexStreamSettings,
|
||||
ffmpeg: defaultFfmpegSettings,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export class Settings {
|
||||
private db: Low<Schema>;
|
||||
@@ -150,7 +149,10 @@ export const getSettingsRawDb = once(async (dbPath?: string) => {
|
||||
|
||||
const needsFlush = !existsSync(actualPath);
|
||||
|
||||
const db = await JSONFilePreset<Schema>(actualPath, defaultSchema);
|
||||
const db = await JSONFilePreset<Schema>(
|
||||
actualPath,
|
||||
defaultSchema(globalOptions().database),
|
||||
);
|
||||
|
||||
await db.read();
|
||||
if (needsFlush) {
|
||||
@@ -16,11 +16,22 @@ import createLogger from './logger.js';
|
||||
import { ServerOptions } from './types.js';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { initServer } from './server.js';
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
|
||||
const maybeEnvPort = () => {
|
||||
const port = process.env['TUNARR_SERVER_PORT'];
|
||||
if (!port) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseInt(port);
|
||||
return isNaN(parsed) ? undefined : parsed;
|
||||
};
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName('dizquetv')
|
||||
.scriptName('tunarr')
|
||||
.option('database', {
|
||||
alias: 'd',
|
||||
type: 'string',
|
||||
@@ -31,7 +42,7 @@ yargs(hideBin(process.argv))
|
||||
})
|
||||
.option('force_migration', {
|
||||
type: 'boolean',
|
||||
desc: 'Forces a migration from a legacy dizque database. Useful for development and debugging. NOTE: This WILL override any settings you have!',
|
||||
desc: 'Forces a migration from a legacy dizquetv database. Useful for development and debugging. NOTE: This WILL override any settings you have!',
|
||||
default: false,
|
||||
})
|
||||
.middleware(setGlobalOptions)
|
||||
@@ -40,19 +51,33 @@ yargs(hideBin(process.argv))
|
||||
})
|
||||
.command(
|
||||
['server', '$0'],
|
||||
'Run the dizqueTV server',
|
||||
'Run the Tunarr server',
|
||||
(yargs) => {
|
||||
return yargs
|
||||
.option('port', {
|
||||
alias: 'p',
|
||||
type: 'number',
|
||||
desc: 'The port to run the dizque server on',
|
||||
default: 8000,
|
||||
desc: 'The port to run the Tunarr server on',
|
||||
default: maybeEnvPort() ?? 8000,
|
||||
})
|
||||
.option('printRoutes', {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.middleware(setServerOptions);
|
||||
},
|
||||
async (args: ArgumentsCamelCase<ServerOptions>) => {
|
||||
(await import('./server.js')).initServer(args);
|
||||
console.log(
|
||||
` \\
|
||||
Tunarr ${constants.VERSION_NAME}
|
||||
.------------.
|
||||
|:::///### o |
|
||||
|:::///### |
|
||||
':::///### o |
|
||||
'------------'
|
||||
`,
|
||||
);
|
||||
await initServer(args);
|
||||
},
|
||||
)
|
||||
.command(
|
||||
@@ -63,13 +88,17 @@ yargs(hideBin(process.argv))
|
||||
.option('port', {
|
||||
alias: 'p',
|
||||
type: 'number',
|
||||
desc: 'The port to run the dizque server on',
|
||||
default: 8000,
|
||||
desc: 'The port to run the Tunarr server on',
|
||||
default: maybeEnvPort() ?? 8000,
|
||||
})
|
||||
.option('printRoutes', {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
.middleware(setServerOptions);
|
||||
},
|
||||
async (args: ArgumentsCamelCase<ServerOptions>) => {
|
||||
const f = await (await import('./server.js')).initServer(args);
|
||||
const f = await initServer(args);
|
||||
const x = await f
|
||||
.inject({ method: 'get', url: '/docs/json' })
|
||||
.then((r) => r.body);
|
||||
@@ -6,6 +6,7 @@ import { isUndefined, join } from 'lodash-es';
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import chalk from 'chalk';
|
||||
import { isProduction } from './util.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -15,27 +16,32 @@ const getLabel = (callingModule: ImportMeta) => {
|
||||
return path.join(parts[parts.length - 2], parts.pop() ?? '');
|
||||
};
|
||||
|
||||
const hformat = (module: ImportMeta) =>
|
||||
winston.format.printf(({ level, label, message, timestamp, ...metadata }) => {
|
||||
let msg = `${timestamp} [${level}] ${getLabel(module)}${
|
||||
label ? `[${label}]` : ''
|
||||
}: ${message} `;
|
||||
for (const key of Object.keys(metadata)) {
|
||||
if (key === 'stack') {
|
||||
msg += metadata.message;
|
||||
if (metadata.stack) {
|
||||
msg += '\n';
|
||||
msg += join(
|
||||
metadata.stack.split('\n').map((line) => '\t' + line),
|
||||
'\n',
|
||||
);
|
||||
const hformat = (module: ImportMeta) => {
|
||||
// Exclude module label in prod build because it will always be the same (bundle.js)
|
||||
const moduleLabel = isProduction ? '' : ` ${getLabel(module)}`;
|
||||
return winston.format.printf(
|
||||
({ level, label, message, timestamp, ...metadata }) => {
|
||||
let msg = `${timestamp} [${level}]${moduleLabel}${
|
||||
label ? `[${label}]` : ''
|
||||
}: ${message} `;
|
||||
for (const key of Object.keys(metadata)) {
|
||||
if (key === 'stack') {
|
||||
msg += metadata.message;
|
||||
if (metadata.stack) {
|
||||
msg += '\n';
|
||||
msg += join(
|
||||
metadata.stack.split('\n').map((line) => '\t' + line),
|
||||
'\n',
|
||||
);
|
||||
}
|
||||
} else if (isUndefined(metadata)) {
|
||||
msg += chalk.gray('undefined');
|
||||
}
|
||||
} else if (isUndefined(metadata)) {
|
||||
msg += chalk.gray('undefined');
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
return msg;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const createLogger = (module: ImportMeta) => {
|
||||
const logger = winston.createLogger({
|
||||
BIN
server/src/resources/images/dizquetv.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
server/src/resources/images/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
136
server/src/resources/images/favicon.svg
Normal file
@@ -0,0 +1,136 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="200"
|
||||
height="200"
|
||||
viewBox="0 0 52.9168 52.916668"
|
||||
version="1.1"
|
||||
id="svg8"
|
||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
||||
sodipodi:docname="favicon.svg"
|
||||
inkscape:export-filename="/home/vx/dev/pseudotv/resources/favicon-16.png"
|
||||
inkscape:export-xdpi="7.6799998"
|
||||
inkscape:export-ydpi="7.6799998">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="3.0547013"
|
||||
inkscape:cx="55.816079"
|
||||
inkscape:cy="84.726326"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1056"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Capa 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(0,-244.08278)">
|
||||
<rect
|
||||
style="opacity:1;fill:#a1a1a1;fill-opacity:0.86666667;stroke:none;stroke-width:1.46508551;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4712"
|
||||
width="52.211964"
|
||||
height="51.512306"
|
||||
x="-0.85796964"
|
||||
y="245.32475"
|
||||
transform="matrix(0.99980416,-0.01978974,0.00448328,0.99998995,0,0)" />
|
||||
<g
|
||||
id="g4581"
|
||||
style="fill:#080808;fill-opacity:1;stroke-width:0.68901283"
|
||||
transform="matrix(1.2119871,0,0,1.7379906,-82.577875,-167.18505)">
|
||||
<rect
|
||||
transform="rotate(-0.94645665)"
|
||||
y="239.28041"
|
||||
x="65.156158"
|
||||
height="27.75024"
|
||||
width="41.471352"
|
||||
id="rect4524"
|
||||
style="opacity:1;fill:#080808;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
</g>
|
||||
<rect
|
||||
style="opacity:1;fill:#9cbc28;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4518"
|
||||
width="10.338528"
|
||||
height="23.042738"
|
||||
x="8.726779"
|
||||
y="258.22861"
|
||||
transform="matrix(0.99995865,0.00909414,-0.00926779,0.99995705,0,0)" />
|
||||
<ellipse
|
||||
style="opacity:1;fill:#a1a1a1;fill-opacity:0.86792453;stroke:none;stroke-width:1.46499991;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path4568"
|
||||
cx="44.118061"
|
||||
cy="261.20392"
|
||||
rx="2.4216392"
|
||||
ry="2.3988426" />
|
||||
<ellipse
|
||||
cy="272.90894"
|
||||
cx="44.765343"
|
||||
id="circle4570"
|
||||
style="opacity:1;fill:#a1a1a1;fill-opacity:0.86792453;stroke:none;stroke-width:1.46499991;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
rx="2.4216392"
|
||||
ry="2.3988426" />
|
||||
<g
|
||||
id="g1705"
|
||||
transform="translate(0,-2.116672)">
|
||||
<rect
|
||||
transform="matrix(0.99967585,0.02545985,-0.02594573,0.99966335,0,0)"
|
||||
style="opacity:1;fill:#289bbc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="rect4520"
|
||||
width="10.338468"
|
||||
height="23.042864"
|
||||
x="23.424755"
|
||||
y="259.99872" />
|
||||
</g>
|
||||
<rect
|
||||
transform="matrix(0.99837418,-0.05699994,0.05808481,0.99831165,0,0)"
|
||||
y="259.69229"
|
||||
x="10.517879"
|
||||
height="23.043449"
|
||||
width="10.33821"
|
||||
id="rect4522"
|
||||
style="opacity:1;fill:#bc289b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.03992438px;line-height:125%;font-family:'Liberation Serif';-inkscape-font-specification:'Liberation Serif';letter-spacing:0px;word-spacing:0px;fill:#e6e6e6;fill-opacity:1;stroke:none;stroke-width:0.264584px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
x="16.979799"
|
||||
y="286.34747"
|
||||
id="text1730"
|
||||
transform="rotate(-1.2296789)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan1728"
|
||||
x="16.979799"
|
||||
y="286.34747"
|
||||
style="fill:#e6e6e6;fill-opacity:1;stroke-width:0.264584px">dizqueTV</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
BIN
server/src/resources/images/generic-error-screen.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
server/src/resources/images/generic-music-screen.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
server/src/resources/images/generic-offline-screen.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
server/src/resources/images/loading-screen.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
@@ -17,11 +17,7 @@ import {
|
||||
import fs from 'fs';
|
||||
import morgan from 'morgan';
|
||||
import { onShutdown } from 'node-graceful-shutdown';
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'path';
|
||||
import serveStatic from 'serve-static';
|
||||
import { URL } from 'url';
|
||||
import path, { dirname } from 'path';
|
||||
import { miscRouter } from './api.js';
|
||||
import { ffmpegSettingsRouter } from './api/ffmpegSettingsApi.js';
|
||||
import { guideRouter } from './api/guideApi.js';
|
||||
@@ -32,7 +28,6 @@ import { schedulerRouter } from './api/schedulerApi.js';
|
||||
import { debugApi } from './api/v2/debugApi.js';
|
||||
import registerV2Routes from './api/v2/index.js';
|
||||
import { xmlTvSettingsRouter } from './api/xmltvSettingsApi.js';
|
||||
import constants from './constants.js';
|
||||
import { EntityManager, initOrm } from './dao/dataSource.js';
|
||||
import { migrateFromLegacyDb } from './dao/legacyDbMigration.js';
|
||||
import { getSettingsRawDb } from './dao/settings.js';
|
||||
@@ -40,36 +35,14 @@ import { serverOptions } from './globals.js';
|
||||
import createLogger from './logger.js';
|
||||
import { serverContext } from './serverContext.js';
|
||||
import { scheduleJobs, scheduledJobsById } from './services/scheduler.js';
|
||||
import { runFixers } from './tasks/fixers/index.js';
|
||||
import { UpdateXmlTvTask } from './tasks/updateXmlTvTask.js';
|
||||
import { ServerOptions } from './types.js';
|
||||
import { wait } from './util.js';
|
||||
import { filename, wait } from './util.js';
|
||||
import { videoRouter } from './video.js';
|
||||
import { runFixers } from './tasks/fixers/index.js';
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
|
||||
// Temporary
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
console.log(
|
||||
` \\
|
||||
Tunarr ${constants.VERSION_NAME}
|
||||
.------------.
|
||||
|:::///### o |
|
||||
|:::///### |
|
||||
':::///### o |
|
||||
'------------'
|
||||
`,
|
||||
);
|
||||
|
||||
const NODE = parseInt(process.version.match(/^[^0-9]*(\d+)\..*$/)![1]);
|
||||
|
||||
if (NODE < 12) {
|
||||
logger.error(
|
||||
`WARNING: Your nodejs version ${process.version} is lower than supported. dizqueTV has been tested best on nodejs 12.16.`,
|
||||
);
|
||||
}
|
||||
const currentDirectory = dirname(filename(import.meta.url));
|
||||
|
||||
function initDbDirectories() {
|
||||
const opts = serverOptions();
|
||||
@@ -106,6 +79,27 @@ function initDbDirectories() {
|
||||
}
|
||||
|
||||
export async function initServer(opts: ServerOptions) {
|
||||
onShutdown('log', [], async () => {
|
||||
const ctx = await serverContext();
|
||||
const t = new Date().getTime();
|
||||
ctx.eventService.push({
|
||||
type: 'lifecycle',
|
||||
message: `Initiated Server Shutdown`,
|
||||
detail: {
|
||||
time: t,
|
||||
},
|
||||
level: 'warning',
|
||||
});
|
||||
|
||||
logger.info('Received exit signal, attempting graceful shutdonw...');
|
||||
await wait(2000);
|
||||
});
|
||||
|
||||
onShutdown('xmltv-writer', [], async () => {
|
||||
const ctx = await serverContext();
|
||||
await ctx.xmltv.shutdown();
|
||||
});
|
||||
|
||||
const hadLegacyDb = initDbDirectories();
|
||||
|
||||
const orm = await initOrm();
|
||||
@@ -122,11 +116,12 @@ export async function initServer(opts: ServerOptions) {
|
||||
|
||||
const updateXMLPromise = scheduledJobsById[UpdateXmlTvTask.ID]!.runNow();
|
||||
|
||||
const app = fastify({ logger: false, bodyLimit: 50 * 1024 * 1024 });
|
||||
await app
|
||||
const app = fastify({ logger: false, bodyLimit: 50 * 1024 * 1024 })
|
||||
.setValidatorCompiler(validatorCompiler)
|
||||
.setSerializerCompiler(serializerCompiler)
|
||||
.withTypeProvider<ZodTypeProvider>()
|
||||
.withTypeProvider<ZodTypeProvider>();
|
||||
|
||||
await app
|
||||
.register(fastifySwagger, {
|
||||
openapi: {
|
||||
info: {
|
||||
@@ -149,7 +144,6 @@ export async function initServer(opts: ServerOptions) {
|
||||
RequestContext.create(orm.em, done),
|
||||
)
|
||||
.addHook('onClose', async () => await orm.close())
|
||||
.register(fastifyPrintRoutes)
|
||||
.register(
|
||||
fp((f, _, done) => {
|
||||
f.decorateRequest('serverCtx', null);
|
||||
@@ -162,7 +156,11 @@ export async function initServer(opts: ServerOptions) {
|
||||
}),
|
||||
);
|
||||
|
||||
await app.use(
|
||||
if (serverOptions().printRoutes) {
|
||||
await app.register(fastifyPrintRoutes);
|
||||
}
|
||||
|
||||
app.use(
|
||||
morgan(':method :url :status :res[content-length] - :response-time ms', {
|
||||
stream: {
|
||||
write: (message) => logger.http(message.trim()),
|
||||
@@ -175,20 +173,28 @@ export async function initServer(opts: ServerOptions) {
|
||||
|
||||
ctx.eventService.setup(app);
|
||||
|
||||
await app
|
||||
.use(serveStatic(fileURLToPath(new URL('../web/public', import.meta.url))))
|
||||
.use('/images', serveStatic(path.join(opts.database, 'images')))
|
||||
.use(
|
||||
'/favicon.svg',
|
||||
serveStatic(path.join(__dirname, 'resources', 'favicon.svg')),
|
||||
)
|
||||
.use('/custom.css', serveStatic(path.join(opts.database, 'custom.css')));
|
||||
|
||||
// API Routers
|
||||
await app
|
||||
.register(fpStatic, {
|
||||
root: path.join(currentDirectory, 'resources', 'images'),
|
||||
prefix: '/images',
|
||||
})
|
||||
.get('/favicon.svg', async (_, res) => {
|
||||
return res.sendFile(
|
||||
'favicon.svg',
|
||||
path.join(currentDirectory, 'resources', 'images'),
|
||||
);
|
||||
})
|
||||
.get('/favicon.ico', async (_, res) => {
|
||||
return res.sendFile(
|
||||
'favicon.ico',
|
||||
path.join(currentDirectory, 'resources', 'images'),
|
||||
);
|
||||
})
|
||||
.register(async (f) => {
|
||||
await f.register(fpStatic, {
|
||||
root: path.join(opts.database, 'cache', 'images'),
|
||||
decorateReply: false,
|
||||
});
|
||||
// f.addHook('onRequest', async (req, res) => ctx.cacheImageService.routerInterceptor(req, res));
|
||||
f.get<{ Params: { hash: string } }>(
|
||||
@@ -220,16 +226,27 @@ export async function initServer(opts: ServerOptions) {
|
||||
prefix: '/api/cache/images',
|
||||
})
|
||||
.register(videoRouter)
|
||||
.register(ctx.hdhrService.createRouter());
|
||||
.register(ctx.hdhrService.createRouter())
|
||||
.register(async (f) => {
|
||||
await f.register(fpStatic, {
|
||||
root: path.join(currentDirectory, 'web'),
|
||||
prefix: '/web',
|
||||
});
|
||||
f.get('/web', async (_, res) =>
|
||||
res.sendFile('index.html', path.join(currentDirectory, 'web')),
|
||||
);
|
||||
});
|
||||
|
||||
await updateXMLPromise;
|
||||
|
||||
const host = process.env['TUNARR_BIND_ADDR'] ?? 'localhost';
|
||||
app.listen(
|
||||
{
|
||||
host,
|
||||
port: opts.port,
|
||||
},
|
||||
() => {
|
||||
logger.info(`HTTP server running on port: http://*:${opts.port}`);
|
||||
logger.info(`HTTP server running on port: http://${host}:${opts.port}`);
|
||||
const hdhrSettings = ctx.settings.hdhrSettings();
|
||||
if (hdhrSettings.autoDiscoveryEnabled) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
|
||||
@@ -249,24 +266,3 @@ export async function initServer(opts: ServerOptions) {
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
onShutdown('log', [], async () => {
|
||||
const ctx = await serverContext();
|
||||
const t = new Date().getTime();
|
||||
ctx.eventService.push({
|
||||
type: 'lifecycle',
|
||||
message: `Initiated Server Shutdown`,
|
||||
detail: {
|
||||
time: t,
|
||||
},
|
||||
level: 'warning',
|
||||
});
|
||||
|
||||
logger.info('Received exit signal, attempting graceful shutdonw...');
|
||||
await wait(2000);
|
||||
});
|
||||
|
||||
onShutdown('xmltv-writer', [], async () => {
|
||||
const ctx = await serverContext();
|
||||
await ctx.xmltv.shutdown();
|
||||
});
|
||||
@@ -1,12 +1,11 @@
|
||||
import fs, { promises as fsPromises } from 'fs';
|
||||
import { once } from 'lodash-es';
|
||||
import path from 'path';
|
||||
import { ChannelCache } from './channelCache.js';
|
||||
import { ChannelDB } from './dao/channelDb.js';
|
||||
import { CustomShowDB } from './dao/customShowDb.js';
|
||||
import { Settings, getSettings } from './dao/settings.js';
|
||||
import { FillerDB } from './dao/fillerDb.js';
|
||||
import { PlexServerDB } from './dao/plexServerDb.js';
|
||||
import { Settings, getSettings } from './dao/settings.js';
|
||||
import { serverOptions } from './globals.js';
|
||||
import { HdhrService } from './hdhr.js';
|
||||
import { CacheImageService } from './services/cacheImageService.js';
|
||||
@@ -16,43 +15,6 @@ import { M3uService } from './services/m3uService.js';
|
||||
import { TVGuideService } from './services/tvGuideService.js';
|
||||
import { XmlTvWriter } from './xmltv.js';
|
||||
|
||||
async function copyIfMissingFromDatabase(
|
||||
targetPath: string,
|
||||
resourcePath: string,
|
||||
): Promise<void> {
|
||||
const opts = serverOptions();
|
||||
if (!fs.existsSync(path.join(opts.database, targetPath))) {
|
||||
await fsPromises.copyFile(
|
||||
new URL('../' + resourcePath, import.meta.url),
|
||||
path.join(opts.database, targetPath),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function initDBDirectory() {
|
||||
return Promise.all([
|
||||
copyIfMissingFromDatabase('images/dizquetv.png', 'resources/dizquetv.png'),
|
||||
copyIfMissingFromDatabase('font.ttf', 'resources/font.ttf'),
|
||||
copyIfMissingFromDatabase(
|
||||
'images/generic-error-screen.png',
|
||||
'resources/generic-error-screen.png',
|
||||
),
|
||||
copyIfMissingFromDatabase(
|
||||
'images/generic-offline-screen.png',
|
||||
'resources/generic-offline-screen.png',
|
||||
),
|
||||
copyIfMissingFromDatabase(
|
||||
'images/generic-music-screen.png',
|
||||
'resources/generic-music-screen.png',
|
||||
),
|
||||
copyIfMissingFromDatabase(
|
||||
'images/loading-screen.png',
|
||||
'resources/loading-screen.png',
|
||||
),
|
||||
copyIfMissingFromDatabase('custom.css', 'resources/default-custom.css'),
|
||||
]);
|
||||
}
|
||||
|
||||
export type ServerContext = {
|
||||
channelDB: ChannelDB;
|
||||
fillerDB: FillerDB;
|
||||
@@ -83,8 +45,6 @@ export const serverContext: () => Promise<ServerContext> = once(async () => {
|
||||
const eventService = new EventService();
|
||||
const xmltv = new XmlTvWriter();
|
||||
|
||||
await initDBDirectory();
|
||||
|
||||
const guideService = new TVGuideService(
|
||||
xmltv,
|
||||
cacheImageService,
|
||||