mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03: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)
|
streamType: isDefined(stream.index)
|
||||||
? 'subtitles'
|
? 'subtitles'
|
||||||
: 'external_subtitles',
|
: 'external_subtitles',
|
||||||
// type: isDefined(stream.index) ? 'embedded' : 'external',
|
|
||||||
codec: stream.codec.toLocaleLowerCase(),
|
codec: stream.codec.toLocaleLowerCase(),
|
||||||
default: stream.default ?? false,
|
default: stream.default ?? false,
|
||||||
index: stream.index ?? 0,
|
index: stream.index ?? 0,
|
||||||
title: stream.displayTitle,
|
title: stream.displayTitle,
|
||||||
// description: stream.extendedDisplayTitle,
|
|
||||||
sdh,
|
sdh,
|
||||||
// path: stream.key ? this.getFullUrl(stream.key) : undefined,
|
|
||||||
// languageCodeISO6391: stream.languageTag,
|
|
||||||
languageCodeISO6392: stream.languageCode,
|
languageCodeISO6392: stream.languageCode,
|
||||||
fileName: isNonEmptyString(stream.key)
|
fileName: stream.key,
|
||||||
? this.getFullUrl(stream.key)
|
|
||||||
: undefined,
|
|
||||||
forced: stream.forced ?? false,
|
forced: stream.forced ?? false,
|
||||||
} satisfies MediaStream;
|
} 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;
|
return details;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export class SubtitleStreamPicker {
|
|||||||
externalSourceId: lineupItem.program.mediaSourceId,
|
externalSourceId: lineupItem.program.mediaSourceId,
|
||||||
externalSourceType: lineupItem.program.sourceType,
|
externalSourceType: lineupItem.program.sourceType,
|
||||||
},
|
},
|
||||||
stream,
|
{ streamIndex: stream.index, codec: stream.codec },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
import { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
import { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
||||||
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.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 { inject, injectable, interfaces } from 'inversify';
|
||||||
import { ProgramConverter } from '../../db/converters/ProgramConverter.ts';
|
import { ProgramConverter } from '../../db/converters/ProgramConverter.ts';
|
||||||
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
||||||
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts';
|
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts';
|
import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
import { EmbyT } from '../../types/internal.ts';
|
import { EmbyT } from '../../types/internal.ts';
|
||||||
import { EmbyMovie } from '../../types/Media.ts';
|
import { EmbyMovie } from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
|
import { EmbyScanUtil } from './EmbyScanUtil.ts';
|
||||||
import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts';
|
import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
|
|
||||||
@@ -36,6 +42,8 @@ export class EmbyMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner<
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(MeilisearchService) searchService: MeilisearchService,
|
@inject(MeilisearchService) searchService: MeilisearchService,
|
||||||
@inject(ProgramConverter) programConverter: ProgramConverter,
|
@inject(ProgramConverter) programConverter: ProgramConverter,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -45,6 +53,7 @@ export class EmbyMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner<
|
|||||||
searchService,
|
searchService,
|
||||||
programConverter,
|
programConverter,
|
||||||
programMinterFactory(),
|
programMinterFactory(),
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,4 +99,11 @@ export class EmbyMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner<
|
|||||||
.getChildItemCount(libraryKey, 'Movie')
|
.getChildItemCount(libraryKey, 'Movie')
|
||||||
.then((_) => _.getOrThrow());
|
.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 { IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { MediaSourceDB } from '../../db/mediaSourceDB.ts';
|
import { MediaSourceDB } from '../../db/mediaSourceDB.ts';
|
||||||
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts';
|
import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts';
|
||||||
import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts';
|
import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { WrappedError } from '../../types/errors.ts';
|
import { WrappedError } from '../../types/errors.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
import { EmbyT } from '../../types/internal.ts';
|
import { EmbyT } from '../../types/internal.ts';
|
||||||
@@ -18,9 +20,10 @@ import {
|
|||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
|
import { EmbyScanUtil } from './EmbyScanUtil.ts';
|
||||||
import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts';
|
import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
import { ScanContext } from './MediaSourceScanner.ts';
|
import { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts';
|
||||||
|
|
||||||
export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
||||||
EmbyT,
|
EmbyT,
|
||||||
@@ -46,6 +49,8 @@ export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(GetProgramGroupingById)
|
@inject(GetProgramGroupingById)
|
||||||
getProgramGroupingsById: GetProgramGroupingById,
|
getProgramGroupingsById: GetProgramGroupingById,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -56,6 +61,7 @@ export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
|||||||
searchService,
|
searchService,
|
||||||
mediaSourceProgressService,
|
mediaSourceProgressService,
|
||||||
getProgramGroupingsById,
|
getProgramGroupingsById,
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,4 +115,11 @@ export class EmbyMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
|||||||
.getChildItemCount(libraryKey, 'MusicArtist')
|
.getChildItemCount(libraryKey, 'MusicArtist')
|
||||||
.then((_) => _.getOrThrow());
|
.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 { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
||||||
import { MediaSourceType } from '@/db/schema/base.js';
|
import { MediaSourceType } from '@/db/schema/base.js';
|
||||||
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.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 { inject, injectable, interfaces } from 'inversify';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts';
|
import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts';
|
||||||
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
||||||
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts';
|
import { EmbyApiClient } from '../../external/emby/EmbyApiClient.ts';
|
||||||
import { WrappedError } from '../../types/errors.ts';
|
import { WrappedError } from '../../types/errors.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
@@ -21,8 +25,10 @@ import {
|
|||||||
SeasonWithShow,
|
SeasonWithShow,
|
||||||
} from '../../types/Media.ts';
|
} from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
|
import { EmbyScanUtil } from './EmbyScanUtil.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts';
|
import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts';
|
||||||
|
|
||||||
@@ -51,6 +57,8 @@ export class EmbyMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne
|
|||||||
@inject(MeilisearchService) searchService: MeilisearchService,
|
@inject(MeilisearchService) searchService: MeilisearchService,
|
||||||
@inject(GetProgramGroupingById)
|
@inject(GetProgramGroupingById)
|
||||||
getProgramGroupingsById: GetProgramGroupingById,
|
getProgramGroupingsById: GetProgramGroupingById,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -61,6 +69,7 @@ export class EmbyMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne
|
|||||||
searchService,
|
searchService,
|
||||||
mediaSourceProgressService,
|
mediaSourceProgressService,
|
||||||
getProgramGroupingsById,
|
getProgramGroupingsById,
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,4 +164,11 @@ export class EmbyMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne
|
|||||||
protected isSeasonT(grouping: ProgramGrouping): grouping is EmbySeason {
|
protected isSeasonT(grouping: ProgramGrouping): grouping is EmbySeason {
|
||||||
return grouping.sourceType === 'emby' && grouping.type === 'season';
|
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 { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
||||||
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.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 { inject, injectable, interfaces } from 'inversify';
|
||||||
import { ProgramConverter } from '../../db/converters/ProgramConverter.ts';
|
import { ProgramConverter } from '../../db/converters/ProgramConverter.ts';
|
||||||
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
||||||
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts';
|
import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
import { JellyfinT } from '../../types/internal.ts';
|
import { JellyfinT } from '../../types/internal.ts';
|
||||||
import { JellyfinMovie } from '../../types/Media.ts';
|
import { JellyfinMovie } from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
|
import { JellyfinScanUtil } from './JellyfinScanUtil.ts';
|
||||||
import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts';
|
import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
|
|
||||||
@@ -36,6 +42,8 @@ export class JellyfinMediaSourceMovieScanner extends MediaSourceMovieLibraryScan
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(MeilisearchService) searchService: MeilisearchService,
|
@inject(MeilisearchService) searchService: MeilisearchService,
|
||||||
@inject(ProgramConverter) programConverter: ProgramConverter,
|
@inject(ProgramConverter) programConverter: ProgramConverter,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -45,6 +53,7 @@ export class JellyfinMediaSourceMovieScanner extends MediaSourceMovieLibraryScan
|
|||||||
searchService,
|
searchService,
|
||||||
programConverter,
|
programConverter,
|
||||||
programMinterFactory(),
|
programMinterFactory(),
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,4 +99,11 @@ export class JellyfinMediaSourceMovieScanner extends MediaSourceMovieLibraryScan
|
|||||||
.getChildItemCount(libraryKey, 'Movie')
|
.getChildItemCount(libraryKey, 'Movie')
|
||||||
.then((_) => _.getOrThrow());
|
.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 { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
||||||
import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts';
|
import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts';
|
||||||
import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts';
|
import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { WrappedError } from '../../types/errors.ts';
|
import { WrappedError } from '../../types/errors.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
import { JellyfinT } from '../../types/internal.ts';
|
import { JellyfinT } from '../../types/internal.ts';
|
||||||
@@ -16,11 +17,13 @@ import {
|
|||||||
JellyfinMusicTrack,
|
JellyfinMusicTrack,
|
||||||
} from '../../types/Media.ts';
|
} from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
|
import { JellyfinScanUtil } from './JellyfinScanUtil.ts';
|
||||||
import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts';
|
import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
import { ScanContext } from './MediaSourceScanner.ts';
|
import { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts';
|
||||||
|
|
||||||
export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
||||||
JellyfinT,
|
JellyfinT,
|
||||||
@@ -46,6 +49,8 @@ export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScann
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(GetProgramGroupingById)
|
@inject(GetProgramGroupingById)
|
||||||
getProgramGroupingsById: GetProgramGroupingById,
|
getProgramGroupingsById: GetProgramGroupingById,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -56,6 +61,7 @@ export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScann
|
|||||||
searchService,
|
searchService,
|
||||||
mediaSourceProgressService,
|
mediaSourceProgressService,
|
||||||
getProgramGroupingsById,
|
getProgramGroupingsById,
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,4 +115,11 @@ export class JellyfinMediaSourceMusicScanner extends MediaSourceMusicArtistScann
|
|||||||
.getChildItemCount(libraryKey, 'MusicArtist')
|
.getChildItemCount(libraryKey, 'MusicArtist')
|
||||||
.then((_) => _.getOrThrow());
|
.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 { IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { MediaSourceDB } from '../../db/mediaSourceDB.ts';
|
import { MediaSourceDB } from '../../db/mediaSourceDB.ts';
|
||||||
import type { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts';
|
import type { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts';
|
import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts';
|
||||||
import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts';
|
import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { WrappedError } from '../../types/errors.ts';
|
import { WrappedError } from '../../types/errors.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
import type { JellyfinT } from '../../types/internal.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 { Result } from '../../types/result.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
|
import { JellyfinScanUtil } from './JellyfinScanUtil.ts';
|
||||||
import { MediaSourceOtherVideoScanner } from './MediaSourceOtherVideoScanner.ts';
|
import { MediaSourceOtherVideoScanner } from './MediaSourceOtherVideoScanner.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
import type { ScanContext } from './MediaSourceScanner.ts';
|
import type { GetSubtitlesRequest, ScanContext } from './MediaSourceScanner.ts';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScanner<
|
export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScanner<
|
||||||
@@ -37,6 +40,8 @@ export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoS
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(KEYS.ProgramDaoMinterFactory)
|
@inject(KEYS.ProgramDaoMinterFactory)
|
||||||
programMinterFactory: interfaces.AutoFactory<ProgramDaoMinter>,
|
programMinterFactory: interfaces.AutoFactory<ProgramDaoMinter>,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -45,6 +50,7 @@ export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoS
|
|||||||
searchService,
|
searchService,
|
||||||
mediaSourceProgressService,
|
mediaSourceProgressService,
|
||||||
programMinterFactory(),
|
programMinterFactory(),
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,4 +108,11 @@ export class JellyfinMediaSourceOtherVideoScanner extends MediaSourceOtherVideoS
|
|||||||
protected getExternalKey(video: JellyfinOtherVideo): string {
|
protected getExternalKey(video: JellyfinOtherVideo): string {
|
||||||
return video.externalId;
|
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 { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
||||||
import { MediaSourceType } from '@/db/schema/base.js';
|
import { MediaSourceType } from '@/db/schema/base.js';
|
||||||
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.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 { inject, injectable, interfaces } from 'inversify';
|
||||||
import { isNil } from 'lodash-es';
|
import { isNil } from 'lodash-es';
|
||||||
import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts';
|
import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts';
|
||||||
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
||||||
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts';
|
import { JellyfinApiClient } from '../../external/jellyfin/JellyfinApiClient.ts';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { WrappedError } from '../../types/errors.ts';
|
import { WrappedError } from '../../types/errors.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
|
|
||||||
@@ -21,8 +25,10 @@ import {
|
|||||||
SeasonWithShow,
|
SeasonWithShow,
|
||||||
} from '../../types/Media.ts';
|
} from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
|
import { JellyfinScanUtil } from './JellyfinScanUtil.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts';
|
import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts';
|
||||||
|
|
||||||
@@ -51,6 +57,8 @@ export class JellyfinMediaSourceTvShowScanner extends MediaSourceTvShowLibrarySc
|
|||||||
@inject(MeilisearchService) searchService: MeilisearchService,
|
@inject(MeilisearchService) searchService: MeilisearchService,
|
||||||
@inject(GetProgramGroupingById)
|
@inject(GetProgramGroupingById)
|
||||||
getProgramGroupingsById: GetProgramGroupingById,
|
getProgramGroupingsById: GetProgramGroupingById,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -61,6 +69,7 @@ export class JellyfinMediaSourceTvShowScanner extends MediaSourceTvShowLibrarySc
|
|||||||
searchService,
|
searchService,
|
||||||
mediaSourceProgressService,
|
mediaSourceProgressService,
|
||||||
getProgramGroupingsById,
|
getProgramGroupingsById,
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,4 +164,11 @@ export class JellyfinMediaSourceTvShowScanner extends MediaSourceTvShowLibrarySc
|
|||||||
protected isSeasonT(grouping: ProgramGrouping): grouping is JellyfinSeason {
|
protected isSeasonT(grouping: ProgramGrouping): grouping is JellyfinSeason {
|
||||||
return grouping.sourceType === 'jellyfin' && grouping.type === 'season';
|
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 { ProgramType } from '../../db/schema/Program.ts';
|
||||||
import { isMovieProgram } from '../../db/schema/schemaTypeGuards.ts';
|
import { isMovieProgram } from '../../db/schema/schemaTypeGuards.ts';
|
||||||
import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts';
|
import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts';
|
||||||
|
import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import type { HasMediaSourceInfo, Movie } from '../../types/Media.ts';
|
import type { HasMediaSourceInfo, Movie } from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
import type { Logger } from '../../util/logging/LoggerFactory.ts';
|
import type { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
@@ -45,9 +46,9 @@ export abstract class MediaSourceMovieLibraryScanner<
|
|||||||
private searchService: MeilisearchService,
|
private searchService: MeilisearchService,
|
||||||
protected programConverter: ProgramConverter,
|
protected programConverter: ProgramConverter,
|
||||||
protected programMinter: ProgramDaoMinter,
|
protected programMinter: ProgramDaoMinter,
|
||||||
// protected externalSubtitleDownloader: ExternalSubtitleDownloader,
|
protected externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(logger, mediaSourceDB);
|
super(logger, mediaSourceDB, externalSubtitleDownloader);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async scanInternal(
|
protected async scanInternal(
|
||||||
@@ -124,6 +125,13 @@ export abstract class MediaSourceMovieLibraryScanner<
|
|||||||
fullMovie,
|
fullMovie,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.downloadExternalSubtitleStreams(minted, (req) =>
|
||||||
|
this.getSubtitles(context, {
|
||||||
|
...req,
|
||||||
|
externalMediaItemId: fullMovie.mediaItem?.externalKey ?? undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const upsertResult = await Result.attemptAsync(() =>
|
const upsertResult = await Result.attemptAsync(() =>
|
||||||
this.programDB.upsertPrograms([minted]),
|
this.programDB.upsertPrograms([minted]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { Result } from '../../types/result.ts';
|
|||||||
import type { Maybe } from '../../types/util.ts';
|
import type { Maybe } from '../../types/util.ts';
|
||||||
import type { Logger } from '../../util/logging/LoggerFactory.ts';
|
import type { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import type { MeilisearchService } from '../MeilisearchService.ts';
|
import type { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
|
import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import type { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import type { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
import type { ScanContext } from './MediaSourceScanner.ts';
|
import type { ScanContext } from './MediaSourceScanner.ts';
|
||||||
import { MediaSourceScanner } from './MediaSourceScanner.ts';
|
import { MediaSourceScanner } from './MediaSourceScanner.ts';
|
||||||
@@ -78,8 +79,9 @@ export abstract class MediaSourceMusicArtistScanner<
|
|||||||
protected searchService: MeilisearchService,
|
protected searchService: MeilisearchService,
|
||||||
private mediaSourceProgressService: MediaSourceProgressService,
|
private mediaSourceProgressService: MediaSourceProgressService,
|
||||||
private getProgramGroupingByIdCommand: GetProgramGroupingById,
|
private getProgramGroupingByIdCommand: GetProgramGroupingById,
|
||||||
|
protected externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(logger, mediaSourceDB);
|
super(logger, mediaSourceDB, externalSubtitleDownloader);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async scanInternal(
|
protected async scanInternal(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { MediaSourceDB } from '../../db/mediaSourceDB.ts';
|
|||||||
import type { RemoteMediaSourceType } from '../../db/schema/MediaSource.ts';
|
import type { RemoteMediaSourceType } from '../../db/schema/MediaSource.ts';
|
||||||
import { ProgramType } from '../../db/schema/Program.ts';
|
import { ProgramType } from '../../db/schema/Program.ts';
|
||||||
import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts';
|
import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts';
|
||||||
|
import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import type { HasMediaSourceInfo, OtherVideo } from '../../types/Media.ts';
|
import type { HasMediaSourceInfo, OtherVideo } from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
import type { Logger } from '../../util/logging/LoggerFactory.ts';
|
import type { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
@@ -33,8 +34,9 @@ export abstract class MediaSourceOtherVideoScanner<
|
|||||||
private searchService: MeilisearchService,
|
private searchService: MeilisearchService,
|
||||||
private mediaSourceProgressService: MediaSourceProgressService,
|
private mediaSourceProgressService: MediaSourceProgressService,
|
||||||
protected programMinter: ProgramDaoMinter,
|
protected programMinter: ProgramDaoMinter,
|
||||||
|
protected externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(logger, mediaSourceDB);
|
super(logger, mediaSourceDB, externalSubtitleDownloader);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async scanInternal(
|
protected async scanInternal(
|
||||||
@@ -105,6 +107,14 @@ export abstract class MediaSourceOtherVideoScanner<
|
|||||||
fullMetadata,
|
fullMetadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.downloadExternalSubtitleStreams(minted, (req) =>
|
||||||
|
this.getSubtitles(context, {
|
||||||
|
...req,
|
||||||
|
externalMediaItemId:
|
||||||
|
fullMetadata.mediaItem?.externalKey ?? undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const upsertResult = await Result.attemptAsync(() =>
|
const upsertResult = await Result.attemptAsync(() =>
|
||||||
this.programDB.upsertPrograms([minted]),
|
this.programDB.upsertPrograms([minted]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type { MediaSourceLibraryOrm } from '@/db/schema/MediaSourceLibrary.js';
|
import type { MediaSourceLibraryOrm } from '@/db/schema/MediaSourceLibrary.js';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { isEmpty } from 'lodash-es';
|
||||||
import type { MediaSourceDB } from '../../db/mediaSourceDB.ts';
|
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 {
|
import type {
|
||||||
MediaLibraryType,
|
MediaLibraryType,
|
||||||
MediaSourceOrm,
|
MediaSourceOrm,
|
||||||
RemoteMediaSourceType,
|
RemoteMediaSourceType,
|
||||||
} from '../../db/schema/MediaSource.ts';
|
} 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 { devAssert } from '../../util/debug.ts';
|
||||||
import type { Logger } from '../../util/logging/LoggerFactory.ts';
|
import type { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
|
|
||||||
@@ -42,6 +48,14 @@ export type GenericMediaSourceScannerFactory = (
|
|||||||
libraryType: MediaLibraryType,
|
libraryType: MediaLibraryType,
|
||||||
) => GenericMediaSourceScanner;
|
) => GenericMediaSourceScanner;
|
||||||
|
|
||||||
|
export type GetSubtitlesRequest = {
|
||||||
|
key: string;
|
||||||
|
extension: string;
|
||||||
|
externalItemId: string;
|
||||||
|
externalMediaItemId?: string;
|
||||||
|
streamIndex: number; // Only relevant for Jellyfin
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class BaseMediaSourceScanner<ApiClientTypeT, ScanRequestT> {
|
export abstract class BaseMediaSourceScanner<ApiClientTypeT, ScanRequestT> {
|
||||||
abstract scan(req: ScanRequestT): Promise<void>;
|
abstract scan(req: ScanRequestT): Promise<void>;
|
||||||
|
|
||||||
@@ -62,6 +76,7 @@ export abstract class MediaSourceScanner<
|
|||||||
constructor(
|
constructor(
|
||||||
protected logger: Logger,
|
protected logger: Logger,
|
||||||
protected mediaSourceDB: MediaSourceDB,
|
protected mediaSourceDB: MediaSourceDB,
|
||||||
|
protected externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -130,4 +145,55 @@ export abstract class MediaSourceScanner<
|
|||||||
libraryKey: string,
|
libraryKey: string,
|
||||||
context: ScanContext<ApiClientTypeT>,
|
context: ScanContext<ApiClientTypeT>,
|
||||||
): Promise<number>;
|
): 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 { ProgramType } from '../../db/schema/Program.ts';
|
||||||
import { ProgramGroupingType } from '../../db/schema/ProgramGrouping.ts';
|
import { ProgramGroupingType } from '../../db/schema/ProgramGrouping.ts';
|
||||||
import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts';
|
import type { MediaSourceApiClient } from '../../external/MediaSourceApiClient.ts';
|
||||||
|
import type { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import type {
|
import type {
|
||||||
HasMediaSourceAndLibraryId,
|
HasMediaSourceAndLibraryId,
|
||||||
MediaSourceEpisode,
|
MediaSourceEpisode,
|
||||||
@@ -61,8 +62,9 @@ export abstract class MediaSourceTvShowLibraryScanner<
|
|||||||
protected searchService: MeilisearchService,
|
protected searchService: MeilisearchService,
|
||||||
private mediaSourceProgressService: MediaSourceProgressService,
|
private mediaSourceProgressService: MediaSourceProgressService,
|
||||||
private getProgramGroupingByIdCommand: GetProgramGroupingById,
|
private getProgramGroupingByIdCommand: GetProgramGroupingById,
|
||||||
|
protected externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(logger, mediaSourceDB);
|
super(logger, mediaSourceDB, externalSubtitleDownloader);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async scanInternal(
|
protected async scanInternal(
|
||||||
@@ -499,6 +501,14 @@ export abstract class MediaSourceTvShowLibraryScanner<
|
|||||||
episodeWithJoins,
|
episodeWithJoins,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.downloadExternalSubtitleStreams(dao, (req) =>
|
||||||
|
this.getSubtitles(scanContext, {
|
||||||
|
...req,
|
||||||
|
externalMediaItemId:
|
||||||
|
episodeWithJoins.mediaItem?.externalKey ?? undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
dao.program.tvShowUuid = show.uuid;
|
dao.program.tvShowUuid = show.uuid;
|
||||||
dao.program.seasonUuid = season.uuid;
|
dao.program.seasonUuid = season.uuid;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
import { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
||||||
import { MediaSourceType } from '@/db/schema/base.js';
|
import { MediaSourceType } from '@/db/schema/base.js';
|
||||||
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.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 { inject, injectable, interfaces } from 'inversify';
|
||||||
import { ProgramConverter } from '../../db/converters/ProgramConverter.ts';
|
import { ProgramConverter } from '../../db/converters/ProgramConverter.ts';
|
||||||
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
||||||
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { PlexApiClient } from '../../external/plex/PlexApiClient.ts';
|
import { PlexApiClient } from '../../external/plex/PlexApiClient.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
import { PlexMovie } from '../../types/Media.ts';
|
import { PlexMovie } from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
@@ -15,6 +20,7 @@ import { Logger } from '../../util/logging/LoggerFactory.ts';
|
|||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts';
|
import { MediaSourceMovieLibraryScanner } from './MediaSourceMovieLibraryScanner.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
|
import { PlexScanUtil } from './PlexScanUtil.ts';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner<
|
export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner<
|
||||||
@@ -35,6 +41,8 @@ export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner<
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(MeilisearchService) searchService: MeilisearchService,
|
@inject(MeilisearchService) searchService: MeilisearchService,
|
||||||
@inject(ProgramConverter) programConverter: ProgramConverter,
|
@inject(ProgramConverter) programConverter: ProgramConverter,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -44,6 +52,7 @@ export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner<
|
|||||||
searchService,
|
searchService,
|
||||||
programConverter,
|
programConverter,
|
||||||
programMinterFactory(),
|
programMinterFactory(),
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,4 +86,11 @@ export class PlexMediaSourceMovieScanner extends MediaSourceMovieLibraryScanner<
|
|||||||
): Promise<Result<PlexMovie>> {
|
): Promise<Result<PlexMovie>> {
|
||||||
return apiClient.getMovie(incomingMovie.externalId);
|
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 { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
||||||
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.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 { inject, injectable, interfaces } from 'inversify';
|
||||||
import { GetProgramGroupingById } from '../../commands/GetProgramGroupingById.ts';
|
import { GetProgramGroupingById } from '../../commands/GetProgramGroupingById.ts';
|
||||||
import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts';
|
import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter.ts';
|
||||||
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
||||||
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { PlexApiClient } from '../../external/plex/PlexApiClient.ts';
|
import { PlexApiClient } from '../../external/plex/PlexApiClient.ts';
|
||||||
import { WrappedError } from '../../types/errors.ts';
|
import { WrappedError } from '../../types/errors.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
import { PlexT } from '../../types/internal.ts';
|
import { PlexT } from '../../types/internal.ts';
|
||||||
import { PlexAlbum, PlexArtist, PlexTrack } from '../../types/Media.ts';
|
import { PlexAlbum, PlexArtist, PlexTrack } from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts';
|
import { MediaSourceMusicArtistScanner } from './MediaSourceMusicArtistScanner.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
|
import { PlexScanUtil } from './PlexScanUtil.ts';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
||||||
@@ -43,6 +49,8 @@ export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(GetProgramGroupingById)
|
@inject(GetProgramGroupingById)
|
||||||
getProgramGroupingsById: GetProgramGroupingById,
|
getProgramGroupingsById: GetProgramGroupingById,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -53,6 +61,7 @@ export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
|||||||
searchService,
|
searchService,
|
||||||
mediaSourceProgressService,
|
mediaSourceProgressService,
|
||||||
getProgramGroupingsById,
|
getProgramGroupingsById,
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,4 +115,11 @@ export class PlexMediaSourceMusicScanner extends MediaSourceMusicArtistScanner<
|
|||||||
.getLibraryCount(libraryKey)
|
.getLibraryCount(libraryKey)
|
||||||
.then((_) => _.getOrThrow());
|
.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 { IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { MediaSourceDB } from '../../db/mediaSourceDB.ts';
|
import { MediaSourceDB } from '../../db/mediaSourceDB.ts';
|
||||||
import type { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts';
|
import type { MediaSourceWithRelations } from '../../db/schema/derivedTypes.ts';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts';
|
import { MediaSourceApiFactory } from '../../external/MediaSourceApiFactory.ts';
|
||||||
import type { PlexApiClient } from '../../external/plex/PlexApiClient.ts';
|
import type { PlexApiClient } from '../../external/plex/PlexApiClient.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { WrappedError } from '../../types/errors.ts';
|
import { WrappedError } from '../../types/errors.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
import type { PlexT } from '../../types/internal.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 { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
import { MediaSourceOtherVideoScanner } from './MediaSourceOtherVideoScanner.ts';
|
import { MediaSourceOtherVideoScanner } from './MediaSourceOtherVideoScanner.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.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()
|
@injectable()
|
||||||
export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScanner<
|
export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScanner<
|
||||||
@@ -37,6 +40,8 @@ export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScann
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(KEYS.ProgramDaoMinterFactory)
|
@inject(KEYS.ProgramDaoMinterFactory)
|
||||||
programMinterFactory: interfaces.AutoFactory<ProgramDaoMinter>,
|
programMinterFactory: interfaces.AutoFactory<ProgramDaoMinter>,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -45,6 +50,7 @@ export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScann
|
|||||||
searchService,
|
searchService,
|
||||||
mediaSourceProgressService,
|
mediaSourceProgressService,
|
||||||
programMinterFactory(),
|
programMinterFactory(),
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,4 +88,11 @@ export class PlexMediaSourceOtherVideoScanner extends MediaSourceOtherVideoScann
|
|||||||
protected getExternalKey(video: PlexOtherVideo): string {
|
protected getExternalKey(video: PlexOtherVideo): string {
|
||||||
return video.externalId;
|
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 { MediaSourceDB } from '@/db/mediaSourceDB.js';
|
||||||
import { MediaSourceApiFactory } from '@/external/MediaSourceApiFactory.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 { ProgramGrouping } from '@tunarr/types';
|
||||||
import { inject, injectable, interfaces } from 'inversify';
|
import { inject, injectable, interfaces } from 'inversify';
|
||||||
import { GetProgramGroupingById } from '../../commands/GetProgramGroupingById.ts';
|
import { GetProgramGroupingById } from '../../commands/GetProgramGroupingById.ts';
|
||||||
@@ -8,6 +11,7 @@ import { ProgramGroupingMinter } from '../../db/converters/ProgramGroupingMinter
|
|||||||
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
import { ProgramDaoMinter } from '../../db/converters/ProgramMinter.ts';
|
||||||
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
import { type IProgramDB } from '../../db/interfaces/IProgramDB.ts';
|
||||||
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
import { MediaSourceWithRelations } from '../../db/schema/derivedTypes.js';
|
||||||
|
import { QueryResult } from '../../external/BaseApiClient.ts';
|
||||||
import { PlexApiClient } from '../../external/plex/PlexApiClient.ts';
|
import { PlexApiClient } from '../../external/plex/PlexApiClient.ts';
|
||||||
import { WrappedError } from '../../types/errors.ts';
|
import { WrappedError } from '../../types/errors.ts';
|
||||||
import { KEYS } from '../../types/inject.ts';
|
import { KEYS } from '../../types/inject.ts';
|
||||||
@@ -18,10 +22,12 @@ import {
|
|||||||
SeasonWithShow,
|
SeasonWithShow,
|
||||||
} from '../../types/Media.ts';
|
} from '../../types/Media.ts';
|
||||||
import { Result } from '../../types/result.ts';
|
import { Result } from '../../types/result.ts';
|
||||||
|
import { ExternalSubtitleDownloader } from '../../stream/ExternalSubtitleDownloader.ts';
|
||||||
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
import { Logger } from '../../util/logging/LoggerFactory.ts';
|
||||||
import { MeilisearchService } from '../MeilisearchService.ts';
|
import { MeilisearchService } from '../MeilisearchService.ts';
|
||||||
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
import { MediaSourceProgressService } from './MediaSourceProgressService.ts';
|
||||||
import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts';
|
import { MediaSourceTvShowLibraryScanner } from './MediaSourceTvShowLibraryScanner.ts';
|
||||||
|
import { PlexScanUtil } from './PlexScanUtil.ts';
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanner<
|
export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanner<
|
||||||
@@ -48,6 +54,8 @@ export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne
|
|||||||
mediaSourceProgressService: MediaSourceProgressService,
|
mediaSourceProgressService: MediaSourceProgressService,
|
||||||
@inject(GetProgramGroupingById)
|
@inject(GetProgramGroupingById)
|
||||||
getProgramGroupingsById: GetProgramGroupingById,
|
getProgramGroupingsById: GetProgramGroupingById,
|
||||||
|
@inject(ExternalSubtitleDownloader)
|
||||||
|
externalSubtitleDownloader: ExternalSubtitleDownloader,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
logger,
|
logger,
|
||||||
@@ -58,6 +66,7 @@ export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne
|
|||||||
searchService,
|
searchService,
|
||||||
mediaSourceProgressService,
|
mediaSourceProgressService,
|
||||||
getProgramGroupingsById,
|
getProgramGroupingsById,
|
||||||
|
externalSubtitleDownloader,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,4 +145,11 @@ export class PlexMediaSourceTvShowScanner extends MediaSourceTvShowLibraryScanne
|
|||||||
protected isSeasonT(grouping: ProgramGrouping): grouping is PlexSeason {
|
protected isSeasonT(grouping: ProgramGrouping): grouping is PlexSeason {
|
||||||
return grouping.sourceType === 'plex' && grouping.type === 'season';
|
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 { QueryResult } from '../external/BaseApiClient.ts';
|
||||||
import { FileSystemService } from '../services/FileSystemService.ts';
|
import { FileSystemService } from '../services/FileSystemService.ts';
|
||||||
import { KEYS } from '../types/inject.ts';
|
import { KEYS } from '../types/inject.ts';
|
||||||
|
import { Maybe } from '../types/util.ts';
|
||||||
import { fileExists } from '../util/fsUtil.ts';
|
import { fileExists } from '../util/fsUtil.ts';
|
||||||
import { Logger } from '../util/logging/LoggerFactory.ts';
|
import { Logger } from '../util/logging/LoggerFactory.ts';
|
||||||
import {
|
import {
|
||||||
getSubtitleCacheFilePath,
|
getSubtitleCacheFilePath,
|
||||||
subtitleCodecToExt,
|
subtitleCodecToExt,
|
||||||
} from '../util/subtitles.ts';
|
} from '../util/subtitles.ts';
|
||||||
import { SubtitleStreamDetails } from './types.ts';
|
|
||||||
|
|
||||||
type GetSubtitleCallbackArgs = {
|
export type GetSubtitleCallbackArgs = {
|
||||||
extension: string;
|
extension: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GetSubtitlesCallback = (
|
||||||
|
cbArgs: GetSubtitleCallbackArgs,
|
||||||
|
) => Promise<QueryResult<string>>;
|
||||||
|
|
||||||
type ExternalItem = {
|
type ExternalItem = {
|
||||||
externalKey: string;
|
externalKey: string;
|
||||||
externalSourceId: MediaSourceId;
|
externalSourceId: MediaSourceId;
|
||||||
@@ -41,10 +45,8 @@ export class ExternalSubtitleDownloader {
|
|||||||
*/
|
*/
|
||||||
async downloadSubtitlesIfNecessary(
|
async downloadSubtitlesIfNecessary(
|
||||||
item: ExternalItem,
|
item: ExternalItem,
|
||||||
details: SubtitleStreamDetails,
|
details: { streamIndex: Maybe<number>; codec: string },
|
||||||
getSubtitlesCb: (
|
getSubtitlesCb: GetSubtitlesCallback,
|
||||||
args: GetSubtitleCallbackArgs,
|
|
||||||
) => Promise<QueryResult<string>>,
|
|
||||||
) {
|
) {
|
||||||
const outPath = getSubtitleCacheFilePath(
|
const outPath = getSubtitleCacheFilePath(
|
||||||
{
|
{
|
||||||
@@ -53,7 +55,10 @@ export class ExternalSubtitleDownloader {
|
|||||||
externalSourceType: item.sourceType,
|
externalSourceType: item.sourceType,
|
||||||
id: item.uuid,
|
id: item.uuid,
|
||||||
},
|
},
|
||||||
details,
|
{
|
||||||
|
codec: details.codec,
|
||||||
|
streamIndex: details.streamIndex,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const ext = subtitleCodecToExt(details.codec);
|
const ext = subtitleCodecToExt(details.codec);
|
||||||
|
|
||||||
|
|||||||
@@ -350,7 +350,10 @@ export class EmbyStreamDetails extends ExternalStreamDetailsFetcher<EmbyT> {
|
|||||||
sourceType: 'emby',
|
sourceType: 'emby',
|
||||||
uuid: item.uuid,
|
uuid: item.uuid,
|
||||||
},
|
},
|
||||||
details,
|
{
|
||||||
|
codec: details.codec,
|
||||||
|
streamIndex: details.index,
|
||||||
|
},
|
||||||
({ extension: ext }) =>
|
({ extension: ext }) =>
|
||||||
this.emby.getSubtitles(
|
this.emby.getSubtitles(
|
||||||
item.externalKey,
|
item.externalKey,
|
||||||
|
|||||||
@@ -359,7 +359,10 @@ export class JellyfinStreamDetails extends ExternalStreamDetailsFetcher<Jellyfin
|
|||||||
sourceType: 'jellyfin',
|
sourceType: 'jellyfin',
|
||||||
uuid: item.uuid,
|
uuid: item.uuid,
|
||||||
},
|
},
|
||||||
details,
|
{
|
||||||
|
codec: details.codec,
|
||||||
|
streamIndex: details.index,
|
||||||
|
},
|
||||||
({ extension: ext }) =>
|
({ extension: ext }) =>
|
||||||
this.jellyfin.getSubtitles(
|
this.jellyfin.getSubtitles(
|
||||||
item.externalKey,
|
item.externalKey,
|
||||||
|
|||||||
@@ -468,7 +468,10 @@ export class PlexStreamDetails extends ExternalStreamDetailsFetcher<PlexT> {
|
|||||||
sourceType: 'plex',
|
sourceType: 'plex',
|
||||||
uuid: item.uuid,
|
uuid: item.uuid,
|
||||||
},
|
},
|
||||||
details,
|
{
|
||||||
|
codec: details.codec,
|
||||||
|
streamIndex: details.index,
|
||||||
|
},
|
||||||
() => plexApiClient.getSubtitles(key),
|
() => plexApiClient.getSubtitles(key),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export class SubtitleExtractorTask extends Task2<
|
|||||||
externalSourceType: program.externalSourceType,
|
externalSourceType: program.externalSourceType,
|
||||||
id: program.id,
|
id: program.id,
|
||||||
},
|
},
|
||||||
subtitle,
|
{ streamIndex: subtitle.index, codec: subtitle.codec },
|
||||||
);
|
);
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import type { MediaSourceId, MediaSourceType } from '@/db/schema/base.js';
|
|||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { match, P } from 'ts-pattern';
|
import { match, P } from 'ts-pattern';
|
||||||
import type { SubtitleStreamDetails } from '../stream/types.ts';
|
import type { Maybe, Nullable } from '../types/util.ts';
|
||||||
import type { Nullable } from '../types/util.ts';
|
|
||||||
|
|
||||||
type MinimalProgram = {
|
type MinimalProgram = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -22,9 +21,13 @@ export function subtitleCodecToExt(codec: string): Nullable<string> {
|
|||||||
|
|
||||||
export function getSubtitleCacheFilePath(
|
export function getSubtitleCacheFilePath(
|
||||||
program: MinimalProgram,
|
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());
|
const ext = subtitleCodecToExt(subtitleStream.codec.toLowerCase());
|
||||||
if (!ext) {
|
if (!ext) {
|
||||||
return null;
|
return null;
|
||||||
@@ -39,7 +42,8 @@ export function getSubtitleCacheFilePath(
|
|||||||
|
|
||||||
function getSubtitleCacheFileName(
|
function getSubtitleCacheFileName(
|
||||||
program: MinimalProgram,
|
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"
|
// 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
|
// if the underlying program changes at the target
|
||||||
@@ -49,7 +53,7 @@ function getSubtitleCacheFileName(
|
|||||||
.update(program.externalSourceType)
|
.update(program.externalSourceType)
|
||||||
.update(program.externalSourceId)
|
.update(program.externalSourceId)
|
||||||
.update(program.externalKey)
|
.update(program.externalKey)
|
||||||
.update(subtitleStream.index?.toString() ?? '')
|
.update(streamIndex?.toString() ?? '')
|
||||||
.update(subtitleStream.codec)
|
.update(codec)
|
||||||
.digest('hex');
|
.digest('hex');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -422,6 +422,7 @@ export const MediaStream = z.object({
|
|||||||
|
|
||||||
// Subtitles
|
// Subtitles
|
||||||
sdh: z.boolean().nullish(),
|
sdh: z.boolean().nullish(),
|
||||||
|
externalKey: z.string().nullish(),
|
||||||
|
|
||||||
// Audio or Subtitles
|
// Audio or Subtitles
|
||||||
languageCodeISO6392: z.string().nullish(),
|
languageCodeISO6392: z.string().nullish(),
|
||||||
@@ -470,6 +471,7 @@ export const MediaItem = z.object({
|
|||||||
locations: z.array(MediaLocation),
|
locations: z.array(MediaLocation),
|
||||||
chapters: z.array(MediaChapter).nullish(),
|
chapters: z.array(MediaChapter).nullish(),
|
||||||
scanKind: z.enum(['unknown', 'progressive', 'interlaced']).nullish(),
|
scanKind: z.enum(['unknown', 'progressive', 'interlaced']).nullish(),
|
||||||
|
externalKey: z.string().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const WithMediaItemMetadata = z.object({
|
const WithMediaItemMetadata = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user