diff --git a/server/src/external/plex/PlexApiClient.ts b/server/src/external/plex/PlexApiClient.ts index e920b7b4..025eb6b2 100644 --- a/server/src/external/plex/PlexApiClient.ts +++ b/server/src/external/plex/PlexApiClient.ts @@ -2053,49 +2053,15 @@ export class PlexApiClient extends MediaSourceApiClient { streamType: isDefined(stream.index) ? 'subtitles' : 'external_subtitles', - // type: isDefined(stream.index) ? 'embedded' : 'external', codec: stream.codec.toLocaleLowerCase(), default: stream.default ?? false, index: stream.index ?? 0, title: stream.displayTitle, - // description: stream.extendedDisplayTitle, sdh, - // path: stream.key ? this.getFullUrl(stream.key) : undefined, - // languageCodeISO6391: stream.languageTag, languageCodeISO6392: stream.languageCode, - fileName: isNonEmptyString(stream.key) - ? this.getFullUrl(stream.key) - : undefined, + fileName: stream.key, forced: stream.forced ?? false, } satisfies MediaStream; - - // if (details.type === 'external' && isNonEmptyString(stream.key)) { - // const key = stream.key; - // const fullPath = - // await this.externalSubtitleDownloader.downloadSubtitlesIfNecessary( - // { - // externalKey: plexItem.ratingKey, - // externalSourceId: this.options.mediaSource.uuid, - // sourceType: 'plex', - // uuid: - // }, - // details, - // () => this.getSubtitles(key), - // ); - - // if (fullPath) { - // details.path = fullPath; - // return details; - // } - - // this.logger.warn( - // 'Skipping external subtitles at index %d because download failed. Please check logs and file an issue for assistance.', - // stream.index ?? -1, - // ); - - // return; - // } - return details; }, ), diff --git a/server/src/ffmpeg/SubtitleStreamPicker.ts b/server/src/ffmpeg/SubtitleStreamPicker.ts index f59d541c..15d29045 100644 --- a/server/src/ffmpeg/SubtitleStreamPicker.ts +++ b/server/src/ffmpeg/SubtitleStreamPicker.ts @@ -145,7 +145,7 @@ export class SubtitleStreamPicker { externalSourceId: lineupItem.program.mediaSourceId, externalSourceType: lineupItem.program.sourceType, }, - stream, + { streamIndex: stream.index, codec: stream.codec }, ); if (!filePath) { diff --git a/server/src/services/scanner/EmbyMediaSourceMovieScanner.ts b/server/src/services/scanner/EmbyMediaSourceMovieScanner.ts index 6a15a37a..41effcc6 100644 --- a/server/src/services/scanner/EmbyMediaSourceMovieScanner.ts +++ b/server/src/services/scanner/EmbyMediaSourceMovieScanner.ts @@ -1,18 +1,24 @@ import { MediaSourceDB } from '@/db/mediaSourceDB.js'; import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js'; -import { ScanContext } from '@/services/scanner/MediaSourceScanner.js'; +import { + GetSubtitlesRequest, + ScanContext, +} from '@/services/scanner/MediaSourceScanner.js'; import { inject, injectable, interfaces } from 'inversify'; import { ProgramConverter } from '../../db/converters/ProgramConverter.ts'; import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts'; import { KEYS } from '../../types/inject.ts'; import { EmbyT } from '../../types/internal.ts'; import { EmbyMovie } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; +import { EmbyScanUtil } from './EmbyScanUtil.ts'; import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; @@ -36,6 +42,8 @@ export class EmbyMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner< mediaSourceProgressService: MediaSourceProgressService, @inject(MeilisearchService) searchService: MeilisearchService, @inject(ProgramConverter) programConverter: ProgramConverter, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -45,6 +53,7 @@ export class EmbyMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner< searchService, programConverter, programMinterFactory(), + externalSubtitleDownloader, ); } @@ -90,4 +99,11 @@ export class EmbyMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner< .getChildItemCount(libraryKey, 'Movie') .then((_) => _.getOrThrow()); } + + protected getSubtitles( + context: ScanContext, + request: GetSubtitlesRequest, + ): Promise> { + return EmbyScanUtil.getSubtitles(context, request); + } } diff --git a/server/src/services/scanner/EmbyMediaSourceMusicScanner.ts b/server/src/services/scanner/EmbyMediaSourceMusicScanner.ts index 935f31ad..41182001 100644 --- a/server/src/services/scanner/EmbyMediaSourceMusicScanner.ts +++ b/server/src/services/scanner/EmbyMediaSourceMusicScanner.ts @@ -5,8 +5,10 @@ import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { MediaSourceDB } from '../../db/mediaSourceDB.ts'; import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts'; import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { WrappedError } from '../../types/errors.ts'; import { KEYS } from '../../types/inject.ts'; import { EmbyT } from '../../types/internal.ts'; @@ -18,9 +20,10 @@ import { import { Result } from '../../types/result.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; +import { EmbyScanUtil } from './EmbyScanUtil.ts'; import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; -import { ScanContext } from './MediaSourceScanner.ts'; +import { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts'; export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< EmbyT, @@ -46,6 +49,8 @@ export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< mediaSourceProgressService: MediaSourceProgressService, @inject(GetProgramGroupingById) getProgramGroupingsById: GetProgramGroupingById, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -56,6 +61,7 @@ export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< searchService, mediaSourceProgressService, getProgramGroupingsById, + externalSubtitleDownloader, ); } @@ -109,4 +115,11 @@ export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< .getChildItemCount(libraryKey, 'MusicArtist') .then((_) => _.getOrThrow()); } + + protected getSubtitles( + context: ScanContext, + request: GetSubtitlesRequest, + ): Promise> { + return EmbyScanUtil.getSubtitles(context, request); + } } diff --git a/server/src/services/scanner/EmbyMediaSourceTvShowScanner.ts b/server/src/services/scanner/EmbyMediaSourceTvShowScanner.ts index 6f5cf998..1d7c1222 100644 --- a/server/src/services/scanner/EmbyMediaSourceTvShowScanner.ts +++ b/server/src/services/scanner/EmbyMediaSourceTvShowScanner.ts @@ -1,12 +1,16 @@ import { MediaSourceDB } from '@/db/mediaSourceDB.js'; import { MediaSourceType } from '@/db/schema/base.js'; import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js'; -import { ScanContext } from '@/services/scanner/MediaSourceScanner.js'; +import { + GetSubtitlesRequest, + ScanContext, +} from '@/services/scanner/MediaSourceScanner.js'; import { inject, injectable, interfaces } from 'inversify'; import { isNil } from 'lodash-es'; import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts'; import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts'; import { WrappedError } from '../../types/errors.ts'; import { KEYS } from '../../types/inject.ts'; @@ -21,8 +25,10 @@ import { SeasonWithShow, } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; +import { EmbyScanUtil } from './EmbyScanUtil.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts'; @@ -51,6 +57,8 @@ export class EmbyMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne @inject(MeilisearchService) searchService: MeilisearchService, @inject(GetProgramGroupingById) getProgramGroupingsById: GetProgramGroupingById, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -61,6 +69,7 @@ export class EmbyMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne searchService, mediaSourceProgressService, getProgramGroupingsById, + externalSubtitleDownloader, ); } @@ -155,4 +164,11 @@ export class EmbyMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne protected isSeasonT(grouping: ProgramGrouping): grouping is EmbySeason { return grouping.sourceType === 'emby' && grouping.type === 'season'; } + + protected getSubtitles( + context: ScanContext, + request: GetSubtitlesRequest, + ): Promise> { + return EmbyScanUtil.getSubtitles(context, request); + } } diff --git a/server/src/services/scanner/EmbyScanUtil.ts b/server/src/services/scanner/EmbyScanUtil.ts new file mode 100644 index 00000000..37f446f7 --- /dev/null +++ b/server/src/services/scanner/EmbyScanUtil.ts @@ -0,0 +1,19 @@ +import type { QueryResult } from '../../external/BaseApiClient.ts'; +import type { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts'; +import type { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts'; + +export class EmbyScanUtil { + private constructor() {} + + static async getSubtitles( + context: ScanContext, + req: GetSubtitlesRequest, + ): Promise> { + return context.apiClient.getSubtitles( + req.externalItemId, + req.externalMediaItemId ?? req.externalItemId, + req.streamIndex, + req.extension, + ); + } +} diff --git a/server/src/services/scanner/JellyfinMediaSourceMovieScanner.ts b/server/src/services/scanner/JellyfinMediaSourceMovieScanner.ts index 3cced5ef..59ac62b6 100644 --- a/server/src/services/scanner/JellyfinMediaSourceMovieScanner.ts +++ b/server/src/services/scanner/JellyfinMediaSourceMovieScanner.ts @@ -1,18 +1,24 @@ import { MediaSourceDB } from '@/db/mediaSourceDB.js'; import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js'; -import { ScanContext } from '@/services/scanner/MediaSourceScanner.js'; +import { + GetSubtitlesRequest, + ScanContext, +} from '@/services/scanner/MediaSourceScanner.js'; import { inject, injectable, interfaces } from 'inversify'; import { ProgramConverter } from '../../db/converters/ProgramConverter.ts'; import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { KEYS } from '../../types/inject.ts'; import { JellyfinT } from '../../types/internal.ts'; import { JellyfinMovie } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; +import { JellyfinScanUtil } from './JellyfinScanUtil.ts'; import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; @@ -36,6 +42,8 @@ export class JellyfinMediaSourceMovieScanner extends MediaSourceMovieLibraryScan mediaSourceProgressService: MediaSourceProgressService, @inject(MeilisearchService) searchService: MeilisearchService, @inject(ProgramConverter) programConverter: ProgramConverter, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -45,6 +53,7 @@ export class JellyfinMediaSourceMovieScanner extends MediaSourceMovieLibraryScan searchService, programConverter, programMinterFactory(), + externalSubtitleDownloader, ); } @@ -90,4 +99,11 @@ export class JellyfinMediaSourceMovieScanner extends MediaSourceMovieLibraryScan .getChildItemCount(libraryKey, 'Movie') .then((_) => _.getOrThrow()); } + + protected getSubtitles( + context: ScanContext, + request: GetSubtitlesRequest, + ): Promise> { + return JellyfinScanUtil.getSubtitles(context, request); + } } diff --git a/server/src/services/scanner/JellyfinMediaSourceMusicScanner.ts b/server/src/services/scanner/JellyfinMediaSourceMusicScanner.ts index d27806ab..51bc7b42 100644 --- a/server/src/services/scanner/JellyfinMediaSourceMusicScanner.ts +++ b/server/src/services/scanner/JellyfinMediaSourceMusicScanner.ts @@ -7,6 +7,7 @@ import { MediaSourceDB } from '../../db/mediaSourceDB.ts'; import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js'; import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts'; import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { WrappedError } from '../../types/errors.ts'; import { KEYS } from '../../types/inject.ts'; import { JellyfinT } from '../../types/internal.ts'; @@ -16,11 +17,13 @@ import { JellyfinMusicTrack, } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; +import { JellyfinScanUtil } from './JellyfinScanUtil.ts'; import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; -import { ScanContext } from './MediaSourceScanner.ts'; +import { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts'; export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< JellyfinT, @@ -46,6 +49,8 @@ export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScann mediaSourceProgressService: MediaSourceProgressService, @inject(GetProgramGroupingById) getProgramGroupingsById: GetProgramGroupingById, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -56,6 +61,7 @@ export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScann searchService, mediaSourceProgressService, getProgramGroupingsById, + externalSubtitleDownloader, ); } @@ -109,4 +115,11 @@ export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScann .getChildItemCount(libraryKey, 'MusicArtist') .then((_) => _.getOrThrow()); } + + protected getSubtitles( + context: ScanContext, + request: GetSubtitlesRequest, + ): Promise> { + return JellyfinScanUtil.getSubtitles(context, request); + } } diff --git a/server/src/services/scanner/JellyfinMediaSourceOtherVideoScanner.ts b/server/src/services/scanner/JellyfinMediaSourceOtherVideoScanner.ts index 19406f50..bad344bb 100644 --- a/server/src/services/scanner/JellyfinMediaSourceOtherVideoScanner.ts +++ b/server/src/services/scanner/JellyfinMediaSourceOtherVideoScanner.ts @@ -4,8 +4,10 @@ import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { MediaSourceDB } from '../../db/mediaSourceDB.ts'; import type { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts'; import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { WrappedError } from '../../types/errors.ts'; import { KEYS } from '../../types/inject.ts'; import type { JellyfinT } from '../../types/internal.ts'; @@ -13,9 +15,10 @@ import type { JellyfinOtherVideo } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; +import { JellyfinScanUtil } from './JellyfinScanUtil.ts'; import { MediaSourceOtherVideoScanner } from './MediaSourceOtherVideoScanner.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; -import type { ScanContext } from './MediaSourceScanner.ts'; +import type { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts'; @injectable() export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScanner< @@ -37,6 +40,8 @@ export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoS mediaSourceProgressService: MediaSourceProgressService, @inject(KEYS.ProgramDaoMinterFactory) programMinterFactory: interfaces.AutoFactory, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -45,6 +50,7 @@ export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoS searchService, mediaSourceProgressService, programMinterFactory(), + externalSubtitleDownloader, ); } @@ -102,4 +108,11 @@ export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoS protected getExternalKey(video: JellyfinOtherVideo): string { return video.externalId; } + + protected getSubtitles( + context: ScanContext, + request: GetSubtitlesRequest, + ): Promise> { + return JellyfinScanUtil.getSubtitles(context, request); + } } diff --git a/server/src/services/scanner/JellyfinMediaSourceTvShowScanner.ts b/server/src/services/scanner/JellyfinMediaSourceTvShowScanner.ts index 33b5ef9a..4b7a6f3c 100644 --- a/server/src/services/scanner/JellyfinMediaSourceTvShowScanner.ts +++ b/server/src/services/scanner/JellyfinMediaSourceTvShowScanner.ts @@ -1,13 +1,17 @@ import { MediaSourceDB } from '@/db/mediaSourceDB.js'; import { MediaSourceType } from '@/db/schema/base.js'; import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js'; -import { ScanContext } from '@/services/scanner/MediaSourceScanner.js'; +import { + GetSubtitlesRequest, + ScanContext, +} from '@/services/scanner/MediaSourceScanner.js'; import { inject, injectable, interfaces } from 'inversify'; import { isNil } from 'lodash-es'; import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts'; import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { WrappedError } from '../../types/errors.ts'; import { KEYS } from '../../types/inject.ts'; @@ -21,8 +25,10 @@ import { SeasonWithShow, } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; +import { JellyfinScanUtil } from './JellyfinScanUtil.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts'; @@ -51,6 +57,8 @@ export class JellyfinMediaSourceTvShowScanner extends MediaSourceTvShowLibrarySc @inject(MeilisearchService) searchService: MeilisearchService, @inject(GetProgramGroupingById) getProgramGroupingsById: GetProgramGroupingById, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -61,6 +69,7 @@ export class JellyfinMediaSourceTvShowScanner extends MediaSourceTvShowLibrarySc searchService, mediaSourceProgressService, getProgramGroupingsById, + externalSubtitleDownloader, ); } @@ -155,4 +164,11 @@ export class JellyfinMediaSourceTvShowScanner extends MediaSourceTvShowLibrarySc protected isSeasonT(grouping: ProgramGrouping): grouping is JellyfinSeason { return grouping.sourceType === 'jellyfin' && grouping.type === 'season'; } + + protected getSubtitles( + context: ScanContext, + request: GetSubtitlesRequest, + ): Promise> { + return JellyfinScanUtil.getSubtitles(context, request); + } } diff --git a/server/src/services/scanner/JellyfinScanUtil.ts b/server/src/services/scanner/JellyfinScanUtil.ts new file mode 100644 index 00000000..58b50b4e --- /dev/null +++ b/server/src/services/scanner/JellyfinScanUtil.ts @@ -0,0 +1,19 @@ +import type { QueryResult } from '../../external/BaseApiClient.ts'; +import type { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts'; +import type { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts'; + +export class JellyfinScanUtil { + private constructor() {} + + static async getSubtitles( + context: ScanContext, + req: GetSubtitlesRequest, + ): Promise> { + return context.apiClient.getSubtitles( + req.externalItemId, + req.externalMediaItemId ?? req.externalItemId, + req.streamIndex, + req.extension, + ); + } +} diff --git a/server/src/services/scanner/MediaSourceMovieLibraryScanner.ts b/server/src/services/scanner/MediaSourceMovieLibraryScanner.ts index fcbd4c3c..652dfc15 100644 --- a/server/src/services/scanner/MediaSourceMovieLibraryScanner.ts +++ b/server/src/services/scanner/MediaSourceMovieLibraryScanner.ts @@ -8,6 +8,7 @@ import type { RemoteMediaSourceType } from '../../db/schema/MediaSource.ts'; import { ProgramType } from '../../db/schema/Program.ts'; import { isMovieProgram } from '../../db/schema/schemaTypeGuards.ts'; import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts'; +import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import type { HasMediaSourceInfo, Movie } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; import type { Logger } from '../../util/logging/LoggerFactory.ts'; @@ -45,9 +46,9 @@ export abstract class MediaSourceMovieLibraryScanner< private searchService: MeilisearchService, protected programConverter: ProgramConverter, protected programMinter: ProgramDaoMinter, - // protected externalSubtitleDownloader: ExternalSubtitleDownloader, + protected externalSubtitleDownloader: ExternalSubtitleDownloader, ) { - super(logger, mediaSourceDB); + super(logger, mediaSourceDB, externalSubtitleDownloader); } protected async scanInternal( @@ -124,6 +125,13 @@ export abstract class MediaSourceMovieLibraryScanner< fullMovie, ); + await this.downloadExternalSubtitleStreams(minted, (req) => + this.getSubtitles(context, { + ...req, + externalMediaItemId: fullMovie.mediaItem?.externalKey ?? undefined, + }), + ); + const upsertResult = await Result.attemptAsync(() => this.programDB.upsertPrograms([minted]), ); diff --git a/server/src/services/scanner/MediaSourceMusicArtistScanner.ts b/server/src/services/scanner/MediaSourceMusicArtistScanner.ts index 1dadd845..34d759fd 100644 --- a/server/src/services/scanner/MediaSourceMusicArtistScanner.ts +++ b/server/src/services/scanner/MediaSourceMusicArtistScanner.ts @@ -32,6 +32,7 @@ import { Result } from '../../types/result.ts'; import type { Maybe } from '../../types/util.ts'; import type { Logger } from '../../util/logging/LoggerFactory.ts'; import type { MeilisearchService } from '../MeilisearchService.ts'; +import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import type { MediaSourceProgressService } from './MediaSourceProgressService.ts'; import type { ScanContext } from './MediaSourceScanner.ts'; import { MediaSourceScanner } from './MediaSourceScanner.ts'; @@ -78,8 +79,9 @@ export abstract class MediaSourceMusicArtistScanner< protected searchService: MeilisearchService, private mediaSourceProgressService: MediaSourceProgressService, private getProgramGroupingByIdCommand: GetProgramGroupingById, + protected externalSubtitleDownloader: ExternalSubtitleDownloader, ) { - super(logger, mediaSourceDB); + super(logger, mediaSourceDB, externalSubtitleDownloader); } protected async scanInternal( diff --git a/server/src/services/scanner/MediaSourceOtherVideoScanner.ts b/server/src/services/scanner/MediaSourceOtherVideoScanner.ts index 1c107b73..1d1fabf7 100644 --- a/server/src/services/scanner/MediaSourceOtherVideoScanner.ts +++ b/server/src/services/scanner/MediaSourceOtherVideoScanner.ts @@ -5,6 +5,7 @@ import type { MediaSourceDB } from '../../db/mediaSourceDB.ts'; import type { RemoteMediaSourceType } from '../../db/schema/MediaSource.ts'; import { ProgramType } from '../../db/schema/Program.ts'; import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts'; +import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import type { HasMediaSourceInfo, OtherVideo } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; import type { Logger } from '../../util/logging/LoggerFactory.ts'; @@ -33,8 +34,9 @@ export abstract class MediaSourceOtherVideoScanner< private searchService: MeilisearchService, private mediaSourceProgressService: MediaSourceProgressService, protected programMinter: ProgramDaoMinter, + protected externalSubtitleDownloader: ExternalSubtitleDownloader, ) { - super(logger, mediaSourceDB); + super(logger, mediaSourceDB, externalSubtitleDownloader); } protected async scanInternal( @@ -105,6 +107,14 @@ export abstract class MediaSourceOtherVideoScanner< fullMetadata, ); + await this.downloadExternalSubtitleStreams(minted, (req) => + this.getSubtitles(context, { + ...req, + externalMediaItemId: + fullMetadata.mediaItem?.externalKey ?? undefined, + }), + ); + const upsertResult = await Result.attemptAsync(() => this.programDB.upsertPrograms([minted]), ); diff --git a/server/src/services/scanner/MediaSourceScanner.ts b/server/src/services/scanner/MediaSourceScanner.ts index be31083d..8d04826d 100644 --- a/server/src/services/scanner/MediaSourceScanner.ts +++ b/server/src/services/scanner/MediaSourceScanner.ts @@ -1,12 +1,18 @@ import type { MediaSourceLibraryOrm } from '@/db/schema/MediaSourceLibrary.js'; import dayjs from 'dayjs'; +import { isEmpty } from 'lodash-es'; import type { MediaSourceDB } from '../../db/mediaSourceDB.ts'; -import type { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js'; +import type { + MediaSourceWithRelations, + NewProgramWithRelations, +} from '../../db/schema/derivedTypes.js'; import type { MediaLibraryType, MediaSourceOrm, RemoteMediaSourceType, } from '../../db/schema/MediaSource.ts'; +import type { QueryResult } from '../../external/BaseApiClient.ts'; +import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { devAssert } from '../../util/debug.ts'; import type { Logger } from '../../util/logging/LoggerFactory.ts'; @@ -42,6 +48,14 @@ export type GenericMediaSourceScannerFactory = ( libraryType: MediaLibraryType, ) => GenericMediaSourceScanner; +export type GetSubtitlesRequest = { + key: string; + extension: string; + externalItemId: string; + externalMediaItemId?: string; + streamIndex: number; // Only relevant for Jellyfin +}; + export abstract class BaseMediaSourceScanner { abstract scan(req: ScanRequestT): Promise; @@ -62,6 +76,7 @@ export abstract class MediaSourceScanner< constructor( protected logger: Logger, protected mediaSourceDB: MediaSourceDB, + protected externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super(); } @@ -130,4 +145,55 @@ export abstract class MediaSourceScanner< libraryKey: string, context: ScanContext, ): Promise; + + protected abstract getSubtitles( + context: ScanContext, + request: GetSubtitlesRequest, + ): Promise>; + + protected async downloadExternalSubtitleStreams( + { program, subtitles }: NewProgramWithRelations, + getSubtitlesCallback: ( + args: GetSubtitlesRequest, + ) => Promise>, + ) { + const externalSubtitleStreams = + subtitles.filter((stream) => stream.subtitleType === 'sidecar') ?? []; + + for (const stream of externalSubtitleStreams) { + if (isEmpty(stream.path)) { + continue; + } + + const fullPath = + await this.externalSubtitleDownloader.downloadSubtitlesIfNecessary( + { + externalKey: program.externalKey, + externalSourceId: program.mediaSourceId, + sourceType: program.sourceType, + uuid: program.uuid, + }, + { streamIndex: stream.streamIndex ?? undefined, codec: stream.codec }, + (args) => + getSubtitlesCallback({ + ...args, + key: stream.path!, + externalItemId: program.externalKey, + streamIndex: stream.streamIndex ?? 0, + }), + ); + + if (fullPath) { + stream.path = fullPath; + // return details; + } + + this.logger.warn( + 'Skipping external subtitles at index %d because download failed. Please check logs and file an issue for assistance.', + stream.streamIndex ?? -1, + ); + + return; + } + } } diff --git a/server/src/services/scanner/MediaSourceTvShowLibraryScanner.ts b/server/src/services/scanner/MediaSourceTvShowLibraryScanner.ts index 803e50fd..e1029593 100644 --- a/server/src/services/scanner/MediaSourceTvShowLibraryScanner.ts +++ b/server/src/services/scanner/MediaSourceTvShowLibraryScanner.ts @@ -13,6 +13,7 @@ import type { RemoteMediaSourceType } from '../../db/schema/MediaSource.ts'; import { ProgramType } from '../../db/schema/Program.ts'; import { ProgramGroupingType } from '../../db/schema/ProgramGrouping.ts'; import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts'; +import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import type { HasMediaSourceAndLibraryId, MediaSourceEpisode, @@ -61,8 +62,9 @@ export abstract class MediaSourceTvShowLibraryScanner< protected searchService: MeilisearchService, private mediaSourceProgressService: MediaSourceProgressService, private getProgramGroupingByIdCommand: GetProgramGroupingById, + protected externalSubtitleDownloader: ExternalSubtitleDownloader, ) { - super(logger, mediaSourceDB); + super(logger, mediaSourceDB, externalSubtitleDownloader); } protected async scanInternal( @@ -499,6 +501,14 @@ export abstract class MediaSourceTvShowLibraryScanner< episodeWithJoins, ); + await this.downloadExternalSubtitleStreams(dao, (req) => + this.getSubtitles(scanContext, { + ...req, + externalMediaItemId: + episodeWithJoins.mediaItem?.externalKey ?? undefined, + }), + ); + dao.program.tvShowUuid = show.uuid; dao.program.seasonUuid = season.uuid; diff --git a/server/src/services/scanner/PlexMediaSourceMovieScanner.ts b/server/src/services/scanner/PlexMediaSourceMovieScanner.ts index 8880720f..fa5aad87 100644 --- a/server/src/services/scanner/PlexMediaSourceMovieScanner.ts +++ b/server/src/services/scanner/PlexMediaSourceMovieScanner.ts @@ -1,13 +1,18 @@ import { MediaSourceDB } from '@/db/mediaSourceDB.js'; import { MediaSourceType } from '@/db/schema/base.js'; import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js'; -import { ScanContext } from '@/services/scanner/MediaSourceScanner.js'; +import { + GetSubtitlesRequest, + ScanContext, +} from '@/services/scanner/MediaSourceScanner.js'; import { inject, injectable, interfaces } from 'inversify'; import { ProgramConverter } from '../../db/converters/ProgramConverter.ts'; import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { PlexApiClient } from '../../external/plex/PlexApiClient.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { KEYS } from '../../types/inject.ts'; import { PlexMovie } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; @@ -15,6 +20,7 @@ import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; +import { PlexScanUtil } from './PlexScanUtil.ts'; @injectable() export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner< @@ -35,6 +41,8 @@ export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner< mediaSourceProgressService: MediaSourceProgressService, @inject(MeilisearchService) searchService: MeilisearchService, @inject(ProgramConverter) programConverter: ProgramConverter, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -44,6 +52,7 @@ export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner< searchService, programConverter, programMinterFactory(), + externalSubtitleDownloader, ); } @@ -77,4 +86,11 @@ export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner< ): Promise> { return apiClient.getMovie(incomingMovie.externalId); } + + protected getSubtitles( + context: ScanContext, + { key }: GetSubtitlesRequest, + ): Promise> { + return PlexScanUtil.getSubtitles(context, key); + } } diff --git a/server/src/services/scanner/PlexMediaSourceMusicScanner.ts b/server/src/services/scanner/PlexMediaSourceMusicScanner.ts index 548e1a1c..629819f0 100644 --- a/server/src/services/scanner/PlexMediaSourceMusicScanner.ts +++ b/server/src/services/scanner/PlexMediaSourceMusicScanner.ts @@ -1,22 +1,28 @@ import { MediaSourceDB } from '@/db/mediaSourceDB.js'; import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js'; -import { ScanContext } from '@/services/scanner/MediaSourceScanner.js'; +import { + GetSubtitlesRequest, + ScanContext, +} from '@/services/scanner/MediaSourceScanner.js'; import { inject, injectable, interfaces } from 'inversify'; import { GetProgramGroupingById } from '../../commands/GetProgramGroupingById.ts'; import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts'; import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { PlexApiClient } from '../../external/plex/PlexApiClient.ts'; import { WrappedError } from '../../types/errors.ts'; import { KEYS } from '../../types/inject.ts'; import { PlexT } from '../../types/internal.ts'; import { PlexAlbum, PlexArtist, PlexTrack } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; +import { PlexScanUtil } from './PlexScanUtil.ts'; @injectable() export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< @@ -43,6 +49,8 @@ export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< mediaSourceProgressService: MediaSourceProgressService, @inject(GetProgramGroupingById) getProgramGroupingsById: GetProgramGroupingById, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -53,6 +61,7 @@ export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< searchService, mediaSourceProgressService, getProgramGroupingsById, + externalSubtitleDownloader, ); } @@ -106,4 +115,11 @@ export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner< .getLibraryCount(libraryKey) .then((_) => _.getOrThrow()); } + + protected getSubtitles( + context: ScanContext, + { key }: GetSubtitlesRequest, + ): Promise> { + return PlexScanUtil.getSubtitles(context, key); + } } diff --git a/server/src/services/scanner/PlexMediaSourceOtherVideoScanner.ts b/server/src/services/scanner/PlexMediaSourceOtherVideoScanner.ts index 50e93d0d..32cba39b 100644 --- a/server/src/services/scanner/PlexMediaSourceOtherVideoScanner.ts +++ b/server/src/services/scanner/PlexMediaSourceOtherVideoScanner.ts @@ -4,8 +4,10 @@ import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { MediaSourceDB } from '../../db/mediaSourceDB.ts'; import type { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts'; import type { PlexApiClient } from '../../external/plex/PlexApiClient.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { WrappedError } from '../../types/errors.ts'; import { KEYS } from '../../types/inject.ts'; import type { PlexT } from '../../types/internal.ts'; @@ -15,7 +17,8 @@ import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; import { MediaSourceOtherVideoScanner } from './MediaSourceOtherVideoScanner.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; -import type { ScanContext } from './MediaSourceScanner.ts'; +import type { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts'; +import { PlexScanUtil } from './PlexScanUtil.ts'; @injectable() export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScanner< @@ -37,6 +40,8 @@ export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScann mediaSourceProgressService: MediaSourceProgressService, @inject(KEYS.ProgramDaoMinterFactory) programMinterFactory: interfaces.AutoFactory, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -45,6 +50,7 @@ export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScann searchService, mediaSourceProgressService, programMinterFactory(), + externalSubtitleDownloader, ); } @@ -82,4 +88,11 @@ export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScann protected getExternalKey(video: PlexOtherVideo): string { return video.externalId; } + + protected getSubtitles( + context: ScanContext, + { key }: GetSubtitlesRequest, + ): Promise> { + return PlexScanUtil.getSubtitles(context, key); + } } diff --git a/server/src/services/scanner/PlexMediaSourceTvShowScanner.ts b/server/src/services/scanner/PlexMediaSourceTvShowScanner.ts index b5dcb798..4de614a2 100644 --- a/server/src/services/scanner/PlexMediaSourceTvShowScanner.ts +++ b/server/src/services/scanner/PlexMediaSourceTvShowScanner.ts @@ -1,6 +1,9 @@ import { MediaSourceDB } from '@/db/mediaSourceDB.js'; import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.js'; -import { ScanContext } from '@/services/scanner/MediaSourceScanner.js'; +import { + GetSubtitlesRequest, + ScanContext, +} from '@/services/scanner/MediaSourceScanner.js'; import { ProgramGrouping } from '@tunarr/types'; import { inject, injectable, interfaces } from 'inversify'; import { GetProgramGroupingById } from '../../commands/GetProgramGroupingById.ts'; @@ -8,6 +11,7 @@ import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts'; import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts'; import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js'; +import { QueryResult } from '../../external/BaseApiClient.ts'; import { PlexApiClient } from '../../external/plex/PlexApiClient.ts'; import { WrappedError } from '../../types/errors.ts'; import { KEYS } from '../../types/inject.ts'; @@ -18,10 +22,12 @@ import { SeasonWithShow, } from '../../types/Media.ts'; import { Result } from '../../types/result.ts'; +import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts'; import { Logger } from '../../util/logging/LoggerFactory.ts'; import { MeilisearchService } from '../MeilisearchService.ts'; import { MediaSourceProgressService } from './MediaSourceProgressService.ts'; import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts'; +import { PlexScanUtil } from './PlexScanUtil.ts'; @injectable() export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanner< @@ -48,6 +54,8 @@ export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne mediaSourceProgressService: MediaSourceProgressService, @inject(GetProgramGroupingById) getProgramGroupingsById: GetProgramGroupingById, + @inject(ExternalSubtitleDownloader) + externalSubtitleDownloader: ExternalSubtitleDownloader, ) { super( logger, @@ -58,6 +66,7 @@ export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne searchService, mediaSourceProgressService, getProgramGroupingsById, + externalSubtitleDownloader, ); } @@ -136,4 +145,11 @@ export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne protected isSeasonT(grouping: ProgramGrouping): grouping is PlexSeason { return grouping.sourceType === 'plex' && grouping.type === 'season'; } + + protected getSubtitles( + context: ScanContext, + { key }: GetSubtitlesRequest, + ): Promise> { + return PlexScanUtil.getSubtitles(context, key); + } } diff --git a/server/src/services/scanner/PlexScanUtil.ts b/server/src/services/scanner/PlexScanUtil.ts new file mode 100644 index 00000000..2aeb1837 --- /dev/null +++ b/server/src/services/scanner/PlexScanUtil.ts @@ -0,0 +1,14 @@ +import type { QueryResult } from '../../external/BaseApiClient.ts'; +import type { PlexApiClient } from '../../external/plex/PlexApiClient.ts'; +import type { ScanContext } from './MediaSourceScanner.ts'; + +export class PlexScanUtil { + private constructor() {} + + static async getSubtitles( + context: ScanContext, + key: string, + ): Promise> { + return context.apiClient.getSubtitles(key); + } +} diff --git a/server/src/stream/ExternalSubtitleDownloader.ts b/server/src/stream/ExternalSubtitleDownloader.ts index 56b8d16c..9e95f712 100644 --- a/server/src/stream/ExternalSubtitleDownloader.ts +++ b/server/src/stream/ExternalSubtitleDownloader.ts @@ -5,18 +5,22 @@ import { MediaSourceId, MediaSourceType } from '../db/schema/base.ts'; import { QueryResult } from '../external/BaseApiClient.ts'; import { FileSystemService } from '../services/FileSystemService.ts'; import { KEYS } from '../types/inject.ts'; +import { Maybe } from '../types/util.ts'; import { fileExists } from '../util/fsUtil.ts'; import { Logger } from '../util/logging/LoggerFactory.ts'; import { getSubtitleCacheFilePath, subtitleCodecToExt, } from '../util/subtitles.ts'; -import { SubtitleStreamDetails } from './types.ts'; -type GetSubtitleCallbackArgs = { +export type GetSubtitleCallbackArgs = { extension: string; }; +type GetSubtitlesCallback = ( + cbArgs: GetSubtitleCallbackArgs, +) => Promise>; + type ExternalItem = { externalKey: string; externalSourceId: MediaSourceId; @@ -41,10 +45,8 @@ export class ExternalSubtitleDownloader { */ async downloadSubtitlesIfNecessary( item: ExternalItem, - details: SubtitleStreamDetails, - getSubtitlesCb: ( - args: GetSubtitleCallbackArgs, - ) => Promise>, + details: { streamIndex: Maybe; codec: string }, + getSubtitlesCb: GetSubtitlesCallback, ) { const outPath = getSubtitleCacheFilePath( { @@ -53,7 +55,10 @@ export class ExternalSubtitleDownloader { externalSourceType: item.sourceType, id: item.uuid, }, - details, + { + codec: details.codec, + streamIndex: details.streamIndex, + }, ); const ext = subtitleCodecToExt(details.codec); diff --git a/server/src/stream/emby/EmbyStreamDetails.ts b/server/src/stream/emby/EmbyStreamDetails.ts index 55153a32..ff1263dd 100644 --- a/server/src/stream/emby/EmbyStreamDetails.ts +++ b/server/src/stream/emby/EmbyStreamDetails.ts @@ -350,7 +350,10 @@ export class EmbyStreamDetails extends ExternalStreamDetailsFetcher { sourceType: 'emby', uuid: item.uuid, }, - details, + { + codec: details.codec, + streamIndex: details.index, + }, ({ extension: ext }) => this.emby.getSubtitles( item.externalKey, diff --git a/server/src/stream/jellyfin/JellyfinStreamDetails.ts b/server/src/stream/jellyfin/JellyfinStreamDetails.ts index a8bede74..25269d17 100644 --- a/server/src/stream/jellyfin/JellyfinStreamDetails.ts +++ b/server/src/stream/jellyfin/JellyfinStreamDetails.ts @@ -359,7 +359,10 @@ export class JellyfinStreamDetails extends ExternalStreamDetailsFetcher this.jellyfin.getSubtitles( item.externalKey, diff --git a/server/src/stream/plex/PlexStreamDetails.ts b/server/src/stream/plex/PlexStreamDetails.ts index 75f918c5..2e5c0817 100644 --- a/server/src/stream/plex/PlexStreamDetails.ts +++ b/server/src/stream/plex/PlexStreamDetails.ts @@ -468,7 +468,10 @@ export class PlexStreamDetails extends ExternalStreamDetailsFetcher { sourceType: 'plex', uuid: item.uuid, }, - details, + { + codec: details.codec, + streamIndex: details.index, + }, () => plexApiClient.getSubtitles(key), ); diff --git a/server/src/tasks/SubtitleExtractorTask.ts b/server/src/tasks/SubtitleExtractorTask.ts index 456a4fb8..366bb5cf 100644 --- a/server/src/tasks/SubtitleExtractorTask.ts +++ b/server/src/tasks/SubtitleExtractorTask.ts @@ -237,7 +237,7 @@ export class SubtitleExtractorTask extends Task2< externalSourceType: program.externalSourceType, id: program.id, }, - subtitle, + { streamIndex: subtitle.index, codec: subtitle.codec }, ); if (!filePath) { return; diff --git a/server/src/util/subtitles.ts b/server/src/util/subtitles.ts index 14992ec0..5842e6b1 100644 --- a/server/src/util/subtitles.ts +++ b/server/src/util/subtitles.ts @@ -2,8 +2,7 @@ import type { MediaSourceId, MediaSourceType } from '@/db/schema/base.js'; import crypto from 'node:crypto'; import path from 'path'; import { match, P } from 'ts-pattern'; -import type { SubtitleStreamDetails } from '../stream/types.ts'; -import type { Nullable } from '../types/util.ts'; +import type { Maybe, Nullable } from '../types/util.ts'; type MinimalProgram = { id: string; @@ -22,9 +21,13 @@ export function subtitleCodecToExt(codec: string): Nullable { export function getSubtitleCacheFilePath( program: MinimalProgram, - subtitleStream: SubtitleStreamDetails, + subtitleStream: { streamIndex: Maybe; codec: string }, ) { - const outputPath = getSubtitleCacheFileName(program, subtitleStream); + const outputPath = getSubtitleCacheFileName( + program, + subtitleStream.streamIndex, + subtitleStream.codec, + ); const ext = subtitleCodecToExt(subtitleStream.codec.toLowerCase()); if (!ext) { return null; @@ -39,7 +42,8 @@ export function getSubtitleCacheFilePath( function getSubtitleCacheFileName( program: MinimalProgram, - subtitleStream: SubtitleStreamDetails, + streamIndex: Maybe, + codec: string, ) { // TODO: We should not always include the external key in here. but it will bust the "cache" // if the underlying program changes at the target @@ -49,7 +53,7 @@ function getSubtitleCacheFileName( .update(program.externalSourceType) .update(program.externalSourceId) .update(program.externalKey) - .update(subtitleStream.index?.toString() ?? '') - .update(subtitleStream.codec) + .update(streamIndex?.toString() ?? '') + .update(codec) .digest('hex'); } diff --git a/types/src/schemas/programmingSchema.ts b/types/src/schemas/programmingSchema.ts index c0fa0473..2c198502 100644 --- a/types/src/schemas/programmingSchema.ts +++ b/types/src/schemas/programmingSchema.ts @@ -422,6 +422,7 @@ export const MediaStream = z.object({ // Subtitles sdh: z.boolean().nullish(), + externalKey: z.string().nullish(), // Audio or Subtitles languageCodeISO6392: z.string().nullish(), @@ -470,6 +471,7 @@ export const MediaItem = z.object({ locations: z.array(MediaLocation), chapters: z.array(MediaChapter).nullish(), scanKind: z.enum(['unknown', 'progressive', 'interlaced']).nullish(), + externalKey: z.string().nullish(), }); const WithMediaItemMetadata = z.object({