mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
Dynamic channels - phase 1 (#231)
* Checkpoint - dynamic channels * Yet another checkpoint * Bump versions - makes a ton of stuff work magically; needed a patch for ts-essentials because of https://github.com/ts-essentials/ts-essentials/issues/381 * Checkpointing * Checkpoint
This commit is contained in:
committed by
GitHub
parent
bf3f1a0781
commit
67f2d144e2
@@ -17,7 +17,12 @@
|
||||
"esbuild": "^0.19.5",
|
||||
"eslint": "^8.45.0",
|
||||
"turbo": "^1.13.0",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.4+sha256.cea6d0bdf2de3a0549582da3983c70c92ffc577ff4410cbf190817ddc35137c2"
|
||||
"packageManager": "pnpm@8.15.4+sha256.cea6d0bdf2de3a0549582da3983c70c92ffc577ff4410cbf190817ddc35137c2",
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"ts-essentials@9.4.1": "patches/ts-essentials@9.4.1.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
patches/ts-essentials@9.4.1.patch
Normal file
97
patches/ts-essentials@9.4.1.patch
Normal file
@@ -0,0 +1,97 @@
|
||||
diff --git a/CHANGELOG.md b/CHANGELOG.md
|
||||
deleted file mode 100644
|
||||
index 0dc097738ae073897eb86b1fbb5324d34b3d9813..0000000000000000000000000000000000000000
|
||||
diff --git a/dist/deep-nullable/index.d.ts b/dist/deep-nullable/index.d.ts
|
||||
index ba3852eaa05020cb79af42b530916625ab2efaa9..3f6b0e5cf7ef22e76cc7712b72271967ab7cfe1a 100644
|
||||
--- a/dist/deep-nullable/index.d.ts
|
||||
+++ b/dist/deep-nullable/index.d.ts
|
||||
@@ -1,7 +1,31 @@
|
||||
import { Builtin } from "../built-in";
|
||||
import { IsTuple } from "../is-tuple";
|
||||
-export declare type DeepNullable<Type> = Type extends Builtin ? Type | null : Type extends Map<infer Keys, infer Values> ? Map<DeepNullable<Keys>, DeepNullable<Values>> : Type extends ReadonlyMap<infer Keys, infer Values> ? ReadonlyMap<DeepNullable<Keys>, DeepNullable<Values>> : Type extends WeakMap<infer Keys, infer Values> ? WeakMap<DeepNullable<Keys>, DeepNullable<Values>> : Type extends Set<infer Values> ? Set<DeepNullable<Values>> : Type extends ReadonlySet<infer Values> ? ReadonlySet<DeepNullable<Values>> : Type extends WeakSet<infer Values> ? WeakSet<DeepNullable<Values>> : Type extends ReadonlyArray<infer Values> ? Type extends IsTuple<Type> ? {
|
||||
- [Key in keyof Type]: DeepNullable<Type[Key]> | null;
|
||||
-} : Type extends Array<Values> ? Array<DeepNullable<Values>> : ReadonlyArray<DeepNullable<Values>> : Type extends Promise<infer Value> ? Promise<DeepNullable<Value>> : Type extends {} ? {
|
||||
- [Key in keyof Type]: DeepNullable<Type[Key]>;
|
||||
-} : Type | null;
|
||||
+export declare type DeepNullable<Type> = Type extends Builtin
|
||||
+ ? Type | null
|
||||
+ : Type extends Map<infer Keys, infer Values>
|
||||
+ ? Map<DeepNullable<Keys>, DeepNullable<Values>>
|
||||
+ : Type extends ReadonlyMap<infer Keys, infer Values>
|
||||
+ ? ReadonlyMap<DeepNullable<Keys>, DeepNullable<Values>>
|
||||
+ : Type extends WeakMap<infer Keys, infer Values>
|
||||
+ ? WeakMap<Keys, DeepNullable<Values>>
|
||||
+ : Type extends Set<infer Values>
|
||||
+ ? Set<DeepNullable<Values>>
|
||||
+ : Type extends ReadonlySet<infer Values>
|
||||
+ ? ReadonlySet<DeepNullable<Values>>
|
||||
+ : Type extends WeakSet<infer Values>
|
||||
+ ? WeakSet<Values>
|
||||
+ : Type extends ReadonlyArray<infer Values>
|
||||
+ ? Type extends IsTuple<Type>
|
||||
+ ? {
|
||||
+ [Key in keyof Type]: DeepNullable<Type[Key]> | null;
|
||||
+ }
|
||||
+ : Type extends Array<Values>
|
||||
+ ? Array<DeepNullable<Values>>
|
||||
+ : ReadonlyArray<DeepNullable<Values>>
|
||||
+ : Type extends Promise<infer Value>
|
||||
+ ? Promise<DeepNullable<Value>>
|
||||
+ : Type extends {}
|
||||
+ ? {
|
||||
+ [Key in keyof Type]: DeepNullable<Type[Key]>;
|
||||
+ }
|
||||
+ : Type | null;
|
||||
diff --git a/dist/deep-undefinable/index.d.ts b/dist/deep-undefinable/index.d.ts
|
||||
index 9e157cd4b59fbb33c36c82bfc8967ba62e6e9888..04e3026545352b984d686723edf90779b79c7791 100644
|
||||
--- a/dist/deep-undefinable/index.d.ts
|
||||
+++ b/dist/deep-undefinable/index.d.ts
|
||||
@@ -1,7 +1,31 @@
|
||||
import { Builtin } from "../built-in";
|
||||
import { IsTuple } from "../is-tuple";
|
||||
-export declare type DeepUndefinable<Type> = Type extends Builtin ? Type | undefined : Type extends Map<infer Keys, infer Values> ? Map<DeepUndefinable<Keys>, DeepUndefinable<Values>> : Type extends ReadonlyMap<infer Keys, infer Values> ? ReadonlyMap<DeepUndefinable<Keys>, DeepUndefinable<Values>> : Type extends WeakMap<infer Keys, infer Values> ? WeakMap<DeepUndefinable<Keys>, DeepUndefinable<Values>> : Type extends Set<infer Values> ? Set<DeepUndefinable<Values>> : Type extends ReadonlySet<infer Values> ? ReadonlySet<DeepUndefinable<Values>> : Type extends WeakSet<infer Values> ? WeakSet<DeepUndefinable<Values>> : Type extends ReadonlyArray<infer Values> ? Type extends IsTuple<Type> ? {
|
||||
- [Key in keyof Type]: DeepUndefinable<Type[Key]> | undefined;
|
||||
-} : Type extends Array<Values> ? Array<DeepUndefinable<Values>> : ReadonlyArray<DeepUndefinable<Values>> : Type extends Promise<infer Value> ? Promise<DeepUndefinable<Value>> : Type extends {} ? {
|
||||
- [Key in keyof Type]: DeepUndefinable<Type[Key]>;
|
||||
-} : Type | undefined;
|
||||
+export declare type DeepUndefinable<Type> = Type extends Builtin
|
||||
+ ? Type | undefined
|
||||
+ : Type extends Map<infer Keys, infer Values>
|
||||
+ ? Map<DeepUndefinable<Keys>, DeepUndefinable<Values>>
|
||||
+ : Type extends ReadonlyMap<infer Keys, infer Values>
|
||||
+ ? ReadonlyMap<DeepUndefinable<Keys>, DeepUndefinable<Values>>
|
||||
+ : Type extends WeakMap<infer Keys, infer Values>
|
||||
+ ? WeakMap<Keys, DeepUndefinable<Values>>
|
||||
+ : Type extends Set<infer Values>
|
||||
+ ? Set<DeepUndefinable<Values>>
|
||||
+ : Type extends ReadonlySet<infer Values>
|
||||
+ ? ReadonlySet<DeepUndefinable<Values>>
|
||||
+ : Type extends WeakSet<infer Values>
|
||||
+ ? WeakSet<Values>
|
||||
+ : Type extends ReadonlyArray<infer Values>
|
||||
+ ? Type extends IsTuple<Type>
|
||||
+ ? {
|
||||
+ [Key in keyof Type]: DeepUndefinable<Type[Key]> | undefined;
|
||||
+ }
|
||||
+ : Type extends Array<Values>
|
||||
+ ? Array<DeepUndefinable<Values>>
|
||||
+ : ReadonlyArray<DeepUndefinable<Values>>
|
||||
+ : Type extends Promise<infer Value>
|
||||
+ ? Promise<DeepUndefinable<Value>>
|
||||
+ : Type extends {}
|
||||
+ ? {
|
||||
+ [Key in keyof Type]: DeepUndefinable<Type[Key]>;
|
||||
+ }
|
||||
+ : Type | undefined;
|
||||
diff --git a/package.json b/package.json
|
||||
index da3fa96d66333522b05e46b178ff3a2e98808d0a..263d52cd58edf499b6b91e3b17ce83607d862379 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -45,6 +45,6 @@
|
||||
"conditional-type-checks": "^1.0.4",
|
||||
"prettier": "^2.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
- "typescript": "^4.1.0"
|
||||
+ "typescript": "^5.4.3"
|
||||
}
|
||||
}
|
||||
412
pnpm-lock.yaml
generated
412
pnpm-lock.yaml
generated
@@ -4,16 +4,21 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
ts-essentials@9.4.1:
|
||||
hash: 254pvnpgpcwoswa4ncfq4tq6su
|
||||
path: patches/ts-essentials@9.4.1.patch
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.2.2)
|
||||
version: 6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(eslint@8.45.0)(typescript@5.2.2)
|
||||
version: 6.0.0(eslint@8.45.0)(typescript@5.4.3)
|
||||
esbuild:
|
||||
specifier: ^0.19.5
|
||||
version: 0.19.5
|
||||
@@ -24,8 +29,8 @@ importers:
|
||||
specifier: ^1.13.0
|
||||
version: 1.13.0
|
||||
typescript:
|
||||
specifier: ^5.2.2
|
||||
version: 5.2.2
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.3
|
||||
|
||||
server:
|
||||
dependencies:
|
||||
@@ -48,14 +53,14 @@ importers:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
'@mikro-orm/better-sqlite':
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4(@mikro-orm/core@6.0.4)
|
||||
specifier: ^6.1.12
|
||||
version: 6.1.12(@mikro-orm/core@6.1.12)
|
||||
'@mikro-orm/core':
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4
|
||||
specifier: ^6.1.12
|
||||
version: 6.1.12
|
||||
'@mikro-orm/migrations':
|
||||
specifier: 6.0.4
|
||||
version: 6.0.4(@mikro-orm/core@6.0.4)(better-sqlite3@9.1.1)
|
||||
specifier: 6.1.12
|
||||
version: 6.1.12(@mikro-orm/core@6.1.12)(better-sqlite3@9.1.1)
|
||||
'@tunarr/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../shared
|
||||
@@ -65,6 +70,9 @@ importers:
|
||||
JSONStream:
|
||||
specifier: 1.0.5
|
||||
version: 1.0.5
|
||||
async-mutex:
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0
|
||||
async-retry:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
@@ -135,8 +143,8 @@ importers:
|
||||
specifier: 2.1.0
|
||||
version: 2.1.0
|
||||
reflect-metadata:
|
||||
specifier: ^0.1.13
|
||||
version: 0.1.13
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
retry:
|
||||
specifier: ^0.13.1
|
||||
version: 0.13.1
|
||||
@@ -163,11 +171,11 @@ importers:
|
||||
version: 3.22.4
|
||||
devDependencies:
|
||||
'@mikro-orm/cli':
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4(better-sqlite3@9.1.1)
|
||||
specifier: ^6.1.12
|
||||
version: 6.1.12(better-sqlite3@9.1.1)
|
||||
'@mikro-orm/reflection':
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4(@mikro-orm/core@6.0.4)
|
||||
specifier: ^6.1.12
|
||||
version: 6.1.12(@mikro-orm/core@6.1.12)
|
||||
'@types/async-retry':
|
||||
specifier: ^1.4.8
|
||||
version: 1.4.8
|
||||
@@ -251,19 +259,19 @@ importers:
|
||||
version: 3.0.3
|
||||
ts-essentials:
|
||||
specifier: ^9.4.1
|
||||
version: 9.4.1(typescript@5.3.3)
|
||||
version: 9.4.1(patch_hash=254pvnpgpcwoswa4ncfq4tq6su)(typescript@5.4.3)
|
||||
ts-node:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@20.8.9)(typescript@5.3.3)
|
||||
version: 10.9.2(@types/node@20.8.9)(typescript@5.4.3)
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
tsify:
|
||||
specifier: ^5.0.4
|
||||
version: 5.0.4(browserify@17.0.0)(typescript@5.3.3)
|
||||
version: 5.0.4(browserify@17.0.0)(typescript@5.4.3)
|
||||
tsup:
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2(ts-node@10.9.2)(typescript@5.3.3)
|
||||
version: 8.0.2(ts-node@10.9.2)(typescript@5.4.3)
|
||||
tsx:
|
||||
specifier: ^4.7.1
|
||||
version: 4.7.1
|
||||
@@ -271,8 +279,8 @@ importers:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.3.3
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.3
|
||||
vitest:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(@types/node@20.8.9)
|
||||
@@ -306,13 +314,13 @@ importers:
|
||||
version: 5.0.5
|
||||
ts-essentials:
|
||||
specifier: ^9.4.1
|
||||
version: 9.4.1(typescript@5.2.2)
|
||||
version: 9.4.1(patch_hash=254pvnpgpcwoswa4ncfq4tq6su)(typescript@5.4.3)
|
||||
tsup:
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2(@microsoft/api-extractor@7.43.0)(typescript@5.2.2)
|
||||
version: 8.0.2(@microsoft/api-extractor@7.43.0)(typescript@5.4.3)
|
||||
typescript:
|
||||
specifier: 5.2.2
|
||||
version: 5.2.2
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.3
|
||||
|
||||
types:
|
||||
dependencies:
|
||||
@@ -325,10 +333,10 @@ importers:
|
||||
version: 7.43.0
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.2.2)
|
||||
version: 6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0(eslint@8.45.0)(typescript@5.2.2)
|
||||
version: 6.0.0(eslint@8.45.0)(typescript@5.4.3)
|
||||
eslint:
|
||||
specifier: 8.45.0
|
||||
version: 8.45.0
|
||||
@@ -337,10 +345,10 @@ importers:
|
||||
version: 5.0.5
|
||||
tsup:
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2(@microsoft/api-extractor@7.43.0)(typescript@5.2.2)
|
||||
version: 8.0.2(@microsoft/api-extractor@7.43.0)(typescript@5.4.3)
|
||||
typescript:
|
||||
specifier: 5.2.2
|
||||
version: 5.2.2
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.3
|
||||
|
||||
web:
|
||||
dependencies:
|
||||
@@ -461,10 +469,10 @@ importers:
|
||||
version: 9.0.6
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.3.3)
|
||||
version: 6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0(eslint@8.45.0)(typescript@5.3.3)
|
||||
version: 6.0.0(eslint@8.45.0)(typescript@5.4.3)
|
||||
'@vitejs/plugin-react-swc':
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2(vite@4.4.5)
|
||||
@@ -485,10 +493,10 @@ importers:
|
||||
version: 1.14.0(react@18.2.0)
|
||||
ts-essentials:
|
||||
specifier: ^9.4.1
|
||||
version: 9.4.1(typescript@5.3.3)
|
||||
version: 9.4.1(patch_hash=254pvnpgpcwoswa4ncfq4tq6su)(typescript@5.4.3)
|
||||
typescript:
|
||||
specifier: ^5.2.2
|
||||
version: 5.3.3
|
||||
specifier: ^5.4.3
|
||||
version: 5.4.3
|
||||
vite:
|
||||
specifier: ^4.4.5
|
||||
version: 4.4.5
|
||||
@@ -1893,14 +1901,14 @@ packages:
|
||||
resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==}
|
||||
dev: true
|
||||
|
||||
/@mikro-orm/better-sqlite@6.0.4(@mikro-orm/core@6.0.4):
|
||||
resolution: {integrity: sha512-SZjIEcdv2D3dhyBTFAwZ0nITZKf1GxfF3pIBsyu1gSLFE5C9pRILDo2Fo8X5i7orP2vZjGycGFDIDrOEtdBzDg==}
|
||||
/@mikro-orm/better-sqlite@6.1.12(@mikro-orm/core@6.1.12):
|
||||
resolution: {integrity: sha512-7uupNjdoi8FgCfZ8sacC1Pau0xkOO+oRcOtQpa1Ddyh/8em/CMuPbjCzY04HrfAU0JPCL9KGDdL+4hleB3GH5Q==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
peerDependencies:
|
||||
'@mikro-orm/core': ^6.0.0
|
||||
dependencies:
|
||||
'@mikro-orm/core': 6.0.4
|
||||
'@mikro-orm/knex': 6.0.4(@mikro-orm/core@6.0.4)(better-sqlite3@8.7.0)
|
||||
'@mikro-orm/core': 6.1.12
|
||||
'@mikro-orm/knex': 6.1.12(@mikro-orm/core@6.1.12)(better-sqlite3@8.7.0)
|
||||
better-sqlite3: 8.7.0
|
||||
fs-extra: 11.2.0
|
||||
sqlstring-sqlite: 0.1.1
|
||||
@@ -1914,14 +1922,14 @@ packages:
|
||||
- tedious
|
||||
dev: false
|
||||
|
||||
/@mikro-orm/cli@6.0.4(better-sqlite3@9.1.1):
|
||||
resolution: {integrity: sha512-uqDM66Hg+a/f/InUzNwAh836wJn0ZLMqbc/8M9dAmgx0l5xY271zm5TRe+hogvYUyy/6mzQEx8Di5aN8+FJHhw==}
|
||||
/@mikro-orm/cli@6.1.12(better-sqlite3@9.1.1):
|
||||
resolution: {integrity: sha512-9Jf33ZLfEnX1cRwgBdImgpBDesILjm9IPeZxOGa9KI1hQHZ+MrWN9gRl3PFHwQvElHXEt3aU2IBiMO3h6iVXaQ==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@jercle/yargonaut': 1.1.5
|
||||
'@mikro-orm/core': 6.0.4
|
||||
'@mikro-orm/knex': 6.0.4(@mikro-orm/core@6.0.4)(better-sqlite3@9.1.1)
|
||||
'@mikro-orm/core': 6.1.12
|
||||
'@mikro-orm/knex': 6.1.12(@mikro-orm/core@6.1.12)(better-sqlite3@9.1.1)
|
||||
fs-extra: 11.2.0
|
||||
tsconfig-paths: 4.2.0
|
||||
yargs: 17.7.2
|
||||
@@ -1936,25 +1944,25 @@ packages:
|
||||
- tedious
|
||||
dev: true
|
||||
|
||||
/@mikro-orm/core@6.0.4:
|
||||
resolution: {integrity: sha512-Kjrs5TPHEhh5sOjOa7kAy56aury03Xw6pPU/gE81gipGq4fNNUJAjYn3aC/9JVxVsZ/oB2eiqKsBSs4Gyvu9iw==}
|
||||
/@mikro-orm/core@6.1.12:
|
||||
resolution: {integrity: sha512-51/1iBdXoF+bODJMpW8cUsr1TsieIJobiAX4g9A6CgBU6v95vwzyEQRo9v73i+YuPfrjH4YrrSbRaAr1tKe38A==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
dependencies:
|
||||
dataloader: 2.2.2
|
||||
dotenv: 16.3.1
|
||||
dotenv: 16.4.5
|
||||
esprima: 4.0.1
|
||||
fs-extra: 11.2.0
|
||||
globby: 11.1.0
|
||||
mikro-orm: 6.0.4
|
||||
mikro-orm: 6.1.12
|
||||
reflect-metadata: 0.2.1
|
||||
|
||||
/@mikro-orm/knex@6.0.4(@mikro-orm/core@6.0.4)(better-sqlite3@8.7.0):
|
||||
resolution: {integrity: sha512-OoNk6M4S7ZK+h1uOe6dk6dbdCxxOXnVcwRr9a+zspn/yz/zOxt3dqQlburEXevhbNDl4U/h99BWYQ9Jw1zPrjQ==}
|
||||
/@mikro-orm/knex@6.1.12(@mikro-orm/core@6.1.12)(better-sqlite3@8.7.0):
|
||||
resolution: {integrity: sha512-bGRDTM13ASYcmte8BglikDwfoYmCo8YUW5LY4Mn5GUCyzjLV7XP23SrTaerLIxXlNiTnJhTO+On3cOyndFwHpw==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
peerDependencies:
|
||||
'@mikro-orm/core': ^6.0.0
|
||||
dependencies:
|
||||
'@mikro-orm/core': 6.0.4
|
||||
'@mikro-orm/core': 6.1.12
|
||||
fs-extra: 11.2.0
|
||||
knex: 3.1.0(better-sqlite3@8.7.0)
|
||||
sqlstring: 2.3.3
|
||||
@@ -1969,13 +1977,13 @@ packages:
|
||||
- tedious
|
||||
dev: false
|
||||
|
||||
/@mikro-orm/knex@6.0.4(@mikro-orm/core@6.0.4)(better-sqlite3@9.1.1):
|
||||
resolution: {integrity: sha512-OoNk6M4S7ZK+h1uOe6dk6dbdCxxOXnVcwRr9a+zspn/yz/zOxt3dqQlburEXevhbNDl4U/h99BWYQ9Jw1zPrjQ==}
|
||||
/@mikro-orm/knex@6.1.12(@mikro-orm/core@6.1.12)(better-sqlite3@9.1.1):
|
||||
resolution: {integrity: sha512-bGRDTM13ASYcmte8BglikDwfoYmCo8YUW5LY4Mn5GUCyzjLV7XP23SrTaerLIxXlNiTnJhTO+On3cOyndFwHpw==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
peerDependencies:
|
||||
'@mikro-orm/core': ^6.0.0
|
||||
dependencies:
|
||||
'@mikro-orm/core': 6.0.4
|
||||
'@mikro-orm/core': 6.1.12
|
||||
fs-extra: 11.2.0
|
||||
knex: 3.1.0(better-sqlite3@9.1.1)
|
||||
sqlstring: 2.3.3
|
||||
@@ -1989,16 +1997,16 @@ packages:
|
||||
- supports-color
|
||||
- tedious
|
||||
|
||||
/@mikro-orm/migrations@6.0.4(@mikro-orm/core@6.0.4)(better-sqlite3@9.1.1):
|
||||
resolution: {integrity: sha512-HBb3COjeTbeS6JuAxEVTz06DXFaGojjWmBhxWw0GFVsZPkIhTa4oSuDK48qY21+LOSKWVA/9A9Wdhh8Xk4QbLA==}
|
||||
/@mikro-orm/migrations@6.1.12(@mikro-orm/core@6.1.12)(better-sqlite3@9.1.1):
|
||||
resolution: {integrity: sha512-CpKXU/3NEIWnuSYR9t+8IRcGpFj4pI8OTmAgnVp2BihgvaIh1JdnKUkQnmLt9i/0VupAIffTIa9J5rYofsgddQ==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
peerDependencies:
|
||||
'@mikro-orm/core': ^6.0.0
|
||||
dependencies:
|
||||
'@mikro-orm/core': 6.0.4
|
||||
'@mikro-orm/knex': 6.0.4(@mikro-orm/core@6.0.4)(better-sqlite3@9.1.1)
|
||||
'@mikro-orm/core': 6.1.12
|
||||
'@mikro-orm/knex': 6.1.12(@mikro-orm/core@6.1.12)(better-sqlite3@9.1.1)
|
||||
fs-extra: 11.2.0
|
||||
umzug: 3.5.0
|
||||
umzug: 3.7.0
|
||||
transitivePeerDependencies:
|
||||
- better-sqlite3
|
||||
- mysql
|
||||
@@ -2010,15 +2018,15 @@ packages:
|
||||
- tedious
|
||||
dev: false
|
||||
|
||||
/@mikro-orm/reflection@6.0.4(@mikro-orm/core@6.0.4):
|
||||
resolution: {integrity: sha512-eDAVoSUrAQVsTVGB7IUmD3fHZrsk3fJbdk/9mAvCDCJrzG+G/Ng9TfHOdUCgH35Wzj1u6zxuftmYV7dnogKvaA==}
|
||||
/@mikro-orm/reflection@6.1.12(@mikro-orm/core@6.1.12):
|
||||
resolution: {integrity: sha512-mm9rOeSqE4lqn/vNAG2G7Yws8DjH+YYoTE81GGkO8BIUCjT+F+cTKzW871Qj0JfGSuW4spsdDW10gKmXywrveQ==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
peerDependencies:
|
||||
'@mikro-orm/core': ^6.0.0
|
||||
dependencies:
|
||||
'@mikro-orm/core': 6.0.4
|
||||
'@mikro-orm/core': 6.1.12
|
||||
globby: 11.1.0
|
||||
ts-morph: 21.0.1
|
||||
ts-morph: 22.0.0
|
||||
dev: true
|
||||
|
||||
/@mui/base@5.0.0-beta.23(@types/react@18.2.15)(react-dom@18.2.0)(react@18.2.0):
|
||||
@@ -2718,8 +2726,8 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@ts-morph/common@0.22.0:
|
||||
resolution: {integrity: sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==}
|
||||
/@ts-morph/common@0.23.0:
|
||||
resolution: {integrity: sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==}
|
||||
dependencies:
|
||||
fast-glob: 3.3.2
|
||||
minimatch: 9.0.3
|
||||
@@ -3032,7 +3040,7 @@ packages:
|
||||
'@types/yargs-parser': 21.0.3
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.2.2):
|
||||
/@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-xuv6ghKGoiq856Bww/yVYnXGsKa588kY3M0XK7uUW/3fJNNULKRfZfSBkMTSpqGG/8ZCXCadfh8G/z/B4aqS/A==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
@@ -3044,10 +3052,10 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@typescript-eslint/parser': 6.0.0(eslint@8.45.0)(typescript@5.2.2)
|
||||
'@typescript-eslint/parser': 6.0.0(eslint@8.45.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/scope-manager': 6.0.0
|
||||
'@typescript-eslint/type-utils': 6.0.0(eslint@8.45.0)(typescript@5.2.2)
|
||||
'@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@5.2.2)
|
||||
'@typescript-eslint/type-utils': 6.0.0(eslint@8.45.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@5.4.3)
|
||||
'@typescript-eslint/visitor-keys': 6.0.0
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
eslint: 8.45.0
|
||||
@@ -3057,44 +3065,13 @@ packages:
|
||||
natural-compare: 1.4.0
|
||||
natural-compare-lite: 1.4.0
|
||||
semver: 7.5.4
|
||||
ts-api-utils: 1.0.3(typescript@5.2.2)
|
||||
typescript: 5.2.2
|
||||
ts-api-utils: 1.0.3(typescript@5.4.3)
|
||||
typescript: 5.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/eslint-plugin@6.0.0(@typescript-eslint/parser@6.0.0)(eslint@8.45.0)(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-xuv6ghKGoiq856Bww/yVYnXGsKa588kY3M0XK7uUW/3fJNNULKRfZfSBkMTSpqGG/8ZCXCadfh8G/z/B4aqS/A==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@typescript-eslint/parser': 6.0.0(eslint@8.45.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/scope-manager': 6.0.0
|
||||
'@typescript-eslint/type-utils': 6.0.0(eslint@8.45.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/visitor-keys': 6.0.0
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
eslint: 8.45.0
|
||||
grapheme-splitter: 1.0.4
|
||||
graphemer: 1.4.0
|
||||
ignore: 5.2.4
|
||||
natural-compare: 1.4.0
|
||||
natural-compare-lite: 1.4.0
|
||||
semver: 7.5.4
|
||||
ts-api-utils: 1.0.3(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser@6.0.0(eslint@8.45.0)(typescript@5.2.2):
|
||||
/@typescript-eslint/parser@6.0.0(eslint@8.45.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-TNaufYSPrr1U8n+3xN+Yp9g31vQDJqhXzzPSHfQDLcaO4tU+mCfODPxCwf4H530zo7aUBE3QIdxCXamEnG04Tg==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
@@ -3106,32 +3083,11 @@ packages:
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 6.0.0
|
||||
'@typescript-eslint/types': 6.0.0
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.2.2)
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.4.3)
|
||||
'@typescript-eslint/visitor-keys': 6.0.0
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
eslint: 8.45.0
|
||||
typescript: 5.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser@6.0.0(eslint@8.45.0)(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-TNaufYSPrr1U8n+3xN+Yp9g31vQDJqhXzzPSHfQDLcaO4tU+mCfODPxCwf4H530zo7aUBE3QIdxCXamEnG04Tg==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 6.0.0
|
||||
'@typescript-eslint/types': 6.0.0
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.3.3)
|
||||
'@typescript-eslint/visitor-keys': 6.0.0
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
eslint: 8.45.0
|
||||
typescript: 5.3.3
|
||||
typescript: 5.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -3144,7 +3100,7 @@ packages:
|
||||
'@typescript-eslint/visitor-keys': 6.0.0
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/type-utils@6.0.0(eslint@8.45.0)(typescript@5.2.2):
|
||||
/@typescript-eslint/type-utils@6.0.0(eslint@8.45.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-ah6LJvLgkoZ/pyJ9GAdFkzeuMZ8goV6BH7eC9FPmojrnX9yNCIsfjB+zYcnex28YO3RFvBkV6rMV6WpIqkPvoQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
@@ -3154,32 +3110,12 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.2.2)
|
||||
'@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@5.2.2)
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.4.3)
|
||||
'@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@5.4.3)
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
eslint: 8.45.0
|
||||
ts-api-utils: 1.0.3(typescript@5.2.2)
|
||||
typescript: 5.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/type-utils@6.0.0(eslint@8.45.0)(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-ah6LJvLgkoZ/pyJ9GAdFkzeuMZ8goV6BH7eC9FPmojrnX9yNCIsfjB+zYcnex28YO3RFvBkV6rMV6WpIqkPvoQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.3.3)
|
||||
'@typescript-eslint/utils': 6.0.0(eslint@8.45.0)(typescript@5.3.3)
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
eslint: 8.45.0
|
||||
ts-api-utils: 1.0.3(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
ts-api-utils: 1.0.3(typescript@5.4.3)
|
||||
typescript: 5.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
@@ -3189,7 +3125,7 @@ packages:
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree@6.0.0(typescript@5.2.2):
|
||||
/@typescript-eslint/typescript-estree@6.0.0(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-2zq4O7P6YCQADfmJ5OTDQTP3ktajnXIRrYAtHM9ofto/CJZV3QfJ89GEaM2BNGeSr1KgmBuLhEkz5FBkS2RQhQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
@@ -3204,34 +3140,13 @@ packages:
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.5.4
|
||||
ts-api-utils: 1.0.3(typescript@5.2.2)
|
||||
typescript: 5.2.2
|
||||
ts-api-utils: 1.0.3(typescript@5.4.3)
|
||||
typescript: 5.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/typescript-estree@6.0.0(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-2zq4O7P6YCQADfmJ5OTDQTP3ktajnXIRrYAtHM9ofto/CJZV3QfJ89GEaM2BNGeSr1KgmBuLhEkz5FBkS2RQhQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
typescript: '*'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 6.0.0
|
||||
'@typescript-eslint/visitor-keys': 6.0.0
|
||||
debug: 4.3.4(supports-color@5.5.0)
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.5.4
|
||||
ts-api-utils: 1.0.3(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/utils@6.0.0(eslint@8.45.0)(typescript@5.2.2):
|
||||
/@typescript-eslint/utils@6.0.0(eslint@8.45.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-SOr6l4NB6HE4H/ktz0JVVWNXqCJTOo/mHnvIte1ZhBQ0Cvd04x5uKZa3zT6tiodL06zf5xxdK8COiDvPnQ27JQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
@@ -3242,27 +3157,7 @@ packages:
|
||||
'@types/semver': 7.5.4
|
||||
'@typescript-eslint/scope-manager': 6.0.0
|
||||
'@typescript-eslint/types': 6.0.0
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.2.2)
|
||||
eslint: 8.45.0
|
||||
eslint-scope: 5.1.1
|
||||
semver: 7.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/utils@6.0.0(eslint@8.45.0)(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-SOr6l4NB6HE4H/ktz0JVVWNXqCJTOo/mHnvIte1ZhBQ0Cvd04x5uKZa3zT6tiodL06zf5xxdK8COiDvPnQ27JQ==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
eslint: ^7.0.0 || ^8.0.0
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.45.0)
|
||||
'@types/json-schema': 7.0.14
|
||||
'@types/semver': 7.5.4
|
||||
'@typescript-eslint/scope-manager': 6.0.0
|
||||
'@typescript-eslint/types': 6.0.0
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.3.3)
|
||||
'@typescript-eslint/typescript-estree': 6.0.0(typescript@5.4.3)
|
||||
eslint: 8.45.0
|
||||
eslint-scope: 5.1.1
|
||||
semver: 7.5.4
|
||||
@@ -3865,6 +3760,12 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/async-mutex@0.5.0:
|
||||
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
|
||||
dependencies:
|
||||
tslib: 2.6.2
|
||||
dev: false
|
||||
|
||||
/async-retry@1.3.3:
|
||||
resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
|
||||
dependencies:
|
||||
@@ -4480,8 +4381,8 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/code-block-writer@12.0.0:
|
||||
resolution: {integrity: sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==}
|
||||
/code-block-writer@13.0.1:
|
||||
resolution: {integrity: sha512-c5or4P6erEA69TxaxTNcHUNcIn+oyxSRTOWV+pSYF+z4epXqNvwvJ70XPGjPNgue83oAFAPBRQYwpAJ/Hpe/Sg==}
|
||||
dev: true
|
||||
|
||||
/color-convert@1.9.3:
|
||||
@@ -5047,8 +4948,8 @@ packages:
|
||||
engines: {node: '>=0.4', npm: '>=1.2'}
|
||||
dev: true
|
||||
|
||||
/dotenv@16.3.1:
|
||||
resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==}
|
||||
/dotenv@16.4.5:
|
||||
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
/download@8.0.0:
|
||||
@@ -7182,8 +7083,8 @@ packages:
|
||||
braces: 3.0.2
|
||||
picomatch: 2.3.1
|
||||
|
||||
/mikro-orm@6.0.4:
|
||||
resolution: {integrity: sha512-rhC9PJ6rhfOoBQgnVpC+3GeeILQf0cfU1mSZHA+74FjpurfrznS5AZmuTxSKBnfjpMzrPoTOOwMwQC6DE7aVaQ==}
|
||||
/mikro-orm@6.1.12:
|
||||
resolution: {integrity: sha512-pXpZ5dGMM0BBqYouU5EPuWkjWX/xnNzRVxsOTrKyyrm1ICTN7pawHn3UCVAggi+z1qVhpwxThGfZj+ZWD54duw==}
|
||||
engines: {node: '>= 18.12.0'}
|
||||
|
||||
/miller-rabin@4.0.1:
|
||||
@@ -8167,7 +8068,7 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
lilconfig: 3.1.1
|
||||
ts-node: 10.9.2(@types/node@20.8.9)(typescript@5.3.3)
|
||||
ts-node: 10.9.2(@types/node@20.8.9)(typescript@5.4.3)
|
||||
yaml: 2.3.4
|
||||
dev: true
|
||||
|
||||
@@ -8601,12 +8502,13 @@ packages:
|
||||
'@babel/runtime': 7.23.9
|
||||
dev: false
|
||||
|
||||
/reflect-metadata@0.1.13:
|
||||
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
||||
dev: false
|
||||
|
||||
/reflect-metadata@0.2.1:
|
||||
resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==}
|
||||
deprecated: This version has a critical bug in fallback handling. Please upgrade to reflect-metadata@0.2.2 or newer.
|
||||
|
||||
/reflect-metadata@0.2.2:
|
||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||
dev: false
|
||||
|
||||
/regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
@@ -9602,25 +9504,16 @@ packages:
|
||||
engines: {node: '>= 14.0.0'}
|
||||
dev: false
|
||||
|
||||
/ts-api-utils@1.0.3(typescript@5.2.2):
|
||||
/ts-api-utils@1.0.3(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
|
||||
engines: {node: '>=16.13.0'}
|
||||
peerDependencies:
|
||||
typescript: '>=4.2.0'
|
||||
dependencies:
|
||||
typescript: 5.2.2
|
||||
typescript: 5.4.3
|
||||
dev: true
|
||||
|
||||
/ts-api-utils@1.0.3(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==}
|
||||
engines: {node: '>=16.13.0'}
|
||||
peerDependencies:
|
||||
typescript: '>=4.2.0'
|
||||
dependencies:
|
||||
typescript: 5.3.3
|
||||
dev: true
|
||||
|
||||
/ts-essentials@9.4.1(typescript@5.2.2):
|
||||
/ts-essentials@9.4.1(patch_hash=254pvnpgpcwoswa4ncfq4tq6su)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-oke0rI2EN9pzHsesdmrOrnqv1eQODmJpd/noJjwj2ZPC3Z4N2wbjrOEqnsEgmvlO2+4fBb0a794DCna2elEVIQ==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.1.0'
|
||||
@@ -9628,25 +9521,15 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
typescript: 5.2.2
|
||||
dev: true
|
||||
|
||||
/ts-essentials@9.4.1(typescript@5.3.3):
|
||||
resolution: {integrity: sha512-oke0rI2EN9pzHsesdmrOrnqv1eQODmJpd/noJjwj2ZPC3Z4N2wbjrOEqnsEgmvlO2+4fBb0a794DCna2elEVIQ==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.1.0'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
typescript: 5.3.3
|
||||
typescript: 5.4.3
|
||||
dev: true
|
||||
patched: true
|
||||
|
||||
/ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
dev: true
|
||||
|
||||
/ts-loader@9.5.1(typescript@5.4.2)(webpack@5.91.0):
|
||||
/ts-loader@9.5.1(typescript@5.4.3)(webpack@5.91.0):
|
||||
resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
@@ -9654,22 +9537,22 @@ packages:
|
||||
webpack: ^5.0.0
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
enhanced-resolve: 5.15.1
|
||||
enhanced-resolve: 5.16.0
|
||||
micromatch: 4.0.5
|
||||
semver: 7.5.4
|
||||
source-map: 0.7.4
|
||||
typescript: 5.4.2
|
||||
typescript: 5.4.3
|
||||
webpack: 5.91.0(esbuild@0.19.5)(webpack-cli@5.1.4)
|
||||
dev: true
|
||||
|
||||
/ts-morph@21.0.1:
|
||||
resolution: {integrity: sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==}
|
||||
/ts-morph@22.0.0:
|
||||
resolution: {integrity: sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==}
|
||||
dependencies:
|
||||
'@ts-morph/common': 0.22.0
|
||||
code-block-writer: 12.0.0
|
||||
'@ts-morph/common': 0.23.0
|
||||
code-block-writer: 13.0.1
|
||||
dev: true
|
||||
|
||||
/ts-node@10.9.2(@types/node@20.8.9)(typescript@5.3.3):
|
||||
/ts-node@10.9.2(@types/node@20.8.9)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -9695,7 +9578,7 @@ packages:
|
||||
create-require: 1.1.1
|
||||
diff: 4.0.2
|
||||
make-error: 1.3.6
|
||||
typescript: 5.3.3
|
||||
typescript: 5.4.3
|
||||
v8-compile-cache-lib: 3.0.1
|
||||
yn: 3.1.1
|
||||
dev: true
|
||||
@@ -9726,7 +9609,7 @@ packages:
|
||||
strip-json-comments: 2.0.1
|
||||
dev: true
|
||||
|
||||
/tsify@5.0.4(browserify@17.0.0)(typescript@5.3.3):
|
||||
/tsify@5.0.4(browserify@17.0.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-XAZtQ5OMPsJFclkZ9xMZWkSNyMhMxEPsz3D2zu79yoKorH9j/DT4xCloJeXk5+cDhosEibu4bseMVjyPOAyLJA==}
|
||||
engines: {node: '>=0.12'}
|
||||
peerDependencies:
|
||||
@@ -9740,13 +9623,13 @@ packages:
|
||||
semver: 6.3.1
|
||||
through2: 2.0.5
|
||||
tsconfig: 5.0.3
|
||||
typescript: 5.3.3
|
||||
typescript: 5.4.3
|
||||
dev: true
|
||||
|
||||
/tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
|
||||
/tsup@8.0.2(@microsoft/api-extractor@7.43.0)(typescript@5.2.2):
|
||||
/tsup@8.0.2(@microsoft/api-extractor@7.43.0)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
@@ -9780,13 +9663,13 @@ packages:
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.35.0
|
||||
tree-kill: 1.2.2
|
||||
typescript: 5.2.2
|
||||
typescript: 5.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/tsup@8.0.2(ts-node@10.9.2)(typescript@5.3.3):
|
||||
/tsup@8.0.2(ts-node@10.9.2)(typescript@5.4.3):
|
||||
resolution: {integrity: sha512-NY8xtQXdH7hDUAZwcQdY/Vzlw9johQsaqf7iwZ6g1DOUlFYQ5/AtVAjTvihhEyeRlGo4dLRVHtrRaL35M1daqQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
@@ -9805,6 +9688,7 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@microsoft/api-extractor': 7.43.0
|
||||
bundle-require: 4.0.2(esbuild@0.19.12)
|
||||
cac: 6.7.14
|
||||
chokidar: 3.5.3
|
||||
@@ -9819,7 +9703,7 @@ packages:
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.35.0
|
||||
tree-kill: 1.2.2
|
||||
typescript: 5.3.3
|
||||
typescript: 5.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
@@ -9925,6 +9809,12 @@ packages:
|
||||
/type-fest@3.13.1:
|
||||
resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==}
|
||||
engines: {node: '>=14.16'}
|
||||
dev: true
|
||||
|
||||
/type-fest@4.14.0:
|
||||
resolution: {integrity: sha512-on5/Cw89wwqGZQu+yWO0gGMGu8VNxsaW9SB2HE8yJjllEk7IDTwnSN1dUVldYILhYPN5HzD7WAaw2cc/jBfn0Q==}
|
||||
engines: {node: '>=16'}
|
||||
dev: false
|
||||
|
||||
/type-is@1.6.18:
|
||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||
@@ -9950,24 +9840,18 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/typescript@5.2.2:
|
||||
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/typescript@5.3.3:
|
||||
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/typescript@5.4.2:
|
||||
resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/typescript@5.4.3:
|
||||
resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ufo@1.3.1:
|
||||
resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==}
|
||||
dev: true
|
||||
@@ -9985,15 +9869,15 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/umzug@3.5.0:
|
||||
resolution: {integrity: sha512-bL6JjH716l0kg7V2Acrw5UmUgeLxdAZv3drMhKrJCXxEfK/qyM+B5s3ai1BjG1NyEGeXTOkhFIUgkMFo6zqVBg==}
|
||||
/umzug@3.7.0:
|
||||
resolution: {integrity: sha512-r/L2Zlilgv3SKhmP2nkA9x2Xi1PKtu2K34/i/s7AYJ2mLjEO+IxETJAK7CKf6l3QOvoy5/ChykeX9qt6ykRz6Q==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@rushstack/ts-command-line': 4.17.1
|
||||
emittery: 0.13.1
|
||||
glob: 8.1.0
|
||||
pony-cause: 2.1.10
|
||||
type-fest: 3.13.1
|
||||
type-fest: 4.14.0
|
||||
dev: false
|
||||
|
||||
/unbzip2-stream@1.4.3:
|
||||
@@ -10350,9 +10234,9 @@ packages:
|
||||
lodash: 4.17.21
|
||||
source-map-loader: 5.0.0(webpack@5.91.0)
|
||||
string-replace-loader: 3.1.0(webpack@5.91.0)
|
||||
ts-loader: 9.5.1(typescript@5.4.2)(webpack@5.91.0)
|
||||
ts-loader: 9.5.1(typescript@5.4.3)(webpack@5.91.0)
|
||||
tslib: 2.6.2
|
||||
typescript: 5.4.2
|
||||
typescript: 5.4.3
|
||||
webpack: 5.91.0(esbuild@0.19.5)(webpack-cli@5.1.4)
|
||||
webpack-cli: 5.1.4(webpack@5.91.0)
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -29,12 +29,13 @@
|
||||
"@fastify/static": "^6.12.0",
|
||||
"@fastify/swagger": "^8.12.1",
|
||||
"@fastify/swagger-ui": "^2.0.1",
|
||||
"@mikro-orm/better-sqlite": "^6.0.4",
|
||||
"@mikro-orm/core": "^6.0.4",
|
||||
"@mikro-orm/migrations": "6.0.4",
|
||||
"@mikro-orm/better-sqlite": "^6.1.12",
|
||||
"@mikro-orm/core": "^6.1.12",
|
||||
"@mikro-orm/migrations": "6.1.12",
|
||||
"@tunarr/shared": "workspace:*",
|
||||
"@tunarr/types": "workspace:*",
|
||||
"JSONStream": "1.0.5",
|
||||
"async-mutex": "^0.5.0",
|
||||
"async-retry": "^1.3.3",
|
||||
"axios": "^1.6.0",
|
||||
"better-sqlite3": "^9.1.1",
|
||||
@@ -58,7 +59,7 @@
|
||||
"node-ssdp": "^4.0.0",
|
||||
"p-queue": "^8.0.1",
|
||||
"random-js": "2.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"retry": "^0.13.1",
|
||||
"tslib": "^2.6.2",
|
||||
"uuid": "^9.0.1",
|
||||
@@ -69,8 +70,8 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mikro-orm/cli": "^6.0.4",
|
||||
"@mikro-orm/reflection": "^6.0.4",
|
||||
"@mikro-orm/cli": "^6.1.12",
|
||||
"@mikro-orm/reflection": "^6.1.12",
|
||||
"@types/async-retry": "^1.4.8",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/express": "^4.17.20",
|
||||
@@ -105,7 +106,7 @@
|
||||
"tsup": "^8.0.2",
|
||||
"tsx": "^4.7.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"typescript": "^5.3.3",
|
||||
"typescript": "^5.4.3",
|
||||
"vitest": "^1.2.0"
|
||||
},
|
||||
"mikro-orm": {
|
||||
|
||||
@@ -18,7 +18,7 @@ import duration from 'dayjs/plugin/duration.js';
|
||||
import { compact, isError, isNil, omit, sortBy } from 'lodash-es';
|
||||
import z from 'zod';
|
||||
import createLogger from '../logger.js';
|
||||
import { scheduledJobsById } from '../services/scheduler.js';
|
||||
import { GlobalScheduler } from '../services/scheduler.js';
|
||||
import { UpdateXmlTvTask } from '../tasks/updateXmlTvTask.js';
|
||||
import { RouterPluginAsyncCallback } from '../types/serverType.js';
|
||||
import { attempt, mapAsyncSeq } from '../util.js';
|
||||
@@ -115,6 +115,9 @@ export const channelsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
const inserted = await attempt(() =>
|
||||
req.serverCtx.channelDB.saveChannel(req.body),
|
||||
);
|
||||
GlobalScheduler.getScheduledJob(UpdateXmlTvTask.ID)
|
||||
.runNow(true)
|
||||
.catch((err) => logger.error('Error regenerating guide: %O', err));
|
||||
if (isError(inserted)) {
|
||||
return res.status(500).send(inserted);
|
||||
}
|
||||
@@ -183,9 +186,9 @@ export const channelsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
await req.serverCtx.channelDB.deleteChannel(channel.uuid);
|
||||
|
||||
try {
|
||||
scheduledJobsById[UpdateXmlTvTask.ID]
|
||||
?.runNow(true)
|
||||
.catch(console.error);
|
||||
GlobalScheduler.getScheduledJob(UpdateXmlTvTask.ID)
|
||||
.runNow()
|
||||
.catch((err) => logger.error('Error regenerating guide: %O', err));
|
||||
} catch (e) {
|
||||
logger.error('Unable to update guide after lineup update %O', e);
|
||||
}
|
||||
@@ -297,9 +300,9 @@ export const channelsApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
}
|
||||
|
||||
try {
|
||||
scheduledJobsById[UpdateXmlTvTask.ID]
|
||||
?.runNow(true)
|
||||
.catch(console.error);
|
||||
GlobalScheduler.getScheduledJob(UpdateXmlTvTask.ID)
|
||||
.runNow(true)
|
||||
.catch((err) => logger.error('Error regenerating guide: %O', err));
|
||||
} catch (e) {
|
||||
logger.error('Unable to update guide after lineup update %O', e);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import { FFMPEGInfo } from '../ffmpegInfo.js';
|
||||
import { serverOptions } from '../globals.js';
|
||||
import createLogger from '../logger.js';
|
||||
import { Plex } from '../plex.js';
|
||||
import { scheduledJobsById } from '../services/scheduler.js';
|
||||
import { GlobalScheduler } from '../services/scheduler.js';
|
||||
import { UpdateXmlTvTask } from '../tasks/updateXmlTvTask.js';
|
||||
import { RouterPluginAsyncCallback } from '../types/serverType.js';
|
||||
import { fileExists } from '../util/fsUtil.js';
|
||||
import { channelsApi } from './channelsApi.js';
|
||||
@@ -108,7 +109,9 @@ export const apiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
fastify.get('/xmltv-last-refresh', (_req, res) => {
|
||||
try {
|
||||
return res.send({
|
||||
value: scheduledJobsById['update-xmltv']?.lastExecution?.valueOf(),
|
||||
value: GlobalScheduler.getScheduledJob(
|
||||
UpdateXmlTvTask.ID,
|
||||
).lastExecution?.valueOf(),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
@@ -139,7 +142,7 @@ export const apiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
|
||||
// Force an XMLTV refresh
|
||||
fastify.post('/xmltv/refresh', async (_, res) => {
|
||||
await scheduledJobsById['update-xmltv']?.runNow();
|
||||
await GlobalScheduler.getScheduledJob(UpdateXmlTvTask.ID).runNow(false);
|
||||
return res.status(200);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import z from 'zod';
|
||||
import { PlexServerSettings } from '../dao/entities/PlexServerSettings.js';
|
||||
import createLogger from '../logger.js';
|
||||
import { Plex, PlexApiFactory } from '../plex.js';
|
||||
import { scheduledJobsById } from '../services/scheduler.js';
|
||||
import { GlobalScheduler } from '../services/scheduler.js';
|
||||
import { UpdateXmlTvTask } from '../tasks/updateXmlTvTask.js';
|
||||
import { RouterPluginAsyncCallback } from '../types/serverType.js';
|
||||
import { firstDefined, wait } from '../util.js';
|
||||
@@ -164,8 +164,8 @@ export const plexServersRouter: RouterPluginAsyncCallback = async (
|
||||
|
||||
// Regenerate guides
|
||||
try {
|
||||
scheduledJobsById[UpdateXmlTvTask.ID]
|
||||
?.runNow(true)
|
||||
GlobalScheduler.getScheduledJob(UpdateXmlTvTask.ID)
|
||||
.runNow(true)
|
||||
.catch(console.error);
|
||||
} catch (e) {
|
||||
logger.error('Unable to update guide after lineup update %O', e);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BasicIdParamSchema } from '@tunarr/types/api';
|
||||
import { ProgramSchema } from '@tunarr/types/schemas';
|
||||
import { chunk, every, find, isNil, isUndefined, reduce } from 'lodash-es';
|
||||
import { every, find, isNil, isUndefined } from 'lodash-es';
|
||||
import z from 'zod';
|
||||
import { getEm } from '../dao/dataSource.js';
|
||||
import {
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from '../dao/entities/Program.js';
|
||||
import { Plex } from '../plex.js';
|
||||
import { RouterPluginAsyncCallback } from '../types/serverType.js';
|
||||
import { flatMapAsyncSeq, groupByFunc } from '../util.js';
|
||||
|
||||
const LookupExternalProgrammingSchema = z.object({
|
||||
externalId: z
|
||||
@@ -195,38 +194,9 @@ export const programmingApi: RouterPluginAsyncCallback = async (fastify) => {
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
const em = getEm();
|
||||
const allIds = [...req.body.externalIds];
|
||||
const results = await flatMapAsyncSeq(
|
||||
chunk(allIds, 25),
|
||||
async (idChunk) => {
|
||||
return await reduce(
|
||||
idChunk,
|
||||
(acc, [ps, es, ek]) => {
|
||||
return acc.orWhere({
|
||||
sourceType: programSourceTypeFromString(ps)!,
|
||||
externalSourceId: es,
|
||||
externalKey: ek,
|
||||
});
|
||||
},
|
||||
em
|
||||
.qb(Program)
|
||||
.select([
|
||||
'uuid',
|
||||
'sourceType',
|
||||
'externalSourceId',
|
||||
'externalKey',
|
||||
]),
|
||||
);
|
||||
},
|
||||
return res.send(
|
||||
await req.serverCtx.programDB.lookupByExternalIds(req.body.externalIds),
|
||||
);
|
||||
const all = groupByFunc(
|
||||
results,
|
||||
(r) => r.uniqueId(),
|
||||
(r) => r.toDTO(),
|
||||
);
|
||||
|
||||
return res.send(all);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { BaseErrorSchema } from '@tunarr/types/api';
|
||||
import { TaskSchema } from '@tunarr/types/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { chain, hasIn, isNil } from 'lodash-es';
|
||||
import { chain, isNil } from 'lodash-es';
|
||||
import { z } from 'zod';
|
||||
import createLogger from '../logger.js';
|
||||
import { scheduledJobsById } from '../services/scheduler.js';
|
||||
import { TaskId } from '../tasks/task.js';
|
||||
import { GlobalScheduler } from '../services/scheduler.js';
|
||||
import { RouterPluginAsyncCallback } from '../types/serverType.js';
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
@@ -22,7 +21,7 @@ export const tasksApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
},
|
||||
},
|
||||
async (_, res) => {
|
||||
const result = chain(scheduledJobsById)
|
||||
const result = chain(GlobalScheduler.scheduledJobsById)
|
||||
.map((task, id) => {
|
||||
if (isNil(task)) {
|
||||
return;
|
||||
@@ -58,21 +57,23 @@ export const tasksApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string(),
|
||||
background: z.boolean().default(true),
|
||||
}),
|
||||
response: {
|
||||
200: z.any(),
|
||||
202: z.void(),
|
||||
400: BaseErrorSchema,
|
||||
404: z.void(),
|
||||
500: z.void(),
|
||||
},
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
if (!hasIn(scheduledJobsById, req.params.id)) {
|
||||
const task = GlobalScheduler.getScheduledJob(req.params.id);
|
||||
if (isNil(task)) {
|
||||
return res.status(404).send();
|
||||
}
|
||||
|
||||
const task = scheduledJobsById[req.params.id as TaskId];
|
||||
|
||||
if (isNil(task)) {
|
||||
return res.status(404).send();
|
||||
}
|
||||
@@ -81,11 +82,17 @@ export const tasksApiRouter: RouterPluginAsyncCallback = async (fastify) => {
|
||||
return res.status(400).send({ message: 'Task already running' });
|
||||
}
|
||||
|
||||
task.runNow(true).catch((e) => {
|
||||
const taskPromise = task.runNow(req.params.background).catch((e) => {
|
||||
logger.error('Async task triggered by API failed: %O', e);
|
||||
});
|
||||
|
||||
return res.status(202).send();
|
||||
if (!req.params.background) {
|
||||
return taskPromise
|
||||
.then((result) => res.status(200).send(result))
|
||||
.catch(() => res.status(500).send());
|
||||
} else {
|
||||
return res.status(202).send();
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,8 @@ import { z } from 'zod';
|
||||
import { defaultXmlTvSettings } from '../dao/settings.js';
|
||||
import { serverOptions } from '../globals.js';
|
||||
import createLogger from '../logger.js';
|
||||
import { scheduledJobsById } from '../services/scheduler.js';
|
||||
import { GlobalScheduler } from '../services/scheduler.js';
|
||||
import { UpdateXmlTvTask } from '../tasks/updateXmlTvTask.js';
|
||||
import { RouterPluginCallback } from '../types/serverType.js';
|
||||
import { firstDefined } from '../util.js';
|
||||
|
||||
@@ -67,7 +68,7 @@ export const xmlTvSettingsRouter: RouterPluginCallback = (
|
||||
},
|
||||
level: 'success',
|
||||
});
|
||||
await updateXmltv();
|
||||
await GlobalScheduler.getScheduledJob(UpdateXmlTvTask.ID).runNow(false);
|
||||
return res.send(xmltv);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
@@ -113,7 +114,7 @@ export const xmlTvSettingsRouter: RouterPluginCallback = (
|
||||
level: 'warning',
|
||||
});
|
||||
|
||||
await updateXmltv();
|
||||
await GlobalScheduler.getScheduledJob(UpdateXmlTvTask.ID).runNow(false);
|
||||
return res.send(xmltv);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
@@ -134,7 +135,3 @@ export const xmlTvSettingsRouter: RouterPluginCallback = (
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
async function updateXmltv() {
|
||||
await scheduledJobsById['update-xmltv']?.runNow();
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@ import {
|
||||
sumBy,
|
||||
take,
|
||||
} from 'lodash-es';
|
||||
import { Low } from 'lowdb';
|
||||
import { DataFile } from 'lowdb/node';
|
||||
import { Adapter, Low } from 'lowdb';
|
||||
import { TextFile } from 'lowdb/node';
|
||||
import { PathLike } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import { join } from 'path';
|
||||
import { globalOptions } from '../globals.js';
|
||||
@@ -45,6 +46,7 @@ import { getEm } from './dataSource.js';
|
||||
import {
|
||||
Lineup,
|
||||
LineupItem,
|
||||
LineupSchema,
|
||||
OfflineItem,
|
||||
RedirectItem,
|
||||
isContentItem,
|
||||
@@ -88,9 +90,7 @@ function updateRequestToChannel(
|
||||
},
|
||||
duration: updateReq.duration,
|
||||
stealth: updateReq.stealth,
|
||||
fillerRepeatCooldown: updateReq.fillerRepeatCooldown
|
||||
? dayjs.duration({ seconds: updateReq.fillerRepeatCooldown })
|
||||
: undefined,
|
||||
fillerRepeatCooldown: updateReq.fillerRepeatCooldown,
|
||||
},
|
||||
isNil,
|
||||
);
|
||||
@@ -123,16 +123,14 @@ function createRequestToChannel(
|
||||
},
|
||||
duration: saveReq.duration,
|
||||
stealth: saveReq.stealth,
|
||||
fillerRepeatCooldown: saveReq.fillerRepeatCooldown
|
||||
? dayjs.duration({ seconds: saveReq.fillerRepeatCooldown })
|
||||
: undefined,
|
||||
fillerRepeatCooldown: saveReq.fillerRepeatCooldown,
|
||||
};
|
||||
return c;
|
||||
}
|
||||
|
||||
// Let's see if this works... in so we can have many ChannelDb objects flying around.
|
||||
const fileDbCache: Record<string | number, Low<Lineup>> = {};
|
||||
export class ChannelDB {
|
||||
private fileDbCache: Record<string | number, Low<Lineup>> = {};
|
||||
|
||||
getChannelByNumber(channelNumber: number): Promise<Nullable<Channel>> {
|
||||
return getEm().repo(Channel).findOne({ number: channelNumber });
|
||||
}
|
||||
@@ -488,23 +486,17 @@ export class ChannelDB {
|
||||
}
|
||||
|
||||
private async getFileDb(channelId: string) {
|
||||
if (!this.fileDbCache[channelId]) {
|
||||
this.fileDbCache[channelId] = new Low<Lineup>(
|
||||
new DataFile(
|
||||
if (!fileDbCache[channelId]) {
|
||||
fileDbCache[channelId] = new Low<Lineup>(
|
||||
new LineupDbAdapter(
|
||||
join(globalOptions().database, `channel-lineups/${channelId}.json`),
|
||||
{
|
||||
parse: JSON.parse,
|
||||
stringify(data) {
|
||||
return JSON.stringify(data);
|
||||
},
|
||||
},
|
||||
),
|
||||
{ items: [], startTimeOffsets: [] },
|
||||
);
|
||||
await this.fileDbCache[channelId].read();
|
||||
await fileDbCache[channelId].read();
|
||||
}
|
||||
|
||||
return this.fileDbCache[channelId];
|
||||
return fileDbCache[channelId];
|
||||
}
|
||||
|
||||
private async markFileDbForDeletion(
|
||||
@@ -521,7 +513,7 @@ export class ChannelDB {
|
||||
await fs.rename(path, newPath);
|
||||
}
|
||||
if (isDelete) {
|
||||
delete this.fileDbCache[channelId];
|
||||
delete fileDbCache[channelId];
|
||||
} else {
|
||||
// Reload the file into the DB cache
|
||||
await this.getFileDb(channelId);
|
||||
@@ -538,6 +530,43 @@ export class ChannelDB {
|
||||
}
|
||||
}
|
||||
|
||||
class LineupDbAdapter implements Adapter<Lineup> {
|
||||
#path: PathLike;
|
||||
#adapter: TextFile;
|
||||
constructor(filename: PathLike) {
|
||||
this.#path = filename;
|
||||
this.#adapter = new TextFile(filename);
|
||||
}
|
||||
|
||||
async read(): Promise<Lineup | null> {
|
||||
const data = await this.#adapter.read();
|
||||
if (data === null) {
|
||||
return null;
|
||||
}
|
||||
const parseResult = await LineupSchema.safeParseAsync(JSON.parse(data));
|
||||
if (!parseResult.success) {
|
||||
console.error(
|
||||
`Error while trying to load lineup file ${this.#path.toString()}`,
|
||||
parseResult.error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return parseResult.data;
|
||||
}
|
||||
|
||||
async write(data: Lineup): Promise<void> {
|
||||
const parseResult = await LineupSchema.safeParseAsync(data);
|
||||
if (!parseResult.success) {
|
||||
console.warn(
|
||||
'Could not parse lineup before saving to DB',
|
||||
parseResult.error,
|
||||
);
|
||||
}
|
||||
|
||||
return this.#adapter.write(JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
export function buildApiLineup(
|
||||
channel: Loaded<Channel, 'programs' | 'programs.customShows.uuid'>,
|
||||
lineup: LineupItem[],
|
||||
|
||||
@@ -1,44 +1,51 @@
|
||||
import { LineupSchedule } from '@tunarr/types/api';
|
||||
import {
|
||||
DynamicContentConfigSchema,
|
||||
LineupScheduleSchema,
|
||||
} from '@tunarr/types/api';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type Lineup = {
|
||||
items: LineupItem[];
|
||||
// Unsure if we want this DB type to reference the
|
||||
// API type, but for now it will work.
|
||||
schedule?: LineupSchedule;
|
||||
// These are precalculated offsets in milliseconds. The
|
||||
// array is a list of the running 'total' duration sum
|
||||
// of each of the lineup items. It can be used to quickly
|
||||
// determine a start timestamp for a given program by
|
||||
// pulling the offset at a given index and adding it to
|
||||
// a "start" time timestamp.
|
||||
startTimeOffsets?: number[];
|
||||
};
|
||||
const BaseLineupItemSchema = z.object({
|
||||
durationMs: z.number().positive(), // Add a max
|
||||
});
|
||||
|
||||
type BaseLineupItem = {
|
||||
durationMs: number;
|
||||
};
|
||||
export const ContentLineupItemSchema = z
|
||||
.object({
|
||||
type: z.literal('content'),
|
||||
id: z.string().min(1),
|
||||
// If this lineup item was a part of a custom show
|
||||
// this is a pointer to that show.
|
||||
// TODO: If a custom show is deleted, we have to remove
|
||||
// references to these content items in the lineup
|
||||
customShowId: z.string().uuid().optional(),
|
||||
})
|
||||
.merge(BaseLineupItemSchema);
|
||||
|
||||
// This item has to be hydrated from the DB
|
||||
export type ContentItem = BaseLineupItem & {
|
||||
type: 'content';
|
||||
id: string;
|
||||
// If this lineup item was a part of a custom show
|
||||
// this is a pointer to that show.
|
||||
// TODO: If a custom show is deleted, we have to remove
|
||||
// references to these content items in the lineup
|
||||
customShowId?: string;
|
||||
};
|
||||
export type ContentItem = z.infer<typeof ContentLineupItemSchema>;
|
||||
|
||||
export type OfflineItem = BaseLineupItem & {
|
||||
type: 'offline';
|
||||
};
|
||||
export const OfflineLineupItemSchema = z
|
||||
.object({
|
||||
type: z.literal('offline'),
|
||||
})
|
||||
.merge(BaseLineupItemSchema);
|
||||
|
||||
export type RedirectItem = BaseLineupItem & {
|
||||
type: 'redirect';
|
||||
channel: string;
|
||||
};
|
||||
export type OfflineItem = z.infer<typeof OfflineLineupItemSchema>;
|
||||
|
||||
export type LineupItem = ContentItem | OfflineItem | RedirectItem;
|
||||
export const RedirectLineupItemSchema = z
|
||||
.object({
|
||||
type: z.literal('redirect'),
|
||||
channel: z.string().uuid(),
|
||||
})
|
||||
.merge(BaseLineupItemSchema);
|
||||
export type RedirectItem = z.infer<typeof RedirectLineupItemSchema>;
|
||||
|
||||
export const LineupItemSchema = z.discriminatedUnion('type', [
|
||||
ContentLineupItemSchema,
|
||||
OfflineLineupItemSchema,
|
||||
RedirectLineupItemSchema,
|
||||
]);
|
||||
|
||||
export type LineupItem = z.infer<typeof LineupItemSchema>;
|
||||
|
||||
function isItemOfType<T extends LineupItem>(discrim: string) {
|
||||
return function (t: LineupItem | undefined): t is T {
|
||||
@@ -49,3 +56,27 @@ function isItemOfType<T extends LineupItem>(discrim: string) {
|
||||
export const isContentItem = isItemOfType<ContentItem>('content');
|
||||
export const isOfflineItem = isItemOfType<OfflineItem>('offline');
|
||||
export const isRedirectItem = isItemOfType<RedirectItem>('redirect');
|
||||
|
||||
export const LineupSchema = z.object({
|
||||
// The current lineup of a single cycle of this channel
|
||||
items: LineupItemSchema.array(),
|
||||
|
||||
// Defines rules for how to schedule content in the channel
|
||||
// Currently time-based and random-slot-based rulesets are
|
||||
// supported.
|
||||
// Unsure if we want this DB type to reference the
|
||||
// API type, but for now it will work.
|
||||
schedule: LineupScheduleSchema.optional(),
|
||||
|
||||
// These are precalculated offsets in milliseconds. The
|
||||
// array is a list of the running 'total' duration sum
|
||||
// of each of the lineup items. It can be used to quickly
|
||||
// determine a start timestamp for a given program by
|
||||
// pulling the offset at a given index and adding it to
|
||||
// a "start" time timestamp.
|
||||
startTimeOffsets: z.array(z.number()).optional(),
|
||||
|
||||
dynamicContentConfig: DynamicContentConfigSchema.optional(),
|
||||
});
|
||||
|
||||
export type Lineup = z.infer<typeof LineupSchema>;
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Unique,
|
||||
} from '@mikro-orm/core';
|
||||
import { Channel as ChannelDTO, Resolution } from '@tunarr/types';
|
||||
import type { Duration } from 'dayjs/plugin/duration.js';
|
||||
import { nilToUndefined } from '../../util.js';
|
||||
import { DurationType } from '../custom_types/DurationType.js';
|
||||
import { BaseEntity } from './BaseEntity.js';
|
||||
@@ -117,7 +116,7 @@ export class Channel extends BaseEntity {
|
||||
fillers = new Collection<FillerShow>(this);
|
||||
|
||||
@Property({ nullable: true, type: DurationType })
|
||||
fillerRepeatCooldown?: Duration;
|
||||
fillerRepeatCooldown?: number; // Seconds
|
||||
|
||||
@ManyToMany(() => CustomShow)
|
||||
customShows = new Collection<CustomShow>(this);
|
||||
|
||||
36
server/src/dao/programDB.ts
Normal file
36
server/src/dao/programDB.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { chunk, reduce } from 'lodash-es';
|
||||
import { flatMapAsyncSeq, groupByFunc } from '../util';
|
||||
import { getEm } from './dataSource';
|
||||
import { Program, programSourceTypeFromString } from './entities/Program';
|
||||
|
||||
export class ProgramDB {
|
||||
async lookupByExternalIds(
|
||||
ids: Set<[string, string, string]>,
|
||||
chunkSize: number = 25,
|
||||
) {
|
||||
const em = getEm();
|
||||
const results = await flatMapAsyncSeq(
|
||||
chunk([...ids], chunkSize),
|
||||
async (idChunk) => {
|
||||
return await reduce(
|
||||
idChunk,
|
||||
(acc, [ps, es, ek]) => {
|
||||
return acc.orWhere({
|
||||
sourceType: programSourceTypeFromString(ps)!,
|
||||
externalSourceId: es,
|
||||
externalKey: ek,
|
||||
});
|
||||
},
|
||||
em
|
||||
.qb(Program)
|
||||
.select(['uuid', 'sourceType', 'externalSourceId', 'externalKey']),
|
||||
);
|
||||
},
|
||||
);
|
||||
return groupByFunc(
|
||||
results,
|
||||
(r) => r.uniqueId(),
|
||||
(r) => r.toDTO(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { XMLParser } from 'fast-xml-parser';
|
||||
import { isNil, isUndefined } from 'lodash-es';
|
||||
import NodeCache from 'node-cache';
|
||||
import querystring, { ParsedUrlQueryInput } from 'querystring';
|
||||
import { MarkOptional } from 'ts-essentials';
|
||||
import { PlexServerSettings } from './dao/entities/PlexServerSettings.js';
|
||||
import createLogger from './logger.js';
|
||||
import { Maybe } from './types.js';
|
||||
@@ -28,9 +29,12 @@ type AxiosConfigWithMetadata = InternalAxiosRequestConfig & {
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
|
||||
type PlexApiOptions = Pick<
|
||||
EntityDTO<PlexServerSettings>,
|
||||
'accessToken' | 'uri'
|
||||
type PlexApiOptions = MarkOptional<
|
||||
Pick<
|
||||
EntityDTO<PlexServerSettings>,
|
||||
'accessToken' | 'uri' | 'name' | 'clientIdentifier'
|
||||
>,
|
||||
'clientIdentifier'
|
||||
>;
|
||||
|
||||
class PlexApiFactoryImpl {
|
||||
@@ -51,10 +55,12 @@ class PlexApiFactoryImpl {
|
||||
export const PlexApiFactory = new PlexApiFactoryImpl();
|
||||
|
||||
export class Plex {
|
||||
#opts: PlexApiOptions;
|
||||
private axiosInstance: AxiosInstance;
|
||||
private _accessToken: string;
|
||||
|
||||
constructor(opts: PlexApiOptions) {
|
||||
this.#opts = opts;
|
||||
this._accessToken = opts.accessToken;
|
||||
const uri = opts.uri.endsWith('/')
|
||||
? opts.uri.slice(0, opts.uri.length - 1)
|
||||
@@ -106,6 +112,10 @@ export class Plex {
|
||||
);
|
||||
}
|
||||
|
||||
get serverName() {
|
||||
return this.#opts.name;
|
||||
}
|
||||
|
||||
private async doRequest<T>(req: AxiosRequestConfig): Promise<Maybe<T>> {
|
||||
try {
|
||||
const response = await this.axiosInstance.request<T>(req);
|
||||
|
||||
@@ -79,7 +79,7 @@ export class PlexTranscoder {
|
||||
|
||||
constructor(
|
||||
clientId: string,
|
||||
server: DeepReadonly<PlexServerSettings>,
|
||||
server: PlexServerSettings,
|
||||
settings: DeepReadonly<PlexStreamSettings>,
|
||||
channel: ContextChannel,
|
||||
lineupItem: ContentBackedStreamLineupItem,
|
||||
|
||||
@@ -33,7 +33,7 @@ import { getSettingsRawDb } from './dao/settings.js';
|
||||
import { serverOptions } from './globals.js';
|
||||
import createLogger from './logger.js';
|
||||
import { ServerRequestContext, serverContext } from './serverContext.js';
|
||||
import { scheduleJobs, scheduledJobsById } from './services/scheduler.js';
|
||||
import { GlobalScheduler, scheduleJobs } from './services/scheduler.js';
|
||||
import { runFixers } from './tasks/fixers/index.js';
|
||||
import { UpdateXmlTvTask } from './tasks/updateXmlTvTask.js';
|
||||
import { ServerOptions } from './types.js';
|
||||
@@ -113,7 +113,9 @@ export async function initServer(opts: ServerOptions) {
|
||||
scheduleJobs(ctx);
|
||||
await runFixers();
|
||||
|
||||
const updateXMLPromise = scheduledJobsById[UpdateXmlTvTask.ID]!.runNow();
|
||||
const updateXMLPromise = GlobalScheduler.getScheduledJob(
|
||||
UpdateXmlTvTask.ID,
|
||||
).runNow();
|
||||
|
||||
const app = fastify({ logger: false, bodyLimit: 50 * 1024 * 1024 })
|
||||
.setValidatorCompiler(validatorCompiler)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ChannelDB } from './dao/channelDb.js';
|
||||
import { CustomShowDB } from './dao/customShowDb.js';
|
||||
import { FillerDB } from './dao/fillerDb.js';
|
||||
import { PlexServerDB } from './dao/plexServerDb.js';
|
||||
import { ProgramDB } from './dao/programDB.js';
|
||||
import { Settings, getSettings } from './dao/settings.js';
|
||||
import { serverOptions } from './globals.js';
|
||||
import { HdhrService } from './hdhr.js';
|
||||
@@ -30,6 +31,7 @@ export type ServerContext = {
|
||||
xmltv: XmlTvWriter;
|
||||
plexServerDB: PlexServerDB;
|
||||
settings: Settings;
|
||||
programDB: ProgramDB;
|
||||
};
|
||||
|
||||
export const serverContext: () => Promise<ServerContext> = once(async () => {
|
||||
@@ -77,6 +79,7 @@ export const serverContext: () => Promise<ServerContext> = once(async () => {
|
||||
xmltv,
|
||||
plexServerDB: new PlexServerDB(channelDB),
|
||||
settings,
|
||||
programDB: new ProgramDB(),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
89
server/src/services/PlexItemEnumerator.ts
Normal file
89
server/src/services/PlexItemEnumerator.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { createExternalId } from '@tunarr/shared';
|
||||
import {
|
||||
PlexChildMediaViewType,
|
||||
PlexLibrarySection,
|
||||
PlexMedia,
|
||||
PlexTerminalMedia,
|
||||
isPlexDirectory,
|
||||
isTerminalItem,
|
||||
} from '@tunarr/types/plex';
|
||||
import { isNil, uniqBy } from 'lodash-es';
|
||||
import map from 'lodash-es/map';
|
||||
import { ProgramDB } from '../dao/programDB';
|
||||
import { Plex } from '../plex';
|
||||
import { typedProperty } from '../types/path';
|
||||
import { flatMapAsyncSeq } from '../util';
|
||||
|
||||
type EnrichedPlexTerminalMedia = PlexTerminalMedia & {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export class PlexItemEnumerator {
|
||||
#plex: Plex;
|
||||
#programDB: ProgramDB;
|
||||
|
||||
constructor(plex: Plex, programDB: ProgramDB) {
|
||||
this.#plex = plex;
|
||||
this.#programDB = programDB;
|
||||
}
|
||||
|
||||
async enumerateItems(initialItems: (PlexMedia | PlexLibrarySection)[]) {
|
||||
const allItems = await flatMapAsyncSeq(initialItems, (item) =>
|
||||
this.enumerateItem(item),
|
||||
);
|
||||
return uniqBy(allItems, typedProperty('key'));
|
||||
}
|
||||
|
||||
async enumerateItem(
|
||||
initialItem: PlexMedia | PlexLibrarySection,
|
||||
): Promise<EnrichedPlexTerminalMedia[]> {
|
||||
const loopInner = async (
|
||||
item: PlexMedia | PlexLibrarySection,
|
||||
): Promise<PlexTerminalMedia[]> => {
|
||||
if (isTerminalItem(item)) {
|
||||
return [item];
|
||||
} else if (isPlexDirectory(item)) {
|
||||
return [];
|
||||
} else {
|
||||
const plexResult = await this.#plex.doGet<PlexChildMediaViewType>(
|
||||
item.key,
|
||||
);
|
||||
|
||||
if (isNil(plexResult)) {
|
||||
// TODO Log
|
||||
return [];
|
||||
}
|
||||
|
||||
const results: EnrichedPlexTerminalMedia[] = [];
|
||||
for (const listing of plexResult.Metadata) {
|
||||
results.push(...(await loopInner(listing)));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
};
|
||||
|
||||
const res = await loopInner(initialItem);
|
||||
const externalIds: [string, string, string][] = res.map(
|
||||
(m) => ['plex', this.#plex.serverName, m.key] as const,
|
||||
);
|
||||
|
||||
// This is best effort - if the user saves these IDs later, the upsert
|
||||
// logic should figure out what is new/existing
|
||||
try {
|
||||
const existingIdsByExternalId = await this.#programDB.lookupByExternalIds(
|
||||
new Set(externalIds),
|
||||
);
|
||||
return map(res, (media) => ({
|
||||
...media,
|
||||
id: existingIdsByExternalId[
|
||||
createExternalId('plex', this.#plex.serverName, media.key)
|
||||
]?.id,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Unable to retrieve IDs in batch', e);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
88
server/src/services/ScheduledTask.ts
Normal file
88
server/src/services/ScheduledTask.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import schedule from 'node-schedule';
|
||||
import { withDb } from '../dao/dataSource.js';
|
||||
import { Task } from '../tasks/task.js';
|
||||
import { Maybe } from '../types.js';
|
||||
import { logger } from './scheduler.js';
|
||||
|
||||
export type TaskFactoryFn<T> = () => Task<T>;
|
||||
|
||||
type ScheduledTaskOptions = {
|
||||
visible?: boolean;
|
||||
runOnSchedule?: boolean;
|
||||
};
|
||||
|
||||
export class ScheduledTask<OutType = unknown> {
|
||||
#factory: TaskFactoryFn<OutType>;
|
||||
#scheduledJob: schedule.Job;
|
||||
#running: boolean = false;
|
||||
#schedule: string;
|
||||
|
||||
public visible: boolean = true;
|
||||
public lastExecution?: Date;
|
||||
|
||||
constructor(
|
||||
jobName: string,
|
||||
cronSchedule: string,
|
||||
taskFactory: TaskFactoryFn<OutType>,
|
||||
options?: ScheduledTaskOptions,
|
||||
) {
|
||||
this.#schedule = cronSchedule;
|
||||
this.#factory = taskFactory;
|
||||
this.#scheduledJob = schedule.scheduleJob(jobName, cronSchedule, () =>
|
||||
this.jobInternal(),
|
||||
);
|
||||
|
||||
this.visible = options?.visible ?? true;
|
||||
|
||||
if (options?.runOnSchedule) {
|
||||
this.runNow(true).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.#scheduledJob.name;
|
||||
}
|
||||
|
||||
running() {
|
||||
return this.#running;
|
||||
}
|
||||
|
||||
// Runs an instance of this task now, cancels the next invocation
|
||||
// and reschedules the job on the original schedule.
|
||||
// If background=true, this function will not return the underlying
|
||||
// Promise generated by the running job and all errors will be swallowed.
|
||||
async runNow(background: boolean = true) {
|
||||
this.#scheduledJob.cancelNext(false);
|
||||
const rescheduleCb = () => this.#scheduledJob.reschedule(this.#schedule);
|
||||
if (background) {
|
||||
return new Promise<Maybe<OutType>>((resolve, reject) => {
|
||||
this.jobInternal().then(resolve).catch(reject).finally(rescheduleCb);
|
||||
});
|
||||
} else {
|
||||
return this.jobInternal(true).finally(rescheduleCb);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.#scheduledJob.cancel();
|
||||
}
|
||||
|
||||
nextExecution() {
|
||||
return this.#scheduledJob.nextInvocation();
|
||||
}
|
||||
|
||||
private async jobInternal(rethrow: boolean = false) {
|
||||
this.#running = true;
|
||||
const instance = this.#factory();
|
||||
try {
|
||||
return withDb(async () => await instance.run());
|
||||
} catch (e) {
|
||||
logger.error('Error while running job: %s; %O', instance.taskName, e);
|
||||
if (rethrow) throw e;
|
||||
return;
|
||||
} finally {
|
||||
this.#running = false;
|
||||
this.lastExecution = new Date();
|
||||
}
|
||||
}
|
||||
}
|
||||
51
server/src/services/dynamic_channels/ContentSourceUpdater.ts
Normal file
51
server/src/services/dynamic_channels/ContentSourceUpdater.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Loaded } from '@mikro-orm/core';
|
||||
import { DynamicContentConfigSource } from '@tunarr/types/api';
|
||||
import { Mutex, withTimeout } from 'async-mutex';
|
||||
import { EntityManager, withDb } from '../../dao/dataSource';
|
||||
import { Channel } from '../../dao/entities/Channel';
|
||||
|
||||
const locks: Record<DynamicContentConfigSource['type'], Mutex> = {
|
||||
plex: new Mutex(),
|
||||
};
|
||||
|
||||
export abstract class ContentSourceUpdater<
|
||||
T extends DynamicContentConfigSource,
|
||||
> {
|
||||
protected initialized: boolean = false;
|
||||
protected channel: Loaded<Channel>;
|
||||
protected config: T;
|
||||
|
||||
constructor(channel: Loaded<Channel>, config: T) {
|
||||
this.channel = channel;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public update(): Promise<void> {
|
||||
return this.runInternal();
|
||||
}
|
||||
|
||||
private async runInternal() {
|
||||
return withTimeout(locks[this.config.type], 60 * 1000).runExclusive(
|
||||
async () => {
|
||||
return await withDb(async (em) => {
|
||||
await this.prepare(em);
|
||||
return await this.run();
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementations should use this function to prepare any
|
||||
* depedencies or internal resources before the updater runs.
|
||||
* This is run in a DB request context and an entity manager is
|
||||
* provided.
|
||||
*/
|
||||
protected abstract prepare(em: EntityManager): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update the content...
|
||||
* TODO figure out if we can generalize enough to just return programs here
|
||||
*/
|
||||
protected abstract run(): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Loaded } from '@mikro-orm/core';
|
||||
import { DynamicContentConfigSource } from '@tunarr/types/api';
|
||||
import { Channel } from '../../dao/entities/Channel';
|
||||
import { ContentSourceUpdater } from './ContentSourceUpdater';
|
||||
import { PlexContentSourceUpdater } from './PlexContentSourceUpdater';
|
||||
|
||||
export class ContentSourceUpdaterFactory {
|
||||
static getUpdater(
|
||||
channel: Loaded<Channel>,
|
||||
config: DynamicContentConfigSource,
|
||||
): ContentSourceUpdater<DynamicContentConfigSource> {
|
||||
switch (config.type) {
|
||||
case 'plex':
|
||||
return new PlexContentSourceUpdater(channel, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Loaded } from '@mikro-orm/core';
|
||||
import { createExternalId } from '@tunarr/shared';
|
||||
import { buildPlexFilterKey } from '@tunarr/shared/util';
|
||||
import { ContentProgram } from '@tunarr/types';
|
||||
import { DynamicContentConfigPlexSource } from '@tunarr/types/api';
|
||||
import { PlexLibraryListing } from '@tunarr/types/plex';
|
||||
import { isNil, map } from 'lodash-es';
|
||||
import { ChannelDB } from '../../dao/channelDb.js';
|
||||
import { EntityManager } from '../../dao/dataSource.js';
|
||||
import { Channel } from '../../dao/entities/Channel.js';
|
||||
import { PlexServerSettings } from '../../dao/entities/PlexServerSettings.js';
|
||||
import { ProgramDB } from '../../dao/programDB.js';
|
||||
import { Plex } from '../../plex.js';
|
||||
import { PlexItemEnumerator } from '../PlexItemEnumerator.js';
|
||||
import { ContentSourceUpdater } from './ContentSourceUpdater.js';
|
||||
|
||||
export class PlexContentSourceUpdater extends ContentSourceUpdater<DynamicContentConfigPlexSource> {
|
||||
#plex: Plex;
|
||||
#channelDB: ChannelDB;
|
||||
|
||||
constructor(
|
||||
channel: Loaded<Channel>,
|
||||
config: DynamicContentConfigPlexSource,
|
||||
) {
|
||||
super(channel, config);
|
||||
this.#channelDB = new ChannelDB();
|
||||
}
|
||||
|
||||
protected async prepare(em: EntityManager) {
|
||||
const server = await em.repo(PlexServerSettings).findOneOrFail({
|
||||
$or: [
|
||||
{ name: this.config.plexServerId },
|
||||
{ clientIdentifier: this.config.plexServerId },
|
||||
],
|
||||
});
|
||||
|
||||
this.#plex = new Plex(server);
|
||||
}
|
||||
|
||||
protected async run() {
|
||||
const filter = buildPlexFilterKey(this.config.search?.filter);
|
||||
|
||||
// TODO page through the results
|
||||
const plexResult = await this.#plex.doGet<PlexLibraryListing>(
|
||||
`/library/sections/${this.config.plexLibraryKey}/all?${filter.join('&')}`,
|
||||
);
|
||||
|
||||
const enumerator = new PlexItemEnumerator(this.#plex, new ProgramDB());
|
||||
|
||||
const enumeratedItems = await enumerator.enumerateItems(
|
||||
plexResult?.Metadata ?? [],
|
||||
);
|
||||
|
||||
const programs: ContentProgram[] = map(enumeratedItems, (media) => {
|
||||
const uniqueId = createExternalId(
|
||||
'plex',
|
||||
this.#plex.serverName,
|
||||
media.key,
|
||||
);
|
||||
return {
|
||||
id: media.id ?? uniqueId,
|
||||
persisted: !isNil(media.id),
|
||||
originalProgram: media,
|
||||
duration: media.duration,
|
||||
externalSourceName: this.#plex.serverName,
|
||||
externalSourceType: 'plex',
|
||||
externalKey: media.key,
|
||||
uniqueId: uniqueId,
|
||||
type: 'content',
|
||||
subtype: media.type,
|
||||
title: media.type === 'episode' ? media.grandparentTitle : media.title,
|
||||
episodeTitle: media.type === 'episode' ? media.title : undefined,
|
||||
episodeNumber: media.type === 'episode' ? media.index : undefined,
|
||||
seasonNumber: media.type === 'episode' ? media.parentIndex : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(programs.length);
|
||||
|
||||
await this.#channelDB.updateLineup(this.channel.uuid, {
|
||||
type: 'manual',
|
||||
lineup: [],
|
||||
programs: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,93 @@
|
||||
import { once } from 'lodash-es';
|
||||
import schedule from 'node-schedule';
|
||||
import { withDb } from '../dao/dataSource.js';
|
||||
import { isString, once, pickBy } from 'lodash-es';
|
||||
import createLogger from '../logger.js';
|
||||
import { ServerContext } from '../serverContext.js';
|
||||
import { CleanupSessionsTask } from '../tasks/cleanupSessionsTask.js';
|
||||
import { ScheduleDynamicChannelsTask } from '../tasks/scheduleDynamicChannelsTask.js';
|
||||
import { Task, TaskId } from '../tasks/task.js';
|
||||
import { UpdateXmlTvTask } from '../tasks/updateXmlTvTask.js';
|
||||
import { Maybe } from '../types.js';
|
||||
import { typedProperty } from '../types/path.js';
|
||||
import { Tag } from '../types/util.js';
|
||||
import { ScheduledTask } from './ScheduledTask.js';
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
export const logger = createLogger(import.meta);
|
||||
|
||||
type TaskFactoryFn<Data> = () => Task<Data>;
|
||||
class Scheduler {
|
||||
#scheduledJobsById: Record<string, ScheduledTask> = {};
|
||||
|
||||
class ScheduledTask<Data> {
|
||||
#factory: TaskFactoryFn<Data>;
|
||||
#scheduledJob: schedule.Job;
|
||||
#running: boolean = false;
|
||||
#schedule: string;
|
||||
public lastExecution?: Date;
|
||||
|
||||
constructor(
|
||||
jobName: string,
|
||||
cronSchedule: string,
|
||||
taskFactory: TaskFactoryFn<Data>,
|
||||
) {
|
||||
this.#schedule = cronSchedule;
|
||||
this.#factory = taskFactory;
|
||||
this.#scheduledJob = schedule.scheduleJob(jobName, cronSchedule, () =>
|
||||
this.jobInternal(),
|
||||
);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.#scheduledJob.name;
|
||||
}
|
||||
|
||||
running() {
|
||||
return this.#running;
|
||||
}
|
||||
|
||||
// Runs an instance of this task now, cancels the next invocation
|
||||
// and reschedules the job on the original schedule.
|
||||
// If background=true, this function will not return the underlying
|
||||
// Promise generated by the running job and all errors will be swallowed.
|
||||
async runNow(background: boolean = true) {
|
||||
this.#scheduledJob.cancelNext(false);
|
||||
const rescheduleCb = () => this.#scheduledJob.reschedule(this.#schedule);
|
||||
if (background) {
|
||||
await this.jobInternal().finally(rescheduleCb);
|
||||
return;
|
||||
// TaskId values always have an associated task (after server startup)
|
||||
getScheduledJob<
|
||||
Id extends TaskId,
|
||||
OutType = Id extends Tag<TaskId, infer Out> ? Out : unknown,
|
||||
>(id: TaskId): ScheduledTask<OutType>;
|
||||
getScheduledJob<OutType = unknown>(id: string): Maybe<ScheduledTask<OutType>>;
|
||||
getScheduledJob<OutType = unknown>(
|
||||
id: Task<OutType> | string,
|
||||
): Maybe<ScheduledTask<OutType>> {
|
||||
if (isString(id)) {
|
||||
return this.#scheduledJobsById[id] as Maybe<ScheduledTask<OutType>>;
|
||||
} else {
|
||||
return this.jobInternal(true).finally(rescheduleCb);
|
||||
return this.getScheduledJob(id.ID);
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.#scheduledJob.cancel();
|
||||
}
|
||||
|
||||
nextExecution() {
|
||||
return this.#scheduledJob.nextInvocation();
|
||||
}
|
||||
|
||||
private async jobInternal(rethrow: boolean = false) {
|
||||
this.#running = true;
|
||||
const instance = this.#factory();
|
||||
try {
|
||||
return withDb(async () => await instance.run());
|
||||
} catch (e) {
|
||||
logger.error('Error while running job: %s; %O', instance.name, e);
|
||||
if (rethrow) throw e;
|
||||
return;
|
||||
} finally {
|
||||
this.#running = false;
|
||||
this.lastExecution = new Date();
|
||||
scheduleTask(
|
||||
id: string,
|
||||
task: ScheduledTask,
|
||||
overwrite: boolean = true,
|
||||
): boolean {
|
||||
if (!overwrite && this.#scheduledJobsById[id]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.#scheduledJobsById[id] = task;
|
||||
return true;
|
||||
}
|
||||
|
||||
get scheduledJobsById(): Record<string, ScheduledTask> {
|
||||
return pickBy(this.#scheduledJobsById, typedProperty('visible'));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: It is annoying that this has to be partial
|
||||
export const scheduledJobsById: Partial<
|
||||
Record<TaskId, ScheduledTask<unknown>>
|
||||
> = {};
|
||||
export const GlobalScheduler = new Scheduler();
|
||||
|
||||
export const scheduleJobs = once((serverContext: ServerContext) => {
|
||||
const xmlTvSettings = serverContext.settings.xmlTvSettings();
|
||||
|
||||
scheduledJobsById[UpdateXmlTvTask.ID] = new ScheduledTask(
|
||||
UpdateXmlTvTask.name,
|
||||
hoursCrontab(xmlTvSettings.refreshHours),
|
||||
() => UpdateXmlTvTask.create(serverContext),
|
||||
GlobalScheduler.scheduleTask(
|
||||
UpdateXmlTvTask.ID,
|
||||
new ScheduledTask(
|
||||
UpdateXmlTvTask.name,
|
||||
hoursCrontab(xmlTvSettings.refreshHours),
|
||||
() => UpdateXmlTvTask.create(serverContext),
|
||||
),
|
||||
);
|
||||
|
||||
scheduledJobsById[CleanupSessionsTask.ID] = new ScheduledTask(
|
||||
CleanupSessionsTask.name,
|
||||
minutesCrontab(30),
|
||||
() => new CleanupSessionsTask(),
|
||||
GlobalScheduler.scheduleTask(
|
||||
CleanupSessionsTask.ID,
|
||||
new ScheduledTask(
|
||||
CleanupSessionsTask.name,
|
||||
minutesCrontab(30),
|
||||
() => new CleanupSessionsTask(),
|
||||
),
|
||||
);
|
||||
|
||||
GlobalScheduler.scheduleTask(
|
||||
ScheduleDynamicChannelsTask.ID,
|
||||
new ScheduledTask(
|
||||
ScheduleDynamicChannelsTask.name,
|
||||
// Temporary
|
||||
hoursCrontab(1),
|
||||
() => ScheduleDynamicChannelsTask.create(serverContext.channelDB),
|
||||
{
|
||||
runOnSchedule: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
function hoursCrontab(hours: number): string {
|
||||
return `0 0 0/${hours} * * *`;
|
||||
return `0 0 */${hours} * * *`;
|
||||
}
|
||||
|
||||
function minutesCrontab(mins: number): string {
|
||||
|
||||
@@ -156,7 +156,7 @@ export class TVGuideService {
|
||||
* @returns The current cached guide
|
||||
*/
|
||||
async get() {
|
||||
if (this.cachedGuide !== null) {
|
||||
if (!isNil(this.cachedGuide)) {
|
||||
return this.cachedGuide;
|
||||
}
|
||||
|
||||
@@ -232,11 +232,13 @@ export class TVGuideService {
|
||||
);
|
||||
}
|
||||
|
||||
const { channel, programs } = this.cachedGuide[channelId];
|
||||
if (isNil(channel)) {
|
||||
const channelAndLineup = this.cachedGuide[channelId];
|
||||
if (isNil(channelAndLineup)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { channel, programs } = channelAndLineup;
|
||||
|
||||
const result: ChannelLineup = {
|
||||
icon: channel.icon,
|
||||
name: channel.name,
|
||||
|
||||
@@ -48,7 +48,7 @@ export class CleanupSessionsTask extends Task<void> {
|
||||
});
|
||||
}
|
||||
|
||||
get name() {
|
||||
get taskName() {
|
||||
return CleanupSessionsTask.name;
|
||||
}
|
||||
}
|
||||
|
||||
79
server/src/tasks/scheduleDynamicChannelsTask.ts
Normal file
79
server/src/tasks/scheduleDynamicChannelsTask.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Loaded } from '@mikro-orm/core';
|
||||
import { DynamicContentConfigSource } from '@tunarr/types/api';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import filter from 'lodash-es/filter';
|
||||
import { ChannelDB } from '../dao/channelDb';
|
||||
import { Channel } from '../dao/entities/Channel';
|
||||
import { ScheduledTask } from '../services/ScheduledTask';
|
||||
import { ContentSourceUpdaterFactory } from '../services/dynamic_channels/ContentSourceUpdaterFactory';
|
||||
import { GlobalScheduler } from '../services/scheduler';
|
||||
import { Maybe } from '../types';
|
||||
import { Task, TaskId } from './task';
|
||||
|
||||
export class ScheduleDynamicChannelsTask extends Task<void> {
|
||||
public static ID: TaskId = 'schedule-dynamic-channels';
|
||||
|
||||
#channelsDb: ChannelDB;
|
||||
#taskFactory: DynamicChannelUpdaterFactory;
|
||||
|
||||
public ID = ScheduleDynamicChannelsTask.ID;
|
||||
public taskName = ScheduleDynamicChannelsTask.name;
|
||||
|
||||
static create(channelsDb: ChannelDB) {
|
||||
return new ScheduleDynamicChannelsTask(channelsDb);
|
||||
}
|
||||
|
||||
private constructor(channelsDb: ChannelDB) {
|
||||
super();
|
||||
this.#channelsDb = channelsDb;
|
||||
this.#taskFactory = new DynamicChannelUpdaterFactory();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
protected async runInternal(): Promise<Maybe<void>> {
|
||||
const lineups = await this.#channelsDb.loadAllLineups();
|
||||
const dynamicLineups = filter(
|
||||
lineups,
|
||||
({ lineup }) =>
|
||||
!isUndefined(lineup.dynamicContentConfig) &&
|
||||
lineup.dynamicContentConfig.enabled,
|
||||
);
|
||||
|
||||
for (const { channel, lineup } of dynamicLineups) {
|
||||
for (const source of lineup.dynamicContentConfig!.contentSources) {
|
||||
if (!source.enabled) {
|
||||
continue;
|
||||
}
|
||||
const scheduled = GlobalScheduler.scheduleTask(
|
||||
source.updater._id,
|
||||
new ScheduledTask(
|
||||
'UpdateDynamicChannel',
|
||||
source.updater.schedule,
|
||||
() => this.#taskFactory.getTask(channel, source),
|
||||
),
|
||||
);
|
||||
console.log('scheduling task = ' + scheduled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicChannelUpdaterFactory {
|
||||
getTask(
|
||||
channel: Loaded<Channel>,
|
||||
contentSourceDef: DynamicContentConfigSource,
|
||||
): Task<unknown> {
|
||||
// This won't always be anonymous
|
||||
return new (class extends Task<unknown> {
|
||||
public ID = contentSourceDef.updater._id;
|
||||
public taskName = `AnonymousTest_` + contentSourceDef.updater._id;
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
protected async runInternal() {
|
||||
return ContentSourceUpdaterFactory.getUpdater(
|
||||
channel,
|
||||
contentSourceDef,
|
||||
).update();
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { isError, isString, round } from 'lodash-es';
|
||||
import createLogger from '../logger.js';
|
||||
import { Maybe } from '../types.js';
|
||||
import { Tag } from '../types/util.js';
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
|
||||
// Set of all of the possible Task IDs
|
||||
export type TaskId = 'update-xmltv' | 'cleanup-sessions';
|
||||
export type TaskId =
|
||||
| 'update-xmltv'
|
||||
| 'cleanup-sessions'
|
||||
| 'schedule-dynamic-channels';
|
||||
|
||||
export abstract class Task<Data> {
|
||||
private running_ = false;
|
||||
@@ -13,7 +17,7 @@ export abstract class Task<Data> {
|
||||
protected hasRun: boolean = false;
|
||||
protected result: Maybe<Data>;
|
||||
|
||||
public abstract ID: TaskId;
|
||||
public abstract ID: string | Tag<TaskId, Data>;
|
||||
|
||||
protected abstract runInternal(): Promise<Maybe<Data>>;
|
||||
|
||||
@@ -50,5 +54,16 @@ export abstract class Task<Data> {
|
||||
return this.running_;
|
||||
}
|
||||
|
||||
abstract get name(): string;
|
||||
abstract get taskName(): string;
|
||||
}
|
||||
|
||||
export function AnonymousTask(id: string): Task<unknown> {
|
||||
return new (class extends Task<unknown> {
|
||||
public ID = id;
|
||||
public taskName = `AnonymousTest_` + id;
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
protected async runInternal() {
|
||||
console.log('hello girly');
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Loaded } from '@mikro-orm/core';
|
||||
import { PlexDvr } from '@tunarr/types/plex';
|
||||
import dayjs from 'dayjs';
|
||||
import { ChannelCache } from '../channelCache.js';
|
||||
import { withDb } from '../dao/dataSource.js';
|
||||
@@ -10,8 +11,9 @@ import { Plex } from '../plex.js';
|
||||
import { ServerContext } from '../serverContext.js';
|
||||
import { TVGuideService } from '../services/tvGuideService.js';
|
||||
import { Maybe } from '../types.js';
|
||||
import { Tag } from '../types/util.js';
|
||||
import { mapAsyncSeq } from '../util.js';
|
||||
import { Task, TaskId } from './task.js';
|
||||
import { Task } from './task.js';
|
||||
|
||||
const logger = createLogger(import.meta);
|
||||
|
||||
@@ -20,9 +22,8 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
private dbAccess: Settings;
|
||||
private guideService: TVGuideService;
|
||||
|
||||
public static ID: TaskId = 'update-xmltv';
|
||||
public ID: TaskId = UpdateXmlTvTask.ID;
|
||||
public static name = 'Update XMLTV';
|
||||
public static ID = 'update-xmltv' as Tag<'update-xmltv', void>;
|
||||
public ID = UpdateXmlTvTask.ID;
|
||||
|
||||
static create(serverContext: ServerContext): UpdateXmlTvTask {
|
||||
return new UpdateXmlTvTask(
|
||||
@@ -43,7 +44,7 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
this.guideService = guideService;
|
||||
}
|
||||
|
||||
get name() {
|
||||
get taskName() {
|
||||
return UpdateXmlTvTask.name;
|
||||
}
|
||||
|
||||
@@ -62,7 +63,6 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
await this.guideService.refreshGuide(
|
||||
dayjs.duration({ hours: xmltvSettings.programmingHours }),
|
||||
);
|
||||
// xmltvInterval.lastRefresh = new Date();
|
||||
|
||||
logger.info('XMLTV Updated at ' + new Date().toLocaleString());
|
||||
} catch (err) {
|
||||
@@ -78,12 +78,13 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
|
||||
await mapAsyncSeq(allPlexServers, undefined, async (plexServer) => {
|
||||
const plex = new Plex(plexServer);
|
||||
let dvrs;
|
||||
let dvrs: PlexDvr[] = [];
|
||||
|
||||
if (!plexServer.sendGuideUpdates && !plexServer.sendChannelUpdates) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
dvrs = await plex.getDvrs(); // Refresh guide and channel mappings
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
@@ -92,6 +93,11 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dvrs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (plexServer.sendGuideUpdates) {
|
||||
try {
|
||||
await plex.refreshGuide(dvrs);
|
||||
@@ -102,6 +108,7 @@ export class UpdateXmlTvTask extends Task<void> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (plexServer.sendChannelUpdates && channels.length !== 0) {
|
||||
try {
|
||||
await plex.refreshChannels(channels, dvrs);
|
||||
|
||||
2
server/src/types/util.ts
Normal file
2
server/src/types/util.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const tagSymbol: unique symbol;
|
||||
export type Tag<Typ, T> = Typ & { [tagSymbol]: T };
|
||||
@@ -96,10 +96,14 @@ export function groupByUniqAndMap<
|
||||
}
|
||||
|
||||
export async function mapAsyncSeq<T, U>(
|
||||
seq: ReadonlyArray<T>,
|
||||
seq: T[] | null | undefined,
|
||||
ms: number | undefined,
|
||||
itemFn: (item: T) => Promise<U>,
|
||||
): Promise<U[]> {
|
||||
if (isNil(seq)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const all = await seq.reduce(
|
||||
async (prev, item) => {
|
||||
const last = await prev;
|
||||
@@ -175,7 +179,7 @@ Array.prototype.mapAsyncSeq = async function <T, U>(
|
||||
itemFn: (item: T) => Promise<U>,
|
||||
ms: number | undefined,
|
||||
) {
|
||||
return mapAsyncSeq(this as ReadonlyArray<T>, ms, itemFn);
|
||||
return mapAsyncSeq(this as T[], ms, itemFn);
|
||||
};
|
||||
|
||||
export function time<T>(
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import inst, { Dayjs, ManipulateType, PluginFunc, isDuration } from 'dayjs';
|
||||
import duration, { Duration } from 'dayjs/plugin/duration.js';
|
||||
|
||||
declare module 'dayjs' {
|
||||
interface Dayjs {
|
||||
mod(value: number, unit?: inst.ManipulateType): duration.Duration;
|
||||
mod(value: duration.Duration): duration.Duration;
|
||||
}
|
||||
}
|
||||
|
||||
export const mod: PluginFunc = (_opts, dayjsClass, dayjsFactory) => {
|
||||
dayjsFactory.extend(duration);
|
||||
|
||||
dayjsClass.prototype['mod'] = function (
|
||||
value: number | Duration,
|
||||
unit?: ManipulateType,
|
||||
) {
|
||||
let dur: Duration;
|
||||
if (isDuration(value)) {
|
||||
dur = value;
|
||||
} else {
|
||||
dur = dayjsFactory.duration(value, unit ?? 'milliseconds');
|
||||
}
|
||||
|
||||
const djs = this as Dayjs;
|
||||
|
||||
return dayjsFactory.duration((djs.unix() * 1000) % dur.asMilliseconds());
|
||||
};
|
||||
};
|
||||
|
||||
export const min = (l: duration.Duration, r: duration.Duration) => {
|
||||
return l.asMilliseconds() < r.asMilliseconds() ? l : r;
|
||||
};
|
||||
|
||||
export const max = (l: duration.Duration, r: duration.Duration) => {
|
||||
return l.asMilliseconds() >= r.asMilliseconds() ? l : r;
|
||||
};
|
||||
@@ -31,16 +31,18 @@
|
||||
"noErrorTruncation": true
|
||||
},
|
||||
"files": [
|
||||
"src/types/fastify.d.ts"
|
||||
"src/types/fastify.d.ts",
|
||||
],
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./scripts/**/*.ts",
|
||||
"__tests__/**/*",
|
||||
"mikro-orm.prod.config.ts"
|
||||
"mikro-orm.prod.config.ts",
|
||||
"../types/src/**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"./build/**/*",
|
||||
"../types/build/**/*",
|
||||
"./**/*.ignore.ts",
|
||||
"./streams/**/*.ts"
|
||||
]
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@types/node": "18.18.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "5.2.2",
|
||||
"typescript": "^5.4.3",
|
||||
"ts-essentials": "^9.4.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import inst, { Dayjs, ManipulateType, PluginFunc } from 'dayjs';
|
||||
import duration, { type Duration } from 'dayjs/plugin/duration.js';
|
||||
import duration from 'dayjs/plugin/duration.js';
|
||||
|
||||
declare module 'dayjs' {
|
||||
interface Dayjs {
|
||||
@@ -19,10 +19,10 @@ export const mod: PluginFunc = (_opts, dayjsClass, dayjsFactory) => {
|
||||
// be timezone adjusted (i.e. duration - new Date().getTimezoneOffset())
|
||||
// before being used with new timestamps.
|
||||
dayjsClass.prototype['mod'] = function (
|
||||
value: number | Duration,
|
||||
value: number | duration.Duration,
|
||||
unit?: ManipulateType,
|
||||
) {
|
||||
let dur: Duration;
|
||||
let dur: duration.Duration;
|
||||
if (dayjsFactory.isDuration(value)) {
|
||||
dur = value;
|
||||
} else {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './plexSearchUtil.js';
|
||||
import { ChannelProgram } from '@tunarr/types';
|
||||
import isFunction from 'lodash-es/isFunction.js';
|
||||
import { MarkRequired } from 'ts-essentials';
|
||||
|
||||
56
shared/src/util/plexSearchUtil.ts
Normal file
56
shared/src/util/plexSearchUtil.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { PlexFilter, PlexSort } from '@tunarr/types/api';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
|
||||
// Commenting this out for now because it breaks the build but we will need it
|
||||
// later.
|
||||
//const PlexFilterFieldPattern = /(?:([a-zA-Z]*)\.)?([a-zA-Z]+)([!<>=&]*)/;
|
||||
|
||||
// TODO make this a hook
|
||||
export function buildPlexFilterKey(query: PlexFilter | undefined): string[] {
|
||||
if (isUndefined(query)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filters: string[] = [];
|
||||
switch (query.type) {
|
||||
case 'op': {
|
||||
if (query.children.length === 0) {
|
||||
break; // Ignore this node for now
|
||||
} else if (query.children.length === 1) {
|
||||
filters.push(...buildPlexFilterKey(query.children[0]));
|
||||
} else {
|
||||
filters.push('push=1');
|
||||
for (const child of query.children) {
|
||||
filters.push(...buildPlexFilterKey(child));
|
||||
filters.push(`${query.op}=1`);
|
||||
}
|
||||
filters.push('pop=1');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'value': {
|
||||
// Need to validate
|
||||
const operator = query.op.substring(0, query.op.length - 1);
|
||||
filters.push(`${query.field}${operator}=${query.value}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
export function buildPlexSortKey(sort: PlexSort | undefined): string[] {
|
||||
if (isUndefined(sort)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let key: string;
|
||||
if (sort.direction === 'asc') {
|
||||
key = sort.field;
|
||||
} else {
|
||||
key = `${sort.field}:desc`;
|
||||
}
|
||||
|
||||
return ['sort=' + key];
|
||||
}
|
||||
@@ -3,7 +3,11 @@
|
||||
"pipeline": {
|
||||
"clean": {},
|
||||
"build": {
|
||||
"dependsOn": ["clean", "^build"],
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["build/**", "dist/**"]
|
||||
},
|
||||
"clean-build": {
|
||||
"dependsOn": ["clean", "build"],
|
||||
"outputs": ["build/**", "dist/**"]
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"eslint": "8.45.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.2",
|
||||
"typescript": "5.2.2"
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
import { PlexSearchSchema } from './plexSearch.js';
|
||||
|
||||
type Alias<T> = T & { _?: never };
|
||||
//
|
||||
// Time slots
|
||||
//
|
||||
|
||||
export const MovieProgrammingTimeSlotSchema = z.object({
|
||||
type: z.literal('movie'),
|
||||
});
|
||||
|
||||
export const ShowProgrammingTimeSlotSchema = z.object({
|
||||
type: z.literal('show'),
|
||||
showId: z.string(),
|
||||
});
|
||||
|
||||
export const FlexProgrammingTimeSlotSchema = z.object({
|
||||
type: z.literal('flex'),
|
||||
});
|
||||
|
||||
export type MovieProgrammingTimeSlot = Alias<
|
||||
z.infer<typeof MovieProgrammingTimeSlotSchema>
|
||||
export type MovieProgrammingTimeSlot = z.infer<
|
||||
typeof MovieProgrammingTimeSlotSchema
|
||||
>;
|
||||
|
||||
export type ShowProgrammingTimeSlot = Alias<
|
||||
z.infer<typeof ShowProgrammingTimeSlotSchema>
|
||||
export type ShowProgrammingTimeSlot = z.infer<
|
||||
typeof ShowProgrammingTimeSlotSchema
|
||||
>;
|
||||
|
||||
export type FlexProgrammingTimeSlot = Alias<
|
||||
z.infer<typeof FlexProgrammingTimeSlotSchema>
|
||||
export type FlexProgrammingTimeSlot = z.infer<
|
||||
typeof FlexProgrammingTimeSlotSchema
|
||||
>;
|
||||
|
||||
export function slotProgrammingId(slot: TimeSlotProgramming) {
|
||||
@@ -39,9 +44,7 @@ export const TimeSlotProgrammingSchema = z.discriminatedUnion('type', [
|
||||
FlexProgrammingTimeSlotSchema,
|
||||
]);
|
||||
|
||||
export type TimeSlotProgramming = Alias<
|
||||
z.infer<typeof TimeSlotProgrammingSchema>
|
||||
>;
|
||||
export type TimeSlotProgramming = z.infer<typeof TimeSlotProgrammingSchema>;
|
||||
|
||||
export const TimeSlotSchema = z.object({
|
||||
order: z.union([z.literal('next'), z.literal('shuffle')]),
|
||||
@@ -49,7 +52,7 @@ export const TimeSlotSchema = z.object({
|
||||
startTime: z.number(), // Offset from midnight in millis
|
||||
});
|
||||
|
||||
export type TimeSlot = Alias<z.infer<typeof TimeSlotSchema>>;
|
||||
export type TimeSlot = z.infer<typeof TimeSlotSchema>;
|
||||
|
||||
export const TimeSlotScheduleSchema = z.object({
|
||||
type: z.literal('time'),
|
||||
@@ -63,14 +66,18 @@ export const TimeSlotScheduleSchema = z.object({
|
||||
startTomorrow: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type TimeSlotSchedule = Alias<z.infer<typeof TimeSlotScheduleSchema>>;
|
||||
export type TimeSlotSchedule = z.infer<typeof TimeSlotScheduleSchema>;
|
||||
|
||||
//
|
||||
// Random slots
|
||||
//
|
||||
|
||||
export const MovieProgrammingRandomSlotSchema = z.object({
|
||||
type: z.literal('movie'),
|
||||
});
|
||||
|
||||
export type MovieProgrammingRandomSlot = Alias<
|
||||
z.infer<typeof MovieProgrammingRandomSlotSchema>
|
||||
export type MovieProgrammingRandomSlot = z.infer<
|
||||
typeof MovieProgrammingRandomSlotSchema
|
||||
>;
|
||||
|
||||
export const ShowProgrammingRandomSlotSchema = z.object({
|
||||
@@ -78,16 +85,16 @@ export const ShowProgrammingRandomSlotSchema = z.object({
|
||||
showId: z.string(),
|
||||
});
|
||||
|
||||
export type ShowProgrammingRandomSlot = Alias<
|
||||
z.infer<typeof ShowProgrammingRandomSlotSchema>
|
||||
export type ShowProgrammingRandomSlot = z.infer<
|
||||
typeof ShowProgrammingRandomSlotSchema
|
||||
>;
|
||||
|
||||
export const FlexProgrammingRandomSlotSchema = z.object({
|
||||
type: z.literal('flex'),
|
||||
});
|
||||
|
||||
export type FlexProgrammingRandomSlot = Alias<
|
||||
z.infer<typeof FlexProgrammingRandomSlotSchema>
|
||||
export type FlexProgrammingRandomSlot = z.infer<
|
||||
typeof FlexProgrammingRandomSlotSchema
|
||||
>;
|
||||
|
||||
export const RandomSlotProgrammingSchema = z.discriminatedUnion('type', [
|
||||
@@ -96,9 +103,7 @@ export const RandomSlotProgrammingSchema = z.discriminatedUnion('type', [
|
||||
FlexProgrammingRandomSlotSchema,
|
||||
]);
|
||||
|
||||
export type RandomSlotProgramming = Alias<
|
||||
z.infer<typeof RandomSlotProgrammingSchema>
|
||||
>;
|
||||
export type RandomSlotProgramming = z.infer<typeof RandomSlotProgrammingSchema>;
|
||||
|
||||
export const RandomSlotSchema = z.object({
|
||||
order: z.string().optional(), // Present for show slots only
|
||||
@@ -110,7 +115,7 @@ export const RandomSlotSchema = z.object({
|
||||
programming: RandomSlotProgrammingSchema,
|
||||
});
|
||||
|
||||
export type RandomSlot = Alias<z.infer<typeof RandomSlotSchema>>;
|
||||
export type RandomSlot = z.infer<typeof RandomSlotSchema>;
|
||||
|
||||
export const RandomSlotScheduleSchema = z.object({
|
||||
type: z.literal('random'),
|
||||
@@ -124,15 +129,71 @@ export const RandomSlotScheduleSchema = z.object({
|
||||
periodMs: z.number().optional(),
|
||||
});
|
||||
|
||||
// This is used on the frontend too, we will move common
|
||||
// types eventually.
|
||||
export type RandomSlotSchedule = Alias<
|
||||
z.infer<typeof RandomSlotScheduleSchema>
|
||||
export type RandomSlotSchedule = z.infer<typeof RandomSlotScheduleSchema>;
|
||||
|
||||
//
|
||||
// Dynamic content
|
||||
//
|
||||
export const DynamicContentCronUpdaterConfigSchema = z.object({
|
||||
// Unique ID to track scheduling. Not for use outside of bookkeeping
|
||||
_id: z.string(),
|
||||
type: z.literal('cron'),
|
||||
// Cron schedule string compatible with node-schedule
|
||||
schedule: z.string(),
|
||||
});
|
||||
|
||||
export type DynamicContentCronUpdaterConfig = z.infer<
|
||||
typeof DynamicContentCronUpdaterConfigSchema
|
||||
>;
|
||||
|
||||
export const DynamicContentUpdaterConfigSchema = z.discriminatedUnion('type', [
|
||||
DynamicContentCronUpdaterConfigSchema,
|
||||
]);
|
||||
|
||||
export type DynamicContentUpdaterConfig = z.infer<
|
||||
typeof DynamicContentUpdaterConfigSchema
|
||||
>;
|
||||
|
||||
const WithEnabledSchema = z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const DynamicContentConfigPlexSourceSchema = z
|
||||
.object({
|
||||
type: z.literal('plex'),
|
||||
plexServerId: z.string().min(1), // server name or unique ID
|
||||
plexLibraryKey: z.string().min(1),
|
||||
search: PlexSearchSchema.optional(),
|
||||
updater: DynamicContentUpdaterConfigSchema,
|
||||
})
|
||||
.merge(WithEnabledSchema);
|
||||
|
||||
export type DynamicContentConfigPlexSource = z.infer<
|
||||
typeof DynamicContentConfigPlexSourceSchema
|
||||
>;
|
||||
|
||||
export const DynamicContentConfigSourceSchema = z.discriminatedUnion('type', [
|
||||
DynamicContentConfigPlexSourceSchema,
|
||||
]);
|
||||
|
||||
export type DynamicContentConfigSource = z.infer<
|
||||
typeof DynamicContentConfigSourceSchema
|
||||
>;
|
||||
|
||||
export const DynamicContentConfigSchema = z
|
||||
.object({
|
||||
contentSources: z.array(DynamicContentConfigSourceSchema).nonempty(),
|
||||
})
|
||||
.merge(WithEnabledSchema);
|
||||
|
||||
export type DynamicContentConfig = z.infer<typeof DynamicContentConfigSchema>;
|
||||
|
||||
//
|
||||
// Lineups
|
||||
//
|
||||
export const LineupScheduleSchema = z.discriminatedUnion('type', [
|
||||
TimeSlotScheduleSchema,
|
||||
RandomSlotScheduleSchema,
|
||||
]);
|
||||
|
||||
export type LineupSchedule = Alias<z.infer<typeof LineupScheduleSchema>>;
|
||||
export type LineupSchedule = z.infer<typeof LineupScheduleSchema>;
|
||||
|
||||
@@ -11,8 +11,7 @@ import {
|
||||
} from './Scheduling.js';
|
||||
|
||||
export * from './Scheduling.js';
|
||||
|
||||
type Alias<T> = T & { _?: never };
|
||||
export * from './plexSearch.js';
|
||||
|
||||
export const IdPathParamSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -45,8 +44,8 @@ export const CreateCustomShowRequestSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
export type CreateCustomShowRequest = Alias<
|
||||
z.infer<typeof CreateCustomShowRequestSchema>
|
||||
export type CreateCustomShowRequest = z.infer<
|
||||
typeof CreateCustomShowRequestSchema
|
||||
>;
|
||||
|
||||
export const CreateFillerListRequestSchema = z.object({
|
||||
@@ -56,15 +55,15 @@ export const CreateFillerListRequestSchema = z.object({
|
||||
),
|
||||
});
|
||||
|
||||
export type CreateFillerListRequest = Alias<
|
||||
z.infer<typeof CreateFillerListRequestSchema>
|
||||
export type CreateFillerListRequest = z.infer<
|
||||
typeof CreateFillerListRequestSchema
|
||||
>;
|
||||
|
||||
export const UpdateFillerListRequestSchema =
|
||||
CreateFillerListRequestSchema.partial();
|
||||
|
||||
export type UpdateFillerListRequest = Alias<
|
||||
z.infer<typeof UpdateFillerListRequestSchema>
|
||||
export type UpdateFillerListRequest = z.infer<
|
||||
typeof UpdateFillerListRequestSchema
|
||||
>;
|
||||
|
||||
export const BasicIdParamSchema = z.object({
|
||||
@@ -112,8 +111,8 @@ export const UpdateChannelProgrammingRequestSchema = z.discriminatedUnion(
|
||||
],
|
||||
);
|
||||
|
||||
export type UpdateChannelProgrammingRequest = Alias<
|
||||
z.infer<typeof UpdateChannelProgrammingRequestSchema>
|
||||
export type UpdateChannelProgrammingRequest = z.infer<
|
||||
typeof UpdateChannelProgrammingRequestSchema
|
||||
>;
|
||||
|
||||
export const UpdatePlexServerRequestSchema = PlexServerSettingsSchema.partial({
|
||||
@@ -123,8 +122,8 @@ export const UpdatePlexServerRequestSchema = PlexServerSettingsSchema.partial({
|
||||
id: true,
|
||||
});
|
||||
|
||||
export type UpdatePlexServerRequest = Alias<
|
||||
z.infer<typeof UpdatePlexServerRequestSchema>
|
||||
export type UpdatePlexServerRequest = z.infer<
|
||||
typeof UpdatePlexServerRequestSchema
|
||||
>;
|
||||
|
||||
export const InsertPlexServerRequestSchema = PlexServerSettingsSchema.partial({
|
||||
@@ -136,8 +135,8 @@ export const InsertPlexServerRequestSchema = PlexServerSettingsSchema.partial({
|
||||
id: true,
|
||||
});
|
||||
|
||||
export type InsertPlexServerRequest = Alias<
|
||||
z.infer<typeof InsertPlexServerRequestSchema>
|
||||
export type InsertPlexServerRequest = z.infer<
|
||||
typeof InsertPlexServerRequestSchema
|
||||
>;
|
||||
|
||||
export const VersionApiResponseSchema = z.object({
|
||||
|
||||
61
types/src/api/plexSearch.ts
Normal file
61
types/src/api/plexSearch.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const PlexFilterValueNodeSchema = z.object({
|
||||
type: z.literal('value'),
|
||||
field: z.string(),
|
||||
// We should start populating this to know how to parse out the value, if necessary
|
||||
fieldType: z.string().optional(),
|
||||
op: z.string(),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export type PlexFilterValueNode = {
|
||||
type: 'value';
|
||||
field: string;
|
||||
fieldType?: string;
|
||||
op: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type PlexFilterOperatorNode = {
|
||||
type: 'op';
|
||||
op: 'or' | 'and';
|
||||
children: PlexFilter[];
|
||||
};
|
||||
|
||||
// Hack to get recursive types working in zod
|
||||
export const PlexFilterOperatorNodeSchema: z.ZodType<PlexFilterOperatorNode> =
|
||||
z.lazy(() =>
|
||||
z.object({
|
||||
type: z.literal('op'),
|
||||
op: z.union([z.literal('or'), z.literal('and')]),
|
||||
children: PlexFilterSchema.array(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const PlexFilterSchema = PlexFilterOperatorNodeSchema.or(
|
||||
PlexFilterValueNodeSchema,
|
||||
);
|
||||
|
||||
export type PlexFilter = PlexFilterOperatorNode | PlexFilterValueNode;
|
||||
|
||||
export const PlexSortSchema = z.object({
|
||||
field: z.string(),
|
||||
direction: z.union([z.literal('asc'), z.literal('desc')]),
|
||||
});
|
||||
|
||||
export type PlexSort = z.infer<typeof PlexSortSchema>;
|
||||
|
||||
export const PlexSearchSchema = z.object({
|
||||
filter: PlexFilterSchema.optional(),
|
||||
sort: PlexSortSchema.optional(),
|
||||
});
|
||||
|
||||
export type PlexSearch = z.infer<typeof PlexSearchSchema>;
|
||||
|
||||
// A PlexSearch but with a reference to the
|
||||
// library it is for.
|
||||
export type ScopedPlexSearch = {
|
||||
search: PlexSearch;
|
||||
libraryKey: string;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './dvr.js';
|
||||
import z from 'zod';
|
||||
|
||||
export * from './dvr.js';
|
||||
|
||||
type Alias<t> = t & { _?: never };
|
||||
|
||||
// Marker field used to allow directories and non-directories both have
|
||||
@@ -606,6 +607,14 @@ export type PlexMedia = Alias<
|
||||
| PlexMusicTrack
|
||||
>;
|
||||
export type PlexTerminalMedia = PlexMovie | PlexEpisode | PlexMusicTrack; // Media that has no children
|
||||
|
||||
// Results you might get by looking up the children of a parent node type
|
||||
export type PlexChildMediaViewType =
|
||||
| PlexSeasonView
|
||||
| PlexEpisodeView
|
||||
| PlexMusicAlbumView
|
||||
| PlexMusicTrackView;
|
||||
|
||||
export type PlexParentMediaType =
|
||||
| PlexTvShow
|
||||
| PlexTvSeason
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import z from 'zod';
|
||||
import { LineupScheduleSchema } from '../api/Scheduling.js';
|
||||
import {
|
||||
DynamicContentConfigSchema,
|
||||
LineupScheduleSchema,
|
||||
} from '../api/Scheduling.js';
|
||||
import {
|
||||
PlexEpisodeSchema,
|
||||
PlexMovieSchema,
|
||||
@@ -173,4 +176,5 @@ export const CondensedChannelProgrammingSchema = z.object({
|
||||
lineup: z.array(CondensedChannelProgramSchema),
|
||||
startTimeOffsets,
|
||||
schedule: LineupScheduleSchema.optional(),
|
||||
dynamicContentConfig: DynamicContentConfigSchema.optional(),
|
||||
});
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
"ES2022"
|
||||
],
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"moduleResolution": "node16",
|
||||
"rootDir": ".",
|
||||
"outDir": "build",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"importHelpers": true,
|
||||
"alwaysStrict": true,
|
||||
"sourceMap": true,
|
||||
"sourceMap": false,
|
||||
"inlineSourceMap": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
|
||||
@@ -11,4 +11,6 @@ export default defineConfig({
|
||||
dts: true,
|
||||
outDir: 'build',
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
target: 'esnext',
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"nodemon": "^3.0.3",
|
||||
"openapi-zod-client": "^1.14.0",
|
||||
"ts-essentials": "^9.4.1",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript": "^5.4.3",
|
||||
"vite": "^4.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Bolt,
|
||||
FreeBreakfast as BreaksIcon,
|
||||
Expand as FlexIcon,
|
||||
KeyboardArrowDown as KeyboardArrowDownIcon,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
alpha,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import { isNull } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import AddBreaksModal from '../programming_controls/AddBreaksModal';
|
||||
@@ -77,7 +79,7 @@ export default function AddProgrammingButton() {
|
||||
const [addBreaksModalOpen, setAddBreaksModalOpen] = useState(false);
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const open = !isNull(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
@@ -117,14 +119,13 @@ export default function AddProgrammingButton() {
|
||||
to="add"
|
||||
startIcon={<MediaIcon />}
|
||||
>
|
||||
Add Media
|
||||
Schedule
|
||||
</Button>
|
||||
<Button onClick={handleClick}>
|
||||
<KeyboardArrowDownIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<StyledMenu
|
||||
id="demo-customized-menu"
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'demo-customized-button',
|
||||
}}
|
||||
@@ -132,6 +133,9 @@ export default function AddProgrammingButton() {
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<MenuItem disableRipple>
|
||||
<Bolt /> Make Dynamic
|
||||
</MenuItem>
|
||||
<Tooltip
|
||||
title="Add TV Shows or Movies to programming list."
|
||||
placement="right"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, Paper, Stack, Typography } from '@mui/material';
|
||||
import { Box, Button, Paper, Stack } from '@mui/material';
|
||||
import { DateTimePicker } from '@mui/x-date-pickers';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { createContext, useState } from 'react';
|
||||
@@ -25,7 +25,7 @@ export const ScheduleControlsContext = createContext<ScheduleControlsType>({
|
||||
});
|
||||
|
||||
export function ChannelProgrammingConfig() {
|
||||
const { currentEntity: channel, programList } = usePreloadedChannelEdit();
|
||||
const { currentEntity: channel } = usePreloadedChannelEdit();
|
||||
|
||||
const programsDirty = useStore((s) => s.channelEditor.dirty.programs);
|
||||
|
||||
@@ -36,7 +36,6 @@ export function ChannelProgrammingConfig() {
|
||||
};
|
||||
|
||||
const startTime = channel ? dayjs(channel.startTime) : dayjs();
|
||||
const endTime = startTime.add(channel?.duration ?? 0, 'milliseconds');
|
||||
|
||||
const [showScheduleControls, setShowScheduleControls] =
|
||||
useState<boolean>(false);
|
||||
@@ -50,32 +49,11 @@ export function ChannelProgrammingConfig() {
|
||||
>
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Box display="flex" justifyContent={'flex-start'}>
|
||||
<DateTimePicker
|
||||
label="Programming Start"
|
||||
value={startTime}
|
||||
onChange={(newDateTime) => handleStartTimeChange(newDateTime)}
|
||||
sx={{ mr: 2 }}
|
||||
/>
|
||||
<DateTimePicker label="Programming End" value={endTime} disabled />
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
pt: 1,
|
||||
mb: 2,
|
||||
columnGap: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<AdjustScheduleControls />
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent={'flex-start'}></Box>
|
||||
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
gap={{ xs: 1 }}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
pt: 1,
|
||||
@@ -84,9 +62,30 @@ export function ChannelProgrammingConfig() {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ flexGrow: 1 }}>
|
||||
{programList.length} program{programList.length === 1 ? '' : 's'}
|
||||
</Typography>
|
||||
<Box sx={{ mr: { sm: 2 }, flexGrow: 1 }}>
|
||||
<DateTimePicker
|
||||
label="Programming Start"
|
||||
value={startTime}
|
||||
onChange={(newDateTime) => handleStartTimeChange(newDateTime)}
|
||||
slotProps={{ textField: { size: 'small' } }}
|
||||
/>
|
||||
</Box>
|
||||
{showScheduleControls && (
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
pt: 1,
|
||||
mb: 2,
|
||||
columnGap: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<AdjustScheduleControls />
|
||||
</Stack>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => resetLineup()}
|
||||
@@ -100,7 +99,7 @@ export function ChannelProgrammingConfig() {
|
||||
</Stack>
|
||||
|
||||
<ChannelProgrammingList
|
||||
virtualListProps={{ width: '100%', height: 400, itemSize: 35 }}
|
||||
virtualListProps={{ width: '100%', height: 600, itemSize: 35 }}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
FixedSizeListProps,
|
||||
ListChildComponentProps,
|
||||
} from 'react-window';
|
||||
import { betterHumanize } from '../../helpers/dayjs.ts';
|
||||
import { alternateColors, channelProgramUniqueId } from '../../helpers/util.ts';
|
||||
import {
|
||||
deleteProgram,
|
||||
@@ -49,11 +50,13 @@ type Props = {
|
||||
// If given, the list will be rendered using react-window
|
||||
virtualListProps?: Omit<FixedSizeListProps, 'itemCount' | 'children'>;
|
||||
enableDnd?: boolean;
|
||||
showProgramCount?: boolean;
|
||||
};
|
||||
|
||||
const defaultProps: Props = {
|
||||
programListSelector: materializedProgramListSelector,
|
||||
enableDnd: true,
|
||||
showProgramCount: true,
|
||||
};
|
||||
|
||||
type ListItemProps = {
|
||||
@@ -100,7 +103,9 @@ const programListItemTitleFormatter = (() => {
|
||||
|
||||
return (program: ChannelProgram) => {
|
||||
const title = itemTitle(program);
|
||||
const dur = dayjs.duration({ milliseconds: program.duration }).humanize();
|
||||
const dur = betterHumanize(
|
||||
dayjs.duration({ milliseconds: program.duration }),
|
||||
);
|
||||
return `${title} - (${dur})`;
|
||||
};
|
||||
})();
|
||||
@@ -248,6 +253,7 @@ export default function ChannelProgrammingList({
|
||||
programList: passedProgramList,
|
||||
programListSelector = defaultProps.programListSelector,
|
||||
virtualListProps,
|
||||
showProgramCount = defaultProps.showProgramCount,
|
||||
enableDnd = defaultProps.enableDnd,
|
||||
}: Props) {
|
||||
const channel = useStore((s) => s.channelEditor.currentEntity);
|
||||
@@ -335,12 +341,8 @@ export default function ChannelProgrammingList({
|
||||
|
||||
if (programList.length === 0) {
|
||||
return (
|
||||
<Box>
|
||||
<Typography
|
||||
align="center"
|
||||
width={'100%'}
|
||||
sx={{ my: 4, fontStyle: 'italic' }}
|
||||
>
|
||||
<Box width={'100%'}>
|
||||
<Typography align="center" sx={{ my: 4, fontStyle: 'italic' }}>
|
||||
No programming added yet
|
||||
</Typography>
|
||||
</Box>
|
||||
@@ -348,7 +350,19 @@ export default function ChannelProgrammingList({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box width="100%">
|
||||
{showProgramCount && (
|
||||
<Box sx={{ width: '100%', mb: 1, textAlign: 'right' }}>
|
||||
<Typography variant="caption" sx={{ flexGrow: 1, mr: 2 }}>
|
||||
{programList.length} program{programList.length === 1 ? '' : 's'}
|
||||
</Typography>
|
||||
{channel?.duration && (
|
||||
<Typography variant="caption">
|
||||
{dayjs.duration(channel.duration).humanize()}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{virtualListProps ? (
|
||||
<FixedSizeList
|
||||
{...virtualListProps}
|
||||
@@ -359,7 +373,7 @@ export default function ChannelProgrammingList({
|
||||
{ProgramRow}
|
||||
</FixedSizeList>
|
||||
) : (
|
||||
<Box ref={drop} sx={{ flex: 1, maxHeight: 400, overflowY: 'auto' }}>
|
||||
<Box ref={drop} sx={{ flex: 1, maxHeight: 600, overflowY: 'auto' }}>
|
||||
<List dense>{renderPrograms()}</List>
|
||||
</Box>
|
||||
)}
|
||||
@@ -368,7 +382,7 @@ export default function ChannelProgrammingList({
|
||||
onClose={() => setFocusedProgramDetails(undefined)}
|
||||
program={focusedProgramDetails}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ import {
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import {
|
||||
PlexFilter,
|
||||
PlexFilterOperatorNode,
|
||||
PlexFilterValueNode,
|
||||
} from '@tunarr/types/api';
|
||||
import { PlexFilterResponseMeta, PlexFilterType } from '@tunarr/types/plex';
|
||||
import dayjs from 'dayjs';
|
||||
import { find, first, isUndefined, map, size } from 'lodash-es';
|
||||
@@ -35,11 +40,6 @@ import {
|
||||
useForm,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import {
|
||||
PlexFilter,
|
||||
PlexFilterOperatorNode,
|
||||
PlexFilterValueNode,
|
||||
} from '../../helpers/plexSearchUtil.ts';
|
||||
import {
|
||||
usePlexTags,
|
||||
useSelectedLibraryPlexFilters,
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { padStart } from 'lodash-es';
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export function betterHumanize(dur: duration.Duration) {
|
||||
const hrs = Math.floor(dur.asHours());
|
||||
const mins = Math.floor(dur.asMinutes() % 60);
|
||||
if (hrs >= 1) {
|
||||
const s = hrs >= 2 ? 's' : '';
|
||||
const ms = mins === 1 ? '' : 's';
|
||||
return `${hrs} hour${s} ${padStart(mins.toString(), 2, '0')} min${ms}`;
|
||||
} else if (mins >= 1) {
|
||||
const ms = mins === 1 ? '' : 's';
|
||||
return `${padStart(mins.toString(), 2, '0')} min${ms}`;
|
||||
} else {
|
||||
return dur.humanize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,10 @@
|
||||
import { PlexFilter, PlexSort } from '@tunarr/types/api';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
|
||||
// Commenting this out for now because it breaks the build but we will need it
|
||||
// later.
|
||||
//const PlexFilterFieldPattern = /(?:([a-zA-Z]*)\.)?([a-zA-Z]+)([!<>=&]*)/;
|
||||
|
||||
export type PlexFilterValueNode = {
|
||||
type: 'value';
|
||||
field: string;
|
||||
op: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type PlexFilterAndNode = {
|
||||
type: 'op';
|
||||
op: 'and';
|
||||
children: PlexFilter[];
|
||||
};
|
||||
|
||||
export type PlexFilterOrNode = {
|
||||
type: 'op';
|
||||
op: 'or';
|
||||
children: PlexFilter[];
|
||||
};
|
||||
|
||||
export type PlexFilterOperatorNode = PlexFilterAndNode | PlexFilterOrNode;
|
||||
export type PlexFilter = PlexFilterOperatorNode | PlexFilterValueNode;
|
||||
export type PlexSort = { field: string; direction: 'asc' | 'desc' };
|
||||
export type PlexSearch = {
|
||||
filter?: PlexFilter;
|
||||
sort?: PlexSort;
|
||||
};
|
||||
|
||||
// TODO make this a hook
|
||||
export function buildPlexFilterKey(query: PlexFilter | undefined): string[] {
|
||||
if (isUndefined(query)) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { styled } from '@mui/material/styles';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Task } from '@tunarr/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { map } from 'lodash-es';
|
||||
import { apiClient } from '../../external/api.ts';
|
||||
|
||||
const StyledLoopIcon = styled(Loop)({
|
||||
@@ -40,14 +41,29 @@ function TaskRow({ task }: { task: Task }) {
|
||||
queryKey: ['channels', 'all', 'guide'],
|
||||
});
|
||||
},
|
||||
onMutate: async (id: string) => {
|
||||
await queryClient.cancelQueries({ queryKey: ['jobs'] });
|
||||
const prevJobs = queryClient.getQueryData<Task[]>(['jobs']);
|
||||
const now = new Date();
|
||||
queryClient.setQueryData(
|
||||
['jobs'],
|
||||
map(prevJobs, (j) => {
|
||||
return j.id === id
|
||||
? {
|
||||
...j,
|
||||
lastExecution: now,
|
||||
lastExecutionEpoch: now.getTime() / 1000,
|
||||
}
|
||||
: j;
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const runJobWithId = (id: string) => {
|
||||
runJobMutation.mutate(id);
|
||||
};
|
||||
|
||||
console.log(task);
|
||||
|
||||
return (
|
||||
<TableRow key={task.id}>
|
||||
<TableCell>{task.name}</TableCell>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
FillerList,
|
||||
FillerListProgramming,
|
||||
} from '@tunarr/types';
|
||||
import { isPlexEpisode, isPlexMusicTrack } from '@tunarr/types/plex';
|
||||
import { Draft } from 'immer';
|
||||
import {
|
||||
chain,
|
||||
@@ -236,53 +235,22 @@ const plexMediaToContentProgram = (
|
||||
media: EnrichedPlexMedia,
|
||||
): ContentProgram => {
|
||||
const uniqueId = createExternalId('plex', media.serverName, media.key);
|
||||
// TODO Handle music tracks
|
||||
if (isPlexEpisode(media)) {
|
||||
return {
|
||||
id: media.id ?? uniqueId,
|
||||
persisted: !isNil(media.id),
|
||||
originalProgram: media,
|
||||
duration: media.duration,
|
||||
externalSourceName: media.serverName,
|
||||
externalSourceType: 'plex',
|
||||
externalKey: media.key,
|
||||
uniqueId: uniqueId,
|
||||
type: 'content',
|
||||
subtype: 'episode',
|
||||
title: media.grandparentTitle,
|
||||
episodeTitle: media.title,
|
||||
episodeNumber: media.index,
|
||||
seasonNumber: media.parentIndex,
|
||||
};
|
||||
} else if (isPlexMusicTrack(media)) {
|
||||
return {
|
||||
id: media.id ?? uniqueId,
|
||||
persisted: !isNil(media.id),
|
||||
originalProgram: media,
|
||||
duration: media.duration,
|
||||
externalSourceName: media.serverName,
|
||||
externalSourceType: 'plex',
|
||||
uniqueId: uniqueId,
|
||||
type: 'content',
|
||||
subtype: 'track',
|
||||
title: media.title,
|
||||
artistName: media.grandparentTitle,
|
||||
albumName: media.parentTitle,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
id: media.id ?? uniqueId,
|
||||
persisted: !isNil(media.id),
|
||||
originalProgram: media,
|
||||
duration: media.duration,
|
||||
externalSourceName: media.serverName,
|
||||
externalSourceType: 'plex',
|
||||
uniqueId: uniqueId,
|
||||
type: 'content',
|
||||
subtype: 'movie',
|
||||
title: media.title,
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: media.id ?? uniqueId,
|
||||
persisted: !isNil(media.id),
|
||||
originalProgram: media,
|
||||
duration: media.duration,
|
||||
externalSourceName: media.serverName,
|
||||
externalSourceType: 'plex',
|
||||
externalKey: media.key,
|
||||
uniqueId: uniqueId,
|
||||
type: 'content',
|
||||
subtype: media.type,
|
||||
title: media.type === 'episode' ? media.grandparentTitle : media.title,
|
||||
episodeTitle: media.type === 'episode' ? media.title : undefined,
|
||||
episodeNumber: media.type === 'episode' ? media.index : undefined,
|
||||
seasonNumber: media.type === 'episode' ? media.parentIndex : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const addMediaToCurrentChannel = (programs: AddedMedia[]) =>
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
CustomShow,
|
||||
FillerList,
|
||||
} from '@tunarr/types';
|
||||
import { DynamicContentConfig, LineupSchedule } from '@tunarr/types/api';
|
||||
import { StateCreator } from 'zustand';
|
||||
import { UICondensedChannelProgram, UIIndex } from '../../types/index.ts';
|
||||
import { LineupSchedule } from '@tunarr/types/api';
|
||||
|
||||
// Represents a program listing in the editor
|
||||
export interface ProgrammingEditorState<EntityType, ProgramType> {
|
||||
@@ -35,6 +35,7 @@ export type ChannelEditorState = ProgrammingEditorState<
|
||||
// the lookup record for programs by ID
|
||||
programLookup: Record<string, ContentProgram>;
|
||||
schedule?: LineupSchedule;
|
||||
dynamicContentConfiguration?: DynamicContentConfig;
|
||||
};
|
||||
|
||||
export interface EditorsState {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PlexServerSettings } from '@tunarr/types';
|
||||
import { PlexFilter, PlexSort } from '@tunarr/types/api';
|
||||
import {
|
||||
PlexLibrarySection,
|
||||
PlexMedia,
|
||||
@@ -8,8 +9,6 @@ import {
|
||||
import { map, reject, some } from 'lodash-es';
|
||||
import useStore from '..';
|
||||
import {
|
||||
PlexFilter,
|
||||
PlexSort,
|
||||
buildPlexFilterKey,
|
||||
buildPlexSortKey,
|
||||
} from '../../helpers/plexSearchUtil.ts';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CustomProgram, CustomShow, PlexServerSettings } from '@tunarr/types';
|
||||
import { PlexSearch } from '@tunarr/types/api';
|
||||
import { PlexLibrarySection, PlexMedia } from '@tunarr/types/plex';
|
||||
import { StateCreator } from 'zustand';
|
||||
import { PlexSearch } from '../../helpers/plexSearchUtil';
|
||||
|
||||
type ServerName = string;
|
||||
type PlexItemGuid = string;
|
||||
|
||||
Reference in New Issue
Block a user