interfaces normalized and extracted for services
This commit is contained in:
@@ -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) {
|
||||
|
||||
66
src/node/consumer/src/lib/helpers/extension_helpers.ts
Normal file
66
src/node/consumer/src/lib/helpers/extension_helpers.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface CacheOptions {
|
||||
export interface ICacheOptions {
|
||||
ttl: number;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface CommonVideoMetadata {
|
||||
export interface ICommonVideoMetadata {
|
||||
season?: number;
|
||||
episode?: number;
|
||||
released?: string;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
6
src/node/consumer/src/lib/interfaces/logging_service.ts
Normal file
6
src/node/consumer/src/lib/interfaces/logging_service.ts
Normal 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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface MetaDataQuery {
|
||||
export interface IMetaDataQuery {
|
||||
title?: string
|
||||
type?: string
|
||||
year?: number | string
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
14
src/node/consumer/src/lib/interfaces/metadata_service.ts
Normal file
14
src/node/consumer/src/lib/interfaces/metadata_service.ts
Normal 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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ParseTorrentTitleResult {
|
||||
export interface IParseTorrentTitleResult {
|
||||
title?: string;
|
||||
date?: string;
|
||||
year?: number | string;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import {IIngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
|
||||
|
||||
export interface ITorrentProcessingService {
|
||||
processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import {ITorrentFileCollection} from "./torrent_file_collection";
|
||||
|
||||
export interface ITorrentSubtitleService {
|
||||
assignSubtitles(fileCollection: ITorrentFileCollection): ITorrentFileCollection;
|
||||
}
|
||||
3
src/node/consumer/src/lib/interfaces/tracker_service.ts
Normal file
3
src/node/consumer/src/lib/interfaces/tracker_service.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface ITrackerService {
|
||||
getTrackers(): Promise<string[]>;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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'> {
|
||||
}
|
||||
@@ -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'> {
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface IngestedPageAttributes {
|
||||
export interface IIngestedPageAttributes {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface IngestedPageCreationAttributes extends IngestedPageAttributes {
|
||||
export interface IIngestedPageCreationAttributes extends IIngestedPageAttributes {
|
||||
}
|
||||
@@ -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'> {
|
||||
}
|
||||
@@ -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'> {
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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'> {
|
||||
}
|
||||
@@ -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'> {
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user