diff --git a/src/node/consumer/src/lib/services/metadata_service.ts b/src/node/consumer/src/lib/services/metadata_service.ts index 0faeb4d..f986f34 100644 --- a/src/node/consumer/src/lib/services/metadata_service.ts +++ b/src/node/consumer/src/lib/services/metadata_service.ts @@ -15,7 +15,7 @@ import nameToImdb from 'name-to-imdb'; const CINEMETA_URL = 'https://v3-cinemeta.strem.io'; const KITSU_URL = 'https://anime-kitsu.strem.fun'; -const TIMEOUT = 20000; +const TIMEOUT = 60000; @injectable() export class MetadataService implements IMetadataService { @@ -33,7 +33,7 @@ export class MetadataService implements IMetadataService { const query = encodeURIComponent(key); return this.cacheService.cacheWrapKitsuId(key, - () => axios.get(`${KITSU_URL}/catalog/series/kitsu-anime-list/search=${query}.json`, {timeout: 60000}) + () => axios.get(`${KITSU_URL}/catalog/series/kitsu-anime-list/search=${query}.json`, {timeout: TIMEOUT}) .then((response) => { const body = response.data as IKitsuCatalogJsonResponse; if (body && body.metas && body.metas.length) { @@ -75,14 +75,14 @@ export class MetadataService implements IMetadataService { return this.cacheService.cacheWrapMetadata(key.toString(), () => { switch (isImdbId) { case true: - return this.requestCinemetaMetadata(`${CINEMETA_URL}/meta/imdb/${key}.json`); + return this.requestMetadata(`${CINEMETA_URL}/meta/imdb/${key}.json`, this.handleCinemetaResponse); default: - return this.requestKitsuMetadata(`${KITSU_URL}/meta/${metaType}/${key}.json`) + return this.requestMetadata(`${KITSU_URL}/meta/${metaType}/${key}.json`, this.handleKitsuResponse) }}) .catch(() => { // try different type in case there was a mismatch const otherType = metaType === TorrentType.Movie ? TorrentType.Series : TorrentType.Movie; - return this.requestCinemetaMetadata(`${CINEMETA_URL}/meta/${otherType}/${key}.json`) + return this.requestMetadata(`${CINEMETA_URL}/meta/${otherType}/${key}.json`, this.handleCinemetaResponse) }) .catch((error) => { throw new Error(`failed metadata query ${key} due: ${error.message}`); @@ -90,12 +90,16 @@ export class MetadataService implements IMetadataService { }; public isEpisodeImdbId = async (imdbId: string | undefined): Promise => { - if (!imdbId) { + if (!imdbId || !imdbId.toString().match(/^tt\d+$/)) { + return false; + } + + try { + const response = await axios.get(`https://www.imdb.com/title/${imdbId}/`, {timeout: TIMEOUT}); + return response.data.includes('video.episode'); + } catch (error) { return false; } - return axios.get(`https://www.imdb.com/title/${imdbId}/`, {timeout: 10000}) - .then(response => !!(response.data && response.data.includes('video.episode'))) - .catch(() => false); }; public escapeTitle = (title: string): string => title.toLowerCase() @@ -108,72 +112,78 @@ export class MetadataService implements IMetadataService { .replace(/\s{2,}/, ' ') // replace multiple spaces .trim(); - private requestKitsuMetadata = async (url: string): Promise => { - const response = await axios.get(url, {timeout: TIMEOUT}); - const body = response.data; - return this.handleKitsuResponse(body as IKitsuJsonResponse); + private requestMetadata = async (url: string, handler: (body: unknown) => IMetadataResponse): Promise => { + try { + const response = await axios.get(url, { timeout: TIMEOUT }); + const body = response.data; + return handler(body); + } catch (error) { + throw new Error(`HTTP error! status: ${error.response?.status}`); + } }; - private requestCinemetaMetadata = async (url: string): Promise => { - const response = await axios.get(url, {timeout: TIMEOUT}); - const body = response.data; - return this.handleCinemetaResponse(body as ICinemetaJsonResponse); + private handleCinemetaResponse = (response: unknown): IMetadataResponse => { + const body = response as ICinemetaJsonResponse + + return ({ + imdbId: parseInt(body.meta?.id || '0'), + type: body.meta?.type, + title: body.meta?.name, + year: parseInt(body.meta?.year || '0'), + country: body.meta?.country, + genres: body.meta?.genres, + status: body.meta?.status, + videos: body.meta?.videos + ? body.meta.videos.map(video => ({ + name: video.name, + season: video.season, + episode: video.episode, + imdbSeason: video.season, + imdbEpisode: video.episode, + })) + : [], + episodeCount: body.meta?.videos + ? this.getEpisodeCount(body.meta.videos) + : [], + totalCount: body.meta?.videos + ? body.meta.videos.filter( + entry => entry.season !== 0 && entry.episode !== 0 + ).length + : 0, + }); }; - private handleCinemetaResponse = (body: ICinemetaJsonResponse): IMetadataResponse => ({ - imdbId: parseInt(body.meta?.id || '0'), - type: body.meta?.type, - title: body.meta?.name, - year: parseInt(body.meta?.year || '0'), - country: body.meta?.country, - genres: body.meta?.genres, - status: body.meta?.status, - videos: body.meta?.videos - ? body.meta.videos.map(video => ({ - name: video.name, - season: video.season, - episode: video.episode, - imdbSeason: video.season, - imdbEpisode: video.episode, - })) - : [], - episodeCount: body.meta?.videos - ? this.getEpisodeCount(body.meta.videos) - : [], - totalCount: body.meta?.videos - ? body.meta.videos.filter( - entry => entry.season !== 0 && entry.episode !== 0 - ).length - : 0, - }); - - private handleKitsuResponse = (body: IKitsuJsonResponse): IMetadataResponse => ({ - kitsuId: parseInt(body.meta?.kitsu_id || '0'), - type: body.meta?.type, - title: body.meta?.name, - year: parseInt(body.meta?.year || '0'), - country: body.meta?.country, - genres: body.meta?.genres, - status: body.meta?.status, - videos: body.meta?.videos - ? body.meta?.videos.map(video => ({ - name: video.title, - season: video.season, - episode: video.episode, - kitsuId: video.id, - kitsuEpisode: video.episode, - released: video.released, - })) - : [], - episodeCount: body.meta?.videos - ? this.getEpisodeCount(body.meta.videos) - : [], - totalCount: body.meta?.videos - ? body.meta.videos.filter( - entry => entry.season !== 0 && entry.episode !== 0 - ).length - : 0, - }); + private handleKitsuResponse = (response: unknown): IMetadataResponse => { + const body = response as IKitsuJsonResponse; + + return ({ + kitsuId: parseInt(body.meta?.kitsu_id || '0'), + type: body.meta?.type, + title: body.meta?.name, + year: parseInt(body.meta?.year || '0'), + country: body.meta?.country, + genres: body.meta?.genres, + status: body.meta?.status, + videos: body.meta?.videos + ? body.meta?.videos.map(video => ({ + name: video.title, + season: video.season, + episode: video.episode, + kitsuId: video.id, + kitsuEpisode: video.episode, + released: video.released, + })) + : [], + episodeCount: body.meta?.videos + ? this.getEpisodeCount(body.meta.videos) + : [], + totalCount: body.meta?.videos + ? body.meta.videos.filter( + entry => entry.season !== 0 && entry.episode !== 0 + ).length + : 0, + }); + }; private getEpisodeCount = (videos: ICommonVideoMetadata[]): number[] => Object.values( diff --git a/src/node/consumer/test/metadata_service.test.ts b/src/node/consumer/test/metadata_service.test.ts index 9a786bc..64a88d2 100644 --- a/src/node/consumer/test/metadata_service.test.ts +++ b/src/node/consumer/test/metadata_service.test.ts @@ -79,12 +79,38 @@ describe('MetadataService Tests', () => { expect(body.videos.length).toBe(22); }); - it("should check if imdb id is an episode", async () => { + it("should get imdb id the flash 2014", async () => { + const result = await metadataService.getImdbId({ + title: 'The Flash', + year: 2014, + type: 'series' + }); + expect(mockCacheService.cacheWrapImdbId).toHaveBeenCalledWith('the flash_2014_series', expect.any(Function)); + expect(result).not.toBeNull(); + expect(result).toEqual('tt3107288'); + }); + + it("should return false if imdb id is not provided", async () => { + const result = await metadataService.isEpisodeImdbId(undefined); + expect(result).toBe(false); + }); + + it("should return false if kitsu id is provided", async () => { + const result = await metadataService.isEpisodeImdbId("kitsu:11"); + expect(result).toBe(false); + }); + + it("should escape title naruto, with year", () => { + const result = metadataService.escapeTitle('Naruto: Shippuden | 2002'); + expect(result).toEqual('naruto shippuden 2002'); + }); + + it("should check if imdb id is an episode: the flash 1990", async () => { const result = await metadataService.isEpisodeImdbId('tt0579968'); expect(result).toBe(true); }); - it("should escape title", () => { + it("should escape title naruto, no year", () => { const result = metadataService.escapeTitle('Naruto: Shippuden'); expect(result).toEqual('naruto shippuden'); }); diff --git a/src/node/consumer/test/torrent_entries_service.test.ts b/src/node/consumer/test/torrent_entries_service.test.ts new file mode 100644 index 0000000..604aa36 --- /dev/null +++ b/src/node/consumer/test/torrent_entries_service.test.ts @@ -0,0 +1,109 @@ +import "reflect-metadata"; // required +import {TorrentType} from "@enums/torrent_types"; +import {ILoggingService} from "@interfaces/logging_service"; +import {IMetadataService} from "@interfaces/metadata_service"; +import {IParsedTorrent} from "@interfaces/parsed_torrent"; +import {ITorrentFileCollection} from "@interfaces/torrent_file_collection"; +import {ITorrentFileService} from "@interfaces/torrent_file_service"; +import {ITorrentSubtitleService} from "@interfaces/torrent_subtitle_service"; +import {IDatabaseRepository} from "@repository/interfaces/database_repository"; +import {TorrentEntriesService} from "@services/torrent_entries_service"; + + + +jest.mock('@services/logging_service', () => { + return { + error: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + warn: jest.fn(), + } +}) + +jest.mock('@services/torrent_file_service', () => { + return { + parseTorrentFiles: jest.fn(), + } +}) + +jest.mock('@services/metadata_service', () => { + return { + getImdbId: jest.fn(), + } +}) + +jest.mock('@services/torrent_subtitle_service', () => { + return { + assignSubtitles: jest.fn(), + } +}) + +jest.mock('@repository/database_repository', () => { + return { + createTorrent: jest.fn().mockResolvedValue(undefined), + createFile: jest.fn().mockResolvedValue(undefined), + } +}) + +describe('TorrentEntriesService Tests', () => { + let torrentEntriesService: TorrentEntriesService, + mockLoggingService: ILoggingService, + mockFileService: ITorrentFileService, + mockMetadataService: IMetadataService, + mockSubtitleService: ITorrentSubtitleService, + mockDatabaseRepository: IDatabaseRepository; + + beforeEach(() => { + mockFileService = jest.requireMock('@services/torrent_file_service'); + mockMetadataService = jest.requireMock('@services/metadata_service'); + mockSubtitleService = jest.requireMock('@services/torrent_subtitle_service'); + mockLoggingService = jest.requireMock('@services/logging_service'); + mockDatabaseRepository = jest.requireMock('@repository/database_repository'); + torrentEntriesService = new TorrentEntriesService(mockMetadataService, mockLoggingService, mockFileService , mockSubtitleService, mockDatabaseRepository); + }); + + it('should create a torrent entry', async () => { + const torrent : IParsedTorrent = { + title: 'Test title', + provider: 'Test provider', + infoHash: 'Test infoHash', + type: TorrentType.Movie, + }; + + const fileCollection : ITorrentFileCollection = { + videos: [{ + fileIndex: 0, + title: 'Test video', + size: 123456, + imdbId: 'tt1234567', + }], + contents: [], + subtitles: [], + }; + + const fileCollectionWithSubtitles : ITorrentFileCollection = { + ...fileCollection, + subtitles: [ { + fileId: 0, + title: 'Test subtitle', + fileIndex: 0, + path: 'Test path', + infoHash: 'Test infoHash', + }], + }; + + (mockMetadataService.getImdbId as jest.Mock).mockResolvedValue('tt1234567'); + (mockFileService.parseTorrentFiles as jest.Mock).mockResolvedValue(fileCollection); + (mockSubtitleService.assignSubtitles as jest.Mock).mockResolvedValue(fileCollectionWithSubtitles); + (mockDatabaseRepository.createTorrent as jest.Mock).mockResolvedValue(torrent); + + await torrentEntriesService.createTorrentEntry(torrent); + + expect(mockMetadataService.getImdbId).toHaveBeenCalledWith({ title: 'Test title', year: undefined, type: TorrentType.Movie }); + expect(mockFileService.parseTorrentFiles).toHaveBeenCalledWith(torrent); + expect(mockFileService.parseTorrentFiles).toHaveReturnedWith(Promise.resolve(fileCollection)); + expect(mockSubtitleService.assignSubtitles).toHaveBeenCalledWith(fileCollection); + expect(mockSubtitleService.assignSubtitles).toHaveReturnedWith(Promise.resolve(fileCollectionWithSubtitles)); + expect(mockDatabaseRepository.createTorrent).toHaveBeenCalledWith(expect.objectContaining(torrent)); + }); +}); \ No newline at end of file