mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
Merge remote-tracking branch 'origin/main' into dev
This commit is contained in:
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -2,7 +2,7 @@ name: Publish Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
paths:
|
||||
- docs/**
|
||||
- mkdocs.yml
|
||||
|
||||
4
.github/workflows/release-it.yml
vendored
4
.github/workflows/release-it.yml
vendored
@@ -37,12 +37,12 @@ jobs:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: if pnpm run should-semantic-release ; then pnpm release-it --ci --verbose ; fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
||||
DOCKER_CHANNEL: latest # Updates the 'latest' tag on Docker Hub
|
||||
|
||||
- name: Release from Dev (Pre-release)
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: if pnpm run should-semantic-release ; then pnpm release-it --verbose --preRelease=dev --ci ; fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
||||
DOCKER_CHANNEL: dev # Updates the 'dev' tag on Docker Hub
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [1.0.17](https://github.com/chrisbenincasa/tunarr/compare/v1.0.16...v1.0.17) (2026-01-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* default experimental search index memory flag to false ([271555c](https://github.com/chrisbenincasa/tunarr/commit/271555c86fc0c9a660c601dc0003690eb3165dc4))
|
||||
* properly scan Emby episodes ([f1978e8](https://github.com/chrisbenincasa/tunarr/commit/f1978e8060f5559ffd549cf30264c8978f699465))
|
||||
* restore search index from snapshot when necessary ([4a8fd4e](https://github.com/chrisbenincasa/tunarr/commit/4a8fd4ed4a7f20df64c246c8e31196a178cb74d0))
|
||||
|
||||
## [1.0.16](https://github.com/chrisbenincasa/tunarr/compare/v1.0.15...v1.0.16) (2026-01-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* do not stop scanning seasons if one is unchanged ([109a030](https://github.com/chrisbenincasa/tunarr/commit/109a0300a1a144ce9b4f3e5a8bd9d8b30fadfa5a))
|
||||
* strip leading "v" from version strings before building ([6723eee](https://github.com/chrisbenincasa/tunarr/commit/6723eeed201765f2551e2601247512471387427e))
|
||||
|
||||
## [1.0.15](https://github.com/chrisbenincasa/tunarr/compare/v1.0.14...v1.0.15) (2026-01-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -8,10 +8,21 @@
|
||||
content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- Need a Custom Header? Check out this example https://codepen.io/scalarorg/pen/VwOXqam -->
|
||||
<script
|
||||
<!-- <script
|
||||
id="api-reference"
|
||||
data-url="/generated/tunarr-latest-openapi.json"></script>
|
||||
data-url="/generated/tunarr-latest-openapi.json"></script> -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
||||
<script id="sources" src="/generated/openapi-specs.js"></script>
|
||||
<script>
|
||||
Scalar.createApiReference('#app', {
|
||||
theme: "default",
|
||||
favicon: 'assets/favicon.png',
|
||||
hideClientButton: true,
|
||||
|
||||
sources,
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2
docs/generated/openapi-specs.js
Normal file
2
docs/generated/openapi-specs.js
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
const sources = [{"title":"Latest","slug":"latest","url":"/generated/tunarr-latest-openapi.json"},{"title":"1.0.16","slug":"1.0.16","url":"/generated/tunarr-v1.0.16-openapi.json"},{"title":"1.0.8","slug":"1.0.8","url":"/generated/tunarr-v1.0.8-openapi.json"},{"title":"1.0.3","slug":"1.0.3","url":"/generated/tunarr-v1.0.3-openapi.json"},{"title":"0.22.11","slug":"0.22.11","url":"/generated/tunarr-v0.22.11-openapi.json"}]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
docs/generated/tunarr-v1.0.16-openapi.json
Normal file
1
docs/generated/tunarr-v1.0.16-openapi.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tunarr",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.17",
|
||||
"description": "Create LiveTV channels from your Plex media",
|
||||
"type": "module",
|
||||
"author": "chrisbenincasa",
|
||||
@@ -13,7 +13,8 @@
|
||||
"lint-changed": "eslint --fix $(git diff --name-only HEAD -- './**/*.ts*' | xargs)",
|
||||
"test": "turbo run test",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"should-semantic-release": "should-semantic-release --verbose"
|
||||
"should-semantic-release": "should-semantic-release --verbose",
|
||||
"generate-docs-script": "tsx scripts/generate-docs-script.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
@@ -24,6 +25,8 @@
|
||||
"@release-it/bumper": "^7.0.5",
|
||||
"@release-it/conventional-changelog": "^10.0.4",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@types/node": "22.10.7",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.21.0",
|
||||
"@typescript-eslint/parser": "^8.21.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
@@ -42,7 +45,9 @@
|
||||
"release-it": "^19.2.2",
|
||||
"release-it-pnpm": "^4.6.6",
|
||||
"semantic-release": "^25.0.2",
|
||||
"semver": "^7.7.3",
|
||||
"should-semantic-release": "^0.3.5",
|
||||
"tsx": "^4.20.5",
|
||||
"turbo": "^2.5.3",
|
||||
"typescript": "catalog:",
|
||||
"vitest": "^3.2.4"
|
||||
|
||||
36
pnpm-lock.yaml
generated
36
pnpm-lock.yaml
generated
@@ -58,6 +58,12 @@ importers:
|
||||
'@semantic-release/changelog':
|
||||
specifier: ^6.0.3
|
||||
version: 6.0.3(semantic-release@25.0.2(typescript@5.9.3))
|
||||
'@types/node':
|
||||
specifier: 22.10.7
|
||||
version: 22.10.7
|
||||
'@types/semver':
|
||||
specifier: ^7.7.1
|
||||
version: 7.7.1
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^8.21.0
|
||||
version: 8.21.0(@typescript-eslint/parser@8.21.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
@@ -112,9 +118,15 @@ importers:
|
||||
semantic-release:
|
||||
specifier: ^25.0.2
|
||||
version: 25.0.2(typescript@5.9.3)
|
||||
semver:
|
||||
specifier: ^7.7.3
|
||||
version: 7.7.3
|
||||
should-semantic-release:
|
||||
specifier: ^0.3.5
|
||||
version: 0.3.5
|
||||
tsx:
|
||||
specifier: ^4.20.5
|
||||
version: 4.20.6
|
||||
turbo:
|
||||
specifier: ^2.5.3
|
||||
version: 2.5.3
|
||||
@@ -3355,6 +3367,9 @@ packages:
|
||||
'@types/semver@7.5.4':
|
||||
resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==}
|
||||
|
||||
'@types/semver@7.7.1':
|
||||
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
|
||||
|
||||
'@types/split2@4.2.3':
|
||||
resolution: {integrity: sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw==}
|
||||
|
||||
@@ -8111,11 +8126,6 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.6.3:
|
||||
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.7.2:
|
||||
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -11997,6 +12007,8 @@ snapshots:
|
||||
|
||||
'@types/semver@7.5.4': {}
|
||||
|
||||
'@types/semver@7.7.1': {}
|
||||
|
||||
'@types/split2@4.2.3':
|
||||
dependencies:
|
||||
'@types/node': 22.10.7
|
||||
@@ -12041,7 +12053,7 @@ snapshots:
|
||||
ignore: 5.2.4
|
||||
natural-compare: 1.4.0
|
||||
natural-compare-lite: 1.4.0
|
||||
semver: 7.5.4
|
||||
semver: 7.7.3
|
||||
ts-api-utils: 1.0.3(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
@@ -12271,7 +12283,7 @@ snapshots:
|
||||
fast-glob: 3.3.3
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.6.3
|
||||
semver: 7.7.3
|
||||
ts-api-utils: 2.0.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -14526,7 +14538,7 @@ snapshots:
|
||||
process-warning: 5.0.0
|
||||
rfdc: 1.4.1
|
||||
secure-json-parse: 4.1.0
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
toad-cache: 3.7.0
|
||||
|
||||
fastq@1.17.1:
|
||||
@@ -16246,7 +16258,7 @@ snapshots:
|
||||
|
||||
node-abi@3.78.0:
|
||||
dependencies:
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
|
||||
node-cache@5.1.2:
|
||||
dependencies:
|
||||
@@ -16296,7 +16308,7 @@ snapshots:
|
||||
ignore-by-default: 1.0.1
|
||||
minimatch: 3.1.2
|
||||
pstree.remy: 1.1.8
|
||||
semver: 7.5.4
|
||||
semver: 7.7.3
|
||||
simple-update-notifier: 2.0.0
|
||||
supports-color: 5.5.0
|
||||
touch: 3.1.0
|
||||
@@ -17163,7 +17175,7 @@ snapshots:
|
||||
conventional-recommended-bump: 11.2.0
|
||||
kolorist: 1.8.0
|
||||
release-it: 19.2.2(@types/node@22.10.7)(magicast@0.3.5)
|
||||
semver: 7.7.2
|
||||
semver: 7.7.3
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
@@ -17464,8 +17476,6 @@ snapshots:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
semver@7.6.3: {}
|
||||
|
||||
semver@7.7.2: {}
|
||||
|
||||
semver@7.7.3: {}
|
||||
|
||||
43
scripts/generate-docs-script.ts
Normal file
43
scripts/generate-docs-script.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import semver from 'semver';
|
||||
|
||||
function semverRegex() {
|
||||
return /((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)/gi;
|
||||
}
|
||||
|
||||
const generatedDir = await fs.readdir('docs/generated');
|
||||
const openApiSpecs = generatedDir
|
||||
.filter((name) => name.endsWith('openapi.json') && !name.includes('latest'))
|
||||
.sort((l, r) => {
|
||||
const sv1 = semverRegex().exec(l.replace('-openapi.json', ''))?.[0] ?? l;
|
||||
const sv2 = semverRegex().exec(r.replace('-openapi.json', ''))?.[0] ?? r;
|
||||
return semver.rcompare(sv1, sv2);
|
||||
});
|
||||
|
||||
// Generate the script...
|
||||
|
||||
const specs = [
|
||||
{
|
||||
title: 'Latest',
|
||||
slug: 'latest',
|
||||
url: '/generated/tunarr-latest-openapi.json',
|
||||
},
|
||||
...openApiSpecs.map((spec) => {
|
||||
const v = semverRegex().exec(spec.replace('-openapi.json', ''))?.[0];
|
||||
return {
|
||||
title: v,
|
||||
slug: v,
|
||||
url: `/generated/${spec}`,
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
const script = `
|
||||
const sources = ${JSON.stringify(specs)}
|
||||
`;
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(process.cwd(), 'docs', 'generated', 'openapi-specs.js'),
|
||||
script,
|
||||
);
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
pnpm run generate-docs-script
|
||||
|
||||
docker build -f ./docker/docs.Dockerfile -t chrisbenincasa/tunarr-docs .
|
||||
|
||||
docker run --rm -it -p 8088:8000 -v "${PWD}":/docs chrisbenincasa/tunarr-docs
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tunarr/server",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.17",
|
||||
"description": "Create LiveTV channels from your Plex media",
|
||||
"license": "Zlib",
|
||||
"private": true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { rimraf } from 'rimraf';
|
||||
import { nativeNodeModulesPlugin } from '../esbuild/native-node-module.ts';
|
||||
import { nodeProtocolPlugin } from '../esbuild/node-protocol.ts';
|
||||
|
||||
import { trimStart } from 'lodash-es';
|
||||
import { createRequire } from 'node:module';
|
||||
const __require = createRequire(import.meta.url);
|
||||
const esbuildPluginPino = __require('esbuild-plugin-pino');
|
||||
@@ -88,7 +89,7 @@ const result = await esbuild.build({
|
||||
metafile: true,
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"',
|
||||
'process.env.TUNARR_VERSION': `"${process.env.TUNARR_VERSION}"`,
|
||||
'process.env.TUNARR_VERSION': `"${trimStart(process.env.TUNARR_VERSION, 'v')}"`,
|
||||
'process.env.TUNARR_BUILD': `"${process.env.TUNARR_BUILD}"`,
|
||||
'process.env.TUNARR_EDGE_BUILD': `"${isEdgeBuild}"`,
|
||||
'import.meta.url': '__import_meta_url',
|
||||
|
||||
5
server/src/external/emby/EmbyApiClient.ts
vendored
5
server/src/external/emby/EmbyApiClient.ts
vendored
@@ -696,12 +696,13 @@ export class EmbyApiClient extends MediaSourceApiClient<EmbyItemTypes> {
|
||||
(episode) => this.embyApiEpisodeInjection(episode),
|
||||
(page) =>
|
||||
this.doTypeCheckedGet(
|
||||
`/Shows/${showId}/Seasons`,
|
||||
`/Shows/${showId}/Episodes`,
|
||||
EmbyLibraryItemsResponse,
|
||||
{
|
||||
params: {
|
||||
userId: this.options.mediaSource.userId,
|
||||
fields: 'Path,DateCreated,Etag,Taglines,ProviderIds',
|
||||
fields:
|
||||
'Path,DateCreated,Etag,Taglines,ProviderIds,MediaStreams,People',
|
||||
startIndex: page * (pageSize ?? 50),
|
||||
limit: pageSize ?? 50,
|
||||
sortOrder: 'Ascending',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { nullToUndefined, seq } from '@tunarr/shared/util';
|
||||
import {
|
||||
FindChild,
|
||||
MediaStream,
|
||||
tag,
|
||||
Tag,
|
||||
MediaStream,
|
||||
TerminalProgram,
|
||||
TupleToUnion,
|
||||
} from '@tunarr/types';
|
||||
@@ -404,9 +404,11 @@ export class MeilisearchService implements ISearchService {
|
||||
return;
|
||||
}
|
||||
|
||||
const indexFolderExists = await fileExists(this.dbPath);
|
||||
|
||||
// Check for update.
|
||||
// Only run updates on start of the main tunarr thread
|
||||
if ((await fileExists(this.dbPath)) && isMainThread) {
|
||||
if (indexFolderExists && isMainThread) {
|
||||
const indexVersion = await this.getMeilisearchVersion();
|
||||
|
||||
if (indexVersion === serverPackage.meilisearch.version) {
|
||||
@@ -461,6 +463,20 @@ export class MeilisearchService implements ISearchService {
|
||||
'--experimental-dumpless-upgrade',
|
||||
];
|
||||
|
||||
// Restore from snapshot if we don't have an index folder already
|
||||
const snapshotPath = path.join(
|
||||
this.fileSystemService.getMsSnapshotsPath(),
|
||||
'data.ms.snapshot',
|
||||
);
|
||||
const snapshotExists = await fileExists(snapshotPath);
|
||||
if (!indexFolderExists && snapshotExists) {
|
||||
this.logger.debug(
|
||||
'Restoring search index from snapshot: %s',
|
||||
snapshotPath,
|
||||
);
|
||||
args.push('--import-snapshot', snapshotPath);
|
||||
}
|
||||
|
||||
const indexingRamSetting =
|
||||
getEnvVar(TUNARR_ENV_VARS.SEARCH_MAX_RAM) ??
|
||||
this.settingsDB.systemSettings().server.searchSettings
|
||||
@@ -494,7 +510,7 @@ export class MeilisearchService implements ISearchService {
|
||||
!isWindows() &&
|
||||
getBooleanEnvVar(
|
||||
TUNARR_ENV_VARS.SEARCH_REDUCE_INDEXER_MEMORY_USAGE,
|
||||
os.platform() === 'linux',
|
||||
false,
|
||||
)
|
||||
) {
|
||||
args.push('--experimental-reduce-indexing-memory-usage');
|
||||
@@ -1407,10 +1423,12 @@ export class MeilisearchService implements ISearchService {
|
||||
}
|
||||
|
||||
private getUniqueStreamLanguages(
|
||||
streams: {
|
||||
streamType: MediaStream['streamType'];
|
||||
languageCodeISO6392?: string | null;
|
||||
}[] | undefined,
|
||||
streams:
|
||||
| {
|
||||
streamType: MediaStream['streamType'];
|
||||
languageCodeISO6392?: string | null;
|
||||
}[]
|
||||
| undefined,
|
||||
type: 'audio' | 'subtitles',
|
||||
): string[] {
|
||||
return uniq(
|
||||
@@ -1422,8 +1440,6 @@ export class MeilisearchService implements ISearchService {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private convertProgramToSearchDocument<
|
||||
ProgramT extends (Movie | Episode | MusicTrack | OtherVideo) &
|
||||
HasMediaSourceAndLibraryId,
|
||||
@@ -1594,7 +1610,6 @@ export class MeilisearchService implements ISearchService {
|
||||
streamType: 'audio',
|
||||
});
|
||||
|
||||
|
||||
let summary: Nilable<string>;
|
||||
switch (program.type) {
|
||||
case 'movie':
|
||||
|
||||
@@ -413,7 +413,7 @@ export class LocalTvShowScanner extends FileSystemScanner {
|
||||
|
||||
if (!shouldScan) {
|
||||
this.logger.debug('Skipping unchanged season folder %s', fullPath);
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const seasonMetadata = this.seasonForNumber(show, seasonNumber);
|
||||
@@ -462,7 +462,11 @@ export class LocalTvShowScanner extends FileSystemScanner {
|
||||
);
|
||||
|
||||
if (epScanResult.isFailure()) {
|
||||
return;
|
||||
this.logger.warn(
|
||||
epScanResult.error,
|
||||
'Failed to scan episodes for season directory %s',
|
||||
fullPath,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isNew) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { trimStart } from 'lodash-es';
|
||||
import tunarrPackage from '../../package.json' with { type: 'json' };
|
||||
import { getBooleanEnvVar, getEnvVar, TUNARR_ENV_VARS } from './env.ts';
|
||||
import { isNonEmptyString, isProduction } from './index.js';
|
||||
@@ -6,8 +7,10 @@ let tunarrVersion: string;
|
||||
export const getTunarrVersion = () => {
|
||||
if (!tunarrVersion) {
|
||||
// Attempt to set for dev. This is relative to the shared package
|
||||
tunarrVersion =
|
||||
getEnvVar(TUNARR_ENV_VARS.BUILD_ENV_VAR) ?? tunarrPackage.version ?? '';
|
||||
tunarrVersion = trimStart(
|
||||
getEnvVar(TUNARR_ENV_VARS.BUILD_ENV_VAR) ?? tunarrPackage.version ?? '',
|
||||
'v',
|
||||
);
|
||||
|
||||
const isEdgeBuild = getBooleanEnvVar(
|
||||
TUNARR_ENV_VARS.IS_EDGE_BUILD_ENV_VAR,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tunarr/shared",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.17",
|
||||
"description": "Utility functions shared between server and web",
|
||||
"private": true,
|
||||
"keywords": [],
|
||||
|
||||
32
tsconfig.json
Normal file
32
tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"target": "esnext",
|
||||
"lib": [
|
||||
"esnext"
|
||||
],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"verbatimModuleSyntax": true,
|
||||
"isolatedModules": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"moduleDetection": "force",
|
||||
"skipLibCheck": true,
|
||||
},
|
||||
"include": [
|
||||
"scripts/**/*.ts",
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tunarr/types",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.17",
|
||||
"description": "Type definitions and schemas shared between server and web",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tunarr/web",
|
||||
"version": "1.0.15",
|
||||
"version": "1.0.17",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { getProgramSummary } from '@/helpers/programUtil';
|
||||
import { useSettings } from '@/store/settings/selectors';
|
||||
import { OpenInNew } from '@mui/icons-material';
|
||||
import { MoreVert, OpenInNew } from '@mui/icons-material';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
Stack,
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
@@ -18,7 +19,9 @@ import {
|
||||
import { capitalize } from 'lodash-es';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useGetArtworkUrl } from '../../hooks/useThumbnailUrl.ts';
|
||||
import type { Nullable } from '../../types/util.ts';
|
||||
import ProgramInfoBar from './ProgramInfoBar';
|
||||
import { ProgramOperationsMenu } from './ProgramOperationsMenu.tsx';
|
||||
|
||||
type Props = {
|
||||
program: TerminalProgram | ProgramGrouping;
|
||||
@@ -36,6 +39,9 @@ export default function MediaDetailCard({ program }: Props) {
|
||||
return `${settings.backendUri}/api/programs/${program.uuid}/external-link`;
|
||||
}, [settings.backendUri, program]);
|
||||
|
||||
const [moreMenuAnchorEl, setMoreMenuAnchorEl] =
|
||||
useState<Nullable<HTMLElement>>(null);
|
||||
|
||||
const getProgramDescription = useMemo(() => {
|
||||
return getProgramSummary(program);
|
||||
}, [program]);
|
||||
@@ -190,7 +196,22 @@ export default function MediaDetailCard({ program }: Props) {
|
||||
</Box>
|
||||
<Box maxWidth={700}>
|
||||
<Stack spacing={1}>
|
||||
{getProgramTitle}
|
||||
<Stack direction={'row'} sx={{ alignItems: 'center' }}>
|
||||
<Box flex={1}>{getProgramTitle}</Box>
|
||||
<IconButton
|
||||
sx={{ width: 40, height: 40 }}
|
||||
onClick={(e) => setMoreMenuAnchorEl(e.currentTarget)}
|
||||
>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<ProgramOperationsMenu
|
||||
programId={program.uuid}
|
||||
programType={program.type}
|
||||
anchorEl={moreMenuAnchorEl}
|
||||
onClose={() => setMoreMenuAnchorEl(null)}
|
||||
open={!!moreMenuAnchorEl}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isNonEmptyString } from '@/helpers/util.ts';
|
||||
import { useIsDarkMode } from '@/hooks/useTunarrTheme.ts';
|
||||
import { useSettings } from '@/store/settings/selectors.ts';
|
||||
import { Close, MoreVert, Refresh } from '@mui/icons-material';
|
||||
import { Close, MoreVert } from '@mui/icons-material';
|
||||
import {
|
||||
Box,
|
||||
Dialog,
|
||||
@@ -9,10 +9,6 @@ import {
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Tab,
|
||||
@@ -20,26 +16,24 @@ import {
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { ProgramLike, TupleToUnion } from '@tunarr/types';
|
||||
import { isStructuralItemType, isTerminalItemType } from '@tunarr/types';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import { find, isEqual, merge } from 'lodash-es';
|
||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { find, merge } from 'lodash-es';
|
||||
import { Suspense, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import type { DeepRequired } from 'ts-essentials';
|
||||
import {
|
||||
getApiProgramGroupingsByIdOptions,
|
||||
getApiProgramGroupingsByIdQueryKey,
|
||||
getApiProgramsByIdOptions,
|
||||
postApiMoviesByIdScanMutation,
|
||||
postApiShowsByIdScanMutation,
|
||||
} from '../../generated/@tanstack/react-query.gen.ts';
|
||||
import type { Nullable } from '../../types/util.ts';
|
||||
import { ProgramMetadataDialogContent } from '../ProgramMetadataDialogContent.tsx';
|
||||
import { ProgramStreamDetails } from '../ProgramStreamDetails.tsx';
|
||||
import { RawProgramDetails } from '../RawProgramDetails.tsx';
|
||||
import { TabPanel } from '../TabPanel.tsx';
|
||||
import { ProgramOperationsMenu } from './ProgramOperationsMenu.tsx';
|
||||
|
||||
const Panels = ['metadata', 'stream_details', 'program_details'] as const;
|
||||
type Panels = TupleToUnion<typeof Panels>;
|
||||
@@ -77,7 +71,6 @@ export default function ProgramDetailsDialog({
|
||||
const darkMode = useIsDarkMode();
|
||||
const [moreMenuAnchorEl, setMoreMenuAnchorEl] =
|
||||
useState<Nullable<HTMLElement>>(null);
|
||||
const moreMenuOpen = !!moreMenuAnchorEl;
|
||||
|
||||
const visibility = useMemo(
|
||||
() => merge(DefaultPanelVisibility, panelVisibility),
|
||||
@@ -136,60 +129,6 @@ export default function ProgramDetailsDialog({
|
||||
}
|
||||
}, [programData]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const clearQueryCache = useCallback(() => {
|
||||
return queryClient.invalidateQueries({
|
||||
predicate: (key) => {
|
||||
return (
|
||||
isEqual(
|
||||
key,
|
||||
getApiProgramGroupingsByIdQueryKey({ path: { id: programId } }),
|
||||
) ||
|
||||
isEqual(key, getApiProgramsByIdOptions({ path: { id: programId } }))
|
||||
);
|
||||
},
|
||||
});
|
||||
}, [programId, queryClient]);
|
||||
|
||||
const showScanMut = useMutation({
|
||||
...postApiShowsByIdScanMutation(),
|
||||
onSuccess: () => {
|
||||
return clearQueryCache();
|
||||
},
|
||||
});
|
||||
|
||||
const movieScanMut = useMutation({
|
||||
...postApiMoviesByIdScanMutation(),
|
||||
onSuccess: () => {
|
||||
return clearQueryCache();
|
||||
},
|
||||
});
|
||||
|
||||
const scanItem = useCallback(() => {
|
||||
switch (programType) {
|
||||
case 'movie': {
|
||||
movieScanMut.mutate({ path: { id: programId } }, {});
|
||||
break;
|
||||
}
|
||||
case 'show': {
|
||||
showScanMut.mutate({
|
||||
path: { id: programId },
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'season':
|
||||
case 'episode':
|
||||
case 'album':
|
||||
case 'artist':
|
||||
case 'track':
|
||||
case 'music_video':
|
||||
case 'other_video':
|
||||
break;
|
||||
}
|
||||
|
||||
setMoreMenuAnchorEl(null);
|
||||
}, [movieScanMut, programId, programType, showScanMut]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -233,20 +172,13 @@ export default function ProgramDetailsDialog({
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
) : null}
|
||||
<Menu
|
||||
<ProgramOperationsMenu
|
||||
programId={programId}
|
||||
programType={programType}
|
||||
anchorEl={moreMenuAnchorEl}
|
||||
open={moreMenuOpen}
|
||||
onClose={() => setMoreMenuAnchorEl(null)}
|
||||
>
|
||||
{/* <MenuList>
|
||||
</MenuList> */}
|
||||
<MenuItem onClick={() => scanItem()}>
|
||||
<ListItemIcon>
|
||||
<Refresh fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Scan</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
open={!!moreMenuAnchorEl}
|
||||
/>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type ProgramGrouping,
|
||||
type TerminalProgram,
|
||||
} from '@tunarr/types';
|
||||
import { capitalize, isUndefined } from 'lodash-es';
|
||||
import { capitalize, compact, isUndefined } from 'lodash-es';
|
||||
import pluralize from 'pluralize';
|
||||
import React, { useMemo } from 'react';
|
||||
import Genres from './Genres';
|
||||
@@ -74,6 +74,10 @@ export default function ProgramInfoBar({ program, time }: Props) {
|
||||
query: queryStringValue,
|
||||
};
|
||||
|
||||
if (!rating) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/search"
|
||||
@@ -182,7 +186,7 @@ export default function ProgramInfoBar({ program, time }: Props) {
|
||||
seasonTitle,
|
||||
]);
|
||||
|
||||
return itemInfoBar.map((chip, index) => (
|
||||
return compact(itemInfoBar).map((chip, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<Box display="inline-block">{chip}</Box>
|
||||
{index < itemInfoBar.length - 1 && (
|
||||
|
||||
94
web/src/components/programs/ProgramOperationsMenu.tsx
Normal file
94
web/src/components/programs/ProgramOperationsMenu.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Refresh } from '@mui/icons-material';
|
||||
import type { PopoverProps } from '@mui/material';
|
||||
import { ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { ProgramLike } from '@tunarr/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
getApiProgramGroupingsByIdQueryKey,
|
||||
getApiProgramsByIdOptions,
|
||||
postApiMoviesByIdScanMutation,
|
||||
postApiShowsByIdScanMutation,
|
||||
} from '../../generated/@tanstack/react-query.gen.ts';
|
||||
|
||||
type Props = {
|
||||
programId: string;
|
||||
programType: ProgramLike['type'];
|
||||
anchorEl: PopoverProps['anchorEl'];
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const ProgramOperationsMenu = ({
|
||||
programId,
|
||||
programType,
|
||||
anchorEl,
|
||||
open,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const clearQueryCache = useCallback(() => {
|
||||
return queryClient.invalidateQueries({
|
||||
predicate: (key) => {
|
||||
return (
|
||||
isEqual(
|
||||
key,
|
||||
getApiProgramGroupingsByIdQueryKey({ path: { id: programId } }),
|
||||
) ||
|
||||
isEqual(key, getApiProgramsByIdOptions({ path: { id: programId } }))
|
||||
);
|
||||
},
|
||||
});
|
||||
}, [programId, queryClient]);
|
||||
|
||||
const showScanMut = useMutation({
|
||||
...postApiShowsByIdScanMutation(),
|
||||
onSuccess: () => {
|
||||
return clearQueryCache();
|
||||
},
|
||||
});
|
||||
|
||||
const movieScanMut = useMutation({
|
||||
...postApiMoviesByIdScanMutation(),
|
||||
onSuccess: () => {
|
||||
return clearQueryCache();
|
||||
},
|
||||
});
|
||||
|
||||
const scanItem = useCallback(() => {
|
||||
switch (programType) {
|
||||
case 'movie': {
|
||||
movieScanMut.mutate({ path: { id: programId } }, {});
|
||||
break;
|
||||
}
|
||||
case 'show': {
|
||||
showScanMut.mutate({
|
||||
path: { id: programId },
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'season':
|
||||
case 'episode':
|
||||
case 'album':
|
||||
case 'artist':
|
||||
case 'track':
|
||||
case 'music_video':
|
||||
case 'other_video':
|
||||
break;
|
||||
}
|
||||
|
||||
onClose();
|
||||
}, [movieScanMut, onClose, programId, programType, showScanMut]);
|
||||
|
||||
return (
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={() => onClose()}>
|
||||
<MenuItem onClick={() => scanItem()}>
|
||||
<ListItemIcon>
|
||||
<Refresh fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Scan</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -28,8 +28,6 @@ export function invalidateTaggedQueries(tagsToMatch: string | string[]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(query.queryKey, parseResult.data.tags, tagsToMatch);
|
||||
|
||||
return intersection(parseResult.data.tags, tagsToMatch).length > 0;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Close, Refresh } from '@mui/icons-material';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { trimStart } from 'lodash-es';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import type { StrictOmit } from 'ts-essentials';
|
||||
import { getApiVersionOptions } from '../generated/@tanstack/react-query.gen.ts';
|
||||
@@ -21,7 +22,8 @@ export const useVersion = (
|
||||
});
|
||||
|
||||
const versionMismatch =
|
||||
!query.isLoading && query.data?.tunarr !== __TUNARR_VERSION__;
|
||||
!query.isLoading &&
|
||||
trimStart(query.data?.tunarr, 'v') !== trimStart(__TUNARR_VERSION__, 'v');
|
||||
|
||||
if (versionMismatch) {
|
||||
snackbar.enqueueSnackbar({
|
||||
@@ -35,8 +37,8 @@ export const useVersion = (
|
||||
the browser to get the latest. If this message persists, clear your
|
||||
browser cache and reload.
|
||||
<br />
|
||||
Web version = {__TUNARR_VERSION__}, Server version ={' '}
|
||||
{query.data?.tunarr}
|
||||
Web version = {trimStart(__TUNARR_VERSION__, 'v')}, Server version ={' '}
|
||||
{trimStart(query.data?.tunarr, 'v')}
|
||||
</span>
|
||||
),
|
||||
variant: 'warning',
|
||||
|
||||
@@ -14,6 +14,9 @@ const packageVersion = packageDef.version;
|
||||
|
||||
const version = (() => {
|
||||
let tunarrVersion = process.env.TUNARR_VERSION ?? packageVersion;
|
||||
if (tunarrVersion.startsWith('v')) {
|
||||
tunarrVersion = tunarrVersion.slice(1);
|
||||
}
|
||||
const build = process.env[BUILD_ENV_VAR] ?? '';
|
||||
const isEdgeBuildValue = process.env[IS_EDGE_BUILD_ENV_VAR];
|
||||
const isEdgeBuild = isEdgeBuildValue === 'true' || isEdgeBuildValue === '1';
|
||||
|
||||
Reference in New Issue
Block a user