mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 00:53:35 -04:00
fix: download external subtitles as part of scanning
This commit is contained in:
36
server/src/external/plex/PlexApiClient.ts
vendored
36
server/src/external/plex/PlexApiClient.ts
vendored
@@ -2053,49 +2053,15 @@ export class PlexApiClient extends MediaSourceApiClient<PlexTypes> {
|
||||
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;
|
||||
},
|
||||
),
|
||||
|
||||
@@ -145,7 +145,7 @@ export class SubtitleStreamPicker {
|
||||
externalSourceId: lineupItem.program.mediaSourceId,
|
||||
externalSourceType: lineupItem.program.sourceType,
|
||||
},
|
||||
stream,
|
||||
{ streamIndex: stream.index, codec: stream.codec },
|
||||
);
|
||||
|
||||
if (!filePath) {
|
||||
|
||||
@@ -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<EmbyApiClient>,
|
||||
request: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return EmbyScanUtil.getSubtitles(context, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EmbyApiClient>,
|
||||
request: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return EmbyScanUtil.getSubtitles(context, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<EmbyApiClient>,
|
||||
request: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return EmbyScanUtil.getSubtitles(context, request);
|
||||
}
|
||||
}
|
||||
|
||||
19
server/src/services/scanner/EmbyScanUtil.ts
Normal file
19
server/src/services/scanner/EmbyScanUtil.ts
Normal file
@@ -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<EmbyApiClient>,
|
||||
req: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return context.apiClient.getSubtitles(
|
||||
req.externalItemId,
|
||||
req.externalMediaItemId ?? req.externalItemId,
|
||||
req.streamIndex,
|
||||
req.extension,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<JellyfinApiClient>,
|
||||
request: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return JellyfinScanUtil.getSubtitles(context, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<JellyfinApiClient>,
|
||||
request: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return JellyfinScanUtil.getSubtitles(context, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ProgramDaoMinter>,
|
||||
@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<JellyfinApiClient>,
|
||||
request: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return JellyfinScanUtil.getSubtitles(context, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<JellyfinApiClient>,
|
||||
request: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return JellyfinScanUtil.getSubtitles(context, request);
|
||||
}
|
||||
}
|
||||
|
||||
19
server/src/services/scanner/JellyfinScanUtil.ts
Normal file
19
server/src/services/scanner/JellyfinScanUtil.ts
Normal file
@@ -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<JellyfinApiClient>,
|
||||
req: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return context.apiClient.getSubtitles(
|
||||
req.externalItemId,
|
||||
req.externalMediaItemId ?? req.externalItemId,
|
||||
req.streamIndex,
|
||||
req.extension,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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]),
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]),
|
||||
);
|
||||
|
||||
@@ -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<ApiClientTypeT, ScanRequestT> {
|
||||
abstract scan(req: ScanRequestT): Promise<void>;
|
||||
|
||||
@@ -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<ApiClientTypeT>,
|
||||
): Promise<number>;
|
||||
|
||||
protected abstract getSubtitles(
|
||||
context: ScanContext<ApiClientTypeT>,
|
||||
request: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>>;
|
||||
|
||||
protected async downloadExternalSubtitleStreams(
|
||||
{ program, subtitles }: NewProgramWithRelations,
|
||||
getSubtitlesCallback: (
|
||||
args: GetSubtitlesRequest,
|
||||
) => Promise<QueryResult<string>>,
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Result<PlexMovie>> {
|
||||
return apiClient.getMovie(incomingMovie.externalId);
|
||||
}
|
||||
|
||||
protected getSubtitles(
|
||||
context: ScanContext<PlexApiClient>,
|
||||
{ key }: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return PlexScanUtil.getSubtitles(context, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PlexApiClient>,
|
||||
{ key }: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return PlexScanUtil.getSubtitles(context, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ProgramDaoMinter>,
|
||||
@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<PlexApiClient>,
|
||||
{ key }: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return PlexScanUtil.getSubtitles(context, key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PlexApiClient>,
|
||||
{ key }: GetSubtitlesRequest,
|
||||
): Promise<QueryResult<string>> {
|
||||
return PlexScanUtil.getSubtitles(context, key);
|
||||
}
|
||||
}
|
||||
|
||||
14
server/src/services/scanner/PlexScanUtil.ts
Normal file
14
server/src/services/scanner/PlexScanUtil.ts
Normal file
@@ -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<PlexApiClient>,
|
||||
key: string,
|
||||
): Promise<QueryResult<string>> {
|
||||
return context.apiClient.getSubtitles(key);
|
||||
}
|
||||
}
|
||||
@@ -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<QueryResult<string>>;
|
||||
|
||||
type ExternalItem = {
|
||||
externalKey: string;
|
||||
externalSourceId: MediaSourceId;
|
||||
@@ -41,10 +45,8 @@ export class ExternalSubtitleDownloader {
|
||||
*/
|
||||
async downloadSubtitlesIfNecessary(
|
||||
item: ExternalItem,
|
||||
details: SubtitleStreamDetails,
|
||||
getSubtitlesCb: (
|
||||
args: GetSubtitleCallbackArgs,
|
||||
) => Promise<QueryResult<string>>,
|
||||
details: { streamIndex: Maybe<number>; 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);
|
||||
|
||||
|
||||
@@ -350,7 +350,10 @@ export class EmbyStreamDetails extends ExternalStreamDetailsFetcher<EmbyT> {
|
||||
sourceType: 'emby',
|
||||
uuid: item.uuid,
|
||||
},
|
||||
details,
|
||||
{
|
||||
codec: details.codec,
|
||||
streamIndex: details.index,
|
||||
},
|
||||
({ extension: ext }) =>
|
||||
this.emby.getSubtitles(
|
||||
item.externalKey,
|
||||
|
||||
@@ -359,7 +359,10 @@ export class JellyfinStreamDetails extends ExternalStreamDetailsFetcher<Jellyfin
|
||||
sourceType: 'jellyfin',
|
||||
uuid: item.uuid,
|
||||
},
|
||||
details,
|
||||
{
|
||||
codec: details.codec,
|
||||
streamIndex: details.index,
|
||||
},
|
||||
({ extension: ext }) =>
|
||||
this.jellyfin.getSubtitles(
|
||||
item.externalKey,
|
||||
|
||||
@@ -468,7 +468,10 @@ export class PlexStreamDetails extends ExternalStreamDetailsFetcher<PlexT> {
|
||||
sourceType: 'plex',
|
||||
uuid: item.uuid,
|
||||
},
|
||||
details,
|
||||
{
|
||||
codec: details.codec,
|
||||
streamIndex: details.index,
|
||||
},
|
||||
() => plexApiClient.getSubtitles(key),
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string> {
|
||||
|
||||
export function getSubtitleCacheFilePath(
|
||||
program: MinimalProgram,
|
||||
subtitleStream: SubtitleStreamDetails,
|
||||
subtitleStream: { streamIndex: Maybe<number>; 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<number>,
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user