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 {IngestedRabbitMessage, IngestedRabbitTorrent} from "../lib/interfaces/ingested_rabbit_message";
import {IngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes";
import {IIngestedRabbitMessage, IIngestedRabbitTorrent} from "../lib/interfaces/ingested_rabbit_message";
import {IIngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes";
import {configurationService} from '../lib/services/configuration_service';
import {torrentProcessingService} from '../lib/services/torrent_processing_service';
import {logger} from '../lib/services/logging_service';
@@ -23,13 +23,13 @@ class ProcessTorrentsJob {
}
}
private processMessage = (msg: ConsumeMessage) => {
const ingestedTorrent: IngestedTorrentAttributes = this.getMessageAsJson(msg);
const ingestedTorrent: IIngestedTorrentAttributes = this.getMessageAsJson(msg);
return torrentProcessingService.processTorrentRecord(ingestedTorrent);
};
private getMessageAsJson = (msg: ConsumeMessage): IngestedTorrentAttributes => {
private getMessageAsJson = (msg: ConsumeMessage): IIngestedTorrentAttributes => {
const content = msg?.content.toString('utf8') ?? "{}";
const receivedObject: IngestedRabbitMessage = JSON.parse(content);
const receivedTorrent: IngestedRabbitTorrent = receivedObject.message;
const receivedObject: IIngestedRabbitMessage = JSON.parse(content);
const receivedTorrent: IIngestedRabbitTorrent = receivedObject.message;
return {...receivedTorrent, info_hash: receivedTorrent.infoHash};
};
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;
}

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import {KitsuLink, KitsuTrailer} from "./kitsu_metadata";
import {IKitsuLink, IKitsuTrailer} from "./kitsu_metadata";
export interface KitsuCatalogJsonResponse {
metas: KitsuCatalogMetaData[];
export interface IKitsuCatalogJsonResponse {
metas: IKitsuCatalogMetaData[];
}
export interface KitsuCatalogMetaData {
export interface IKitsuCatalogMetaData {
id: string;
type: string;
animeType: string;
@@ -18,6 +18,6 @@ export interface KitsuCatalogMetaData {
logo?: string;
poster: string;
background: string;
trailers: KitsuTrailer[];
links: KitsuLink[];
trailers: IKitsuTrailer[];
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;
meta?: KitsuMeta;
meta?: IKitsuMeta;
}
export interface KitsuMeta {
export interface IKitsuMeta {
aliases?: string[];
animeType?: string;
background?: string;
@@ -15,7 +15,7 @@ export interface KitsuMeta {
imdbRating?: string;
imdb_id?: string;
kitsu_id?: string;
links?: KitsuLink[];
links?: IKitsuLink[];
logo?: string;
name?: string;
poster?: string;
@@ -23,23 +23,23 @@ export interface KitsuMeta {
runtime?: string;
slug?: string;
status?: string;
trailers?: KitsuTrailer[];
trailers?: IKitsuTrailer[];
type?: string;
userCount?: number;
videos?: KitsuVideo[];
videos?: IKitsuVideo[];
year?: string;
}
export interface KitsuVideo extends CommonVideoMetadata {
export interface IKitsuVideo extends ICommonVideoMetadata {
imdbEpisode?: number;
imdbSeason?: number;
imdb_id?: string;
thumbnail?: string;
}
export interface KitsuTrailer {
export interface IKitsuTrailer {
source?: string;
type?: string;
}
export interface KitsuLink {
export interface IKitsuLink {
name?: string;
category?: 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
type?: 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;
imdbId?: number;
type?: string;
@@ -9,7 +9,7 @@ export interface MetadataResponse {
country?: string;
genres?: string[];
status?: string;
videos?: CommonVideoMetadata[];
videos?: ICommonVideoMetadata[];
episodeCount?: 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;
date?: 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 {TorrentFileCollection} from "./torrent_file_collection";
import {ITorrentFileCollection} from "./torrent_file_collection";
export interface ParsedTorrent extends ParseTorrentTitleResult {
export interface IParsedTorrent extends IParseTorrentTitleResult {
size?: number;
isPack?: boolean;
imdbId?: string | number;
@@ -14,5 +14,5 @@ export interface ParsedTorrent extends ParseTorrentTitleResult {
uploadDate?: Date;
seeders?: number;
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 {FileAttributes} from "../../repository/interfaces/file_attributes";
import {SubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
import {IContentAttributes} from "../../repository/interfaces/content_attributes";
import {IFileAttributes} from "../../repository/interfaces/file_attributes";
import {ISubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
export interface TorrentFileCollection {
contents?: ContentAttributes[];
videos?: FileAttributes[];
subtitles?: SubtitleAttributes[];
export interface ITorrentFileCollection {
contents?: IContentAttributes[];
videos?: IFileAttributes[];
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 { mongoDbStore } from '@tirke/node-cache-manager-mongodb'
import { configurationService } from './configuration_service';
import { logger } from './logging_service';
import { CacheType } from "../enums/cache_types";
import {CacheOptions} from "../interfaces/cache_options";
import {mongoDbStore} from '@tirke/node-cache-manager-mongodb'
import {configurationService} from './configuration_service';
import {logger} from './logging_service';
import {CacheType} from "../enums/cache_types";
import {ICacheOptions} from "../interfaces/cache_options";
import {ICacheService} from "../interfaces/cache_service";
const GLOBAL_KEY_PREFIX = 'knightcrawler-consumer';
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;
class CacheService {
class CacheService implements ICacheService {
constructor() {
if (!configurationService.cacheConfig.NO_CACHE) {
logger.info('Cache is disabled');
@@ -27,18 +28,18 @@ class CacheService {
this.memoryCache = this.initiateMemoryCache();
this.remoteCache = this.initiateRemoteCache();
}
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> =>
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> =>
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> =>
this.cacheWrap(CacheType.Memory, `${TRACKERS_KEY_PREFIX}`, method, { ttl: TRACKERS_TTL });
this.cacheWrap(CacheType.Memory, `${TRACKERS_KEY_PREFIX}`, method, {ttl: TRACKERS_TTL});
private initiateMemoryCache = () =>
createCache(memoryStore(), {
@@ -85,7 +86,7 @@ class CacheService {
private readonly remoteCache: Cache;
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);
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 {ILoggingService} from "../interfaces/logging_service";
class LoggingService implements ILoggingService {
private readonly logger: Logger;
constructor() {
this.logger = pino({
level: process.env.LOG_LEVEL || 'info'
});
}
class LoggingService {
public readonly logger: Logger = pino({
level: process.env.LOG_LEVEL || 'info'
});
public info(message: string, ...args: any[]): void {
this.logger.info(message);
this.logger.info(message, args);
}
public error(message: string, ...args: any[]): void {
this.logger.error(message);
this.logger.error(message, args);
}
public debug(message: string, ...args: any[]): void {
this.logger.debug(message);
this.logger.debug(message, args);
}
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 {search, ResultTypes} from 'google-sr';
import {ResultTypes, search} from 'google-sr';
import nameToImdb from 'name-to-imdb';
import { cacheService } from './cache_service';
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";
import {cacheService} from './cache_service';
import {TorrentType} from '../enums/torrent_types';
import {IMetadataResponse} from "../interfaces/metadata_response";
import {ICinemetaJsonResponse} from "../interfaces/cinemeta_metadata";
import {ICommonVideoMetadata} from "../interfaces/common_video_metadata";
import {IKitsuJsonResponse} from "../interfaces/kitsu_metadata";
import {IMetaDataQuery} from "../interfaces/metadata_query";
import {IKitsuCatalogJsonResponse} from "../interfaces/kitsu_catalog_metadata";
import {IMetadataService} from "../interfaces/metadata_service";
const CINEMETA_URL = 'https://v3-cinemeta.strem.io';
const KITSU_URL = 'https://anime-kitsu.strem.fun';
const TIMEOUT = 20000;
class MetadataService {
public async getKitsuId(info: MetaDataQuery): Promise<string | Error> {
class MetadataService implements IMetadataService {
public async getKitsuId(info: IMetaDataQuery): Promise<string | Error> {
const title = this.escapeTitle(info.title.replace(/\s\|\s.*/, ''));
const year = info.year ? ` ${info.year}` : '';
const season = info.season > 1 ? ` S${info.season}` : '';
@@ -23,9 +24,9 @@ class MetadataService {
const query = encodeURIComponent(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) => {
const body = response.data as KitsuCatalogJsonResponse;
const body = response.data as IKitsuCatalogJsonResponse;
if (body && body.metas && body.metas.length) {
return body.metas[0].id.replace('kitsu:', '');
} 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 year = info.year || (info.date && info.date.slice(0, 4));
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) {
return Promise.reject("no valid id provided");
}
@@ -93,14 +94,14 @@ class MetadataService {
.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 result: MetadataResponse;
let result: IMetadataResponse;
const body = response.data;
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) {
result = this.handleCinemetaResponse(body as CinemetaJsonResponse);
result = this.handleCinemetaResponse(body as ICinemetaJsonResponse);
} else {
throw new Error('No valid metadata');
}
@@ -108,7 +109,7 @@ class MetadataService {
return result;
}
private handleCinemetaResponse(body: CinemetaJsonResponse): MetadataResponse {
private handleCinemetaResponse(body: ICinemetaJsonResponse): IMetadataResponse {
return {
imdbId: parseInt(body.meta.imdb_id),
type: body.meta.type,
@@ -137,7 +138,7 @@ class MetadataService {
};
}
private handleKitsuResponse(body: KitsuJsonResponse): MetadataResponse {
private handleKitsuResponse(body: IKitsuJsonResponse): IMetadataResponse {
return {
kitsuId: parseInt(body.meta.kitsu_id),
type: body.meta.type,
@@ -167,7 +168,7 @@ class MetadataService {
};
}
private getEpisodeCount(videos: CommonVideoMetadata[]) {
private getEpisodeCount(videos: ICommonVideoMetadata[]) {
return Object.values(
videos
.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 type = info.type;
return new Promise((resolve, reject) => {

View File

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

View File

@@ -1,20 +1,21 @@
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 {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 {PromiseHelpers} from '../helpers/promises_helpers';
import {logger} from './logging_service';
import {metadataService} from './metadata_service';
import {torrentFileService} from './torrent_file_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 {Subtitle} from "../../repository/models/subtitle";
import {ITorrentEntriesService} from "../interfaces/torrent_entries_service";
class TorrentEntriesService {
public async createTorrentEntry(torrent: ParsedTorrent, overwrite = false): Promise<void> {
class TorrentEntriesService implements ITorrentEntriesService {
public async createTorrentEntry(torrent: IParsedTorrent, overwrite = false): Promise<void> {
const titleInfo = parse(torrent.title);
if (!torrent.imdbId && torrent.type !== TorrentType.Anime) {
@@ -49,9 +50,9 @@ class TorrentEntriesService {
return;
}
const fileCollection: TorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
.then((torrentContents: TorrentFileCollection) => overwrite ? this.overwriteExistingFiles(torrent, torrentContents) : torrentContents)
.then((torrentContents: TorrentFileCollection) => torrentSubtitleService.assignSubtitles(torrentContents))
const fileCollection: ITorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
.then((torrentContents: ITorrentFileCollection) => overwrite ? this.overwriteExistingFiles(torrent, torrentContents) : torrentContents)
.then((torrentContents: ITorrentFileCollection) => torrentSubtitleService.assignSubtitles(torrentContents))
.catch(error => {
logger.warn(`Failed getting files for ${torrent.title}`, error.message);
return {};
@@ -86,8 +87,8 @@ class TorrentEntriesService {
.catch(() => undefined);
}
public async checkAndUpdateTorrent(torrent: ParsedTorrent): Promise<boolean> {
const query: TorrentAttributes = {
public async checkAndUpdateTorrent(torrent: IParsedTorrent): Promise<boolean> {
const query: ITorrentAttributes = {
infoHash: torrent.infoHash,
provider: torrent.provider,
}
@@ -128,7 +129,7 @@ class TorrentEntriesService {
const imdbId: string | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.imdbId));
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 => torrentSubtitleService.assignSubtitles(torrentContents))
.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));
}
public async updateTorrentSeeders(torrent: TorrentAttributes) {
public async updateTorrentSeeders(torrent: ITorrentAttributes) {
if (!(torrent.infoHash || (torrent.provider && torrent.torrentId)) || !Number.isInteger(torrent.seeders)) {
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) {
fileCollection.videos.forEach(video => {
video.imdbId = imdbId;
@@ -199,7 +200,7 @@ class TorrentEntriesService {
return fileCollection;
}
private async overwriteExistingFiles(torrent: ParsedTorrent, torrentContents: TorrentFileCollection) {
private async overwriteExistingFiles(torrent: IParsedTorrent, torrentContents: ITorrentFileCollection) {
const videos = torrentContents && torrentContents.videos;
if (videos && videos.length) {
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 {TorrentType} from '../enums/torrent_types';
import {configurationService} from './configuration_service';
import {extensionService} from './extension_service';
import {ExtensionHelpers} from '../helpers/extension_helpers';
import {metadataService} from './metadata_service';
import {torrentDownloadService} from "./torrent_download_service";
import {logger} from "./logging_service";
import {MetadataResponse} from "../interfaces/metadata_response";
import {MetaDataQuery} from "../interfaces/metadata_query";
import {CommonVideoMetadata} from "../interfaces/common_video_metadata";
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
import {ParsedTorrent} from "../interfaces/parsed_torrent";
import {FileAttributes} from "../../repository/interfaces/file_attributes";
import {ContentAttributes} from "../../repository/interfaces/content_attributes";
import {IMetadataResponse} from "../interfaces/metadata_response";
import {IMetaDataQuery} from "../interfaces/metadata_query";
import {ICommonVideoMetadata} from "../interfaces/common_video_metadata";
import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
import {IParsedTorrent} from "../interfaces/parsed_torrent";
import {IFileAttributes} from "../../repository/interfaces/file_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 MULTIPLE_FILES_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB
class TorrentFileService {
class TorrentFileService implements ITorrentFileService {
private readonly imdb_limiter: Bottleneck = new Bottleneck({
maxConcurrent: configurationService.metadataConfig.IMDB_CONCURRENT,
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 query: MetaDataQuery = {
const query: IMetaDataQuery = {
id: torrent.kitsuId || torrent.imdbId,
type: torrent.type || TorrentType.Movie,
};
@@ -48,7 +49,7 @@ class TorrentFileService {
return this.parseSeriesFiles(torrent, metadata)
}
public isPackTorrent(torrent: ParsedTorrent): boolean {
public isPackTorrent(torrent: IParsedTorrent): boolean {
if (torrent.isPack) {
return true;
}
@@ -65,7 +66,7 @@ class TorrentFileService {
return hasMultipleEpisodes && !hasSingleEpisode;
}
private parseSeriesVideos(torrent: ParsedTorrent, videos: FileAttributes[]): FileAttributes[] {
private parseSeriesVideos(torrent: IParsedTorrent, videos: IFileAttributes[]): IFileAttributes[] {
const parsedTorrentName = parse(torrent.title);
const hasMovies = parsedTorrentName.complete || !!torrent.title.match(/movies?(?:\W|$)/i);
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) }));
}
private async parseMovieFiles(torrent: ParsedTorrent, metadata: MetadataResponse): Promise<TorrentFileCollection> {
const fileCollection: TorrentFileCollection = await this.getMoviesTorrentContent(torrent);
private async parseMovieFiles(torrent: IParsedTorrent, metadata: IMetadataResponse): Promise<ITorrentFileCollection> {
const fileCollection: ITorrentFileCollection = await this.getMoviesTorrentContent(torrent);
const filteredVideos = fileCollection.videos
.filter(video => video.size > MIN_SIZE)
.filter(video => !this.isFeaturette(video));
@@ -103,9 +104,9 @@ class TorrentFileService {
return {...fileCollection, videos: parsedVideos};
}
private async parseSeriesFiles(torrent: ParsedTorrent, metadata: MetadataResponse): Promise<TorrentFileCollection> {
const fileCollection: TorrentFileCollection = await this.getSeriesTorrentContent(torrent);
const parsedVideos: FileAttributes[] = await Promise.resolve(fileCollection.videos)
private async parseSeriesFiles(torrent: IParsedTorrent, metadata: IMetadataResponse): Promise<ITorrentFileCollection> {
const fileCollection: ITorrentFileCollection = await this.getSeriesTorrentContent(torrent);
const parsedVideos: IFileAttributes[] = await Promise.resolve(fileCollection.videos)
.then(videos => videos.filter(video => videos.length === 1 || video.size > MIN_SIZE))
.then(videos => this.parseSeriesVideos(torrent, videos))
.then(videos => this.decomposeEpisodes(torrent, videos, metadata))
@@ -119,7 +120,7 @@ class TorrentFileService {
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)
.catch(error => {
if (!this.isPackTorrent(torrent)) {
@@ -135,11 +136,11 @@ class TorrentFileService {
return files;
}
private getDefaultFileEntries(torrent: ParsedTorrent): FileAttributes[] {
private getDefaultFileEntries(torrent: IParsedTorrent): IFileAttributes[] {
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)
.catch(error => {
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 (files.length === 1 || files.some(f => f.episodes || f.episodes) || parse(torrent.title).seasons) {
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)
.then(result => {
if (result instanceof Error) {
@@ -191,7 +192,7 @@ class TorrentFileService {
const imdbId = !kitsuId ? await this.findMovieImdbId(file) : undefined;
const query: MetaDataQuery = {
const query: IMetaDataQuery = {
id: kitsuId || imdbId,
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)) {
return files;
}
@@ -275,7 +276,7 @@ class TorrentFileService {
return files;
}
private preprocessEpisodes(files: FileAttributes[]) {
private preprocessEpisodes(files: IFileAttributes[]) {
// reverse special episode naming when they named with 0 episode, ie. S02E00
files
.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) {
// anime does not use this naming scheme in 99% of cases;
return false;
@@ -313,11 +314,11 @@ class TorrentFileService {
|| 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);
}
private isAbsoluteEpisodeFiles(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse) {
private isAbsoluteEpisodeFiles(torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse) {
const threshold = Math.ceil(files.length / 5);
const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId;
const nonMovieEpisodes = files
@@ -330,7 +331,7 @@ class TorrentFileService {
|| 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.
// detect this if episode number is larger than the last episode or season is larger than the last one
// only for non anime metas
@@ -341,7 +342,7 @@ class TorrentFileService {
&& video.episodes.every(ep => ep > (metadata.episodeCount[video.season - 1] || 0));
}
private decomposeConcatSeasonAndEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) {
private decomposeConcatSeasonAndEpisodeFiles(files: IFileAttributes[], metadata: IMetadataResponse) {
files
.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)
@@ -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) {
videos
.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) {
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 (torrent.type === TorrentType.Anime) {
// assign episodes as kitsu episodes for anime when no metadata available for imdb mapping
@@ -429,7 +430,7 @@ class TorrentFileService {
return files;
}
const seriesMapping: CommonVideoMetadata = metadata.videos
const seriesMapping: ICommonVideoMetadata = metadata.videos
.reduce((map, video) => {
const episodeMap = map[video.season] || {};
episodeMap[video.episode] = video;
@@ -464,13 +465,13 @@ class TorrentFileService {
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode);
} else if (seriesMapping[file.season - 1]) {
// 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 firstKitsuId = episodes.length && episodes[0];
const differentTitlesCount = new Set(episodes.map(ep => ep.id)).size
const skippedCount = episodes.filter(ep => ep.id === firstKitsuId).length;
const seasonEpisodes = files
.filter((otherFile: FileAttributes) => otherFile.season === file.season)
.filter((otherFile: IFileAttributes) => otherFile.season === file.season)
.reduce((a, b) => a.concat(b.episodes), []);
const isAbsoluteOrder = seasonEpisodes.every(ep => ep > skippedCount && ep <= episodes.length)
const isNormalOrder = seasonEpisodes.every(ep => ep + skippedCount <= episodes.length)
@@ -494,7 +495,7 @@ class TorrentFileService {
return files;
}
private needsCinemetaMetadataForAnime(files: FileAttributes[], metadata: MetadataResponse) {
private needsCinemetaMetadataForAnime(files: IFileAttributes[], metadata: IMetadataResponse) {
if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) {
return false;
}
@@ -510,8 +511,8 @@ class TorrentFileService {
.some(file => file.season < minSeason || file.season > maxSeason || file.episodes.every(ep => ep > total));
}
private async updateToCinemetaMetadata(metadata: MetadataResponse) {
const query: MetaDataQuery = {
private async updateToCinemetaMetadata(metadata: IMetadataResponse) {
const query: IMetaDataQuery = {
id: metadata.imdbId,
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;
logger.debug(`Finding movie imdbId for ${title}`);
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 kitsuQuery = {
title: parsedTitle.title,
@@ -568,22 +569,22 @@ class TorrentFileService {
}
}
private isDiskTorrent(contents: ContentAttributes[]) {
return contents.some(content => extensionService.isDisk(content.path));
private isDiskTorrent(contents: IContentAttributes[]) {
return contents.some(content => ExtensionHelpers.isDisk(content.path));
}
private isSingleMovie(videos: FileAttributes[]) {
private isSingleMovie(videos: IFileAttributes[]) {
return videos.length === 1 ||
(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?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);
}
private parseSeriesVideo(video: FileAttributes): FileAttributes {
private parseSeriesVideo(video: IFileAttributes): IFileAttributes {
const videoInfo = parse(video.title);
// the episode may be in a folder containing season number
if (!Number.isInteger(videoInfo.season) && video.path.includes('/')) {
@@ -629,7 +630,7 @@ class TorrentFileService {
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)) {
// not movie if video has season
return false;
@@ -653,7 +654,7 @@ class TorrentFileService {
&& 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.imdbSeason = undefined;
video.imdbEpisode = undefined;

View File

@@ -2,14 +2,15 @@ import {TorrentType} from "../enums/torrent_types";
import {logger} from "./logging_service";
import {trackerService} from "./tracker_service";
import {torrentEntriesService} from "./torrent_entries_service";
import {IngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
import {ParsedTorrent} from "../interfaces/parsed_torrent";
import {IIngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
import {IParsedTorrent} from "../interfaces/parsed_torrent";
import {ITorrentProcessingService} from "../interfaces/torrent_processing_service";
class TorrentProcessingService {
public async processTorrentRecord(torrent: IngestedTorrentAttributes): Promise<void> {
const { category } = torrent;
class TorrentProcessingService implements ITorrentProcessingService {
public async processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise<void> {
const {category} = torrent;
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}`);
@@ -25,7 +26,7 @@ class TorrentProcessingService {
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()
return {
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) {
return undefined;
}

View File

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

View File

@@ -1,13 +1,14 @@
import axios, { AxiosResponse } from 'axios';
import { cacheService } from "./cache_service";
import { configurationService } from './configuration_service';
import { logger } from "./logging_service";
import axios, {AxiosResponse} from 'axios';
import {cacheService} from "./cache_service";
import {configurationService} from './configuration_service';
import {logger} from "./logging_service";
import {ITrackerService} from "../interfaces/tracker_service";
class TrackerService {
public async getTrackers() : Promise<string[]> {
class TrackerService implements ITrackerService {
public async getTrackers(): Promise<string[]> {
return cacheService.cacheTrackers(this.downloadTrackers);
};
private async downloadTrackers(): Promise<string[]> {
const response: AxiosResponse<string> = await axios.get(configurationService.trackerConfig.TRACKERS_URL);
const trackersListText: string = response.data;

View File

@@ -10,8 +10,8 @@ import {IngestedTorrent} from "./models/ingestedTorrent";
import {Subtitle} from "./models/subtitle";
import {Content} from "./models/content";
import {SkipTorrent} from "./models/skipTorrent";
import {FileAttributes} from "./interfaces/file_attributes";
import {TorrentAttributes} from "./interfaces/torrent_attributes";
import {IFileAttributes} from "./interfaces/file_attributes";
import {ITorrentAttributes} from "./interfaces/torrent_attributes";
import {IngestedPage} from "./models/ingestedPage";
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
? { infoHash: torrent.infoHash }
: { provider: torrent.provider, torrentId: torrent.torrentId };
@@ -61,11 +61,11 @@ class DatabaseRepository {
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 });
}
public async getFilesBasedOnQuery(where: WhereOptions<FileAttributes>): Promise<File[]> {
public async getFilesBasedOnQuery(where: WhereOptions<IFileAttributes>): Promise<File[]> {
return await File.findAll({ where });
}
@@ -118,7 +118,7 @@ class DatabaseRepository {
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
? { infoHash: torrent.infoHash }
: { provider: torrent.provider, torrentId: torrent.torrentId };

View File

@@ -1,11 +1,11 @@
import {Optional} from "sequelize";
export interface ContentAttributes {
export interface IContentAttributes {
infoHash: string;
fileIndex: number;
path: string;
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 {SubtitleAttributes} from "./subtitle_attributes";
import {ParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result";
import {ISubtitleAttributes} from "./subtitle_attributes";
import {IParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result";
export interface FileAttributes extends ParseTorrentTitleResult {
export interface IFileAttributes extends IParseTorrentTitleResult {
id?: number;
infoHash?: string;
fileIndex?: number;
@@ -13,10 +13,10 @@ export interface FileAttributes extends ParseTorrentTitleResult {
imdbEpisode?: number;
kitsuId?: number;
kitsuEpisode?: number;
subtitles?: SubtitleAttributes[];
subtitles?: ISubtitleAttributes[];
path?: string;
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;
}
export interface IngestedPageCreationAttributes extends IngestedPageAttributes {
export interface IIngestedPageCreationAttributes extends IIngestedPageAttributes {
}

View File

@@ -1,6 +1,6 @@
import {Optional} from "sequelize";
export interface IngestedTorrentAttributes {
export interface IIngestedTorrentAttributes {
name: string;
source: string;
category: string;
@@ -13,5 +13,5 @@ export interface IngestedTorrentAttributes {
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";
export interface ProviderAttributes {
export interface IProviderAttributes {
name: string;
lastScraped: Date;
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";
export interface SkipTorrentAttributes {
export interface ISkipTorrentAttributes {
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";
export interface SubtitleAttributes {
export interface ISubtitleAttributes {
infoHash: string;
fileIndex: number;
fileId?: number;
@@ -8,5 +8,5 @@ export interface SubtitleAttributes {
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 {ContentAttributes} from "./content_attributes";
import {SubtitleAttributes} from "./subtitle_attributes";
import {FileAttributes} from "./file_attributes";
import {IContentAttributes} from "./content_attributes";
import {ISubtitleAttributes} from "./subtitle_attributes";
import {IFileAttributes} from "./file_attributes";
export interface TorrentAttributes {
export interface ITorrentAttributes {
infoHash: string;
provider?: string;
torrentId?: string;
@@ -17,10 +17,10 @@ export interface TorrentAttributes {
resolution?: string;
reviewed?: boolean;
opened?: boolean;
contents?: ContentAttributes[];
files?: FileAttributes[];
subtitles?: SubtitleAttributes[];
contents?: IContentAttributes[];
files?: IFileAttributes[];
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 {ContentAttributes, ContentCreationAttributes} from "../interfaces/content_attributes";
import {IContentAttributes, IContentCreationAttributes} from "../interfaces/content_attributes";
import {Torrent} from "./torrent";
@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' })
@ForeignKey(() => Torrent)
declare infoHash: string;

View File

@@ -1,8 +1,8 @@
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 {Subtitle} from "./subtitle";
import {SubtitleAttributes} from "../interfaces/subtitle_attributes";
import {ISubtitleAttributes} from "../interfaces/subtitle_attributes";
const indexes = [
{
@@ -23,7 +23,7 @@ const 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' })
@ForeignKey(() => Torrent)
declare infoHash: string;

View File

@@ -1,5 +1,5 @@
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 = [
{
@@ -10,7 +10,7 @@ const 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 })
declare url: string;
}

View File

@@ -1,5 +1,5 @@
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 = [
{
@@ -10,7 +10,7 @@ const 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) })
declare name: string;

View File

@@ -1,8 +1,8 @@
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})
export class Provider extends Model<ProviderAttributes, ProviderCreationAttributes> {
export class Provider extends Model<IProviderAttributes, IProviderCreationAttributes> {
@Column({ type: DataType.STRING(32), primaryKey: true })
declare name: string;

View File

@@ -1,9 +1,9 @@
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})
export class SkipTorrent extends Model<SkipTorrentAttributes, SkipTorrentCreationAttributes> {
export class SkipTorrent extends Model<ISkipTorrentAttributes, ISkipTorrentCreationAttributes> {
@Column({ type: DataType.STRING(64), primaryKey: true })
declare infoHash: string;

View File

@@ -1,5 +1,5 @@
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 {Torrent} from "./torrent";
@@ -17,7 +17,7 @@ const 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' })
declare infoHash: string;

View File

@@ -1,12 +1,12 @@
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 {File} from "./file";
import {Subtitle} from "./subtitle";
@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})
declare infoHash: string;