mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
chore: add Scalar generated API docs (#856)
This commit is contained in:
committed by
GitHub
parent
67881e21a8
commit
8b703d50de
17
docs/api-docs.html
Normal file
17
docs/api-docs.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Scalar API Reference</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Need a Custom Header? Check out this example https://codepen.io/scalarorg/pen/VwOXqam -->
|
||||
<script
|
||||
id="api-reference"
|
||||
data-url="/generated/tunarr-latest-openapi.json"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
</body>
|
||||
</html>
|
||||
0
docs/generated/.gitkeep
Normal file
0
docs/generated/.gitkeep
Normal file
1
docs/generated/tunarr-latest-openapi.json
Normal file
1
docs/generated/tunarr-latest-openapi.json
Normal file
File diff suppressed because one or more lines are too long
1
docs/generated/tunarr-v0.18.2-openapi.json
Normal file
1
docs/generated/tunarr-v0.18.2-openapi.json
Normal file
File diff suppressed because one or more lines are too long
@@ -37,6 +37,7 @@ theme:
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- API Docs: api-docs.html
|
||||
- Getting Started:
|
||||
- Install: getting-started/installation.md
|
||||
- Run: getting-started/run.md
|
||||
|
||||
82
pnpm-lock.yaml
generated
82
pnpm-lock.yaml
generated
@@ -100,8 +100,8 @@ importers:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
'@scalar/fastify-api-reference':
|
||||
specifier: ^1.25.33
|
||||
version: 1.25.33
|
||||
specifier: ^1.25.106
|
||||
version: 1.25.106
|
||||
'@tunarr/playlist':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0
|
||||
@@ -2116,16 +2116,16 @@ packages:
|
||||
'@rushstack/ts-command-line@4.19.1':
|
||||
resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==}
|
||||
|
||||
'@scalar/fastify-api-reference@1.25.33':
|
||||
resolution: {integrity: sha512-icj/WJg5sSfBplP5OL5+4hqQBXHUgmBNCpPW+4ZsJ/ZKPMKvnQ539okrBCplfFYtQHVjH2s0PoCUCXL7YnQcwA==}
|
||||
'@scalar/fastify-api-reference@1.25.106':
|
||||
resolution: {integrity: sha512-6BbqE4eHJLe8f/m3AePT/p0MulD/aipSAuhPxQl8SQmaiUlePcukruo72KhoyqVPO0WEoE1haE8SvRLJpAgzww==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@scalar/openapi-types@0.1.2':
|
||||
resolution: {integrity: sha512-TxiAbs9Rw5qnMs/vvg+4Zx1Xf/5RhJXf8w6JOYSgvp4d2IKkOKc9eSOhid8ySvz7bWCjF2yWd8eHNc/BFs8cXg==}
|
||||
'@scalar/openapi-types@0.1.6':
|
||||
resolution: {integrity: sha512-V+KnESyVJqorJzEN0QFlu3tAImCHjnvPov6QcQvjfY7s0+CjrI3rRO3oVIRlXURTQrQGrnhxvK0SkXGAZ+dxvw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@scalar/types@0.0.14':
|
||||
resolution: {integrity: sha512-4yzW5d9nWtRE3eVNfLnuVUScSMf325PYJ9qCJ8CpaVP7hnWrTv9xGw/2n7csEKzu3QJkdff0myibHfxXJ6ICig==}
|
||||
'@scalar/types@0.0.27':
|
||||
resolution: {integrity: sha512-5f293PX78gwE3NwcBNXf7+qc9OIB6lYsRpzGZts7eaN3aN9i23ysWewF76DOs74oARRP5LgNBMC7JCo9dIxM1Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1':
|
||||
@@ -2620,8 +2620,8 @@ packages:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
|
||||
'@unhead/schema@1.11.7':
|
||||
resolution: {integrity: sha512-j9uN7T63aUXrZ6yx2CfjVT7xZHjn0PZO7TPMaWqMFjneIH/NONKvDVCMEqDlXeqdSIERIYtk/xTHgCUMer5eyw==}
|
||||
'@unhead/schema@1.11.18':
|
||||
resolution: {integrity: sha512-a3TA/OJCRdfbFhcA3Hq24k1ZU1o9szicESrw8DZcGyQFacHnh84mVgnyqSkMnwgCmfN4kvjSiTBlLEHS6+wATw==}
|
||||
|
||||
'@vitejs/plugin-react-swc@3.7.1':
|
||||
resolution: {integrity: sha512-vgWOY0i1EROUK0Ctg1hwhtC3SdcDjZcdit4Ups4aPkDcB1jYhmo+RMYWY87cmXMhvtD5uf8lV89j2w16vkdSVg==}
|
||||
@@ -7791,7 +7791,7 @@ snapshots:
|
||||
outdent: 0.5.0
|
||||
prettier: 2.8.8
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
|
||||
'@changesets/assemble-release-plan@6.0.3':
|
||||
dependencies:
|
||||
@@ -7801,7 +7801,7 @@ snapshots:
|
||||
'@changesets/should-skip-package': 0.1.0
|
||||
'@changesets/types': 6.0.0
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
|
||||
'@changesets/changelog-git@0.2.0':
|
||||
dependencies:
|
||||
@@ -7862,7 +7862,7 @@ snapshots:
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
chalk: 2.4.2
|
||||
fs-extra: 7.0.1
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
|
||||
'@changesets/get-release-plan@4.0.3':
|
||||
dependencies:
|
||||
@@ -8619,7 +8619,7 @@ snapshots:
|
||||
'@rushstack/ts-command-line': 4.19.1(@types/node@22.10.7)
|
||||
lodash: 4.17.21
|
||||
minimatch: 3.0.8
|
||||
resolve: 1.22.8
|
||||
resolve: 1.22.10
|
||||
semver: 7.5.4
|
||||
source-map: 0.6.1
|
||||
typescript: 5.4.2
|
||||
@@ -8897,7 +8897,7 @@ snapshots:
|
||||
fs-extra: 7.0.1
|
||||
import-lazy: 4.0.0
|
||||
jju: 1.4.0
|
||||
resolve: 1.22.8
|
||||
resolve: 1.22.10
|
||||
semver: 7.5.4
|
||||
z-schema: 5.0.5
|
||||
optionalDependencies:
|
||||
@@ -8905,7 +8905,7 @@ snapshots:
|
||||
|
||||
'@rushstack/rig-package@0.5.2':
|
||||
dependencies:
|
||||
resolve: 1.22.8
|
||||
resolve: 1.22.10
|
||||
strip-json-comments: 3.1.1
|
||||
|
||||
'@rushstack/terminal@0.10.0(@types/node@22.10.7)':
|
||||
@@ -8924,17 +8924,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
'@scalar/fastify-api-reference@1.25.33':
|
||||
'@scalar/fastify-api-reference@1.25.106':
|
||||
dependencies:
|
||||
'@scalar/types': 0.0.14
|
||||
'@scalar/types': 0.0.27
|
||||
fastify-plugin: 4.5.1
|
||||
|
||||
'@scalar/openapi-types@0.1.2': {}
|
||||
'@scalar/openapi-types@0.1.6': {}
|
||||
|
||||
'@scalar/types@0.0.14':
|
||||
'@scalar/types@0.0.27':
|
||||
dependencies:
|
||||
'@scalar/openapi-types': 0.1.2
|
||||
'@unhead/schema': 1.11.7
|
||||
'@scalar/openapi-types': 0.1.6
|
||||
'@unhead/schema': 1.11.18
|
||||
|
||||
'@sec-ant/readable-stream@0.4.1': {}
|
||||
|
||||
@@ -9460,7 +9460,7 @@ snapshots:
|
||||
debug: 4.3.7
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
ts-api-utils: 1.0.3(typescript@5.4.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.4.3
|
||||
@@ -9505,7 +9505,7 @@ snapshots:
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.4.3)
|
||||
eslint: 9.17.0(jiti@2.4.1)
|
||||
eslint-scope: 5.1.1
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -9547,7 +9547,7 @@ snapshots:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
|
||||
'@unhead/schema@1.11.7':
|
||||
'@unhead/schema@1.11.18':
|
||||
dependencies:
|
||||
hookable: 5.5.3
|
||||
zhead: 2.2.4
|
||||
@@ -10248,8 +10248,8 @@ snapshots:
|
||||
call-bind@1.0.8:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.1
|
||||
es-define-property: 1.0.0
|
||||
get-intrinsic: 1.2.4
|
||||
es-define-property: 1.0.1
|
||||
get-intrinsic: 1.2.6
|
||||
set-function-length: 1.2.2
|
||||
|
||||
call-bound@1.0.3:
|
||||
@@ -10660,6 +10660,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
debug@4.3.4:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
debug@4.3.4(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
@@ -10703,9 +10707,9 @@ snapshots:
|
||||
|
||||
define-data-property@1.1.4:
|
||||
dependencies:
|
||||
es-define-property: 1.0.0
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.0.1
|
||||
gopd: 1.2.0
|
||||
|
||||
define-properties@1.2.1:
|
||||
dependencies:
|
||||
@@ -11560,7 +11564,7 @@ snapshots:
|
||||
dunder-proto: 1.0.1
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.0.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
@@ -11743,7 +11747,7 @@ snapshots:
|
||||
|
||||
has-property-descriptors@1.0.2:
|
||||
dependencies:
|
||||
es-define-property: 1.0.0
|
||||
es-define-property: 1.0.1
|
||||
|
||||
has-proto@1.0.3: {}
|
||||
|
||||
@@ -11757,7 +11761,7 @@ snapshots:
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.0.3
|
||||
has-symbols: 1.1.0
|
||||
|
||||
has@1.0.4: {}
|
||||
|
||||
@@ -12703,7 +12707,7 @@ snapshots:
|
||||
|
||||
node-abi@3.51.0:
|
||||
dependencies:
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
|
||||
node-cache@5.1.2:
|
||||
dependencies:
|
||||
@@ -12806,7 +12810,7 @@ snapshots:
|
||||
call-bind: 1.0.8
|
||||
call-bound: 1.0.3
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.0.0
|
||||
es-object-atoms: 1.1.1
|
||||
has-symbols: 1.1.0
|
||||
object-keys: 1.1.1
|
||||
|
||||
@@ -13493,7 +13497,7 @@ snapshots:
|
||||
|
||||
resolve@1.19.0:
|
||||
dependencies:
|
||||
is-core-module: 2.13.1
|
||||
is-core-module: 2.16.1
|
||||
path-parse: 1.0.7
|
||||
|
||||
resolve@1.22.10:
|
||||
@@ -13663,8 +13667,8 @@ snapshots:
|
||||
define-data-property: 1.1.4
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
get-intrinsic: 1.2.4
|
||||
gopd: 1.0.1
|
||||
get-intrinsic: 1.2.6
|
||||
gopd: 1.2.0
|
||||
has-property-descriptors: 1.0.2
|
||||
|
||||
set-function-name@2.0.2:
|
||||
@@ -13758,7 +13762,7 @@ snapshots:
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
dependencies:
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
@@ -14329,7 +14333,7 @@ snapshots:
|
||||
bundle-require: 4.0.2(esbuild@0.19.12)
|
||||
cac: 6.7.14
|
||||
chokidar: 3.5.3
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
debug: 4.3.4
|
||||
esbuild: 0.19.12
|
||||
execa: 5.1.1
|
||||
globby: 11.1.0
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"@fastify/static": "^8.0.1",
|
||||
"@fastify/swagger": "^9.0.1",
|
||||
"@iptv/xmltv": "^1.0.1",
|
||||
"@scalar/fastify-api-reference": "^1.25.33",
|
||||
"@scalar/fastify-api-reference": "^1.25.106",
|
||||
"@tunarr/playlist": "^1.1.0",
|
||||
"@tunarr/shared": "workspace:*",
|
||||
"@tunarr/types": "workspace:*",
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import schedule from 'node-schedule';
|
||||
import path, { dirname } from 'path';
|
||||
import 'reflect-metadata';
|
||||
import { z } from 'zod';
|
||||
import { HdhrApiRouter } from './api/hdhrApi.js';
|
||||
import { apiRouter } from './api/index.js';
|
||||
import { streamApi } from './api/streamApi.js';
|
||||
@@ -76,7 +77,6 @@ export class Server {
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
const start = performance.now();
|
||||
this.logger.info(
|
||||
'Using Tunarr database directory: %s',
|
||||
this.serverOptions.databaseDirectory,
|
||||
@@ -178,6 +178,42 @@ export class Server {
|
||||
{
|
||||
name: 'Channels',
|
||||
},
|
||||
{
|
||||
name: 'Custom Shows',
|
||||
},
|
||||
{
|
||||
name: 'Filler Lists',
|
||||
},
|
||||
{
|
||||
name: 'Guide',
|
||||
},
|
||||
{
|
||||
name: 'Media Source',
|
||||
},
|
||||
{
|
||||
name: 'Programs',
|
||||
},
|
||||
{
|
||||
name: 'Sessions',
|
||||
},
|
||||
{
|
||||
name: 'Streaming',
|
||||
},
|
||||
{
|
||||
name: 'HDHR',
|
||||
},
|
||||
{
|
||||
name: 'Settings',
|
||||
},
|
||||
{
|
||||
name: 'System',
|
||||
},
|
||||
{
|
||||
name: 'Tasks',
|
||||
},
|
||||
{
|
||||
name: 'Debug',
|
||||
},
|
||||
],
|
||||
},
|
||||
transform: jsonSchemaTransform,
|
||||
@@ -189,6 +225,16 @@ export class Server {
|
||||
// ? join(dirname(process.argv[1]), 'static')
|
||||
// : undefined,
|
||||
// })
|
||||
// Hitting api docs on local instances of Tunarr is blocked on
|
||||
// https://github.com/scalar/scalar/pull/4528
|
||||
// .register(fastifyApiReference, {
|
||||
// routePrefix: '/docs',
|
||||
// configuration: {
|
||||
// spec: {
|
||||
// content: () => this.app.swagger(),
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
.register(cors, {
|
||||
origin: '*', // Testing
|
||||
})
|
||||
@@ -285,7 +331,7 @@ export class Server {
|
||||
done();
|
||||
})
|
||||
|
||||
.register(async (f) => {
|
||||
.register(async (f: ServerType) => {
|
||||
await f.register(fpStatic, {
|
||||
root: path.join(
|
||||
this.serverOptions.databaseDirectory,
|
||||
@@ -295,14 +341,18 @@ export class Server {
|
||||
decorateReply: false,
|
||||
serve: false, // Use the interceptor
|
||||
});
|
||||
f.get<{ Params: { hash: string } }>(
|
||||
f.get(
|
||||
'/cache/images/:hash',
|
||||
{
|
||||
schema: {
|
||||
hide: true,
|
||||
params: z.object({ hash: z.string() }),
|
||||
},
|
||||
// Workaround for https://github.com/fastify/fastify/issues/4859
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
onRequest: (req, res) => {
|
||||
return req.serverCtx.cacheImageService.routerInterceptor(
|
||||
req,
|
||||
req.params.hash,
|
||||
res,
|
||||
);
|
||||
},
|
||||
@@ -312,15 +362,24 @@ export class Server {
|
||||
},
|
||||
);
|
||||
|
||||
f.delete('/api/cache/images', async (req, res) => {
|
||||
try {
|
||||
await req.serverCtx.cacheImageService.clearCache();
|
||||
return res.status(200).send({ msg: 'Cache Image are Cleared' });
|
||||
} catch (error) {
|
||||
this.logger.error('Error deleting cached images', error);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
});
|
||||
f.delete(
|
||||
'/api/cache/images',
|
||||
{
|
||||
schema: {
|
||||
// TODO: Expose and add button to UI
|
||||
hide: true,
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
try {
|
||||
await req.serverCtx.cacheImageService.clearCache();
|
||||
return res.status(200).send({ msg: 'Cache Image are Cleared' });
|
||||
} catch (error) {
|
||||
this.logger.error('Error deleting cached images', error);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.register(async (f) => {
|
||||
f.addHook('onError', (req, _, error, done) => {
|
||||
@@ -328,7 +387,9 @@ export class Server {
|
||||
done();
|
||||
});
|
||||
await f
|
||||
.get('/', async (_, res) => res.redirect('/web', 302))
|
||||
.get('/', { schema: { hide: true } }, async (_, res) =>
|
||||
res.redirect('/web', 302),
|
||||
)
|
||||
.register(new HdhrApiRouter().router)
|
||||
.register(apiRouter, { prefix: '/api' });
|
||||
})
|
||||
@@ -368,8 +429,6 @@ export class Server {
|
||||
|
||||
await updateXMLPromise;
|
||||
|
||||
const host = process.env['TUNARR_BIND_ADDR'] ?? '0.0.0.0';
|
||||
|
||||
this.app.after(() => {
|
||||
this.app.gracefulShutdown(async (signal) => {
|
||||
this.logger.info(
|
||||
@@ -439,8 +498,15 @@ export class Server {
|
||||
this.logger.debug('All done, shutting down!');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const url = await this.app.listen({
|
||||
async initAndRun() {
|
||||
const start = performance.now();
|
||||
await this.init();
|
||||
|
||||
const host = process.env['TUNARR_BIND_ADDR'] ?? '0.0.0.0';
|
||||
|
||||
await this.app.listen({
|
||||
host,
|
||||
port: this.serverOptions.port,
|
||||
});
|
||||
@@ -466,7 +532,16 @@ export class Server {
|
||||
},
|
||||
level: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
return { app: this.app, url };
|
||||
getOpenApiDocument() {
|
||||
return this.app.swagger();
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.app) {
|
||||
return this.app.close();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { scheduleTimeSlots } from '@tunarr/shared';
|
||||
import {
|
||||
BasicIdParamSchema,
|
||||
BasicPagingSchema,
|
||||
GetChannelProgrammingResponseSchema,
|
||||
TimeSlotScheduleSchema,
|
||||
UpdateChannelProgrammingRequestSchema,
|
||||
} from '@tunarr/types/api';
|
||||
@@ -59,7 +60,7 @@ export const channelsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/channels',
|
||||
{
|
||||
schema: {
|
||||
operationId: 'getChannelsV2',
|
||||
operationId: 'getChannels',
|
||||
tags: ['Channels'],
|
||||
response: {
|
||||
200: z.array(ChannelSchema),
|
||||
@@ -292,7 +293,7 @@ export const channelsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
querystring: BasicPagingSchema,
|
||||
tags: ['Channels'],
|
||||
response: {
|
||||
200: CondensedChannelProgrammingSchema,
|
||||
200: GetChannelProgrammingResponseSchema,
|
||||
404: z.object({ error: z.string() }),
|
||||
},
|
||||
},
|
||||
@@ -389,6 +390,8 @@ export const channelsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
{
|
||||
schema: {
|
||||
params: BasicIdParamSchema,
|
||||
operationId: 'GetChannelFallbacks',
|
||||
description: "Returns a channel's fallback programs.",
|
||||
tags: ['Channels'],
|
||||
querystring: ChannelLineupQuery,
|
||||
response: {
|
||||
|
||||
@@ -25,6 +25,7 @@ export const customShowsApiV2: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/custom-shows',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Custom Shows'],
|
||||
response: {
|
||||
200: z.array(CustomShowSchema),
|
||||
},
|
||||
@@ -48,6 +49,7 @@ export const customShowsApiV2: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/custom-shows/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Custom Shows'],
|
||||
params: IdPathParamSchema,
|
||||
response: {
|
||||
200: CustomShowSchema,
|
||||
@@ -77,6 +79,7 @@ export const customShowsApiV2: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/custom-shows/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Custom Shows'],
|
||||
params: IdPathParamSchema,
|
||||
body: UpdateCustomShowRequestSchema,
|
||||
response: {
|
||||
@@ -108,6 +111,7 @@ export const customShowsApiV2: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/custom-shows/:id/programs',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Custom Shows'],
|
||||
params: IdPathParamSchema,
|
||||
response: {
|
||||
200: z.array(CustomProgramSchema),
|
||||
@@ -126,6 +130,9 @@ export const customShowsApiV2: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/custom-shows',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Custom Shows'],
|
||||
operationId: 'createCustomShow',
|
||||
description: 'Creates a new Custom Show',
|
||||
body: CreateCustomShowRequestSchema,
|
||||
response: {
|
||||
201: z.object({ id: z.string() }),
|
||||
@@ -143,6 +150,9 @@ export const customShowsApiV2: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/custom-shows/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Custom Shows'],
|
||||
operationId: 'deleteCustomShow',
|
||||
description: 'Delets a custom show with the given ID',
|
||||
params: IdPathParamSchema,
|
||||
response: {
|
||||
200: z.object({ id: z.string() }),
|
||||
|
||||
@@ -16,6 +16,7 @@ export const debugFfmpegApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/ffmpeg/probe',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
@@ -34,6 +35,7 @@ export const debugFfmpegApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/ffmpeg/pipeline',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z.object({
|
||||
channel: z.coerce.number().or(z.string()),
|
||||
path: z.string(),
|
||||
|
||||
@@ -14,6 +14,7 @@ export const DebugJellyfinApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/jellyfin/libraries',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z.object({
|
||||
userId: z.string(),
|
||||
uri: z.string().url(),
|
||||
@@ -35,6 +36,7 @@ export const DebugJellyfinApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/jellyfin/library/items',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z
|
||||
.object({
|
||||
uri: z.string().url(),
|
||||
@@ -69,6 +71,7 @@ export const DebugJellyfinApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/jellyfin/match_program/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
|
||||
@@ -12,6 +12,7 @@ export const DebugPlexApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/plex/stream_details',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z.object({
|
||||
key: z.string(),
|
||||
mediaSource: z.string(),
|
||||
|
||||
@@ -34,6 +34,7 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/streams/offline',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z.object({
|
||||
duration: z.coerce.number().default(30_000),
|
||||
useNewPipeline: TruthyQueryParam.optional(),
|
||||
@@ -91,6 +92,7 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/streams/error',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z.object({
|
||||
useNewPipeline: TruthyQueryParam.optional(),
|
||||
}),
|
||||
@@ -191,6 +193,7 @@ export const debugStreamApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/streams/programs/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
|
||||
@@ -26,11 +26,9 @@ import { debugFfmpegApiRouter } from './debug/debugFfmpegApi.ts';
|
||||
import { DebugJellyfinApiRouter } from './debug/debugJellyfinApi.js';
|
||||
import { debugStreamApiRouter } from './debug/debugStreamApi.js';
|
||||
|
||||
const ChannelQuerySchema = {
|
||||
querystring: z.object({
|
||||
channelId: z.string(),
|
||||
}),
|
||||
};
|
||||
const ChannelQuerySchema = z.object({
|
||||
channelId: z.string(),
|
||||
});
|
||||
|
||||
export const debugApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
await fastify
|
||||
@@ -50,7 +48,10 @@ export const debugApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
fastify.get(
|
||||
'/debug/helpers/current_program',
|
||||
{
|
||||
schema: ChannelQuerySchema,
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: ChannelQuerySchema,
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
const channel = await req.serverCtx.channelDB.getChannelAndPrograms(
|
||||
@@ -78,17 +79,20 @@ export const debugApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
},
|
||||
);
|
||||
|
||||
const CreateLineupSchema = {
|
||||
querystring: ChannelQuerySchema.querystring.extend({
|
||||
live: z.coerce.boolean(),
|
||||
startTime: z.coerce.number().optional(),
|
||||
endTime: z.coerce.number().optional(),
|
||||
}),
|
||||
};
|
||||
const CreateLineupSchema = ChannelQuerySchema.extend({
|
||||
live: z.coerce.boolean(),
|
||||
startTime: z.coerce.number().optional(),
|
||||
endTime: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
fastify.get(
|
||||
'/debug/helpers/create_guide',
|
||||
{ schema: CreateLineupSchema },
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: CreateLineupSchema,
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
const channel = await req.serverCtx.channelDB.getChannelAndPrograms(
|
||||
req.query.channelId,
|
||||
@@ -123,6 +127,7 @@ export const debugApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/debug/helpers/channels/:id/build_guide',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
@@ -197,16 +202,17 @@ export const debugApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
},
|
||||
);
|
||||
|
||||
const RandomFillerSchema = {
|
||||
querystring: CreateLineupSchema.querystring.extend({
|
||||
maxDuration: z.coerce.number(),
|
||||
}),
|
||||
};
|
||||
const RandomFillerSchema = CreateLineupSchema.extend({
|
||||
maxDuration: z.coerce.number(),
|
||||
});
|
||||
|
||||
fastify.get(
|
||||
'/debug/helpers/random_filler',
|
||||
{
|
||||
schema: RandomFillerSchema,
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: RandomFillerSchema,
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
const channel = await req.serverCtx.channelDB.getChannel(
|
||||
@@ -227,24 +233,33 @@ export const debugApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get('/debug/db/backup', async (_, res) => {
|
||||
await container
|
||||
.get<ArchiveDatabaseBackupFactory>(ArchiveDatabaseBackupKey)({
|
||||
type: 'file',
|
||||
outputPath: os.tmpdir(),
|
||||
archiveFormat: 'tar',
|
||||
gzip: true,
|
||||
maxBackups: 3,
|
||||
})
|
||||
.backup();
|
||||
fastify.get(
|
||||
'/debug/db/backup',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
},
|
||||
},
|
||||
async (_, res) => {
|
||||
await container
|
||||
.get<ArchiveDatabaseBackupFactory>(ArchiveDatabaseBackupKey)({
|
||||
type: 'file',
|
||||
outputPath: os.tmpdir(),
|
||||
archiveFormat: 'tar',
|
||||
gzip: true,
|
||||
maxBackups: 3,
|
||||
})
|
||||
.backup();
|
||||
|
||||
return res.send();
|
||||
});
|
||||
return res.send();
|
||||
},
|
||||
);
|
||||
|
||||
fastify.post(
|
||||
'/debug/plex/:programId/update_external_ids',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
params: z.object({
|
||||
programId: z.string(),
|
||||
}),
|
||||
@@ -268,6 +283,7 @@ export const debugApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/debug/helpers/promote_lineup',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z.object({
|
||||
channelId: z.string().uuid(),
|
||||
}),
|
||||
@@ -284,15 +300,24 @@ export const debugApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get('/debug/channels/reload_all_lineups', async (req, res) => {
|
||||
await req.serverCtx.channelDB.loadAllLineupConfigs(true);
|
||||
return res.send();
|
||||
});
|
||||
fastify.get(
|
||||
'/debug/channels/reload_all_lineups',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
await req.serverCtx.channelDB.loadAllLineupConfigs(true);
|
||||
return res.send();
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/debug/db/test_direct_access',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
querystring: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
|
||||
@@ -24,20 +24,33 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
className: 'FfmpegSettingsApi',
|
||||
});
|
||||
|
||||
fastify.get('/ffmpeg-settings', (req, res) => {
|
||||
try {
|
||||
const ffmpeg = req.serverCtx.settings.ffmpegSettings();
|
||||
return res.send(ffmpeg);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
});
|
||||
fastify.get(
|
||||
'/ffmpeg-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: FfmpegSettingsSchema,
|
||||
500: z.literal('error'),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
try {
|
||||
const ffmpeg = req.serverCtx.settings.ffmpegSettings();
|
||||
return res.send(makeWritable(ffmpeg));
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
fastify.put(
|
||||
'/ffmpeg-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
body: FfmpegSettingsSchema,
|
||||
response: {
|
||||
200: FfmpegSettingsSchema,
|
||||
@@ -98,8 +111,20 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
},
|
||||
);
|
||||
|
||||
fastify.post<{ Body: { ffmpegPath: string } }>(
|
||||
fastify.post(
|
||||
'/ffmpeg-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
body: z.object({
|
||||
ffmpegPath: z.string(),
|
||||
}),
|
||||
repsonse: {
|
||||
200: FfmpegSettingsSchema,
|
||||
500: z.literal('error'),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
// RESET
|
||||
try {
|
||||
@@ -137,6 +162,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
'/transcode_configs',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: z.array(TranscodeConfigSchema),
|
||||
},
|
||||
@@ -153,6 +179,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
'/transcode_configs/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
@@ -178,6 +205,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
'/transcode_configs',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
body: TranscodeConfigSchema.omit({
|
||||
id: true,
|
||||
}),
|
||||
@@ -198,6 +226,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
'/transcode_configs/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
body: TranscodeConfigSchema,
|
||||
params: IdPathParamSchema,
|
||||
response: {
|
||||
@@ -218,6 +247,7 @@ export const ffmpegSettingsRouter: RouterPluginCallback = (
|
||||
'/transcode_configs/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
params: IdPathParamSchema,
|
||||
response: {
|
||||
200: z.void(),
|
||||
|
||||
@@ -21,6 +21,7 @@ export const fillerListsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/filler-lists',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Filler Lists'],
|
||||
response: {
|
||||
200: z.array(FillerListSchema),
|
||||
},
|
||||
@@ -43,6 +44,7 @@ export const fillerListsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/filler-lists/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Filler Lists'],
|
||||
params: z.object({ id: fillerShowIdSchema }),
|
||||
response: {
|
||||
200: FillerListSchema,
|
||||
@@ -68,6 +70,7 @@ export const fillerListsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/filler-lists/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Filler Lists'],
|
||||
params: z.object({ id: fillerShowIdSchema }),
|
||||
response: {
|
||||
200: z.void(),
|
||||
@@ -89,6 +92,7 @@ export const fillerListsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/filler-lists',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Filler Lists'],
|
||||
body: CreateFillerListRequestSchema,
|
||||
response: {
|
||||
201: z.object({ id: z.string() }),
|
||||
@@ -105,6 +109,7 @@ export const fillerListsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/filler-lists/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Filler Lists'],
|
||||
params: z.object({
|
||||
id: fillerShowIdSchema,
|
||||
}),
|
||||
@@ -124,8 +129,6 @@ export const fillerListsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
return res.status(404).send();
|
||||
}
|
||||
|
||||
console.log('sending response');
|
||||
|
||||
return res.send({
|
||||
id: result.uuid,
|
||||
name: result.name,
|
||||
@@ -138,6 +141,7 @@ export const fillerListsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/filler-lists/:id/programs',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Filler Lists'],
|
||||
params: IdPathParamSchema.extend({ id: fillerShowIdSchema }),
|
||||
response: {
|
||||
200: FillerListProgrammingSchema,
|
||||
|
||||
@@ -12,30 +12,47 @@ export const guideRouter: RouterPluginCallback = (fastify, _opts, done) => {
|
||||
className: 'GuideApi',
|
||||
});
|
||||
|
||||
fastify.get('/guide/status', async (req, res) => {
|
||||
try {
|
||||
const s = await req.serverCtx.guideService.getStatus();
|
||||
return res.send(s);
|
||||
} catch (err) {
|
||||
logger.error('%s, %O', req.routeOptions.url, err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
});
|
||||
fastify.get(
|
||||
'/guide/status',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Guide'],
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
try {
|
||||
const s = await req.serverCtx.guideService.getStatus();
|
||||
return res.send(s);
|
||||
} catch (err) {
|
||||
logger.error('%s, %O', req.routeOptions.url, err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get('/guide/debug', async (req, res) => {
|
||||
try {
|
||||
const s = await req.serverCtx.guideService.get();
|
||||
return res.send(s);
|
||||
} catch (err) {
|
||||
logger.error('%s, %O', req.routeOptions.url, err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
});
|
||||
fastify.get(
|
||||
'/guide/debug',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Debug'],
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
try {
|
||||
const s = await req.serverCtx.guideService.get();
|
||||
return res.send(s);
|
||||
} catch (err) {
|
||||
logger.error('%s, %O', req.routeOptions.url, err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/guide/channels',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Guide'],
|
||||
querystring: z.object({
|
||||
dateFrom: z.coerce.date(),
|
||||
dateTo: z.coerce.date(),
|
||||
@@ -61,29 +78,41 @@ export const guideRouter: RouterPluginCallback = (fastify, _opts, done) => {
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get<{
|
||||
Params: { id: string };
|
||||
Querystring: { dateFrom: string; dateTo: string };
|
||||
}>('/guide/channels/:number', async (req, res) => {
|
||||
try {
|
||||
// TODO determine if these params are numbers or strings
|
||||
const dateFrom = new Date(req.query.dateFrom);
|
||||
const dateTo = new Date(req.query.dateTo);
|
||||
const lineup = await req.serverCtx.guideService.getChannelLineup(
|
||||
req.params.id,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
);
|
||||
if (lineup == null) {
|
||||
return res.status(404).send('Channel not found in TV guide');
|
||||
} else {
|
||||
return res.send(lineup);
|
||||
fastify.get(
|
||||
'/guide/channels/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Guide'],
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
querystring: z.object({
|
||||
dateFrom: z.string().pipe(z.date()),
|
||||
dateTo: z.string().pipe(z.date()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
try {
|
||||
// TODO determine if these params are numbers or strings
|
||||
const dateFrom = req.query.dateFrom;
|
||||
const dateTo = req.query.dateTo;
|
||||
const lineup = await req.serverCtx.guideService.getChannelLineup(
|
||||
req.params.id,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
);
|
||||
if (lineup == null) {
|
||||
return res.status(404).send('Channel not found in TV guide');
|
||||
} else {
|
||||
return res.send(lineup);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('%s, %O', req.routeOptions.url, err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('%s, %O', req.routeOptions.url, err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
@@ -28,6 +28,10 @@ export class HdhrApiRouter {
|
||||
routeOpts.config = {};
|
||||
}
|
||||
routeOpts.config.disableRequestLogging = true;
|
||||
if (!routeOpts.schema) {
|
||||
routeOpts.schema = {};
|
||||
}
|
||||
routeOpts.schema.hide = true;
|
||||
});
|
||||
|
||||
fastify.get('/device.xml', (req, res) => {
|
||||
@@ -37,27 +41,44 @@ export class HdhrApiRouter {
|
||||
.send(req.serverCtx.hdhrService.getHdhrDeviceXml(host));
|
||||
});
|
||||
|
||||
fastify.get('/discover.json', (req, res) => {
|
||||
return res.send(
|
||||
req.serverCtx.hdhrService.getHdhrDevice(
|
||||
req.protocol + '://' + req.host,
|
||||
),
|
||||
);
|
||||
});
|
||||
fastify.get(
|
||||
'/discover.json',
|
||||
{
|
||||
schema: {
|
||||
tags: ['HDHR'],
|
||||
},
|
||||
},
|
||||
(req, res) => {
|
||||
return res.send(
|
||||
req.serverCtx.hdhrService.getHdhrDevice(
|
||||
req.protocol + '://' + req.host,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get('/lineup_status.json', (_, res) => {
|
||||
return res.send({
|
||||
ScanInProgress: 0,
|
||||
ScanPossible: 1,
|
||||
Source: 'Cable',
|
||||
SourceList: ['Cable'],
|
||||
});
|
||||
});
|
||||
fastify.get(
|
||||
'/lineup_status.json',
|
||||
{
|
||||
schema: {
|
||||
tags: ['HDHR'],
|
||||
},
|
||||
},
|
||||
(_, res) => {
|
||||
return res.send({
|
||||
ScanInProgress: 0,
|
||||
ScanPossible: 1,
|
||||
Source: 'Cable',
|
||||
SourceList: ['Cable'],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/lineup.json',
|
||||
{
|
||||
schema: {
|
||||
tags: ['HDHR'],
|
||||
response: {
|
||||
200: z.array(HdhrLineupSchema),
|
||||
},
|
||||
|
||||
@@ -20,6 +20,7 @@ export const hdhrSettingsRouter: RouterPluginCallback = (
|
||||
'/hdhr-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: HdhrSettingsSchema,
|
||||
500: BaseErrorSchema,
|
||||
@@ -41,6 +42,7 @@ export const hdhrSettingsRouter: RouterPluginCallback = (
|
||||
'/hdhr-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
body: HdhrSettingsSchema,
|
||||
response: {
|
||||
200: HdhrSettingsSchema,
|
||||
@@ -83,6 +85,7 @@ export const hdhrSettingsRouter: RouterPluginCallback = (
|
||||
'/hdhr-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: HdhrSettingsSchema,
|
||||
500: BaseErrorSchema,
|
||||
|
||||
@@ -72,6 +72,7 @@ export const apiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/version',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System'],
|
||||
response: {
|
||||
200: VersionApiResponseSchema,
|
||||
500: z.void(),
|
||||
@@ -99,25 +100,33 @@ export const apiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get('/ffmpeg-info', async (req, res) => {
|
||||
const info = new FfmpegInfo(req.serverCtx.settings);
|
||||
const [audioEncoders, videoEncoders] = await Promise.all([
|
||||
run(async () => {
|
||||
const res = await info.getAvailableAudioEncoders();
|
||||
return isError(res) ? [] : res;
|
||||
}),
|
||||
run(async () => {
|
||||
const res = await info.getAvailableVideoEncoders();
|
||||
return isError(res) ? [] : res;
|
||||
}),
|
||||
]);
|
||||
const hwAccels = await info.getHwAccels();
|
||||
return res.send({
|
||||
audioEncoders,
|
||||
videoEncoders,
|
||||
hardwareAccelerationTypes: hwAccels,
|
||||
});
|
||||
});
|
||||
fastify.get(
|
||||
'/ffmpeg-info',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System'],
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
const info = new FfmpegInfo(req.serverCtx.settings);
|
||||
const [audioEncoders, videoEncoders] = await Promise.all([
|
||||
run(async () => {
|
||||
const res = await info.getAvailableAudioEncoders();
|
||||
return isError(res) ? [] : res;
|
||||
}),
|
||||
run(async () => {
|
||||
const res = await info.getAvailableVideoEncoders();
|
||||
return isError(res) ? [] : res;
|
||||
}),
|
||||
]);
|
||||
const hwAccels = await info.getHwAccels();
|
||||
return res.send({
|
||||
audioEncoders,
|
||||
videoEncoders,
|
||||
hardwareAccelerationTypes: hwAccels,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
fastify.post('/upload/image', async (req, res) => {
|
||||
try {
|
||||
@@ -189,6 +198,9 @@ export const apiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
fastify.route({
|
||||
url: '/xmltv.xml',
|
||||
method: ['HEAD', 'GET'],
|
||||
schema: {
|
||||
tags: ['Streaming'],
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
const host = `${req.protocol}://${req.host}`;
|
||||
@@ -219,6 +231,9 @@ export const apiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
// CHANNELS.M3U Download
|
||||
fastify.route({
|
||||
url: '/channels.m3u',
|
||||
schema: {
|
||||
tags: ['Streaming'],
|
||||
},
|
||||
method: ['HEAD', 'GET'],
|
||||
handler: async (req, res) => {
|
||||
try {
|
||||
@@ -233,15 +248,28 @@ export const apiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
},
|
||||
});
|
||||
|
||||
fastify.delete('/channels.m3u', async (req, res) => {
|
||||
await req.serverCtx.m3uService.regenerateCache();
|
||||
return res.send(204);
|
||||
});
|
||||
fastify.delete(
|
||||
'/channels.m3u',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Streaming'],
|
||||
description: 'Clears the channels m3u cache',
|
||||
response: {
|
||||
204: z.void(),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
await req.serverCtx.m3uService.regenerateCache();
|
||||
return res.status(204).send();
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/plex',
|
||||
{
|
||||
schema: {
|
||||
hide: true,
|
||||
querystring: z.object({ id: z.string(), path: z.string() }),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -43,6 +43,14 @@ function isNonEmptyTyped<T>(f: T[]): f is [T, ...T[]] {
|
||||
}
|
||||
|
||||
export const jellyfinApiRouter: RouterPluginCallback = (fastify, _, done) => {
|
||||
fastify.addHook('onRoute', (routeOptions) => {
|
||||
if (!routeOptions.schema) {
|
||||
routeOptions.schema = {};
|
||||
}
|
||||
|
||||
routeOptions.schema.hide = true;
|
||||
});
|
||||
|
||||
fastify.post(
|
||||
'/jellyfin/login',
|
||||
{
|
||||
@@ -80,7 +88,7 @@ export const jellyfinApiRouter: RouterPluginCallback = (fastify, _, done) => {
|
||||
const response = await api.getUserViews();
|
||||
|
||||
if (isQueryError(response)) {
|
||||
throw response;
|
||||
throw new Error(response.message);
|
||||
}
|
||||
|
||||
const sanitizedResponse: JellyfinLibraryItemsResponseTyp = {
|
||||
@@ -180,7 +188,7 @@ export const jellyfinApiRouter: RouterPluginCallback = (fastify, _, done) => {
|
||||
);
|
||||
|
||||
if (isQueryError(response)) {
|
||||
throw response;
|
||||
throw new Error(response.message);
|
||||
}
|
||||
|
||||
return res.send(response.data);
|
||||
|
||||
@@ -32,6 +32,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
|
||||
'/media-sources',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Media Source'],
|
||||
response: {
|
||||
200: z.array(MediaSourceSettingsSchema),
|
||||
500: z.string(),
|
||||
@@ -70,6 +71,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
|
||||
'/media-sources/:id/status',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Media Source'],
|
||||
params: BasicIdParamSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
@@ -129,6 +131,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
|
||||
'/media-sources/foreignstatus',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Media Source'],
|
||||
body: z.object({
|
||||
name: z.string().optional(),
|
||||
accessToken: z.string(),
|
||||
@@ -189,6 +192,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
|
||||
'/media-sources/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Media Source'],
|
||||
params: BasicIdParamSchema,
|
||||
response: {
|
||||
200: z.void(),
|
||||
@@ -247,6 +251,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
|
||||
'/media-sources/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Media Source'],
|
||||
params: BasicIdParamSchema,
|
||||
body: UpdateMediaSourceRequestSchema,
|
||||
response: {
|
||||
@@ -300,6 +305,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
|
||||
'/media-sources',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Media Source'],
|
||||
body: InsertMediaSourceRequestSchema,
|
||||
response: {
|
||||
201: z.object({
|
||||
@@ -349,6 +355,7 @@ export const mediaSourceRouter: RouterPluginAsyncCallback = async (
|
||||
'/plex/status',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Media Source'],
|
||||
querystring: z.object({
|
||||
serverName: z.string(),
|
||||
}),
|
||||
|
||||
@@ -80,6 +80,7 @@ export const metadataApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/metadata/external',
|
||||
{
|
||||
schema: {
|
||||
hide: true,
|
||||
querystring: ExternalMetadataQuerySchema,
|
||||
},
|
||||
config: {
|
||||
|
||||
@@ -20,6 +20,7 @@ export const plexSettingsRouter: RouterPluginCallback = (
|
||||
'/plex-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: PlexStreamSettingsSchema,
|
||||
500: z.string(),
|
||||
@@ -43,6 +44,7 @@ export const plexSettingsRouter: RouterPluginCallback = (
|
||||
'/plex-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
body: PlexStreamSettingsSchema,
|
||||
response: {
|
||||
200: PlexStreamSettingsSchema,
|
||||
@@ -85,6 +87,7 @@ export const plexSettingsRouter: RouterPluginCallback = (
|
||||
'/plex-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: PlexStreamSettingsSchema,
|
||||
500: z.string(),
|
||||
|
||||
@@ -71,6 +71,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/programs/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Programs'],
|
||||
params: BasicIdParamSchema,
|
||||
},
|
||||
},
|
||||
@@ -98,6 +99,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/programs/:id/thumb',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Programs'],
|
||||
params: BasicIdParamSchema,
|
||||
querystring: z.object({
|
||||
width: z.number().optional(),
|
||||
@@ -316,6 +318,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/programs/:id/external-link',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Programs'],
|
||||
params: BasicIdParamSchema,
|
||||
querystring: z.object({
|
||||
forward: z.coerce.boolean().default(true),
|
||||
@@ -392,6 +395,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/programming/:externalId',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Programs'],
|
||||
operationId: 'getProgramByExternalId',
|
||||
params: LookupExternalProgrammingSchema,
|
||||
response: {
|
||||
@@ -427,6 +431,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/programming/batch/lookup',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Programs'],
|
||||
operationId: 'batchGetProgramsByExternalIds',
|
||||
body: BatchLookupExternalProgrammingSchema,
|
||||
response: {
|
||||
@@ -445,6 +450,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/programming/shows/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Programs'],
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
@@ -504,6 +510,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/programming/seasons/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Programs'],
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
@@ -536,6 +543,7 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/programming/shows/:id/seasons',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Programs'],
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
|
||||
@@ -15,6 +15,7 @@ export const sessionApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/sessions',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Sessions'],
|
||||
response: {
|
||||
200: z.record(z.array(ChannelSessionsResponseSchema)),
|
||||
},
|
||||
@@ -52,6 +53,7 @@ export const sessionApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/channels/:id/sessions',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Sessions'],
|
||||
params: z.object({
|
||||
id: z.coerce.number().or(z.string().uuid()),
|
||||
}),
|
||||
@@ -103,6 +105,7 @@ export const sessionApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/channels/:id/sessions',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Sessions'],
|
||||
params: z.object({
|
||||
id: z.coerce.number().or(z.string().uuid()),
|
||||
}),
|
||||
|
||||
@@ -41,6 +41,7 @@ export const streamApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/stream/channels/:id',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Streaming'],
|
||||
params: z.object({
|
||||
id: z.coerce.number().or(z.string()),
|
||||
}),
|
||||
@@ -88,6 +89,9 @@ export const streamApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/stream/channels/:id.ts',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Streaming'],
|
||||
description:
|
||||
'Returns a continuous, direct MPEGTS video stream for the given channel',
|
||||
params: z.object({
|
||||
id: z.coerce.number().or(z.string()),
|
||||
}),
|
||||
@@ -198,6 +202,7 @@ export const streamApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/stream/channels/:id/radio.ts',
|
||||
{
|
||||
schema: {
|
||||
hide: true,
|
||||
params: z.object({
|
||||
id: z.coerce.number().or(z.string().uuid()),
|
||||
}),
|
||||
@@ -267,6 +272,7 @@ export const streamApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/stream/channels/:id.m3u8',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Streaming'],
|
||||
params: z.object({
|
||||
id: z.string().uuid().or(z.coerce.number()),
|
||||
}),
|
||||
|
||||
@@ -20,15 +20,24 @@ export const systemApiRouter: RouterPluginAsyncCallback = async (
|
||||
fastify,
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
) => {
|
||||
fastify.get('/system/health', async (req, res) => {
|
||||
const results = await req.serverCtx.healthCheckService.runAll();
|
||||
return res.send(results);
|
||||
});
|
||||
fastify.get(
|
||||
'/system/health',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System'],
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
const results = await req.serverCtx.healthCheckService.runAll();
|
||||
return res.send(results);
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get(
|
||||
'/system/settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System', 'Settings'],
|
||||
response: {
|
||||
200: SystemSettingsResponseSchema,
|
||||
},
|
||||
@@ -40,14 +49,23 @@ export const systemApiRouter: RouterPluginAsyncCallback = async (
|
||||
},
|
||||
);
|
||||
|
||||
fastify.get('/system/state', async (req, res) => {
|
||||
return res.send(req.serverCtx.settings.migrationState);
|
||||
});
|
||||
fastify.get(
|
||||
'/system/state',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System'],
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
return res.send(req.serverCtx.settings.migrationState);
|
||||
},
|
||||
);
|
||||
|
||||
fastify.post(
|
||||
'/system/fixers/:fixerId/run',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System'],
|
||||
params: z.object({
|
||||
fixerId: z.string(),
|
||||
}),
|
||||
@@ -78,6 +96,7 @@ export const systemApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/system/settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System', 'Settings'],
|
||||
body: UpdateSystemSettingsRequestSchema,
|
||||
response: {
|
||||
200: SystemSettingsResponseSchema,
|
||||
@@ -118,6 +137,7 @@ export const systemApiRouter: RouterPluginAsyncCallback = async (
|
||||
'/system/settings/backup',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System', 'Settings'],
|
||||
body: UpdateBackupSettingsRequestSchema,
|
||||
response: {
|
||||
200: BackupSettingsSchema,
|
||||
|
||||
@@ -18,6 +18,7 @@ export const tasksApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/jobs',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System', 'Tasks'],
|
||||
response: {
|
||||
200: z.array(TaskSchema),
|
||||
},
|
||||
@@ -61,6 +62,7 @@ export const tasksApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/jobs/:id/run',
|
||||
{
|
||||
schema: {
|
||||
tags: ['System', 'Tasks'],
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
background: z.boolean().default(true),
|
||||
|
||||
@@ -3,7 +3,6 @@ import { FfmpegText } from '@/ffmpeg/ffmpegText.js';
|
||||
import { VideoStream } from '@/stream/VideoStream.js';
|
||||
import { TruthyQueryParam } from '@/types/schemas.js';
|
||||
import { RouterPluginAsyncCallback } from '@/types/serverType.js';
|
||||
import { isProduction } from '@/util/index.js';
|
||||
import { LoggerFactory } from '@/util/logging/LoggerFactory.js';
|
||||
import { makeLocalUrl } from '@/util/serverUtil.js';
|
||||
import { ChannelStreamModeSchema } from '@tunarr/types/schemas';
|
||||
@@ -29,55 +28,61 @@ export const videoApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
className: 'VideoApi',
|
||||
});
|
||||
|
||||
fastify.get('/setup', async (req, res) => {
|
||||
const ffmpegSettings = req.serverCtx.settings.ffmpegSettings();
|
||||
// Check if ffmpeg path is valid
|
||||
if (!fsSync.existsSync(ffmpegSettings.ffmpegExecutablePath)) {
|
||||
logger.error(
|
||||
`FFMPEG path (${ffmpegSettings.ffmpegExecutablePath}) is invalid. The file (executable) doesn't exist.`,
|
||||
);
|
||||
|
||||
return res
|
||||
.status(500)
|
||||
.send(
|
||||
fastify.get(
|
||||
'/setup',
|
||||
{
|
||||
schema: { hide: true },
|
||||
},
|
||||
async (req, res) => {
|
||||
const ffmpegSettings = req.serverCtx.settings.ffmpegSettings();
|
||||
// Check if ffmpeg path is valid
|
||||
if (!fsSync.existsSync(ffmpegSettings.ffmpegExecutablePath)) {
|
||||
logger.error(
|
||||
`FFMPEG path (${ffmpegSettings.ffmpegExecutablePath}) is invalid. The file (executable) doesn't exist.`,
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`\r\nStream starting. Channel: 1 (Tunarr)`);
|
||||
return res
|
||||
.status(500)
|
||||
.send(
|
||||
`FFMPEG path (${ffmpegSettings.ffmpegExecutablePath}) is invalid. The file (executable) doesn't exist.`,
|
||||
);
|
||||
}
|
||||
|
||||
const ffmpeg = new FfmpegText(
|
||||
ffmpegSettings,
|
||||
'Tunarr (No Channels Configured)',
|
||||
'Configure your channels using the Tunarr Web UI',
|
||||
);
|
||||
logger.info(`\r\nStream starting. Channel: 1 (Tunarr)`);
|
||||
|
||||
const buffer = new Readable();
|
||||
buffer._read = () => {};
|
||||
const ffmpeg = new FfmpegText(
|
||||
ffmpegSettings,
|
||||
'Tunarr (No Channels Configured)',
|
||||
'Configure your channels using the Tunarr Web UI',
|
||||
);
|
||||
|
||||
ffmpeg.on('data', (data) => {
|
||||
buffer.push(data);
|
||||
});
|
||||
const buffer = new Readable();
|
||||
buffer._read = () => {};
|
||||
|
||||
ffmpeg.on('error', (err) => {
|
||||
logger.error('FFMPEG ERROR', err);
|
||||
buffer.push(null);
|
||||
void res.status(500).send('FFMPEG ERROR');
|
||||
return;
|
||||
});
|
||||
ffmpeg.on('data', (data) => {
|
||||
buffer.push(data);
|
||||
});
|
||||
|
||||
ffmpeg.on('close', () => {
|
||||
buffer.push(null);
|
||||
});
|
||||
ffmpeg.on('error', (err) => {
|
||||
logger.error('FFMPEG ERROR', err);
|
||||
buffer.push(null);
|
||||
void res.status(500).send('FFMPEG ERROR');
|
||||
return;
|
||||
});
|
||||
|
||||
res.raw.on('close', () => {
|
||||
// on HTTP close, kill ffmpeg
|
||||
ffmpeg.kill();
|
||||
logger.info(`\r\nStream ended. Channel: 1 (Tunarr)`);
|
||||
});
|
||||
ffmpeg.on('close', () => {
|
||||
buffer.push(null);
|
||||
});
|
||||
|
||||
return res.send(buffer);
|
||||
});
|
||||
res.raw.on('close', () => {
|
||||
// on HTTP close, kill ffmpeg
|
||||
ffmpeg.kill();
|
||||
logger.info(`\r\nStream ended. Channel: 1 (Tunarr)`);
|
||||
});
|
||||
|
||||
return res.send(buffer);
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Internal endpoint which returns the single, raw stream for a video
|
||||
@@ -87,7 +92,7 @@ export const videoApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/stream',
|
||||
{
|
||||
schema: {
|
||||
hide: isProduction,
|
||||
hide: true,
|
||||
querystring: z.object({
|
||||
channel: z.coerce.number().or(z.string().uuid()),
|
||||
audioOnly: TruthyQueryParam.catch(false),
|
||||
@@ -182,6 +187,9 @@ export const videoApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
'/ffmpeg/playlist',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Streaming'],
|
||||
description:
|
||||
'Return a playlist in ffconcat file format for the given channel',
|
||||
querystring: FfmpegPlaylistQuerySchema,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ export const xmlTvSettingsRouter: RouterPluginCallback = (
|
||||
'/xmltv-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: XmlTvSettingsSchema,
|
||||
500: z.string(),
|
||||
@@ -34,6 +35,7 @@ export const xmlTvSettingsRouter: RouterPluginCallback = (
|
||||
try {
|
||||
return res.send(req.serverCtx.settings.xmlTvSettings());
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
},
|
||||
@@ -43,6 +45,7 @@ export const xmlTvSettingsRouter: RouterPluginCallback = (
|
||||
'/xmltv-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: XmlTvSettingsSchema,
|
||||
500: BaseErrorSchema,
|
||||
@@ -94,6 +97,7 @@ export const xmlTvSettingsRouter: RouterPluginCallback = (
|
||||
'/xmltv-settings',
|
||||
{
|
||||
schema: {
|
||||
tags: ['Settings'],
|
||||
response: {
|
||||
200: XmlTvSettingsSchema,
|
||||
500: z.string(),
|
||||
|
||||
68
server/src/cli/GenerateOpenApiCommand.ts
Normal file
68
server/src/cli/GenerateOpenApiCommand.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { ArgumentsCamelCase, CommandModule } from 'yargs';
|
||||
import { container } from '../container.ts';
|
||||
import { setServerOptions } from '../globals.ts';
|
||||
import { Server } from '../Server.ts';
|
||||
import { TruthyQueryParam } from '../types/schemas.ts';
|
||||
import { isNonEmptyString, isProduction } from '../util/index.ts';
|
||||
import { getTunarrVersion } from '../util/version.ts';
|
||||
import { ServerArgsType } from './RunServerCommand.ts';
|
||||
import { GlobalArgsType } from './types.ts';
|
||||
|
||||
type GenerateOpenApiCommandArgs = ServerArgsType & {
|
||||
apiVersion: string;
|
||||
};
|
||||
|
||||
export const GenerateOpenApiCommand: CommandModule<
|
||||
GlobalArgsType,
|
||||
GenerateOpenApiCommandArgs
|
||||
> = {
|
||||
command: ['generate-openapi'],
|
||||
describe: 'Generates the OpenAPI JSON definition of the Tunarr API',
|
||||
builder: {
|
||||
port: {
|
||||
alias: 'p',
|
||||
type: 'number',
|
||||
desc: 'The port to run the Tunarr server on',
|
||||
default: 0,
|
||||
},
|
||||
printRoutes: {
|
||||
type: 'boolean',
|
||||
default: () =>
|
||||
TruthyQueryParam.catch(false).parse(
|
||||
process.env['TUNARR_SERVER_PRINT_ROUTES'],
|
||||
),
|
||||
},
|
||||
admin: {
|
||||
type: 'boolean',
|
||||
default: () => {
|
||||
if (isNonEmptyString(process.env['TUNARR_SERVER_ADMIN_MODE'])) {
|
||||
return TruthyQueryParam.catch(false).parse(
|
||||
process.env['TUNARR_SERVER_ADMIN_MODE'],
|
||||
);
|
||||
}
|
||||
return !isProduction;
|
||||
},
|
||||
},
|
||||
apiVersion: {
|
||||
type: 'string',
|
||||
default: 'latest',
|
||||
},
|
||||
},
|
||||
handler: async (args: ArgumentsCamelCase<GenerateOpenApiCommandArgs>) => {
|
||||
console.log('Generating OpenAPI doc for version ' + args.apiVersion);
|
||||
setServerOptions(args);
|
||||
const server = container.get<Server>(Server);
|
||||
await server.initAndRun();
|
||||
await server.close();
|
||||
const version = getTunarrVersion();
|
||||
const outputDir = path.resolve(process.cwd(), '..', 'docs', 'generated');
|
||||
const fileName = `tunarr-v${version}-openapi.json`;
|
||||
await fs.writeFile(
|
||||
path.join(outputDir, fileName),
|
||||
JSON.stringify(server.getOpenApiDocument()),
|
||||
);
|
||||
process.exit(0);
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GenerateOpenApiCommand } from './GenerateOpenApiCommand.ts';
|
||||
import { RunServerCommand } from './RunServerCommand.ts';
|
||||
import { databaseCommands } from './database/databaseCommands.ts';
|
||||
import { LegacyMigrateCommand } from './legacyMigrateCommand.ts';
|
||||
@@ -10,4 +11,5 @@ export const commands = [
|
||||
RunFixerCommand,
|
||||
RunServerCommand,
|
||||
databaseCommands,
|
||||
GenerateOpenApiCommand,
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CachedImage } from '@/db/schema/CachedImage.js';
|
||||
import { LoggerFactory } from '@/util/logging/LoggerFactory.js';
|
||||
import axios, { AxiosHeaders, AxiosRequestConfig } from 'axios';
|
||||
import crypto from 'crypto';
|
||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import { createWriteStream, promises as fs } from 'fs';
|
||||
import { injectable } from 'inversify';
|
||||
import { isString, isUndefined } from 'lodash-es';
|
||||
@@ -39,14 +39,11 @@ export class CacheImageService {
|
||||
* @returns
|
||||
* @memberof CacheImageService
|
||||
*/
|
||||
async routerInterceptor(
|
||||
req: FastifyRequest<{ Params: { hash: string } }>,
|
||||
res: FastifyReply,
|
||||
) {
|
||||
async routerInterceptor(hash: string, res: FastifyReply) {
|
||||
try {
|
||||
const imgItem = await getDatabase()
|
||||
.selectFrom('cachedImage')
|
||||
.where('hash', '=', req.params.hash)
|
||||
.where('hash', '=', hash)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
|
||||
@@ -60,6 +57,7 @@ export class CacheImageService {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return res.status(500).send('error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
CacheSettingsSchema,
|
||||
LogLevelsSchema,
|
||||
LoggingSettingsSchema,
|
||||
SystemSettingsSchema,
|
||||
} from '../SystemSettings.js';
|
||||
import { JellyfinItemFields, JellyfinItemKind } from '../jellyfin/index.js';
|
||||
import {
|
||||
ChannelConcatStreamModes,
|
||||
@@ -6,27 +12,26 @@ import {
|
||||
} from '../schemas/channelSchema.js';
|
||||
import {
|
||||
ChannelProgramSchema,
|
||||
CondensedChannelProgrammingSchema,
|
||||
CondensedContentProgramSchema,
|
||||
CondensedCustomProgramSchema,
|
||||
ContentProgramSchema,
|
||||
CustomProgramSchema,
|
||||
FlexProgramSchema,
|
||||
RedirectProgramSchema,
|
||||
} from '../schemas/programmingSchema.js';
|
||||
import {
|
||||
BackupSettingsSchema,
|
||||
JellyfinServerSettingsSchema,
|
||||
PlexServerSettingsSchema,
|
||||
} from '../schemas/settingsSchemas.js';
|
||||
import {
|
||||
CacheSettingsSchema,
|
||||
LoggingSettingsSchema,
|
||||
LogLevelsSchema,
|
||||
SystemSettingsSchema,
|
||||
} from '../SystemSettings.js';
|
||||
import {
|
||||
RandomSlotScheduleSchema,
|
||||
TimeSlotScheduleSchema,
|
||||
} from './Scheduling.js';
|
||||
|
||||
export * from './plexSearch.js';
|
||||
export * from './Scheduling.js';
|
||||
export * from './plexSearch.js';
|
||||
|
||||
export const IdPathParamSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -255,6 +260,25 @@ export type ChannelSessionsResponse = z.infer<
|
||||
typeof ChannelSessionsResponseSchema
|
||||
>;
|
||||
|
||||
const CondensedChannelProgramWithNoOriginalSchema = z.discriminatedUnion(
|
||||
'type',
|
||||
[
|
||||
CondensedContentProgramSchema,
|
||||
CondensedCustomProgramSchema.extend({
|
||||
program: CondensedContentProgramSchema.optional(),
|
||||
}),
|
||||
RedirectProgramSchema,
|
||||
FlexProgramSchema,
|
||||
],
|
||||
);
|
||||
|
||||
// This is sorta hacky.
|
||||
export const GetChannelProgrammingResponseSchema =
|
||||
CondensedChannelProgrammingSchema.extend({
|
||||
lineup: z.array(CondensedChannelProgramWithNoOriginalSchema),
|
||||
programs: z.record(ContentProgramSchema),
|
||||
});
|
||||
|
||||
export const JellyfinGetLibraryItemsQuerySchema = z.object({
|
||||
offset: z.coerce.number().nonnegative().optional(),
|
||||
limit: z.coerce.number().positive().optional(),
|
||||
|
||||
Reference in New Issue
Block a user