interfaces normalized and extracted for services

This commit is contained in:
iPromKnight
2024-02-07 09:48:32 +00:00
committed by iPromKnight
parent 2ca2c77b94
commit b95f433315
49 changed files with 424 additions and 338 deletions

View File

@@ -1,6 +1,6 @@
import client, {Channel, Connection, ConsumeMessage, Options} from 'amqplib' import client, {Channel, Connection, ConsumeMessage, Options} from 'amqplib'
import {IngestedRabbitMessage, IngestedRabbitTorrent} from "../lib/interfaces/ingested_rabbit_message"; import {IIngestedRabbitMessage, IIngestedRabbitTorrent} from "../lib/interfaces/ingested_rabbit_message";
import {IngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes"; import {IIngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes";
import {configurationService} from '../lib/services/configuration_service'; import {configurationService} from '../lib/services/configuration_service';
import {torrentProcessingService} from '../lib/services/torrent_processing_service'; import {torrentProcessingService} from '../lib/services/torrent_processing_service';
import {logger} from '../lib/services/logging_service'; import {logger} from '../lib/services/logging_service';
@@ -23,13 +23,13 @@ class ProcessTorrentsJob {
} }
} }
private processMessage = (msg: ConsumeMessage) => { private processMessage = (msg: ConsumeMessage) => {
const ingestedTorrent: IngestedTorrentAttributes = this.getMessageAsJson(msg); const ingestedTorrent: IIngestedTorrentAttributes = this.getMessageAsJson(msg);
return torrentProcessingService.processTorrentRecord(ingestedTorrent); return torrentProcessingService.processTorrentRecord(ingestedTorrent);
}; };
private getMessageAsJson = (msg: ConsumeMessage): IngestedTorrentAttributes => { private getMessageAsJson = (msg: ConsumeMessage): IIngestedTorrentAttributes => {
const content = msg?.content.toString('utf8') ?? "{}"; const content = msg?.content.toString('utf8') ?? "{}";
const receivedObject: IngestedRabbitMessage = JSON.parse(content); const receivedObject: IIngestedRabbitMessage = JSON.parse(content);
const receivedTorrent: IngestedRabbitTorrent = receivedObject.message; const receivedTorrent: IIngestedRabbitTorrent = receivedObject.message;
return {...receivedTorrent, info_hash: receivedTorrent.infoHash}; return {...receivedTorrent, info_hash: receivedTorrent.infoHash};
}; };
private async assertAndConsumeQueue(channel: Channel) { private async assertAndConsumeQueue(channel: Channel) {

View File

@@ -0,0 +1,66 @@
const VIDEO_EXTENSIONS = [
"3g2",
"3gp",
"avi",
"flv",
"mkv",
"mk3d",
"mov",
"mp2",
"mp4",
"m4v",
"mpe",
"mpeg",
"mpg",
"mpv",
"webm",
"wmv",
"ogm",
"divx"
];
const SUBTITLE_EXTENSIONS = [
"aqt",
"gsub",
"jss",
"sub",
"ttxt",
"pjs",
"psb",
"rt",
"smi",
"slt",
"ssf",
"srt",
"ssa",
"ass",
"usf",
"idx",
"vtt"
];
const DISK_EXTENSIONS = [
"iso",
"m2ts",
"ts",
"vob"
];
export const ExtensionHelpers = {
isVideo(filename: string) {
return this.isExtension(filename, VIDEO_EXTENSIONS);
},
isSubtitle(filename: string) {
return this.isExtension(filename, SUBTITLE_EXTENSIONS);
},
isDisk(filename: string) {
return this.isExtension(filename, DISK_EXTENSIONS);
},
isExtension(filename: string, extensions: string[]) {
const extensionMatch = filename.match(/\.(\w{2,4})$/);
return extensionMatch !== null && extensions.includes(extensionMatch[1].toLowerCase());
}
}

View File

@@ -1,3 +1,3 @@
export interface CacheOptions { export interface ICacheOptions {
ttl: number; ttl: number;
} }

View File

@@ -0,0 +1,8 @@
import {CacheMethod} from "../services/cache_service";
export interface ICacheService {
cacheWrapImdbId: (key: string, method: CacheMethod) => Promise<any>;
cacheWrapKitsuId: (key: string, method: CacheMethod) => Promise<any>;
cacheWrapMetadata: (id: string, method: CacheMethod) => Promise<any>;
cacheTrackers: (method: CacheMethod) => Promise<any>;
}

View File

@@ -1,12 +1,12 @@
import {CommonVideoMetadata} from "./common_video_metadata"; import {ICommonVideoMetadata} from "./common_video_metadata";
export interface CinemetaJsonResponse { export interface ICinemetaJsonResponse {
meta?: CinemetaMetaData; meta?: ICinemetaMetaData;
trailerStreams?: CinemetaTrailerStream[]; trailerStreams?: ICinemetaTrailerStream[];
links?: CinemetaLink[]; links?: ICinemetaLink[];
behaviorHints?: CinemetaBehaviorHints; behaviorHints?: ICinemetaBehaviorHints;
} }
export interface CinemetaMetaData { export interface ICinemetaMetaData {
awards?: string; awards?: string;
cast?: string[]; cast?: string[];
country?: string; country?: string;
@@ -28,16 +28,16 @@ export interface CinemetaMetaData {
year?: string; year?: string;
background?: string; background?: string;
logo?: string; logo?: string;
popularities?: CinemetaPopularities; popularities?: ICinemetaPopularities;
moviedb_id?: number; moviedb_id?: number;
slug?: string; slug?: string;
trailers?: CinemetaTrailer[]; trailers?: ICinemetaTrailer[];
id?: string; id?: string;
genres?: string[]; genres?: string[];
releaseInfo?: string; releaseInfo?: string;
videos?: CinemetaVideo[]; videos?: ICinemetaVideo[];
} }
export interface CinemetaPopularities { export interface ICinemetaPopularities {
PXS_TEST?: number; PXS_TEST?: number;
PXS?: number; PXS?: number;
SCM?: number; SCM?: number;
@@ -49,11 +49,11 @@ export interface CinemetaPopularities {
stremio?: number; stremio?: number;
stremio_lib?: number; stremio_lib?: number;
} }
export interface CinemetaTrailer { export interface ICinemetaTrailer {
source?: string; source?: string;
type?: string; type?: string;
} }
export interface CinemetaVideo extends CommonVideoMetadata { export interface ICinemetaVideo extends ICommonVideoMetadata {
name?: string; name?: string;
number?: number; number?: number;
firstAired?: string; firstAired?: string;
@@ -63,16 +63,16 @@ export interface CinemetaVideo extends CommonVideoMetadata {
thumbnail?: string; thumbnail?: string;
description?: string; description?: string;
} }
export interface CinemetaTrailerStream { export interface ICinemetaTrailerStream {
title?: string; title?: string;
ytId?: string; ytId?: string;
} }
export interface CinemetaLink { export interface ICinemetaLink {
name?: string; name?: string;
category?: string; category?: string;
url?: string; url?: string;
} }
export interface CinemetaBehaviorHints { export interface ICinemetaBehaviorHints {
defaultVideoId?: null; defaultVideoId?: null;
hasScheduledVideos?: boolean; hasScheduledVideos?: boolean;
} }

View File

@@ -1,4 +1,4 @@
export interface CommonVideoMetadata { export interface ICommonVideoMetadata {
season?: number; season?: number;
episode?: number; episode?: number;
released?: string; released?: string;

View File

@@ -1,4 +1,4 @@
export interface IngestedRabbitTorrent { export interface IIngestedRabbitTorrent {
name: string; name: string;
source: string; source: string;
category: string; category: string;
@@ -10,6 +10,6 @@ export interface IngestedRabbitTorrent {
processed: boolean; processed: boolean;
} }
export interface IngestedRabbitMessage { export interface IIngestedRabbitMessage {
message: IngestedRabbitTorrent; message: IIngestedRabbitTorrent;
} }

View File

@@ -1,10 +1,10 @@
import {KitsuLink, KitsuTrailer} from "./kitsu_metadata"; import {IKitsuLink, IKitsuTrailer} from "./kitsu_metadata";
export interface KitsuCatalogJsonResponse { export interface IKitsuCatalogJsonResponse {
metas: KitsuCatalogMetaData[]; metas: IKitsuCatalogMetaData[];
} }
export interface KitsuCatalogMetaData { export interface IKitsuCatalogMetaData {
id: string; id: string;
type: string; type: string;
animeType: string; animeType: string;
@@ -18,6 +18,6 @@ export interface KitsuCatalogMetaData {
logo?: string; logo?: string;
poster: string; poster: string;
background: string; background: string;
trailers: KitsuTrailer[]; trailers: IKitsuTrailer[];
links: KitsuLink[]; links: IKitsuLink[];
} }

View File

@@ -1,10 +1,10 @@
import {CommonVideoMetadata} from "./common_video_metadata"; import {ICommonVideoMetadata} from "./common_video_metadata";
export interface KitsuJsonResponse { export interface IKitsuJsonResponse {
cacheMaxAge?: number; cacheMaxAge?: number;
meta?: KitsuMeta; meta?: IKitsuMeta;
} }
export interface KitsuMeta { export interface IKitsuMeta {
aliases?: string[]; aliases?: string[];
animeType?: string; animeType?: string;
background?: string; background?: string;
@@ -15,7 +15,7 @@ export interface KitsuMeta {
imdbRating?: string; imdbRating?: string;
imdb_id?: string; imdb_id?: string;
kitsu_id?: string; kitsu_id?: string;
links?: KitsuLink[]; links?: IKitsuLink[];
logo?: string; logo?: string;
name?: string; name?: string;
poster?: string; poster?: string;
@@ -23,23 +23,23 @@ export interface KitsuMeta {
runtime?: string; runtime?: string;
slug?: string; slug?: string;
status?: string; status?: string;
trailers?: KitsuTrailer[]; trailers?: IKitsuTrailer[];
type?: string; type?: string;
userCount?: number; userCount?: number;
videos?: KitsuVideo[]; videos?: IKitsuVideo[];
year?: string; year?: string;
} }
export interface KitsuVideo extends CommonVideoMetadata { export interface IKitsuVideo extends ICommonVideoMetadata {
imdbEpisode?: number; imdbEpisode?: number;
imdbSeason?: number; imdbSeason?: number;
imdb_id?: string; imdb_id?: string;
thumbnail?: string; thumbnail?: string;
} }
export interface KitsuTrailer { export interface IKitsuTrailer {
source?: string; source?: string;
type?: string; type?: string;
} }
export interface KitsuLink { export interface IKitsuLink {
name?: string; name?: string;
category?: string; category?: string;
url?: string; url?: string;

View File

@@ -0,0 +1,6 @@
export interface ILoggingService {
info(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
debug(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
}

View File

@@ -1,4 +1,4 @@
export interface MetaDataQuery { export interface IMetaDataQuery {
title?: string title?: string
type?: string type?: string
year?: number | string year?: number | string

View File

@@ -1,6 +1,6 @@
import {CommonVideoMetadata} from "./common_video_metadata"; import {ICommonVideoMetadata} from "./common_video_metadata";
export interface MetadataResponse { export interface IMetadataResponse {
kitsuId?: number; kitsuId?: number;
imdbId?: number; imdbId?: number;
type?: string; type?: string;
@@ -9,7 +9,7 @@ export interface MetadataResponse {
country?: string; country?: string;
genres?: string[]; genres?: string[];
status?: string; status?: string;
videos?: CommonVideoMetadata[]; videos?: ICommonVideoMetadata[];
episodeCount?: number[]; episodeCount?: number[];
totalCount?: number; totalCount?: number;
} }

View File

@@ -0,0 +1,14 @@
import {IMetaDataQuery} from "./metadata_query";
import {IMetadataResponse} from "./metadata_response";
export interface IMetadataService {
getKitsuId(info: IMetaDataQuery): Promise<string | Error>;
getImdbId(info: IMetaDataQuery): Promise<string | undefined>;
getMetadata(query: IMetaDataQuery): Promise<IMetadataResponse | Error>;
isEpisodeImdbId(imdbId: string | undefined): Promise<boolean>;
escapeTitle(title: string): string;
}

View File

@@ -1,4 +1,4 @@
export interface ParseTorrentTitleResult { export interface IParseTorrentTitleResult {
title?: string; title?: string;
date?: string; date?: string;
year?: number | string; year?: number | string;

View File

@@ -1,8 +1,8 @@
import {ParseTorrentTitleResult} from "./parse_torrent_title_result"; import {IParseTorrentTitleResult} from "./parse_torrent_title_result";
import {TorrentType} from "../enums/torrent_types"; import {TorrentType} from "../enums/torrent_types";
import {TorrentFileCollection} from "./torrent_file_collection"; import {ITorrentFileCollection} from "./torrent_file_collection";
export interface ParsedTorrent extends ParseTorrentTitleResult { export interface IParsedTorrent extends IParseTorrentTitleResult {
size?: number; size?: number;
isPack?: boolean; isPack?: boolean;
imdbId?: string | number; imdbId?: string | number;
@@ -14,5 +14,5 @@ export interface ParsedTorrent extends ParseTorrentTitleResult {
uploadDate?: Date; uploadDate?: Date;
seeders?: number; seeders?: number;
torrentId?: string; torrentId?: string;
fileCollection?: TorrentFileCollection; fileCollection?: ITorrentFileCollection;
} }

View File

@@ -0,0 +1,6 @@
import {IParsedTorrent} from "./parsed_torrent";
import {ITorrentFileCollection} from "./torrent_file_collection";
export interface ITorrentDownloadService {
getTorrentFiles(torrent: IParsedTorrent, timeout: number): Promise<ITorrentFileCollection>;
}

View File

@@ -0,0 +1,18 @@
import {IParsedTorrent} from "./parsed_torrent";
import {Torrent} from "../../repository/models/torrent";
import {ITorrentAttributes} from "../../repository/interfaces/torrent_attributes";
import {SkipTorrent} from "../../repository/models/skipTorrent";
export interface ITorrentEntriesService {
createTorrentEntry(torrent: IParsedTorrent, overwrite): Promise<void>;
createSkipTorrentEntry(torrent: Torrent): Promise<[SkipTorrent, boolean]>;
getStoredTorrentEntry(torrent: Torrent): Promise<SkipTorrent | Torrent>;
checkAndUpdateTorrent(torrent: IParsedTorrent): Promise<boolean>;
createTorrentContents(torrent: Torrent): Promise<void>;
updateTorrentSeeders(torrent: ITorrentAttributes): Promise<[number] | ITorrentAttributes>;
}

View File

@@ -1,9 +1,9 @@
import {ContentAttributes} from "../../repository/interfaces/content_attributes"; import {IContentAttributes} from "../../repository/interfaces/content_attributes";
import {FileAttributes} from "../../repository/interfaces/file_attributes"; import {IFileAttributes} from "../../repository/interfaces/file_attributes";
import {SubtitleAttributes} from "../../repository/interfaces/subtitle_attributes"; import {ISubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
export interface TorrentFileCollection { export interface ITorrentFileCollection {
contents?: ContentAttributes[]; contents?: IContentAttributes[];
videos?: FileAttributes[]; videos?: IFileAttributes[];
subtitles?: SubtitleAttributes[]; subtitles?: ISubtitleAttributes[];
} }

View File

@@ -0,0 +1,8 @@
import {IParsedTorrent} from "./parsed_torrent";
import {ITorrentFileCollection} from "./torrent_file_collection";
export interface ITorrentFileService {
parseTorrentFiles(torrent: IParsedTorrent): Promise<ITorrentFileCollection>;
isPackTorrent(torrent: IParsedTorrent): boolean;
}

View File

@@ -0,0 +1,5 @@
import {IIngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
export interface ITorrentProcessingService {
processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise<void>;
}

View File

@@ -0,0 +1,5 @@
import {ITorrentFileCollection} from "./torrent_file_collection";
export interface ITorrentSubtitleService {
assignSubtitles(fileCollection: ITorrentFileCollection): ITorrentFileCollection;
}

View File

@@ -0,0 +1,3 @@
export interface ITrackerService {
getTrackers(): Promise<string[]>;
}

View File

@@ -1,9 +1,10 @@
import {Cache, createCache, memoryStore} from 'cache-manager'; import {Cache, createCache, memoryStore} from 'cache-manager';
import { mongoDbStore } from '@tirke/node-cache-manager-mongodb' import {mongoDbStore} from '@tirke/node-cache-manager-mongodb'
import { configurationService } from './configuration_service'; import {configurationService} from './configuration_service';
import { logger } from './logging_service'; import {logger} from './logging_service';
import { CacheType } from "../enums/cache_types"; import {CacheType} from "../enums/cache_types";
import {CacheOptions} from "../interfaces/cache_options"; import {ICacheOptions} from "../interfaces/cache_options";
import {ICacheService} from "../interfaces/cache_service";
const GLOBAL_KEY_PREFIX = 'knightcrawler-consumer'; const GLOBAL_KEY_PREFIX = 'knightcrawler-consumer';
const IMDB_ID_PREFIX = `${GLOBAL_KEY_PREFIX}|imdb_id`; const IMDB_ID_PREFIX = `${GLOBAL_KEY_PREFIX}|imdb_id`;
@@ -17,7 +18,7 @@ const TRACKERS_TTL: number = 2 * 24 * 60 * 60; // 2 days
export type CacheMethod = () => any; export type CacheMethod = () => any;
class CacheService { class CacheService implements ICacheService {
constructor() { constructor() {
if (!configurationService.cacheConfig.NO_CACHE) { if (!configurationService.cacheConfig.NO_CACHE) {
logger.info('Cache is disabled'); logger.info('Cache is disabled');
@@ -29,16 +30,16 @@ class CacheService {
} }
public cacheWrapImdbId = (key: string, method: CacheMethod): Promise<any> => public cacheWrapImdbId = (key: string, method: CacheMethod): Promise<any> =>
this.cacheWrap(CacheType.MongoDb, `${IMDB_ID_PREFIX}:${key}`, method, { ttl: GLOBAL_TTL }); this.cacheWrap(CacheType.MongoDb, `${IMDB_ID_PREFIX}:${key}`, method, {ttl: GLOBAL_TTL});
public cacheWrapKitsuId = (key: string, method: CacheMethod): Promise<any> => public cacheWrapKitsuId = (key: string, method: CacheMethod): Promise<any> =>
this.cacheWrap(CacheType.MongoDb, `${KITSU_ID_PREFIX}:${key}`, method, { ttl: GLOBAL_TTL }); this.cacheWrap(CacheType.MongoDb, `${KITSU_ID_PREFIX}:${key}`, method, {ttl: GLOBAL_TTL});
public cacheWrapMetadata = (id: string, method: CacheMethod): Promise<any> => public cacheWrapMetadata = (id: string, method: CacheMethod): Promise<any> =>
this.cacheWrap(CacheType.Memory, `${METADATA_PREFIX}:${id}`, method, { ttl: MEMORY_TTL }); this.cacheWrap(CacheType.Memory, `${METADATA_PREFIX}:${id}`, method, {ttl: MEMORY_TTL});
public cacheTrackers = (method: CacheMethod): Promise<any> => public cacheTrackers = (method: CacheMethod): Promise<any> =>
this.cacheWrap(CacheType.Memory, `${TRACKERS_KEY_PREFIX}`, method, { ttl: TRACKERS_TTL }); this.cacheWrap(CacheType.Memory, `${TRACKERS_KEY_PREFIX}`, method, {ttl: TRACKERS_TTL});
private initiateMemoryCache = () => private initiateMemoryCache = () =>
createCache(memoryStore(), { createCache(memoryStore(), {
@@ -85,7 +86,7 @@ class CacheService {
private readonly remoteCache: Cache; private readonly remoteCache: Cache;
private cacheWrap = async ( private cacheWrap = async (
cacheType: CacheType, key: string, method: CacheMethod, options: CacheOptions): Promise<any> => { cacheType: CacheType, key: string, method: CacheMethod, options: ICacheOptions): Promise<any> => {
const cache = this.getCacheType(cacheType); const cache = this.getCacheType(cacheType);
if (configurationService.cacheConfig.NO_CACHE || !cache) { if (configurationService.cacheConfig.NO_CACHE || !cache) {

View File

@@ -1,68 +0,0 @@
class ExtensionService {
private readonly VIDEO_EXTENSIONS: string[] = [
"3g2",
"3gp",
"avi",
"flv",
"mkv",
"mk3d",
"mov",
"mp2",
"mp4",
"m4v",
"mpe",
"mpeg",
"mpg",
"mpv",
"webm",
"wmv",
"ogm",
"divx"
];
private readonly SUBTITLE_EXTENSIONS: string[] = [
"aqt",
"gsub",
"jss",
"sub",
"ttxt",
"pjs",
"psb",
"rt",
"smi",
"slt",
"ssf",
"srt",
"ssa",
"ass",
"usf",
"idx",
"vtt"
];
private readonly DISK_EXTENSIONS: string[] = [
"iso",
"m2ts",
"ts",
"vob"
]
public isVideo(filename: string): boolean {
return this.isExtension(filename, this.VIDEO_EXTENSIONS);
}
public isSubtitle(filename: string): boolean {
return this.isExtension(filename, this.SUBTITLE_EXTENSIONS);
}
public isDisk(filename: string): boolean {
return this.isExtension(filename, this.DISK_EXTENSIONS);
}
public isExtension(filename: string, extensions: string[]): boolean {
const extensionMatch = filename.match(/\.(\w{2,4})$/);
return extensionMatch !== null && extensions.includes(extensionMatch[1].toLowerCase());
}
}
export const extensionService = new ExtensionService();

View File

@@ -1,26 +1,30 @@
import {Logger, pino} from "pino"; import {Logger, pino} from "pino";
import {ILoggingService} from "../interfaces/logging_service";
class LoggingService { class LoggingService implements ILoggingService {
public readonly logger: Logger = pino({ private readonly logger: Logger;
level: process.env.LOG_LEVEL || 'info'
}); constructor() {
this.logger = pino({
level: process.env.LOG_LEVEL || 'info'
});
}
public info(message: string, ...args: any[]): void { public info(message: string, ...args: any[]): void {
this.logger.info(message); this.logger.info(message, args);
} }
public error(message: string, ...args: any[]): void { public error(message: string, ...args: any[]): void {
this.logger.error(message); this.logger.error(message, args);
} }
public debug(message: string, ...args: any[]): void { public debug(message: string, ...args: any[]): void {
this.logger.debug(message); this.logger.debug(message, args);
} }
public warn(message: string, ...args: any[]): void { public warn(message: string, ...args: any[]): void {
this.logger.warn(message); this.logger.warn(message, args);
} }
} }
export const logger = new LoggingService(); export const logger = new LoggingService();

View File

@@ -1,21 +1,22 @@
import axios, {AxiosResponse} from 'axios'; import axios, {AxiosResponse} from 'axios';
import {search, ResultTypes} from 'google-sr'; import {ResultTypes, search} from 'google-sr';
import nameToImdb from 'name-to-imdb'; import nameToImdb from 'name-to-imdb';
import { cacheService } from './cache_service'; import {cacheService} from './cache_service';
import { TorrentType } from '../enums/torrent_types'; import {TorrentType} from '../enums/torrent_types';
import {MetadataResponse} from "../interfaces/metadata_response"; import {IMetadataResponse} from "../interfaces/metadata_response";
import {CinemetaJsonResponse} from "../interfaces/cinemeta_metadata"; import {ICinemetaJsonResponse} from "../interfaces/cinemeta_metadata";
import {CommonVideoMetadata} from "../interfaces/common_video_metadata"; import {ICommonVideoMetadata} from "../interfaces/common_video_metadata";
import {KitsuJsonResponse} from "../interfaces/kitsu_metadata"; import {IKitsuJsonResponse} from "../interfaces/kitsu_metadata";
import {MetaDataQuery} from "../interfaces/metadata_query"; import {IMetaDataQuery} from "../interfaces/metadata_query";
import {KitsuCatalogJsonResponse} from "../interfaces/kitsu_catalog_metadata"; import {IKitsuCatalogJsonResponse} from "../interfaces/kitsu_catalog_metadata";
import {IMetadataService} from "../interfaces/metadata_service";
const CINEMETA_URL = 'https://v3-cinemeta.strem.io'; const CINEMETA_URL = 'https://v3-cinemeta.strem.io';
const KITSU_URL = 'https://anime-kitsu.strem.fun'; const KITSU_URL = 'https://anime-kitsu.strem.fun';
const TIMEOUT = 20000; const TIMEOUT = 20000;
class MetadataService { class MetadataService implements IMetadataService {
public async getKitsuId(info: MetaDataQuery): Promise<string | Error> { public async getKitsuId(info: IMetaDataQuery): Promise<string | Error> {
const title = this.escapeTitle(info.title.replace(/\s\|\s.*/, '')); const title = this.escapeTitle(info.title.replace(/\s\|\s.*/, ''));
const year = info.year ? ` ${info.year}` : ''; const year = info.year ? ` ${info.year}` : '';
const season = info.season > 1 ? ` S${info.season}` : ''; const season = info.season > 1 ? ` S${info.season}` : '';
@@ -23,9 +24,9 @@ class MetadataService {
const query = encodeURIComponent(key); const query = encodeURIComponent(key);
return cacheService.cacheWrapKitsuId(key, return 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: 60000})
.then((response) => { .then((response) => {
const body = response.data as KitsuCatalogJsonResponse; const body = response.data as IKitsuCatalogJsonResponse;
if (body && body.metas && body.metas.length) { if (body && body.metas && body.metas.length) {
return body.metas[0].id.replace('kitsu:', ''); return body.metas[0].id.replace('kitsu:', '');
} else { } else {
@@ -34,7 +35,7 @@ class MetadataService {
})); }));
} }
public async getImdbId(info: MetaDataQuery): Promise<string | undefined> { public async getImdbId(info: IMetaDataQuery): Promise<string | undefined> {
const name = this.escapeTitle(info.title); const name = this.escapeTitle(info.title);
const year = info.year || (info.date && info.date.slice(0, 4)); const year = info.year || (info.date && info.date.slice(0, 4));
const key = `${name}_${year || 'NA'}_${info.type}`; const key = `${name}_${year || 'NA'}_${info.type}`;
@@ -53,7 +54,7 @@ class MetadataService {
} }
} }
public getMetadata(query: MetaDataQuery): Promise<MetadataResponse | Error> { public getMetadata(query: IMetaDataQuery): Promise<IMetadataResponse | Error> {
if (!query.id) { if (!query.id) {
return Promise.reject("no valid id provided"); return Promise.reject("no valid id provided");
} }
@@ -93,14 +94,14 @@ class MetadataService {
.trim(); .trim();
} }
private async requestMetadata(url: string): Promise<MetadataResponse> { private async requestMetadata(url: string): Promise<IMetadataResponse> {
let response: AxiosResponse<any, any> = await axios.get(url, {timeout: TIMEOUT}); let response: AxiosResponse<any, any> = await axios.get(url, {timeout: TIMEOUT});
let result: MetadataResponse; let result: IMetadataResponse;
const body = response.data; const body = response.data;
if ('kitsu_id' in body.meta) { if ('kitsu_id' in body.meta) {
result = this.handleKitsuResponse(body as KitsuJsonResponse); result = this.handleKitsuResponse(body as IKitsuJsonResponse);
} else if ('imdb_id' in body.meta) { } else if ('imdb_id' in body.meta) {
result = this.handleCinemetaResponse(body as CinemetaJsonResponse); result = this.handleCinemetaResponse(body as ICinemetaJsonResponse);
} else { } else {
throw new Error('No valid metadata'); throw new Error('No valid metadata');
} }
@@ -108,7 +109,7 @@ class MetadataService {
return result; return result;
} }
private handleCinemetaResponse(body: CinemetaJsonResponse): MetadataResponse { private handleCinemetaResponse(body: ICinemetaJsonResponse): IMetadataResponse {
return { return {
imdbId: parseInt(body.meta.imdb_id), imdbId: parseInt(body.meta.imdb_id),
type: body.meta.type, type: body.meta.type,
@@ -137,7 +138,7 @@ class MetadataService {
}; };
} }
private handleKitsuResponse(body: KitsuJsonResponse): MetadataResponse { private handleKitsuResponse(body: IKitsuJsonResponse): IMetadataResponse {
return { return {
kitsuId: parseInt(body.meta.kitsu_id), kitsuId: parseInt(body.meta.kitsu_id),
type: body.meta.type, type: body.meta.type,
@@ -167,7 +168,7 @@ class MetadataService {
}; };
} }
private getEpisodeCount(videos: CommonVideoMetadata[]) { private getEpisodeCount(videos: ICommonVideoMetadata[]) {
return Object.values( return Object.values(
videos videos
.filter(entry => entry.season !== 0 && entry.episode !== 0) .filter(entry => entry.season !== 0 && entry.episode !== 0)
@@ -179,7 +180,7 @@ class MetadataService {
); );
} }
private getIMDbIdFromNameToImdb(name: string, info: MetaDataQuery): Promise<string | Error> { private getIMDbIdFromNameToImdb(name: string, info: IMetaDataQuery): Promise<string | Error> {
const year = info.year; const year = info.year;
const type = info.type; const type = info.type;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@@ -1,22 +1,23 @@
import {encode} from 'magnet-uri'; import {encode} from 'magnet-uri';
import torrentStream from 'torrent-stream'; import torrentStream from 'torrent-stream';
import {configurationService} from './configuration_service'; import {configurationService} from './configuration_service';
import {extensionService} from './extension_service'; import {ExtensionHelpers} from '../helpers/extension_helpers';
import {TorrentFileCollection} from "../interfaces/torrent_file_collection"; import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
import {ParsedTorrent} from "../interfaces/parsed_torrent"; import {IParsedTorrent} from "../interfaces/parsed_torrent";
import {FileAttributes} from "../../repository/interfaces/file_attributes"; import {IFileAttributes} from "../../repository/interfaces/file_attributes";
import {SubtitleAttributes} from "../../repository/interfaces/subtitle_attributes"; import {ISubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
import {ContentAttributes} from "../../repository/interfaces/content_attributes"; import {IContentAttributes} from "../../repository/interfaces/content_attributes";
import {parse} from "parse-torrent-title"; import {parse} from "parse-torrent-title";
import {ITorrentDownloadService} from "../interfaces/torrent_download_service";
interface TorrentFile { interface ITorrentFile {
name: string; name: string;
path: string; path: string;
length: number; length: number;
fileIndex: number; fileIndex: number;
} }
class TorrentDownloadService { class TorrentDownloadService implements ITorrentDownloadService {
private engineOptions: TorrentStream.TorrentEngineOptions = { private engineOptions: TorrentStream.TorrentEngineOptions = {
connections: configurationService.torrentConfig.MAX_CONNECTIONS_PER_TORRENT, connections: configurationService.torrentConfig.MAX_CONNECTIONS_PER_TORRENT,
uploads: 0, uploads: 0,
@@ -25,8 +26,8 @@ class TorrentDownloadService {
tracker: true, tracker: true,
}; };
public async getTorrentFiles(torrent: ParsedTorrent, timeout: number = 30000): Promise<TorrentFileCollection> { public async getTorrentFiles(torrent: IParsedTorrent, timeout: number = 30000): Promise<ITorrentFileCollection> {
const torrentFiles: TorrentFile[] = await this.filesFromTorrentStream(torrent, timeout); const torrentFiles: ITorrentFile[] = await this.filesFromTorrentStream(torrent, timeout);
const videos = this.filterVideos(torrent, torrentFiles); const videos = this.filterVideos(torrent, torrentFiles);
const subtitles = this.filterSubtitles(torrent, torrentFiles); const subtitles = this.filterSubtitles(torrent, torrentFiles);
@@ -39,7 +40,7 @@ class TorrentDownloadService {
}; };
} }
private async filesFromTorrentStream(torrent: ParsedTorrent, timeout: number): Promise<TorrentFile[]> { private async filesFromTorrentStream(torrent: IParsedTorrent, timeout: number): Promise<ITorrentFile[]> {
if (!torrent.infoHash) { if (!torrent.infoHash) {
return Promise.reject(new Error("No infoHash...")); return Promise.reject(new Error("No infoHash..."));
} }
@@ -57,7 +58,7 @@ class TorrentDownloadService {
engine = torrentStream(magnet, this.engineOptions); engine = torrentStream(magnet, this.engineOptions);
engine.on("ready", () => { engine.on("ready", () => {
const files: TorrentFile[] = engine.files.map((file, fileId) => ({ const files: ITorrentFile[] = engine.files.map((file, fileId) => ({
...file, ...file,
fileIndex: fileId, fileIndex: fileId,
size: file.length, size: file.length,
@@ -73,22 +74,22 @@ class TorrentDownloadService {
}); });
} }
private filterVideos(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): FileAttributes[] { private filterVideos(torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): IFileAttributes[] {
if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) { if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) {
return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])]; return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])];
} }
const videos = torrentFiles.filter(file => extensionService.isVideo(file.path || '')); const videos = torrentFiles.filter(file => ExtensionHelpers.isVideo(file.path || ''));
const maxSize = Math.max(...videos.map((video: TorrentFile) => video.length)); const maxSize = Math.max(...videos.map((video: ITorrentFile) => video.length));
const minSampleRatio = videos.length <= 3 ? 3 : 10; const minSampleRatio = videos.length <= 3 ? 3 : 10;
const minAnimeExtraRatio = 5; const minAnimeExtraRatio = 5;
const minRedundantRatio = videos.length <= 3 ? 30 : Number.MAX_VALUE; const minRedundantRatio = videos.length <= 3 ? 30 : Number.MAX_VALUE;
const isSample = (video: TorrentFile) => video.path?.match(/sample|bonus|promo/i) && maxSize / parseInt(video.path.toString()) > minSampleRatio; const isSample = (video: ITorrentFile) => video.path?.match(/sample|bonus|promo/i) && maxSize / parseInt(video.path.toString()) > minSampleRatio;
const isRedundant = (video: TorrentFile) => maxSize / parseInt(video.path.toString()) > minRedundantRatio; const isRedundant = (video: ITorrentFile) => maxSize / parseInt(video.path.toString()) > minRedundantRatio;
const isExtra = (video: TorrentFile) => video.path?.match(/extras?\//i); const isExtra = (video: ITorrentFile) => video.path?.match(/extras?\//i);
const isAnimeExtra = (video: TorrentFile) => video.path?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i) const isAnimeExtra = (video: ITorrentFile) => video.path?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i)
&& maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio; && maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio;
const isWatermark = (video: TorrentFile) => video.path?.match(/^[A-Z-]+(?:\.[A-Z]+)?\.\w{3,4}$/) const isWatermark = (video: ITorrentFile) => video.path?.match(/^[A-Z-]+(?:\.[A-Z]+)?\.\w{3,4}$/)
&& maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio && maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio
return videos return videos
@@ -100,17 +101,17 @@ class TorrentDownloadService {
.map(video => this.mapTorrentFileToFileAttributes(torrent, video)); .map(video => this.mapTorrentFileToFileAttributes(torrent, video));
} }
private filterSubtitles(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): SubtitleAttributes[] { private filterSubtitles(torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): ISubtitleAttributes[] {
return torrentFiles.filter(file => extensionService.isSubtitle(file.name || '')) return torrentFiles.filter(file => ExtensionHelpers.isSubtitle(file.name || ''))
.map(file => this.mapTorrentFileToSubtitleAttributes(torrent, file)); .map(file => this.mapTorrentFileToSubtitleAttributes(torrent, file));
} }
private createContent(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): ContentAttributes[] { private createContent(torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): IContentAttributes[] {
return torrentFiles.map(file => this.mapTorrentFileToContentAttributes(torrent, file)); return torrentFiles.map(file => this.mapTorrentFileToContentAttributes(torrent, file));
} }
private mapTorrentFileToFileAttributes(torrent: ParsedTorrent, file: TorrentFile): FileAttributes { private mapTorrentFileToFileAttributes(torrent: IParsedTorrent, file: ITorrentFile): IFileAttributes {
const videoFile: FileAttributes = { const videoFile: IFileAttributes = {
title: file.name, title: file.name,
size: file.length, size: file.length,
fileIndex: file.fileIndex || 0, fileIndex: file.fileIndex || 0,
@@ -125,7 +126,7 @@ class TorrentDownloadService {
return {...videoFile, ...parse(file.name)}; return {...videoFile, ...parse(file.name)};
} }
private mapTorrentFileToSubtitleAttributes(torrent: ParsedTorrent, file: TorrentFile): SubtitleAttributes { private mapTorrentFileToSubtitleAttributes(torrent: IParsedTorrent, file: ITorrentFile): ISubtitleAttributes {
return { return {
title: file.name, title: file.name,
infoHash: torrent.infoHash, infoHash: torrent.infoHash,
@@ -135,7 +136,7 @@ class TorrentDownloadService {
}; };
} }
private mapTorrentFileToContentAttributes(torrent: ParsedTorrent, file: TorrentFile): ContentAttributes { private mapTorrentFileToContentAttributes(torrent: IParsedTorrent, file: ITorrentFile): IContentAttributes {
return { return {
infoHash: torrent.infoHash, infoHash: torrent.infoHash,
fileIndex: file.fileIndex, fileIndex: file.fileIndex,

View File

@@ -1,20 +1,21 @@
import {parse} from 'parse-torrent-title'; import {parse} from 'parse-torrent-title';
import {ParsedTorrent} from "../interfaces/parsed_torrent"; import {IParsedTorrent} from "../interfaces/parsed_torrent";
import {repository} from '../../repository/database_repository'; import {repository} from '../../repository/database_repository';
import {TorrentType} from '../enums/torrent_types'; import {TorrentType} from '../enums/torrent_types';
import {TorrentFileCollection} from "../interfaces/torrent_file_collection"; import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
import {Torrent} from "../../repository/models/torrent"; import {Torrent} from "../../repository/models/torrent";
import {PromiseHelpers} from '../helpers/promises_helpers'; import {PromiseHelpers} from '../helpers/promises_helpers';
import {logger} from './logging_service'; import {logger} from './logging_service';
import {metadataService} from './metadata_service'; import {metadataService} from './metadata_service';
import {torrentFileService} from './torrent_file_service'; import {torrentFileService} from './torrent_file_service';
import {torrentSubtitleService} from './torrent_subtitle_service'; import {torrentSubtitleService} from './torrent_subtitle_service';
import {TorrentAttributes} from "../../repository/interfaces/torrent_attributes"; import {ITorrentAttributes} from "../../repository/interfaces/torrent_attributes";
import {File} from "../../repository/models/file"; import {File} from "../../repository/models/file";
import {Subtitle} from "../../repository/models/subtitle"; import {Subtitle} from "../../repository/models/subtitle";
import {ITorrentEntriesService} from "../interfaces/torrent_entries_service";
class TorrentEntriesService { class TorrentEntriesService implements ITorrentEntriesService {
public async createTorrentEntry(torrent: ParsedTorrent, overwrite = false): Promise<void> { public async createTorrentEntry(torrent: IParsedTorrent, overwrite = false): Promise<void> {
const titleInfo = parse(torrent.title); const titleInfo = parse(torrent.title);
if (!torrent.imdbId && torrent.type !== TorrentType.Anime) { if (!torrent.imdbId && torrent.type !== TorrentType.Anime) {
@@ -49,9 +50,9 @@ class TorrentEntriesService {
return; return;
} }
const fileCollection: TorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent) const fileCollection: ITorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
.then((torrentContents: TorrentFileCollection) => overwrite ? this.overwriteExistingFiles(torrent, torrentContents) : torrentContents) .then((torrentContents: ITorrentFileCollection) => overwrite ? this.overwriteExistingFiles(torrent, torrentContents) : torrentContents)
.then((torrentContents: TorrentFileCollection) => torrentSubtitleService.assignSubtitles(torrentContents)) .then((torrentContents: ITorrentFileCollection) => torrentSubtitleService.assignSubtitles(torrentContents))
.catch(error => { .catch(error => {
logger.warn(`Failed getting files for ${torrent.title}`, error.message); logger.warn(`Failed getting files for ${torrent.title}`, error.message);
return {}; return {};
@@ -86,8 +87,8 @@ class TorrentEntriesService {
.catch(() => undefined); .catch(() => undefined);
} }
public async checkAndUpdateTorrent(torrent: ParsedTorrent): Promise<boolean> { public async checkAndUpdateTorrent(torrent: IParsedTorrent): Promise<boolean> {
const query: TorrentAttributes = { const query: ITorrentAttributes = {
infoHash: torrent.infoHash, infoHash: torrent.infoHash,
provider: torrent.provider, provider: torrent.provider,
} }
@@ -128,7 +129,7 @@ class TorrentEntriesService {
const imdbId: string | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.imdbId)); const imdbId: string | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.imdbId));
const kitsuId: number | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.kitsuId)); const kitsuId: number | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.kitsuId));
const fileCollection: TorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent) const fileCollection: ITorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
.then(torrentContents => notOpenedVideo ? torrentContents : {...torrentContents, videos: storedVideos}) .then(torrentContents => notOpenedVideo ? torrentContents : {...torrentContents, videos: storedVideos})
.then(torrentContents => torrentSubtitleService.assignSubtitles(torrentContents)) .then(torrentContents => torrentSubtitleService.assignSubtitles(torrentContents))
.then(torrentContents => this.assignMetaIds(torrentContents, imdbId, kitsuId)) .then(torrentContents => this.assignMetaIds(torrentContents, imdbId, kitsuId))
@@ -176,7 +177,7 @@ class TorrentEntriesService {
.catch(error => logger.error(`Failed saving contents for [${torrent.infoHash}] ${torrent.title}`, error)); .catch(error => logger.error(`Failed saving contents for [${torrent.infoHash}] ${torrent.title}`, error));
} }
public async updateTorrentSeeders(torrent: TorrentAttributes) { public async updateTorrentSeeders(torrent: ITorrentAttributes) {
if (!(torrent.infoHash || (torrent.provider && torrent.torrentId)) || !Number.isInteger(torrent.seeders)) { if (!(torrent.infoHash || (torrent.provider && torrent.torrentId)) || !Number.isInteger(torrent.seeders)) {
return torrent; return torrent;
} }
@@ -188,7 +189,7 @@ class TorrentEntriesService {
}); });
} }
private assignMetaIds(fileCollection: TorrentFileCollection, imdbId: string, kitsuId: number): TorrentFileCollection { private assignMetaIds(fileCollection: ITorrentFileCollection, imdbId: string, kitsuId: number): ITorrentFileCollection {
if (fileCollection.videos && fileCollection.videos.length) { if (fileCollection.videos && fileCollection.videos.length) {
fileCollection.videos.forEach(video => { fileCollection.videos.forEach(video => {
video.imdbId = imdbId; video.imdbId = imdbId;
@@ -199,7 +200,7 @@ class TorrentEntriesService {
return fileCollection; return fileCollection;
} }
private async overwriteExistingFiles(torrent: ParsedTorrent, torrentContents: TorrentFileCollection) { private async overwriteExistingFiles(torrent: IParsedTorrent, torrentContents: ITorrentFileCollection) {
const videos = torrentContents && torrentContents.videos; const videos = torrentContents && torrentContents.videos;
if (videos && videos.length) { if (videos && videos.length) {
const existingFiles = await repository.getFiles(torrent.infoHash) const existingFiles = await repository.getFiles(torrent.infoHash)

View File

@@ -4,30 +4,31 @@ import {parse} from 'parse-torrent-title';
import {PromiseHelpers} from '../helpers/promises_helpers'; import {PromiseHelpers} from '../helpers/promises_helpers';
import {TorrentType} from '../enums/torrent_types'; import {TorrentType} from '../enums/torrent_types';
import {configurationService} from './configuration_service'; import {configurationService} from './configuration_service';
import {extensionService} from './extension_service'; import {ExtensionHelpers} from '../helpers/extension_helpers';
import {metadataService} from './metadata_service'; import {metadataService} from './metadata_service';
import {torrentDownloadService} from "./torrent_download_service"; import {torrentDownloadService} from "./torrent_download_service";
import {logger} from "./logging_service"; import {logger} from "./logging_service";
import {MetadataResponse} from "../interfaces/metadata_response"; import {IMetadataResponse} from "../interfaces/metadata_response";
import {MetaDataQuery} from "../interfaces/metadata_query"; import {IMetaDataQuery} from "../interfaces/metadata_query";
import {CommonVideoMetadata} from "../interfaces/common_video_metadata"; import {ICommonVideoMetadata} from "../interfaces/common_video_metadata";
import {TorrentFileCollection} from "../interfaces/torrent_file_collection"; import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
import {ParsedTorrent} from "../interfaces/parsed_torrent"; import {IParsedTorrent} from "../interfaces/parsed_torrent";
import {FileAttributes} from "../../repository/interfaces/file_attributes"; import {IFileAttributes} from "../../repository/interfaces/file_attributes";
import {ContentAttributes} from "../../repository/interfaces/content_attributes"; import {IContentAttributes} from "../../repository/interfaces/content_attributes";
import {ITorrentFileService} from "../interfaces/torrent_file_service";
const MIN_SIZE: number = 5 * 1024 * 1024; // 5 MB const MIN_SIZE: number = 5 * 1024 * 1024; // 5 MB
const MULTIPLE_FILES_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB const MULTIPLE_FILES_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB
class TorrentFileService { class TorrentFileService implements ITorrentFileService {
private readonly imdb_limiter: Bottleneck = new Bottleneck({ private readonly imdb_limiter: Bottleneck = new Bottleneck({
maxConcurrent: configurationService.metadataConfig.IMDB_CONCURRENT, maxConcurrent: configurationService.metadataConfig.IMDB_CONCURRENT,
minTime: configurationService.metadataConfig.IMDB_INTERVAL_MS minTime: configurationService.metadataConfig.IMDB_INTERVAL_MS
}); });
public async parseTorrentFiles(torrent: ParsedTorrent): Promise<TorrentFileCollection> { public async parseTorrentFiles(torrent: IParsedTorrent): Promise<ITorrentFileCollection> {
const parsedTorrentName = parse(torrent.title); const parsedTorrentName = parse(torrent.title);
const query: MetaDataQuery = { const query: IMetaDataQuery = {
id: torrent.kitsuId || torrent.imdbId, id: torrent.kitsuId || torrent.imdbId,
type: torrent.type || TorrentType.Movie, type: torrent.type || TorrentType.Movie,
}; };
@@ -48,7 +49,7 @@ class TorrentFileService {
return this.parseSeriesFiles(torrent, metadata) return this.parseSeriesFiles(torrent, metadata)
} }
public isPackTorrent(torrent: ParsedTorrent): boolean { public isPackTorrent(torrent: IParsedTorrent): boolean {
if (torrent.isPack) { if (torrent.isPack) {
return true; return true;
} }
@@ -65,7 +66,7 @@ class TorrentFileService {
return hasMultipleEpisodes && !hasSingleEpisode; return hasMultipleEpisodes && !hasSingleEpisode;
} }
private parseSeriesVideos(torrent: ParsedTorrent, videos: FileAttributes[]): FileAttributes[] { private parseSeriesVideos(torrent: IParsedTorrent, videos: IFileAttributes[]): IFileAttributes[] {
const parsedTorrentName = parse(torrent.title); const parsedTorrentName = parse(torrent.title);
const hasMovies = parsedTorrentName.complete || !!torrent.title.match(/movies?(?:\W|$)/i); const hasMovies = parsedTorrentName.complete || !!torrent.title.match(/movies?(?:\W|$)/i);
const parsedVideos = videos.map(video => this.parseSeriesVideo(video)); const parsedVideos = videos.map(video => this.parseSeriesVideo(video));
@@ -73,8 +74,8 @@ class TorrentFileService {
return parsedVideos.map(video => ({ ...video, isMovie: this.isMovieVideo(torrent, video, parsedVideos, hasMovies) })); return parsedVideos.map(video => ({ ...video, isMovie: this.isMovieVideo(torrent, video, parsedVideos, hasMovies) }));
} }
private async parseMovieFiles(torrent: ParsedTorrent, metadata: MetadataResponse): Promise<TorrentFileCollection> { private async parseMovieFiles(torrent: IParsedTorrent, metadata: IMetadataResponse): Promise<ITorrentFileCollection> {
const fileCollection: TorrentFileCollection = await this.getMoviesTorrentContent(torrent); const fileCollection: ITorrentFileCollection = await this.getMoviesTorrentContent(torrent);
const filteredVideos = fileCollection.videos const filteredVideos = fileCollection.videos
.filter(video => video.size > MIN_SIZE) .filter(video => video.size > MIN_SIZE)
.filter(video => !this.isFeaturette(video)); .filter(video => !this.isFeaturette(video));
@@ -103,9 +104,9 @@ class TorrentFileService {
return {...fileCollection, videos: parsedVideos}; return {...fileCollection, videos: parsedVideos};
} }
private async parseSeriesFiles(torrent: ParsedTorrent, metadata: MetadataResponse): Promise<TorrentFileCollection> { private async parseSeriesFiles(torrent: IParsedTorrent, metadata: IMetadataResponse): Promise<ITorrentFileCollection> {
const fileCollection: TorrentFileCollection = await this.getSeriesTorrentContent(torrent); const fileCollection: ITorrentFileCollection = await this.getSeriesTorrentContent(torrent);
const parsedVideos: FileAttributes[] = await Promise.resolve(fileCollection.videos) const parsedVideos: IFileAttributes[] = await Promise.resolve(fileCollection.videos)
.then(videos => videos.filter(video => videos.length === 1 || video.size > MIN_SIZE)) .then(videos => videos.filter(video => videos.length === 1 || video.size > MIN_SIZE))
.then(videos => this.parseSeriesVideos(torrent, videos)) .then(videos => this.parseSeriesVideos(torrent, videos))
.then(videos => this.decomposeEpisodes(torrent, videos, metadata)) .then(videos => this.decomposeEpisodes(torrent, videos, metadata))
@@ -119,7 +120,7 @@ class TorrentFileService {
return {...torrent.fileCollection, videos: parsedVideos}; return {...torrent.fileCollection, videos: parsedVideos};
} }
private async getMoviesTorrentContent(torrent: ParsedTorrent): Promise<TorrentFileCollection> { private async getMoviesTorrentContent(torrent: IParsedTorrent): Promise<ITorrentFileCollection> {
const files = await torrentDownloadService.getTorrentFiles(torrent) const files = await torrentDownloadService.getTorrentFiles(torrent)
.catch(error => { .catch(error => {
if (!this.isPackTorrent(torrent)) { if (!this.isPackTorrent(torrent)) {
@@ -135,11 +136,11 @@ class TorrentFileService {
return files; return files;
} }
private getDefaultFileEntries(torrent: ParsedTorrent): FileAttributes[] { private getDefaultFileEntries(torrent: IParsedTorrent): IFileAttributes[] {
return [{title: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}]; return [{title: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}];
} }
private async getSeriesTorrentContent(torrent: ParsedTorrent): Promise<TorrentFileCollection> { private async getSeriesTorrentContent(torrent: IParsedTorrent): Promise<ITorrentFileCollection> {
return torrentDownloadService.getTorrentFiles(torrent) return torrentDownloadService.getTorrentFiles(torrent)
.catch(error => { .catch(error => {
if (!this.isPackTorrent(torrent)) { if (!this.isPackTorrent(torrent)) {
@@ -149,7 +150,7 @@ class TorrentFileService {
}); });
} }
private async mapSeriesEpisode(torrent: ParsedTorrent, file: FileAttributes, files: FileAttributes[]) : Promise<FileAttributes[]> { private async mapSeriesEpisode(torrent: IParsedTorrent, file: IFileAttributes, files: IFileAttributes[]) : Promise<IFileAttributes[]> {
if (!file.episodes && !file.episodes) { if (!file.episodes && !file.episodes) {
if (files.length === 1 || files.some(f => f.episodes || f.episodes) || parse(torrent.title).seasons) { if (files.length === 1 || files.some(f => f.episodes || f.episodes) || parse(torrent.title).seasons) {
return Promise.resolve([{ return Promise.resolve([{
@@ -179,7 +180,7 @@ class TorrentFileService {
}))) })))
} }
private async mapSeriesMovie(torrent: ParsedTorrent, file: FileAttributes): Promise<FileAttributes[]> { private async mapSeriesMovie(torrent: IParsedTorrent, file: IFileAttributes): Promise<IFileAttributes[]> {
const kitsuId= torrent.type === TorrentType.Anime ? await this.findMovieKitsuId(file) const kitsuId= torrent.type === TorrentType.Anime ? await this.findMovieKitsuId(file)
.then(result => { .then(result => {
if (result instanceof Error) { if (result instanceof Error) {
@@ -191,7 +192,7 @@ class TorrentFileService {
const imdbId = !kitsuId ? await this.findMovieImdbId(file) : undefined; const imdbId = !kitsuId ? await this.findMovieImdbId(file) : undefined;
const query: MetaDataQuery = { const query: IMetaDataQuery = {
id: kitsuId || imdbId, id: kitsuId || imdbId,
type: TorrentType.Movie type: TorrentType.Movie
}; };
@@ -230,7 +231,7 @@ class TorrentFileService {
}]; }];
} }
private async decomposeEpisodes(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse = { episodeCount: [] }) { private async decomposeEpisodes(torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse = { episodeCount: [] }) {
if (files.every(file => !file.episodes && !file.date)) { if (files.every(file => !file.episodes && !file.date)) {
return files; return files;
} }
@@ -275,7 +276,7 @@ class TorrentFileService {
return files; return files;
} }
private preprocessEpisodes(files: FileAttributes[]) { private preprocessEpisodes(files: IFileAttributes[]) {
// reverse special episode naming when they named with 0 episode, ie. S02E00 // reverse special episode naming when they named with 0 episode, ie. S02E00
files files
.filter(file => Number.isInteger(file.season) && file.episode === 0) .filter(file => Number.isInteger(file.season) && file.episode === 0)
@@ -286,7 +287,7 @@ class TorrentFileService {
}) })
} }
private isConcatSeasonAndEpisodeFiles(files: FileAttributes[], sortedEpisodes: number[], metadata: MetadataResponse) { private isConcatSeasonAndEpisodeFiles(files: IFileAttributes[], sortedEpisodes: number[], metadata: IMetadataResponse) {
if (metadata.kitsuId !== undefined) { if (metadata.kitsuId !== undefined) {
// anime does not use this naming scheme in 99% of cases; // anime does not use this naming scheme in 99% of cases;
return false; return false;
@@ -313,11 +314,11 @@ class TorrentFileService {
|| concatAboveTotalEpisodeCount.length >= thresholdAbove; || concatAboveTotalEpisodeCount.length >= thresholdAbove;
} }
private isDateEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) { private isDateEpisodeFiles(files: IFileAttributes[], metadata: IMetadataResponse) {
return files.every(file => (!file.season || !metadata.episodeCount[file.season - 1]) && file.date); return files.every(file => (!file.season || !metadata.episodeCount[file.season - 1]) && file.date);
} }
private isAbsoluteEpisodeFiles(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse) { private isAbsoluteEpisodeFiles(torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse) {
const threshold = Math.ceil(files.length / 5); const threshold = Math.ceil(files.length / 5);
const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId; const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId;
const nonMovieEpisodes = files const nonMovieEpisodes = files
@@ -330,7 +331,7 @@ class TorrentFileService {
|| absoluteEpisodes.length >= threshold; || absoluteEpisodes.length >= threshold;
} }
private isNewEpisodeNotInMetadata(torrent: ParsedTorrent, video: FileAttributes, metadata: MetadataResponse) { private isNewEpisodeNotInMetadata(torrent: IParsedTorrent, video: IFileAttributes, metadata: IMetadataResponse) {
// new episode might not yet been indexed by cinemeta. // new episode might not yet been indexed by cinemeta.
// detect this if episode number is larger than the last episode or season is larger than the last one // detect this if episode number is larger than the last episode or season is larger than the last one
// only for non anime metas // only for non anime metas
@@ -341,7 +342,7 @@ class TorrentFileService {
&& video.episodes.every(ep => ep > (metadata.episodeCount[video.season - 1] || 0)); && video.episodes.every(ep => ep > (metadata.episodeCount[video.season - 1] || 0));
} }
private decomposeConcatSeasonAndEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) { private decomposeConcatSeasonAndEpisodeFiles(files: IFileAttributes[], metadata: IMetadataResponse) {
files files
.filter(file => file.episodes && file.season !== 0 && file.episodes.every(ep => ep > 100)) .filter(file => file.episodes && file.season !== 0 && file.episodes.every(ep => ep > 100))
.filter(file => metadata.episodeCount[(file.season || this.div100(file.episodes[0])) - 1] < 100) .filter(file => metadata.episodeCount[(file.season || this.div100(file.episodes[0])) - 1] < 100)
@@ -353,7 +354,7 @@ class TorrentFileService {
} }
private decomposeAbsoluteEpisodeFiles(torrent: ParsedTorrent, videos: FileAttributes[], metadata: MetadataResponse) { private decomposeAbsoluteEpisodeFiles(torrent: IParsedTorrent, videos: IFileAttributes[], metadata: IMetadataResponse) {
if (metadata.episodeCount.length === 0) { if (metadata.episodeCount.length === 0) {
videos videos
.filter(file => !Number.isInteger(file.season) && file.episodes && !file.isMovie) .filter(file => !Number.isInteger(file.season) && file.episodes && !file.isMovie)
@@ -377,7 +378,7 @@ class TorrentFileService {
}); });
} }
private decomposeDateEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) { private decomposeDateEpisodeFiles(files: IFileAttributes[], metadata: IMetadataResponse) {
if (!metadata || !metadata.videos || !metadata.videos.length) { if (!metadata || !metadata.videos || !metadata.videos.length) {
return; return;
} }
@@ -411,7 +412,7 @@ class TorrentFileService {
} }
} }
private assignKitsuOrImdbEpisodes(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse) { private assignKitsuOrImdbEpisodes(torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse) {
if (!metadata || !metadata.videos || !metadata.videos.length) { if (!metadata || !metadata.videos || !metadata.videos.length) {
if (torrent.type === TorrentType.Anime) { if (torrent.type === TorrentType.Anime) {
// assign episodes as kitsu episodes for anime when no metadata available for imdb mapping // assign episodes as kitsu episodes for anime when no metadata available for imdb mapping
@@ -429,7 +430,7 @@ class TorrentFileService {
return files; return files;
} }
const seriesMapping: CommonVideoMetadata = metadata.videos const seriesMapping: ICommonVideoMetadata = metadata.videos
.reduce((map, video) => { .reduce((map, video) => {
const episodeMap = map[video.season] || {}; const episodeMap = map[video.season] || {};
episodeMap[video.episode] = video; episodeMap[video.episode] = video;
@@ -464,13 +465,13 @@ class TorrentFileService {
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode); file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode);
} else if (seriesMapping[file.season - 1]) { } else if (seriesMapping[file.season - 1]) {
// sometimes a second season might be a continuation of the previous season // sometimes a second season might be a continuation of the previous season
const seasonMapping = seriesMapping[file.season - 1] as CommonVideoMetadata; const seasonMapping = seriesMapping[file.season - 1] as ICommonVideoMetadata;
const episodes = Object.values(seasonMapping); const episodes = Object.values(seasonMapping);
const firstKitsuId = episodes.length && episodes[0]; const firstKitsuId = episodes.length && episodes[0];
const differentTitlesCount = new Set(episodes.map(ep => ep.id)).size const differentTitlesCount = new Set(episodes.map(ep => ep.id)).size
const skippedCount = episodes.filter(ep => ep.id === firstKitsuId).length; const skippedCount = episodes.filter(ep => ep.id === firstKitsuId).length;
const seasonEpisodes = files const seasonEpisodes = files
.filter((otherFile: FileAttributes) => otherFile.season === file.season) .filter((otherFile: IFileAttributes) => otherFile.season === file.season)
.reduce((a, b) => a.concat(b.episodes), []); .reduce((a, b) => a.concat(b.episodes), []);
const isAbsoluteOrder = seasonEpisodes.every(ep => ep > skippedCount && ep <= episodes.length) const isAbsoluteOrder = seasonEpisodes.every(ep => ep > skippedCount && ep <= episodes.length)
const isNormalOrder = seasonEpisodes.every(ep => ep + skippedCount <= episodes.length) const isNormalOrder = seasonEpisodes.every(ep => ep + skippedCount <= episodes.length)
@@ -494,7 +495,7 @@ class TorrentFileService {
return files; return files;
} }
private needsCinemetaMetadataForAnime(files: FileAttributes[], metadata: MetadataResponse) { private needsCinemetaMetadataForAnime(files: IFileAttributes[], metadata: IMetadataResponse) {
if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) { if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) {
return false; return false;
} }
@@ -510,8 +511,8 @@ class TorrentFileService {
.some(file => file.season < minSeason || file.season > maxSeason || file.episodes.every(ep => ep > total)); .some(file => file.season < minSeason || file.season > maxSeason || file.episodes.every(ep => ep > total));
} }
private async updateToCinemetaMetadata(metadata: MetadataResponse) { private async updateToCinemetaMetadata(metadata: IMetadataResponse) {
const query: MetaDataQuery = { const query: IMetaDataQuery = {
id: metadata.imdbId, id: metadata.imdbId,
type: metadata.type type: metadata.type
}; };
@@ -536,7 +537,7 @@ class TorrentFileService {
}) })
} }
private findMovieImdbId(title: FileAttributes | string) { private findMovieImdbId(title: IFileAttributes | string) {
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 this.imdb_limiter.schedule(async () => { return this.imdb_limiter.schedule(async () => {
@@ -553,7 +554,7 @@ class TorrentFileService {
}); });
} }
private async findMovieKitsuId(title: FileAttributes | string) { private async findMovieKitsuId(title: IFileAttributes | string) {
const parsedTitle = typeof title === 'string' ? parse(title) : title; const parsedTitle = typeof title === 'string' ? parse(title) : title;
const kitsuQuery = { const kitsuQuery = {
title: parsedTitle.title, title: parsedTitle.title,
@@ -568,22 +569,22 @@ class TorrentFileService {
} }
} }
private isDiskTorrent(contents: ContentAttributes[]) { private isDiskTorrent(contents: IContentAttributes[]) {
return contents.some(content => extensionService.isDisk(content.path)); return contents.some(content => ExtensionHelpers.isDisk(content.path));
} }
private isSingleMovie(videos: FileAttributes[]) { private isSingleMovie(videos: IFileAttributes[]) {
return videos.length === 1 || return videos.length === 1 ||
(videos.length === 2 && (videos.length === 2 &&
videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?1\b|^0?1\.\w{2,4}$/i.test(v.path)) && videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?1\b|^0?1\.\w{2,4}$/i.test(v.path)) &&
videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?2\b|^0?2\.\w{2,4}$/i.test(v.path))); videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?2\b|^0?2\.\w{2,4}$/i.test(v.path)));
} }
private isFeaturette(video: FileAttributes) { private isFeaturette(video: IFileAttributes) {
return /featurettes?\/|extras-grym/i.test(video.path); return /featurettes?\/|extras-grym/i.test(video.path);
} }
private parseSeriesVideo(video: FileAttributes): FileAttributes { private parseSeriesVideo(video: IFileAttributes): IFileAttributes {
const videoInfo = parse(video.title); const videoInfo = parse(video.title);
// the episode may be in a folder containing season number // the episode may be in a folder containing season number
if (!Number.isInteger(videoInfo.season) && video.path.includes('/')) { if (!Number.isInteger(videoInfo.season) && video.path.includes('/')) {
@@ -629,7 +630,7 @@ class TorrentFileService {
return { ...video, ...videoInfo }; return { ...video, ...videoInfo };
} }
private isMovieVideo(torrent: ParsedTorrent, video: FileAttributes, otherVideos: FileAttributes[], hasMovies: boolean): boolean { private isMovieVideo(torrent: IParsedTorrent, video: IFileAttributes, otherVideos: IFileAttributes[], hasMovies: boolean): boolean {
if (Number.isInteger(torrent.season) && Array.isArray(torrent.episodes)) { if (Number.isInteger(torrent.season) && Array.isArray(torrent.episodes)) {
// not movie if video has season // not movie if video has season
return false; return false;
@@ -653,7 +654,7 @@ class TorrentFileService {
&& otherVideos.filter(other => other.title === video.title && other.year === video.year).length < 3; && otherVideos.filter(other => other.title === video.title && other.year === video.year).length < 3;
} }
private clearInfoFields(video: FileAttributes) { private clearInfoFields(video: IFileAttributes) {
video.imdbId = undefined; video.imdbId = undefined;
video.imdbSeason = undefined; video.imdbSeason = undefined;
video.imdbEpisode = undefined; video.imdbEpisode = undefined;

View File

@@ -2,14 +2,15 @@ import {TorrentType} from "../enums/torrent_types";
import {logger} from "./logging_service"; import {logger} from "./logging_service";
import {trackerService} from "./tracker_service"; import {trackerService} from "./tracker_service";
import {torrentEntriesService} from "./torrent_entries_service"; import {torrentEntriesService} from "./torrent_entries_service";
import {IngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes"; import {IIngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
import {ParsedTorrent} from "../interfaces/parsed_torrent"; import {IParsedTorrent} from "../interfaces/parsed_torrent";
import {ITorrentProcessingService} from "../interfaces/torrent_processing_service";
class TorrentProcessingService { class TorrentProcessingService implements ITorrentProcessingService {
public async processTorrentRecord(torrent: IngestedTorrentAttributes): Promise<void> { public async processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise<void> {
const { category } = torrent; const {category} = torrent;
const type = category === 'tv' ? TorrentType.Series : TorrentType.Movie; const type = category === 'tv' ? TorrentType.Series : TorrentType.Movie;
const torrentInfo: ParsedTorrent = await this.parseTorrent(torrent, type); const torrentInfo: IParsedTorrent = await this.parseTorrent(torrent, type);
logger.info(`Processing torrent ${torrentInfo.title} with infoHash ${torrentInfo.infoHash}`); logger.info(`Processing torrent ${torrentInfo.title} with infoHash ${torrentInfo.infoHash}`);
@@ -25,7 +26,7 @@ class TorrentProcessingService {
return trackers.join(','); return trackers.join(',');
} }
private async parseTorrent(torrent: IngestedTorrentAttributes, category: string): Promise<ParsedTorrent> { private async parseTorrent(torrent: IIngestedTorrentAttributes, category: string): Promise<IParsedTorrent> {
const infoHash = torrent.info_hash?.trim().toLowerCase() const infoHash = torrent.info_hash?.trim().toLowerCase()
return { return {
title: torrent.name, title: torrent.name,
@@ -41,7 +42,7 @@ class TorrentProcessingService {
} }
} }
private parseImdbId(torrent: IngestedTorrentAttributes): string | undefined { private parseImdbId(torrent: IIngestedTorrentAttributes): string | undefined {
if (torrent.imdb === undefined || torrent.imdb === null) { if (torrent.imdb === undefined || torrent.imdb === null) {
return undefined; return undefined;
} }

View File

@@ -1,28 +1,32 @@
import { parse } from 'parse-torrent-title'; import {parse} from 'parse-torrent-title';
import {TorrentFileCollection} from "../interfaces/torrent_file_collection"; import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
import {FileAttributes} from "../../repository/interfaces/file_attributes"; import {IFileAttributes} from "../../repository/interfaces/file_attributes";
import {ITorrentSubtitleService} from "../interfaces/torrent_subtitle_service";
class TorrentSubtitleService { class TorrentSubtitleService implements ITorrentSubtitleService {
public assignSubtitles(fileCollection: TorrentFileCollection) : TorrentFileCollection { public assignSubtitles(fileCollection: ITorrentFileCollection): ITorrentFileCollection {
if (fileCollection.videos && fileCollection.videos.length && fileCollection.subtitles && fileCollection.subtitles.length) { if (fileCollection.videos && fileCollection.videos.length && fileCollection.subtitles && fileCollection.subtitles.length) {
if (fileCollection.videos.length === 1) { if (fileCollection.videos.length === 1) {
fileCollection.videos[0].subtitles = fileCollection.subtitles; fileCollection.videos[0].subtitles = fileCollection.subtitles;
return { ...fileCollection, subtitles: [] }; return {...fileCollection, subtitles: []};
} }
const parsedVideos = fileCollection.videos.map(video => this.parseVideo(video)); const parsedVideos = fileCollection.videos.map(video => this.parseVideo(video));
const assignedSubs = fileCollection.subtitles.map(subtitle => ({ subtitle, videos: this.mostProbableSubtitleVideos(subtitle, parsedVideos) })); const assignedSubs = fileCollection.subtitles.map(subtitle => ({
subtitle,
videos: this.mostProbableSubtitleVideos(subtitle, parsedVideos)
}));
const unassignedSubs = assignedSubs.filter(assignedSub => !assignedSub.videos).map(assignedSub => assignedSub.subtitle); const unassignedSubs = assignedSubs.filter(assignedSub => !assignedSub.videos).map(assignedSub => assignedSub.subtitle);
assignedSubs assignedSubs
.filter(assignedSub => assignedSub.videos) .filter(assignedSub => assignedSub.videos)
.forEach(assignedSub => assignedSub.videos.forEach(video => video.subtitles = (video.subtitles || []).concat(assignedSub.subtitle))); .forEach(assignedSub => assignedSub.videos.forEach(video => video.subtitles = (video.subtitles || []).concat(assignedSub.subtitle)));
return { ...fileCollection, subtitles: unassignedSubs }; return {...fileCollection, subtitles: unassignedSubs};
} }
return fileCollection; return fileCollection;
} }
private parseVideo(video: FileAttributes) { private parseVideo(video: IFileAttributes) {
const fileName = video.title.split('/').pop().replace(/\.(\w{2,4})$/, ''); const fileName = video.title.split('/').pop().replace(/\.(\w{2,4})$/, '');
const folderName = video.title.replace(/\/?[^/]+$/, ''); const folderName = video.title.replace(/\/?[^/]+$/, '');
return { return {

View File

@@ -1,10 +1,11 @@
import axios, { AxiosResponse } from 'axios'; import axios, {AxiosResponse} from 'axios';
import { cacheService } from "./cache_service"; import {cacheService} from "./cache_service";
import { configurationService } from './configuration_service'; import {configurationService} from './configuration_service';
import { logger } from "./logging_service"; import {logger} from "./logging_service";
import {ITrackerService} from "../interfaces/tracker_service";
class TrackerService { class TrackerService implements ITrackerService {
public async getTrackers() : Promise<string[]> { public async getTrackers(): Promise<string[]> {
return cacheService.cacheTrackers(this.downloadTrackers); return cacheService.cacheTrackers(this.downloadTrackers);
}; };

View File

@@ -10,8 +10,8 @@ import {IngestedTorrent} from "./models/ingestedTorrent";
import {Subtitle} from "./models/subtitle"; import {Subtitle} from "./models/subtitle";
import {Content} from "./models/content"; import {Content} from "./models/content";
import {SkipTorrent} from "./models/skipTorrent"; import {SkipTorrent} from "./models/skipTorrent";
import {FileAttributes} from "./interfaces/file_attributes"; import {IFileAttributes} from "./interfaces/file_attributes";
import {TorrentAttributes} from "./interfaces/torrent_attributes"; import {ITorrentAttributes} from "./interfaces/torrent_attributes";
import {IngestedPage} from "./models/ingestedPage"; import {IngestedPage} from "./models/ingestedPage";
import {logger} from "../lib/services/logging_service"; import {logger} from "../lib/services/logging_service";
@@ -50,7 +50,7 @@ class DatabaseRepository {
} }
} }
public async getTorrent(torrent: TorrentAttributes): Promise<Torrent | null> { public async getTorrent(torrent: ITorrentAttributes): Promise<Torrent | null> {
const where = torrent.infoHash const where = torrent.infoHash
? { infoHash: torrent.infoHash } ? { infoHash: torrent.infoHash }
: { provider: torrent.provider, torrentId: torrent.torrentId }; : { provider: torrent.provider, torrentId: torrent.torrentId };
@@ -61,11 +61,11 @@ class DatabaseRepository {
return this.getTorrentsBasedOnQuery({ title: { [Op.regexp]: `${titleQuery}` }, type }); return this.getTorrentsBasedOnQuery({ title: { [Op.regexp]: `${titleQuery}` }, type });
} }
public async getTorrentsBasedOnQuery(where: WhereOptions<TorrentAttributes>): Promise<Torrent[]> { public async getTorrentsBasedOnQuery(where: WhereOptions<ITorrentAttributes>): Promise<Torrent[]> {
return await Torrent.findAll({ where }); return await Torrent.findAll({ where });
} }
public async getFilesBasedOnQuery(where: WhereOptions<FileAttributes>): Promise<File[]> { public async getFilesBasedOnQuery(where: WhereOptions<IFileAttributes>): Promise<File[]> {
return await File.findAll({ where }); return await File.findAll({ where });
} }
@@ -118,7 +118,7 @@ class DatabaseRepository {
await this.createSubtitles(torrent.infoHash, torrent.subtitles); await this.createSubtitles(torrent.infoHash, torrent.subtitles);
} }
public async setTorrentSeeders(torrent: TorrentAttributes, seeders: number): Promise<[number]> { public async setTorrentSeeders(torrent: ITorrentAttributes, seeders: number): Promise<[number]> {
const where = torrent.infoHash const where = torrent.infoHash
? { infoHash: torrent.infoHash } ? { infoHash: torrent.infoHash }
: { provider: torrent.provider, torrentId: torrent.torrentId }; : { provider: torrent.provider, torrentId: torrent.torrentId };

View File

@@ -1,11 +1,11 @@
import {Optional} from "sequelize"; import {Optional} from "sequelize";
export interface ContentAttributes { export interface IContentAttributes {
infoHash: string; infoHash: string;
fileIndex: number; fileIndex: number;
path: string; path: string;
size: number; size: number;
} }
export interface ContentCreationAttributes extends Optional<ContentAttributes, 'fileIndex' | 'size'> { export interface IContentCreationAttributes extends Optional<IContentAttributes, 'fileIndex' | 'size'> {
} }

View File

@@ -1,8 +1,8 @@
import {Optional} from "sequelize"; import {Optional} from "sequelize";
import {SubtitleAttributes} from "./subtitle_attributes"; import {ISubtitleAttributes} from "./subtitle_attributes";
import {ParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result"; import {IParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result";
export interface FileAttributes extends ParseTorrentTitleResult { export interface IFileAttributes extends IParseTorrentTitleResult {
id?: number; id?: number;
infoHash?: string; infoHash?: string;
fileIndex?: number; fileIndex?: number;
@@ -13,10 +13,10 @@ export interface FileAttributes extends ParseTorrentTitleResult {
imdbEpisode?: number; imdbEpisode?: number;
kitsuId?: number; kitsuId?: number;
kitsuEpisode?: number; kitsuEpisode?: number;
subtitles?: SubtitleAttributes[]; subtitles?: ISubtitleAttributes[];
path?: string; path?: string;
isMovie?: boolean; isMovie?: boolean;
} }
export interface FileCreationAttributes extends Optional<FileAttributes, 'fileIndex' | 'size' | 'imdbId' | 'imdbSeason' | 'imdbEpisode' | 'kitsuId' | 'kitsuEpisode'> { export interface IFileCreationAttributes extends Optional<IFileAttributes, 'fileIndex' | 'size' | 'imdbId' | 'imdbSeason' | 'imdbEpisode' | 'kitsuId' | 'kitsuEpisode'> {
} }

View File

@@ -1,6 +1,6 @@
export interface IngestedPageAttributes { export interface IIngestedPageAttributes {
url: string; url: string;
} }
export interface IngestedPageCreationAttributes extends IngestedPageAttributes { export interface IIngestedPageCreationAttributes extends IIngestedPageAttributes {
} }

View File

@@ -1,6 +1,6 @@
import {Optional} from "sequelize"; import {Optional} from "sequelize";
export interface IngestedTorrentAttributes { export interface IIngestedTorrentAttributes {
name: string; name: string;
source: string; source: string;
category: string; category: string;
@@ -13,5 +13,5 @@ export interface IngestedTorrentAttributes {
createdAt?: Date; createdAt?: Date;
} }
export interface IngestedTorrentCreationAttributes extends Optional<IngestedTorrentAttributes, 'processed'> { export interface IIngestedTorrentCreationAttributes extends Optional<IIngestedTorrentAttributes, 'processed'> {
} }

View File

@@ -1,10 +1,10 @@
import {Optional} from "sequelize"; import {Optional} from "sequelize";
export interface ProviderAttributes { export interface IProviderAttributes {
name: string; name: string;
lastScraped: Date; lastScraped: Date;
lastScrapedId: string; lastScrapedId: string;
} }
export interface ProviderCreationAttributes extends Optional<ProviderAttributes, 'lastScraped' | 'lastScrapedId'> { export interface IProviderCreationAttributes extends Optional<IProviderAttributes, 'lastScraped' | 'lastScrapedId'> {
} }

View File

@@ -1,8 +1,8 @@
import {Optional} from "sequelize"; import {Optional} from "sequelize";
export interface SkipTorrentAttributes { export interface ISkipTorrentAttributes {
infoHash: string; infoHash: string;
} }
export interface SkipTorrentCreationAttributes extends Optional<SkipTorrentAttributes, never> { export interface ISkipTorrentCreationAttributes extends Optional<ISkipTorrentAttributes, never> {
} }

View File

@@ -1,6 +1,6 @@
import {Optional} from "sequelize"; import {Optional} from "sequelize";
export interface SubtitleAttributes { export interface ISubtitleAttributes {
infoHash: string; infoHash: string;
fileIndex: number; fileIndex: number;
fileId?: number; fileId?: number;
@@ -8,5 +8,5 @@ export interface SubtitleAttributes {
path: string; path: string;
} }
export interface SubtitleCreationAttributes extends Optional<SubtitleAttributes, 'fileId'> { export interface ISubtitleCreationAttributes extends Optional<ISubtitleAttributes, 'fileId'> {
} }

View File

@@ -1,9 +1,9 @@
import {Optional} from "sequelize"; import {Optional} from "sequelize";
import {ContentAttributes} from "./content_attributes"; import {IContentAttributes} from "./content_attributes";
import {SubtitleAttributes} from "./subtitle_attributes"; import {ISubtitleAttributes} from "./subtitle_attributes";
import {FileAttributes} from "./file_attributes"; import {IFileAttributes} from "./file_attributes";
export interface TorrentAttributes { export interface ITorrentAttributes {
infoHash: string; infoHash: string;
provider?: string; provider?: string;
torrentId?: string; torrentId?: string;
@@ -17,10 +17,10 @@ export interface TorrentAttributes {
resolution?: string; resolution?: string;
reviewed?: boolean; reviewed?: boolean;
opened?: boolean; opened?: boolean;
contents?: ContentAttributes[]; contents?: IContentAttributes[];
files?: FileAttributes[]; files?: IFileAttributes[];
subtitles?: SubtitleAttributes[]; subtitles?: ISubtitleAttributes[];
} }
export interface TorrentCreationAttributes extends Optional<TorrentAttributes, 'torrentId' | 'size' | 'seeders' | 'trackers' | 'languages' | 'resolution' | 'reviewed' | 'opened'> { export interface ITorrentCreationAttributes extends Optional<ITorrentAttributes, 'torrentId' | 'size' | 'seeders' | 'trackers' | 'languages' | 'resolution' | 'reviewed' | 'opened'> {
} }

View File

@@ -1,9 +1,9 @@
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript'; import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
import {ContentAttributes, ContentCreationAttributes} from "../interfaces/content_attributes"; import {IContentAttributes, IContentCreationAttributes} from "../interfaces/content_attributes";
import {Torrent} from "./torrent"; import {Torrent} from "./torrent";
@Table({modelName: 'content', timestamps: false}) @Table({modelName: 'content', timestamps: false})
export class Content extends Model<ContentAttributes, ContentCreationAttributes> { export class Content extends Model<IContentAttributes, IContentCreationAttributes> {
@Column({ type: DataType.STRING(64), primaryKey: true, allowNull: false, onDelete: 'CASCADE' }) @Column({ type: DataType.STRING(64), primaryKey: true, allowNull: false, onDelete: 'CASCADE' })
@ForeignKey(() => Torrent) @ForeignKey(() => Torrent)
declare infoHash: string; declare infoHash: string;

View File

@@ -1,8 +1,8 @@
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript'; import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
import {FileAttributes, FileCreationAttributes} from "../interfaces/file_attributes"; import {IFileAttributes, IFileCreationAttributes} from "../interfaces/file_attributes";
import {Torrent} from "./torrent"; import {Torrent} from "./torrent";
import {Subtitle} from "./subtitle"; import {Subtitle} from "./subtitle";
import {SubtitleAttributes} from "../interfaces/subtitle_attributes"; import {ISubtitleAttributes} from "../interfaces/subtitle_attributes";
const indexes = [ const indexes = [
{ {
@@ -23,7 +23,7 @@ const indexes = [
]; ];
@Table({modelName: 'file', timestamps: true, indexes: indexes }) @Table({modelName: 'file', timestamps: true, indexes: indexes })
export class File extends Model<FileAttributes, FileCreationAttributes> { export class File extends Model<IFileAttributes, IFileCreationAttributes> {
@Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' }) @Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' })
@ForeignKey(() => Torrent) @ForeignKey(() => Torrent)
declare infoHash: string; declare infoHash: string;

View File

@@ -1,5 +1,5 @@
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
import {IngestedPageAttributes, IngestedPageCreationAttributes} from "../interfaces/ingested_page_attributes"; import {IIngestedPageAttributes, IIngestedPageCreationAttributes} from "../interfaces/ingested_page_attributes";
const indexes = [ const indexes = [
{ {
@@ -10,7 +10,7 @@ const indexes = [
]; ];
@Table({modelName: 'ingested_page', timestamps: true, indexes: indexes}) @Table({modelName: 'ingested_page', timestamps: true, indexes: indexes})
export class IngestedPage extends Model<IngestedPageAttributes, IngestedPageCreationAttributes> { export class IngestedPage extends Model<IIngestedPageAttributes, IIngestedPageCreationAttributes> {
@Column({ type: DataType.STRING(512), allowNull: false }) @Column({ type: DataType.STRING(512), allowNull: false })
declare url: string; declare url: string;
} }

View File

@@ -1,5 +1,5 @@
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
import {IngestedTorrentAttributes, IngestedTorrentCreationAttributes} from "../interfaces/ingested_torrent_attributes"; import {IIngestedTorrentAttributes, IIngestedTorrentCreationAttributes} from "../interfaces/ingested_torrent_attributes";
const indexes = [ const indexes = [
{ {
@@ -10,7 +10,7 @@ const indexes = [
]; ];
@Table({modelName: 'ingested_torrent', timestamps: true, indexes: indexes}) @Table({modelName: 'ingested_torrent', timestamps: true, indexes: indexes})
export class IngestedTorrent extends Model<IngestedTorrentAttributes, IngestedTorrentCreationAttributes> { export class IngestedTorrent extends Model<IIngestedTorrentAttributes, IIngestedTorrentCreationAttributes> {
@Column({ type: DataType.STRING(512) }) @Column({ type: DataType.STRING(512) })
declare name: string; declare name: string;

View File

@@ -1,8 +1,8 @@
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
import {ProviderAttributes, ProviderCreationAttributes} from "../interfaces/provider_attributes"; import {IProviderAttributes, IProviderCreationAttributes} from "../interfaces/provider_attributes";
@Table({modelName: 'provider', timestamps: false}) @Table({modelName: 'provider', timestamps: false})
export class Provider extends Model<ProviderAttributes, ProviderCreationAttributes> { export class Provider extends Model<IProviderAttributes, IProviderCreationAttributes> {
@Column({ type: DataType.STRING(32), primaryKey: true }) @Column({ type: DataType.STRING(32), primaryKey: true })
declare name: string; declare name: string;

View File

@@ -1,9 +1,9 @@
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
import {SkipTorrentAttributes, SkipTorrentCreationAttributes} from "../interfaces/skip_torrent_attributes"; import {ISkipTorrentAttributes, ISkipTorrentCreationAttributes} from "../interfaces/skip_torrent_attributes";
@Table({modelName: 'skip_torrent', timestamps: false}) @Table({modelName: 'skip_torrent', timestamps: false})
export class SkipTorrent extends Model<SkipTorrentAttributes, SkipTorrentCreationAttributes> { export class SkipTorrent extends Model<ISkipTorrentAttributes, ISkipTorrentCreationAttributes> {
@Column({ type: DataType.STRING(64), primaryKey: true }) @Column({ type: DataType.STRING(64), primaryKey: true })
declare infoHash: string; declare infoHash: string;

View File

@@ -1,5 +1,5 @@
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript'; import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
import {SubtitleAttributes, SubtitleCreationAttributes} from "../interfaces/subtitle_attributes"; import {ISubtitleAttributes, ISubtitleCreationAttributes} from "../interfaces/subtitle_attributes";
import {File} from "./file"; import {File} from "./file";
import {Torrent} from "./torrent"; import {Torrent} from "./torrent";
@@ -17,7 +17,7 @@ const indexes = [
]; ];
@Table({modelName: 'subtitle', timestamps: false, indexes: indexes}) @Table({modelName: 'subtitle', timestamps: false, indexes: indexes})
export class Subtitle extends Model<SubtitleAttributes, SubtitleCreationAttributes> { export class Subtitle extends Model<ISubtitleAttributes, ISubtitleCreationAttributes> {
@Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' }) @Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' })
declare infoHash: string; declare infoHash: string;

View File

@@ -1,12 +1,12 @@
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript'; import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
import {TorrentAttributes, TorrentCreationAttributes} from "../interfaces/torrent_attributes"; import {ITorrentAttributes, ITorrentCreationAttributes} from "../interfaces/torrent_attributes";
import {Content} from "./content"; import {Content} from "./content";
import {File} from "./file"; import {File} from "./file";
import {Subtitle} from "./subtitle"; import {Subtitle} from "./subtitle";
@Table({modelName: 'torrent', timestamps: true}) @Table({modelName: 'torrent', timestamps: true})
export class Torrent extends Model<TorrentAttributes, TorrentCreationAttributes> { export class Torrent extends Model<ITorrentAttributes, ITorrentCreationAttributes> {
@Column({type: DataType.STRING(64), primaryKey: true}) @Column({type: DataType.STRING(64), primaryKey: true})
declare infoHash: string; declare infoHash: string;