metadata service now ts
This commit is contained in:
80
src/node/consumer/src/lib/interfaces/cinemeta_metadata.ts
Normal file
80
src/node/consumer/src/lib/interfaces/cinemeta_metadata.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {CommonVideoMetadata} from "./common_video_metadata";
|
||||||
|
|
||||||
|
export interface CinemetaJsonResponse {
|
||||||
|
meta?: CinemetaMetaData;
|
||||||
|
trailerStreams?: CinemetaTrailerStream[];
|
||||||
|
links?: CinemetaLink[];
|
||||||
|
behaviorHints?: CinemetaBehaviorHints;
|
||||||
|
}
|
||||||
|
export interface CinemetaMetaData {
|
||||||
|
awards?: string;
|
||||||
|
cast?: string[];
|
||||||
|
country?: string;
|
||||||
|
description?: string;
|
||||||
|
director?: null;
|
||||||
|
dvdRelease?: null;
|
||||||
|
genre?: string[];
|
||||||
|
imdbRating?: string;
|
||||||
|
imdb_id?: string;
|
||||||
|
name?: string;
|
||||||
|
popularity?: number;
|
||||||
|
poster?: string;
|
||||||
|
released?: string;
|
||||||
|
runtime?: string;
|
||||||
|
status?: string;
|
||||||
|
tvdb_id?: number;
|
||||||
|
type?: string;
|
||||||
|
writer?: string[];
|
||||||
|
year?: string;
|
||||||
|
background?: string;
|
||||||
|
logo?: string;
|
||||||
|
popularities?: CinemetaPopularities;
|
||||||
|
moviedb_id?: number;
|
||||||
|
slug?: string;
|
||||||
|
trailers?: CinemetaTrailer[];
|
||||||
|
id?: string;
|
||||||
|
genres?: string[];
|
||||||
|
releaseInfo?: string;
|
||||||
|
videos?: CinemetaVideo[];
|
||||||
|
}
|
||||||
|
export interface CinemetaPopularities {
|
||||||
|
PXS_TEST?: number;
|
||||||
|
PXS?: number;
|
||||||
|
SCM?: number;
|
||||||
|
EXMD?: number;
|
||||||
|
ALLIANCE?: number;
|
||||||
|
EJD?: number;
|
||||||
|
moviedb?: number;
|
||||||
|
trakt?: number;
|
||||||
|
stremio?: number;
|
||||||
|
stremio_lib?: number;
|
||||||
|
}
|
||||||
|
export interface CinemetaTrailer {
|
||||||
|
source?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
export interface CinemetaVideo extends CommonVideoMetadata {
|
||||||
|
name?: string;
|
||||||
|
number?: number;
|
||||||
|
firstAired?: string;
|
||||||
|
tvdb_id?: number;
|
||||||
|
rating?: string;
|
||||||
|
overview?: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
id?: string;
|
||||||
|
released?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
export interface CinemetaTrailerStream {
|
||||||
|
title?: string;
|
||||||
|
ytId?: string;
|
||||||
|
}
|
||||||
|
export interface CinemetaLink {
|
||||||
|
name?: string;
|
||||||
|
category?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
export interface CinemetaBehaviorHints {
|
||||||
|
defaultVideoId?: null;
|
||||||
|
hasScheduledVideos?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface CommonVideoMetadata {
|
||||||
|
season?: number;
|
||||||
|
episode?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import {KitsuLink, KitsuTrailer} from "./kitsu_metadata";
|
||||||
|
|
||||||
|
export interface KitsuCatalogJsonResponse {
|
||||||
|
metas: KitsuCatalogMetaData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KitsuCatalogMetaData {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
animeType: string;
|
||||||
|
name: string;
|
||||||
|
aliases: string[];
|
||||||
|
description: string;
|
||||||
|
releaseInfo: string;
|
||||||
|
runtime: string;
|
||||||
|
imdbRating: string;
|
||||||
|
genres: string[];
|
||||||
|
logo?: string;
|
||||||
|
poster: string;
|
||||||
|
background: string;
|
||||||
|
trailers: KitsuTrailer[];
|
||||||
|
links: KitsuLink[];
|
||||||
|
}
|
||||||
49
src/node/consumer/src/lib/interfaces/kitsu_metadata.ts
Normal file
49
src/node/consumer/src/lib/interfaces/kitsu_metadata.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {CommonVideoMetadata} from "./common_video_metadata";
|
||||||
|
|
||||||
|
export interface KitsuJsonResponse {
|
||||||
|
cacheMaxAge?: number;
|
||||||
|
meta?: KitsuMeta;
|
||||||
|
}
|
||||||
|
export interface KitsuMeta {
|
||||||
|
aliases?: string[];
|
||||||
|
animeType?: string;
|
||||||
|
background?: string;
|
||||||
|
description?: string;
|
||||||
|
country?: string;
|
||||||
|
genres?: string[];
|
||||||
|
id?: string;
|
||||||
|
imdbRating?: string;
|
||||||
|
imdb_id?: string;
|
||||||
|
kitsu_id?: string;
|
||||||
|
links?: KitsuLink[];
|
||||||
|
logo?: string;
|
||||||
|
name?: string;
|
||||||
|
poster?: string;
|
||||||
|
releaseInfo?: string;
|
||||||
|
runtime?: string;
|
||||||
|
slug?: string;
|
||||||
|
status?: string;
|
||||||
|
trailers?: KitsuTrailer[];
|
||||||
|
type?: string;
|
||||||
|
userCount?: number;
|
||||||
|
videos?: KitsuVideo[];
|
||||||
|
year?: string;
|
||||||
|
}
|
||||||
|
export interface KitsuVideo extends CommonVideoMetadata {
|
||||||
|
id?: string;
|
||||||
|
imdbEpisode?: number;
|
||||||
|
imdbSeason?: number;
|
||||||
|
imdb_id?: string;
|
||||||
|
released?: string;
|
||||||
|
thumbnail?: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
export interface KitsuTrailer {
|
||||||
|
source?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
export interface KitsuLink {
|
||||||
|
name?: string;
|
||||||
|
category?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
8
src/node/consumer/src/lib/interfaces/metadata_query.ts
Normal file
8
src/node/consumer/src/lib/interfaces/metadata_query.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface MetaDataQuery {
|
||||||
|
title?: string
|
||||||
|
type?: string
|
||||||
|
year?: number | string
|
||||||
|
date?: string
|
||||||
|
season?: number
|
||||||
|
episode?: number
|
||||||
|
}
|
||||||
13
src/node/consumer/src/lib/interfaces/metadata_response.ts
Normal file
13
src/node/consumer/src/lib/interfaces/metadata_response.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export interface MetadataResponse {
|
||||||
|
kitsuId?: number;
|
||||||
|
imdbId?: number;
|
||||||
|
type?: string;
|
||||||
|
title?: string;
|
||||||
|
year?: number;
|
||||||
|
country?: string;
|
||||||
|
genres?: string[];
|
||||||
|
status?: string;
|
||||||
|
videos?: any[];
|
||||||
|
episodeCount?: number[];
|
||||||
|
totalCount?: number;
|
||||||
|
}
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { search } from 'google-sr';
|
|
||||||
import nameToImdb from 'name-to-imdb';
|
|
||||||
import { cacheWrapImdbId, cacheWrapKitsuId, cacheWrapMetadata } from './cache.js';
|
|
||||||
import { TorrentType } from './enums/torrent_types';
|
|
||||||
|
|
||||||
const CINEMETA_URL = 'https://v3-cinemeta.strem.io';
|
|
||||||
const KITSU_URL = 'https://anime-kitsu.strem.fun';
|
|
||||||
const TIMEOUT = 20000;
|
|
||||||
|
|
||||||
export function getMetadata(id, type = TorrentType.SERIES) {
|
|
||||||
if (!id) {
|
|
||||||
return Promise.reject("no valid id provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = Number.isInteger(id) || id.match(/^\d+$/) ? `kitsu:${id}` : id;
|
|
||||||
const metaType = type === TorrentType.MOVIE ? TorrentType.MOVIE : TorrentType.SERIES;
|
|
||||||
return cacheWrapMetadata(key, () => _requestMetadata(`${KITSU_URL}/meta/${metaType}/${key}.json`)
|
|
||||||
.catch(() => _requestMetadata(`${CINEMETA_URL}/meta/${metaType}/${key}.json`))
|
|
||||||
.catch(() => {
|
|
||||||
// try different type in case there was a mismatch
|
|
||||||
const otherType = metaType === TorrentType.MOVIE ? TorrentType.SERIES : TorrentType.MOVIE;
|
|
||||||
return _requestMetadata(`${CINEMETA_URL}/meta/${otherType}/${key}.json`)
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw new Error(`failed metadata query ${key} due: ${error.message}`);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function _requestMetadata(url) {
|
|
||||||
return axios.get(url, { timeout: TIMEOUT })
|
|
||||||
.then((response) => {
|
|
||||||
const body = response.data;
|
|
||||||
if (body && body.meta && (body.meta.imdb_id || body.meta.kitsu_id)) {
|
|
||||||
return {
|
|
||||||
kitsuId: body.meta.kitsu_id,
|
|
||||||
imdbId: body.meta.imdb_id,
|
|
||||||
type: body.meta.type,
|
|
||||||
title: body.meta.name,
|
|
||||||
year: body.meta.year,
|
|
||||||
country: body.meta.country,
|
|
||||||
genres: body.meta.genres,
|
|
||||||
status: body.meta.status,
|
|
||||||
videos: (body.meta.videos || [])
|
|
||||||
.map((video) => Number.isInteger(video.imdbSeason)
|
|
||||||
? {
|
|
||||||
name: video.name || video.title,
|
|
||||||
season: video.season,
|
|
||||||
episode: video.episode,
|
|
||||||
imdbSeason: video.imdbSeason,
|
|
||||||
imdbEpisode: video.imdbEpisode
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
name: video.name || video.title,
|
|
||||||
season: video.season,
|
|
||||||
episode: video.episode,
|
|
||||||
kitsuId: video.kitsu_id,
|
|
||||||
kitsuEpisode: video.kitsuEpisode,
|
|
||||||
released: video.released
|
|
||||||
}
|
|
||||||
),
|
|
||||||
episodeCount: Object.values((body.meta.videos || [])
|
|
||||||
.filter((entry) => entry.season !== 0 && entry.episode !== 0)
|
|
||||||
.sort((a, b) => a.season - b.season)
|
|
||||||
.reduce((map, next) => {
|
|
||||||
map[next.season] = map[next.season] + 1 || 1;
|
|
||||||
return map;
|
|
||||||
}, {})),
|
|
||||||
totalCount: body.meta.videos && body.meta.videos
|
|
||||||
.filter((entry) => entry.season !== 0 && entry.episode !== 0).length
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error('No search results');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function escapeTitle(title) {
|
|
||||||
return title.toLowerCase()
|
|
||||||
.normalize('NFKD') // normalize non-ASCII characters
|
|
||||||
.replace(/[\u0300-\u036F]/g, '')
|
|
||||||
.replace(/&/g, 'and')
|
|
||||||
.replace(/[;, ~./]+/g, ' ') // replace dots, commas or underscores with spaces
|
|
||||||
.replace(/[^\w \-()×+#@!'\u0400-\u04ff]+/g, '') // remove all non-alphanumeric chars
|
|
||||||
.replace(/^\d{1,2}[.#\s]+(?=(?:\d+[.\s]*)?[\u0400-\u04ff])/i, '') // remove russian movie numbering
|
|
||||||
.replace(/\s{2,}/, ' ') // replace multiple spaces
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getImdbId(info, type) {
|
|
||||||
const name = escapeTitle(info.title);
|
|
||||||
const year = info.year || (info.date && info.date.slice(0, 4));
|
|
||||||
const key = `${name}_${year || 'NA'}_${type}`;
|
|
||||||
const query = `${name} ${year || ''} ${type} imdb`;
|
|
||||||
const fallbackQuery = `${name} ${type} imdb`;
|
|
||||||
const googleQuery = year ? query : fallbackQuery;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const imdbId = await cacheWrapImdbId(key,
|
|
||||||
() => getIMDbIdFromNameToImdb(name, info.year, type)
|
|
||||||
);
|
|
||||||
return imdbId && 'tt' + imdbId.replace(/tt0*([1-9][0-9]*)$/, '$1').padStart(7, '0');
|
|
||||||
} catch (error) {
|
|
||||||
const imdbIdFallback = await getIMDbIdFromGoogle(googleQuery);
|
|
||||||
return imdbIdFallback && 'tt' + imdbIdFallback.replace(/tt0*([1-9][0-9]*)$/, '$1').padStart(7, '0');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIMDbIdFromNameToImdb(name, year, type) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
nameToImdb({ name, year, type }, function(err, res) {
|
|
||||||
if (res) {
|
|
||||||
resolve(res);
|
|
||||||
} else {
|
|
||||||
reject(err || new Error('Failed IMDbId search'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getIMDbIdFromGoogle(query) {
|
|
||||||
try {
|
|
||||||
const searchResults = await search({ query: query });
|
|
||||||
for (const result of searchResults) {
|
|
||||||
if (result.link.includes('imdb.com/title/')) {
|
|
||||||
const match = result.link.match(/imdb\.com\/title\/(tt\d+)/);
|
|
||||||
if (match) {
|
|
||||||
return match[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
throw new Error('Failed to find IMDb ID from Google search');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getKitsuId(info) {
|
|
||||||
const title = escapeTitle(info.title.replace(/\s\|\s.*/, ''));
|
|
||||||
const year = info.year ? ` ${info.year}` : '';
|
|
||||||
const season = info.season > 1 ? ` S${info.season}` : '';
|
|
||||||
const key = `${title}${year}${season}`;
|
|
||||||
const query = encodeURIComponent(key);
|
|
||||||
|
|
||||||
return cacheWrapKitsuId(key,
|
|
||||||
() => axios.get(`${KITSU_URL}/catalog/series/kitsu-anime-list/search=${query}.json`, { timeout: 60000 })
|
|
||||||
.then((response) => {
|
|
||||||
const body = response.data;
|
|
||||||
if (body && body.metas && body.metas.length) {
|
|
||||||
return body.metas[0].id.replace('kitsu:', '');
|
|
||||||
} else {
|
|
||||||
throw new Error('No search results');
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function isEpisodeImdbId(imdbId) {
|
|
||||||
if (!imdbId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return axios.get(`https://www.imdb.com/title/${imdbId}/`, { timeout: 10000 })
|
|
||||||
.then(response => !!(response.data && response.data.includes('video.episode')))
|
|
||||||
.catch(() => false);
|
|
||||||
}
|
|
||||||
216
src/node/consumer/src/lib/metadata.ts
Normal file
216
src/node/consumer/src/lib/metadata.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import axios, {AxiosResponse} from 'axios';
|
||||||
|
import {search, ResultTypes} from 'google-sr';
|
||||||
|
import nameToImdb from 'name-to-imdb';
|
||||||
|
import { cacheWrapImdbId, cacheWrapKitsuId, cacheWrapMetadata } from './cache.js';
|
||||||
|
import { TorrentType } from './enums/torrent_types';
|
||||||
|
import {MetadataResponse} from "./interfaces/metadata_response";
|
||||||
|
import {CinemetaJsonResponse} from "./interfaces/cinemeta_metadata";
|
||||||
|
import {CommonVideoMetadata} from "./interfaces/common_video_metadata";
|
||||||
|
import {KitsuJsonResponse} from "./interfaces/kitsu_metadata";
|
||||||
|
import {MetaDataQuery} from "./interfaces/metadata_query";
|
||||||
|
import {KitsuCatalogJsonResponse} from "./interfaces/kitsu_catalog_metadata";
|
||||||
|
|
||||||
|
const CINEMETA_URL = 'https://v3-cinemeta.strem.io';
|
||||||
|
const KITSU_URL = 'https://anime-kitsu.strem.fun';
|
||||||
|
const TIMEOUT = 20000;
|
||||||
|
|
||||||
|
async function _requestMetadata(url: string): Promise<MetadataResponse> {
|
||||||
|
let response: AxiosResponse<any, any> = await axios.get(url, {timeout: TIMEOUT});
|
||||||
|
let result : MetadataResponse;
|
||||||
|
const body = response.data;
|
||||||
|
if ('kitsu_id' in body.meta) {
|
||||||
|
result = handleKitsuResponse(body as KitsuJsonResponse);
|
||||||
|
}
|
||||||
|
else if ('imdb_id' in body.meta) {
|
||||||
|
result = handleCinemetaResponse(body as CinemetaJsonResponse);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error('No valid metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCinemetaResponse(body: CinemetaJsonResponse) : MetadataResponse {
|
||||||
|
return {
|
||||||
|
imdbId: parseInt(body.meta.imdb_id),
|
||||||
|
type: body.meta.type,
|
||||||
|
title: body.meta.name,
|
||||||
|
year: parseInt(body.meta.year),
|
||||||
|
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
|
||||||
|
? getEpisodeCount(body.meta.videos)
|
||||||
|
: [],
|
||||||
|
totalCount: body.meta.videos
|
||||||
|
? body.meta.videos.filter(
|
||||||
|
entry => entry.season !== 0 && entry.episode !== 0
|
||||||
|
).length
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKitsuResponse(body: KitsuJsonResponse) : MetadataResponse {
|
||||||
|
return {
|
||||||
|
kitsuId: parseInt(body.meta.kitsu_id),
|
||||||
|
type: body.meta.type,
|
||||||
|
title: body.meta.name,
|
||||||
|
year: parseInt(body.meta.year),
|
||||||
|
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
|
||||||
|
? getEpisodeCount(body.meta.videos)
|
||||||
|
: [],
|
||||||
|
totalCount: body.meta.videos
|
||||||
|
? body.meta.videos.filter(
|
||||||
|
entry => entry.season !== 0 && entry.episode !== 0
|
||||||
|
).length
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEpisodeCount(videos: CommonVideoMetadata[]) {
|
||||||
|
return Object.values(
|
||||||
|
videos
|
||||||
|
.filter(entry => entry.season !== 0 && entry.episode !== 0)
|
||||||
|
.sort((a, b) => a.season - b.season)
|
||||||
|
.reduce((map, next) => {
|
||||||
|
map[next.season] = map[next.season] + 1 || 1;
|
||||||
|
return map;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function escapeTitle(title: string): string {
|
||||||
|
return title.toLowerCase()
|
||||||
|
.normalize('NFKD') // normalize non-ASCII characters
|
||||||
|
.replace(/[\u0300-\u036F]/g, '')
|
||||||
|
.replace(/&/g, 'and')
|
||||||
|
.replace(/[;, ~./]+/g, ' ') // replace dots, commas or underscores with spaces
|
||||||
|
.replace(/[^\w \-()×+#@!'\u0400-\u04ff]+/g, '') // remove all non-alphanumeric chars
|
||||||
|
.replace(/^\d{1,2}[.#\s]+(?=(?:\d+[.\s]*)?[\u0400-\u04ff])/i, '') // remove russian movie numbering
|
||||||
|
.replace(/\s{2,}/, ' ') // replace multiple spaces
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIMDbIdFromNameToImdb(name: string, info: MetaDataQuery) : Promise<string | Error> {
|
||||||
|
const year = info.year;
|
||||||
|
const type = info.type;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
nameToImdb({ name, year, type }, function(err: Error, res: string) {
|
||||||
|
if (res) {
|
||||||
|
resolve(res);
|
||||||
|
} else {
|
||||||
|
reject(err || new Error('Failed IMDbId search'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getIMDbIdFromGoogle(query: string): Promise<string | undefined>{
|
||||||
|
try {
|
||||||
|
const searchResults = await search({ query: query });
|
||||||
|
for(const result of searchResults) {
|
||||||
|
if(result.type === ResultTypes.SearchResult) {
|
||||||
|
if(result.link.includes('imdb.com/title/')){
|
||||||
|
const match = result.link.match(/imdb\.com\/title\/(tt\d+)/);
|
||||||
|
if(match){
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new Error('Failed to find IMDb ID from Google search');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getKitsuId(info: MetaDataQuery): Promise<string | Error> {
|
||||||
|
const title = escapeTitle(info.title.replace(/\s\|\s.*/, ''));
|
||||||
|
const year = info.year ? ` ${info.year}` : '';
|
||||||
|
const season = info.season > 1 ? ` S${info.season}` : '';
|
||||||
|
const key = `${title}${year}${season}`;
|
||||||
|
const query = encodeURIComponent(key);
|
||||||
|
|
||||||
|
return cacheWrapKitsuId(key,
|
||||||
|
() => axios.get(`${KITSU_URL}/catalog/series/kitsu-anime-list/search=${query}.json`, { timeout: 60000 })
|
||||||
|
.then((response) => {
|
||||||
|
const body = response.data as KitsuCatalogJsonResponse;
|
||||||
|
if (body && body.metas && body.metas.length) {
|
||||||
|
return body.metas[0].id.replace('kitsu:', '');
|
||||||
|
} else {
|
||||||
|
throw new Error('No search results');
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getImdbId(info: MetaDataQuery): Promise<string | undefined> {
|
||||||
|
const name = escapeTitle(info.title);
|
||||||
|
const year = info.year || (info.date && info.date.slice(0, 4));
|
||||||
|
const key = `${name}_${year || 'NA'}_${info.type}`;
|
||||||
|
const query = `${name} ${year || ''} ${info.type} imdb`;
|
||||||
|
const fallbackQuery = `${name} ${info.type} imdb`;
|
||||||
|
const googleQuery = year ? query : fallbackQuery;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const imdbId = await cacheWrapImdbId(key,
|
||||||
|
() => getIMDbIdFromNameToImdb(name, info)
|
||||||
|
);
|
||||||
|
return imdbId && 'tt' + imdbId.replace(/tt0*([1-9][0-9]*)$/, '$1').padStart(7, '0');
|
||||||
|
} catch (error) {
|
||||||
|
const imdbIdFallback = await getIMDbIdFromGoogle(googleQuery);
|
||||||
|
return imdbIdFallback && 'tt' + imdbIdFallback.toString().replace(/tt0*([1-9][0-9]*)$/, '$1').padStart(7, '0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMetadata(id: string | number, type: TorrentType = TorrentType.SERIES): Promise<MetadataResponse | Error> {
|
||||||
|
if (!id) {
|
||||||
|
return Promise.reject("no valid id provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = Number.isInteger(id) || id.toString().match(/^\d+$/) ? `kitsu:${id}` : id;
|
||||||
|
const metaType = type === TorrentType.MOVIE ? TorrentType.MOVIE : TorrentType.SERIES;
|
||||||
|
return cacheWrapMetadata(key, () => _requestMetadata(`${KITSU_URL}/meta/${metaType}/${key}.json`)
|
||||||
|
.catch(() => _requestMetadata(`${CINEMETA_URL}/meta/${metaType}/${key}.json`))
|
||||||
|
.catch(() => {
|
||||||
|
// try different type in case there was a mismatch
|
||||||
|
const otherType = metaType === TorrentType.MOVIE ? TorrentType.SERIES : TorrentType.MOVIE;
|
||||||
|
return _requestMetadata(`${CINEMETA_URL}/meta/${otherType}/${key}.json`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
throw new Error(`failed metadata query ${key} due: ${error.message}`);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isEpisodeImdbId(imdbId: string | undefined): Promise<boolean> {
|
||||||
|
if (!imdbId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return axios.get(`https://www.imdb.com/title/${imdbId}/`, { timeout: 10000 })
|
||||||
|
.then(response => !!(response.data && response.data.includes('video.episode')))
|
||||||
|
.catch(() => false);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { parse } from 'parse-torrent-title';
|
import { parse } from 'parse-torrent-title';
|
||||||
import { getImdbId, getKitsuId } from './metadata.js';
|
import { getImdbId, getKitsuId } from './metadata';
|
||||||
import { isPackTorrent } from './parseHelper.js';
|
import { isPackTorrent } from './parseHelper.js';
|
||||||
import * as Promises from './promises.js';
|
import * as Promises from './promises.js';
|
||||||
import { repository } from '../repository/database_repository';
|
import { repository } from '../repository/database_repository';
|
||||||
@@ -12,7 +12,12 @@ export async function createTorrentEntry(torrent, overwrite = false) {
|
|||||||
const titleInfo = parse(torrent.title);
|
const titleInfo = parse(torrent.title);
|
||||||
|
|
||||||
if (!torrent.imdbId && torrent.type !== TorrentType.ANIME) {
|
if (!torrent.imdbId && torrent.type !== TorrentType.ANIME) {
|
||||||
torrent.imdbId = await getImdbId(titleInfo, torrent.type)
|
const imdbQuery = {
|
||||||
|
title: titleInfo.title,
|
||||||
|
year: titleInfo.year,
|
||||||
|
type: torrent.type
|
||||||
|
};
|
||||||
|
torrent.imdbId = await getImdbId(imdbQuery)
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
if (torrent.imdbId && torrent.imdbId.length < 9) {
|
if (torrent.imdbId && torrent.imdbId.length < 9) {
|
||||||
@@ -24,7 +29,12 @@ export async function createTorrentEntry(torrent, overwrite = false) {
|
|||||||
torrent.imdbId = torrent.imdbId.replace(/tt0+([0-9]{7,})$/, 'tt$1');
|
torrent.imdbId = torrent.imdbId.replace(/tt0+([0-9]{7,})$/, 'tt$1');
|
||||||
}
|
}
|
||||||
if (!torrent.kitsuId && torrent.type === TorrentType.ANIME) {
|
if (!torrent.kitsuId && torrent.type === TorrentType.ANIME) {
|
||||||
torrent.kitsuId = await getKitsuId(titleInfo)
|
const kitsuQuery = {
|
||||||
|
title: titleInfo.title,
|
||||||
|
year: titleInfo.year,
|
||||||
|
season: titleInfo.season,
|
||||||
|
};
|
||||||
|
torrent.kitsuId = await getKitsuId(kitsuQuery)
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import moment from 'moment';
|
|||||||
import { parse } from 'parse-torrent-title';
|
import { parse } from 'parse-torrent-title';
|
||||||
import { metadataConfig } from './config.js';
|
import { metadataConfig } from './config.js';
|
||||||
import { isDisk } from './extension.js';
|
import { isDisk } from './extension.js';
|
||||||
import { getMetadata, getImdbId, getKitsuId } from './metadata.js';
|
import { getMetadata, getImdbId, getKitsuId } from './metadata';
|
||||||
import { parseSeriesVideos, isPackTorrent } from './parseHelper.js';
|
import { parseSeriesVideos, isPackTorrent } from './parseHelper.js';
|
||||||
import * as Promises from './promises.js';
|
import * as Promises from './promises.js';
|
||||||
import {torrentFiles} from "./torrent.js";
|
import {torrentFiles} from "./torrent.js";
|
||||||
@@ -472,12 +472,25 @@ async function updateToCinemetaMetadata(metadata) {
|
|||||||
function findMovieImdbId(title) {
|
function findMovieImdbId(title) {
|
||||||
const parsedTitle = typeof title === 'string' ? parse(title) : title;
|
const parsedTitle = typeof title === 'string' ? parse(title) : title;
|
||||||
logger.debug(`Finding movie imdbId for ${title}`);
|
logger.debug(`Finding movie imdbId for ${title}`);
|
||||||
return imdb_limiter.schedule(() => getImdbId(parsedTitle, TorrentType.MOVIE).catch(() => undefined));
|
return imdb_limiter.schedule(() => {
|
||||||
|
const imdbQuery = {
|
||||||
|
title: parsedTitle.title,
|
||||||
|
year: parsedTitle.year,
|
||||||
|
type: TorrentType.MOVIE
|
||||||
|
};
|
||||||
|
return getImdbId(imdbQuery).catch(() => undefined);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMovieKitsuId(title) {
|
function findMovieKitsuId(title) {
|
||||||
const parsedTitle = typeof title === 'string' ? parse(title) : title;
|
const parsedTitle = typeof title === 'string' ? parse(title) : title;
|
||||||
return getKitsuId(parsedTitle, TorrentType.MOVIE).catch(() => undefined);
|
const kitsuQuery = {
|
||||||
|
title: parsedTitle.title,
|
||||||
|
year: parsedTitle.year,
|
||||||
|
season: parsedTitle.season,
|
||||||
|
type: TorrentType.MOVIE
|
||||||
|
};
|
||||||
|
return getKitsuId(kitsuQuery).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDiskTorrent(contents) {
|
function isDiskTorrent(contents) {
|
||||||
|
|||||||
Reference in New Issue
Block a user