mirror of
https://github.com/knightcrawler-stremio/knightcrawler.git
synced 2024-12-20 03:29:51 +00:00
interfaces normalized and extracted for services
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import client, {Channel, Connection, ConsumeMessage, Options} from 'amqplib'
|
import client, {Channel, Connection, ConsumeMessage, Options} from 'amqplib'
|
||||||
import {IngestedRabbitMessage, IngestedRabbitTorrent} from "../lib/interfaces/ingested_rabbit_message";
|
import {IIngestedRabbitMessage, IIngestedRabbitTorrent} from "../lib/interfaces/ingested_rabbit_message";
|
||||||
import {IngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes";
|
import {IIngestedTorrentAttributes} from "../repository/interfaces/ingested_torrent_attributes";
|
||||||
import {configurationService} from '../lib/services/configuration_service';
|
import {configurationService} from '../lib/services/configuration_service';
|
||||||
import {torrentProcessingService} from '../lib/services/torrent_processing_service';
|
import {torrentProcessingService} from '../lib/services/torrent_processing_service';
|
||||||
import {logger} from '../lib/services/logging_service';
|
import {logger} from '../lib/services/logging_service';
|
||||||
@@ -23,13 +23,13 @@ class ProcessTorrentsJob {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private processMessage = (msg: ConsumeMessage) => {
|
private processMessage = (msg: ConsumeMessage) => {
|
||||||
const ingestedTorrent: IngestedTorrentAttributes = this.getMessageAsJson(msg);
|
const ingestedTorrent: IIngestedTorrentAttributes = this.getMessageAsJson(msg);
|
||||||
return torrentProcessingService.processTorrentRecord(ingestedTorrent);
|
return torrentProcessingService.processTorrentRecord(ingestedTorrent);
|
||||||
};
|
};
|
||||||
private getMessageAsJson = (msg: ConsumeMessage): IngestedTorrentAttributes => {
|
private getMessageAsJson = (msg: ConsumeMessage): IIngestedTorrentAttributes => {
|
||||||
const content = msg?.content.toString('utf8') ?? "{}";
|
const content = msg?.content.toString('utf8') ?? "{}";
|
||||||
const receivedObject: IngestedRabbitMessage = JSON.parse(content);
|
const receivedObject: IIngestedRabbitMessage = JSON.parse(content);
|
||||||
const receivedTorrent: IngestedRabbitTorrent = receivedObject.message;
|
const receivedTorrent: IIngestedRabbitTorrent = receivedObject.message;
|
||||||
return {...receivedTorrent, info_hash: receivedTorrent.infoHash};
|
return {...receivedTorrent, info_hash: receivedTorrent.infoHash};
|
||||||
};
|
};
|
||||||
private async assertAndConsumeQueue(channel: Channel) {
|
private async assertAndConsumeQueue(channel: Channel) {
|
||||||
|
|||||||
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;
|
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 {
|
export interface ICinemetaJsonResponse {
|
||||||
meta?: CinemetaMetaData;
|
meta?: ICinemetaMetaData;
|
||||||
trailerStreams?: CinemetaTrailerStream[];
|
trailerStreams?: ICinemetaTrailerStream[];
|
||||||
links?: CinemetaLink[];
|
links?: ICinemetaLink[];
|
||||||
behaviorHints?: CinemetaBehaviorHints;
|
behaviorHints?: ICinemetaBehaviorHints;
|
||||||
}
|
}
|
||||||
export interface CinemetaMetaData {
|
export interface ICinemetaMetaData {
|
||||||
awards?: string;
|
awards?: string;
|
||||||
cast?: string[];
|
cast?: string[];
|
||||||
country?: string;
|
country?: string;
|
||||||
@@ -28,16 +28,16 @@ export interface CinemetaMetaData {
|
|||||||
year?: string;
|
year?: string;
|
||||||
background?: string;
|
background?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
popularities?: CinemetaPopularities;
|
popularities?: ICinemetaPopularities;
|
||||||
moviedb_id?: number;
|
moviedb_id?: number;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
trailers?: CinemetaTrailer[];
|
trailers?: ICinemetaTrailer[];
|
||||||
id?: string;
|
id?: string;
|
||||||
genres?: string[];
|
genres?: string[];
|
||||||
releaseInfo?: string;
|
releaseInfo?: string;
|
||||||
videos?: CinemetaVideo[];
|
videos?: ICinemetaVideo[];
|
||||||
}
|
}
|
||||||
export interface CinemetaPopularities {
|
export interface ICinemetaPopularities {
|
||||||
PXS_TEST?: number;
|
PXS_TEST?: number;
|
||||||
PXS?: number;
|
PXS?: number;
|
||||||
SCM?: number;
|
SCM?: number;
|
||||||
@@ -49,11 +49,11 @@ export interface CinemetaPopularities {
|
|||||||
stremio?: number;
|
stremio?: number;
|
||||||
stremio_lib?: number;
|
stremio_lib?: number;
|
||||||
}
|
}
|
||||||
export interface CinemetaTrailer {
|
export interface ICinemetaTrailer {
|
||||||
source?: string;
|
source?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
export interface CinemetaVideo extends CommonVideoMetadata {
|
export interface ICinemetaVideo extends ICommonVideoMetadata {
|
||||||
name?: string;
|
name?: string;
|
||||||
number?: number;
|
number?: number;
|
||||||
firstAired?: string;
|
firstAired?: string;
|
||||||
@@ -63,16 +63,16 @@ export interface CinemetaVideo extends CommonVideoMetadata {
|
|||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
export interface CinemetaTrailerStream {
|
export interface ICinemetaTrailerStream {
|
||||||
title?: string;
|
title?: string;
|
||||||
ytId?: string;
|
ytId?: string;
|
||||||
}
|
}
|
||||||
export interface CinemetaLink {
|
export interface ICinemetaLink {
|
||||||
name?: string;
|
name?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
export interface CinemetaBehaviorHints {
|
export interface ICinemetaBehaviorHints {
|
||||||
defaultVideoId?: null;
|
defaultVideoId?: null;
|
||||||
hasScheduledVideos?: boolean;
|
hasScheduledVideos?: boolean;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface CommonVideoMetadata {
|
export interface ICommonVideoMetadata {
|
||||||
season?: number;
|
season?: number;
|
||||||
episode?: number;
|
episode?: number;
|
||||||
released?: string;
|
released?: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IngestedRabbitTorrent {
|
export interface IIngestedRabbitTorrent {
|
||||||
name: string;
|
name: string;
|
||||||
source: string;
|
source: string;
|
||||||
category: string;
|
category: string;
|
||||||
@@ -10,6 +10,6 @@ export interface IngestedRabbitTorrent {
|
|||||||
processed: boolean;
|
processed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IngestedRabbitMessage {
|
export interface IIngestedRabbitMessage {
|
||||||
message: IngestedRabbitTorrent;
|
message: IIngestedRabbitTorrent;
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import {KitsuLink, KitsuTrailer} from "./kitsu_metadata";
|
import {IKitsuLink, IKitsuTrailer} from "./kitsu_metadata";
|
||||||
|
|
||||||
export interface KitsuCatalogJsonResponse {
|
export interface IKitsuCatalogJsonResponse {
|
||||||
metas: KitsuCatalogMetaData[];
|
metas: IKitsuCatalogMetaData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KitsuCatalogMetaData {
|
export interface IKitsuCatalogMetaData {
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
animeType: string;
|
animeType: string;
|
||||||
@@ -18,6 +18,6 @@ export interface KitsuCatalogMetaData {
|
|||||||
logo?: string;
|
logo?: string;
|
||||||
poster: string;
|
poster: string;
|
||||||
background: string;
|
background: string;
|
||||||
trailers: KitsuTrailer[];
|
trailers: IKitsuTrailer[];
|
||||||
links: KitsuLink[];
|
links: IKitsuLink[];
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import {CommonVideoMetadata} from "./common_video_metadata";
|
import {ICommonVideoMetadata} from "./common_video_metadata";
|
||||||
|
|
||||||
export interface KitsuJsonResponse {
|
export interface IKitsuJsonResponse {
|
||||||
cacheMaxAge?: number;
|
cacheMaxAge?: number;
|
||||||
meta?: KitsuMeta;
|
meta?: IKitsuMeta;
|
||||||
}
|
}
|
||||||
export interface KitsuMeta {
|
export interface IKitsuMeta {
|
||||||
aliases?: string[];
|
aliases?: string[];
|
||||||
animeType?: string;
|
animeType?: string;
|
||||||
background?: string;
|
background?: string;
|
||||||
@@ -15,7 +15,7 @@ export interface KitsuMeta {
|
|||||||
imdbRating?: string;
|
imdbRating?: string;
|
||||||
imdb_id?: string;
|
imdb_id?: string;
|
||||||
kitsu_id?: string;
|
kitsu_id?: string;
|
||||||
links?: KitsuLink[];
|
links?: IKitsuLink[];
|
||||||
logo?: string;
|
logo?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
@@ -23,23 +23,23 @@ export interface KitsuMeta {
|
|||||||
runtime?: string;
|
runtime?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
trailers?: KitsuTrailer[];
|
trailers?: IKitsuTrailer[];
|
||||||
type?: string;
|
type?: string;
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
videos?: KitsuVideo[];
|
videos?: IKitsuVideo[];
|
||||||
year?: string;
|
year?: string;
|
||||||
}
|
}
|
||||||
export interface KitsuVideo extends CommonVideoMetadata {
|
export interface IKitsuVideo extends ICommonVideoMetadata {
|
||||||
imdbEpisode?: number;
|
imdbEpisode?: number;
|
||||||
imdbSeason?: number;
|
imdbSeason?: number;
|
||||||
imdb_id?: string;
|
imdb_id?: string;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
}
|
}
|
||||||
export interface KitsuTrailer {
|
export interface IKitsuTrailer {
|
||||||
source?: string;
|
source?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
export interface KitsuLink {
|
export interface IKitsuLink {
|
||||||
name?: string;
|
name?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
|||||||
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
|
title?: string
|
||||||
type?: string
|
type?: string
|
||||||
year?: number | 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;
|
kitsuId?: number;
|
||||||
imdbId?: number;
|
imdbId?: number;
|
||||||
type?: string;
|
type?: string;
|
||||||
@@ -9,7 +9,7 @@ export interface MetadataResponse {
|
|||||||
country?: string;
|
country?: string;
|
||||||
genres?: string[];
|
genres?: string[];
|
||||||
status?: string;
|
status?: string;
|
||||||
videos?: CommonVideoMetadata[];
|
videos?: ICommonVideoMetadata[];
|
||||||
episodeCount?: number[];
|
episodeCount?: number[];
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
}
|
}
|
||||||
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;
|
title?: string;
|
||||||
date?: string;
|
date?: string;
|
||||||
year?: number | 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 {TorrentType} from "../enums/torrent_types";
|
||||||
import {TorrentFileCollection} from "./torrent_file_collection";
|
import {ITorrentFileCollection} from "./torrent_file_collection";
|
||||||
|
|
||||||
export interface ParsedTorrent extends ParseTorrentTitleResult {
|
export interface IParsedTorrent extends IParseTorrentTitleResult {
|
||||||
size?: number;
|
size?: number;
|
||||||
isPack?: boolean;
|
isPack?: boolean;
|
||||||
imdbId?: string | number;
|
imdbId?: string | number;
|
||||||
@@ -14,5 +14,5 @@ export interface ParsedTorrent extends ParseTorrentTitleResult {
|
|||||||
uploadDate?: Date;
|
uploadDate?: Date;
|
||||||
seeders?: number;
|
seeders?: number;
|
||||||
torrentId?: string;
|
torrentId?: string;
|
||||||
fileCollection?: TorrentFileCollection;
|
fileCollection?: ITorrentFileCollection;
|
||||||
}
|
}
|
||||||
@@ -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 {IContentAttributes} from "../../repository/interfaces/content_attributes";
|
||||||
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
import {IFileAttributes} from "../../repository/interfaces/file_attributes";
|
||||||
import {SubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
|
import {ISubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
|
||||||
|
|
||||||
export interface TorrentFileCollection {
|
export interface ITorrentFileCollection {
|
||||||
contents?: ContentAttributes[];
|
contents?: IContentAttributes[];
|
||||||
videos?: FileAttributes[];
|
videos?: IFileAttributes[];
|
||||||
subtitles?: SubtitleAttributes[];
|
subtitles?: ISubtitleAttributes[];
|
||||||
}
|
}
|
||||||
@@ -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 {Cache, createCache, memoryStore} from 'cache-manager';
|
||||||
import { mongoDbStore } from '@tirke/node-cache-manager-mongodb'
|
import {mongoDbStore} from '@tirke/node-cache-manager-mongodb'
|
||||||
import { configurationService } from './configuration_service';
|
import {configurationService} from './configuration_service';
|
||||||
import { logger } from './logging_service';
|
import {logger} from './logging_service';
|
||||||
import { CacheType } from "../enums/cache_types";
|
import {CacheType} from "../enums/cache_types";
|
||||||
import {CacheOptions} from "../interfaces/cache_options";
|
import {ICacheOptions} from "../interfaces/cache_options";
|
||||||
|
import {ICacheService} from "../interfaces/cache_service";
|
||||||
|
|
||||||
const GLOBAL_KEY_PREFIX = 'knightcrawler-consumer';
|
const GLOBAL_KEY_PREFIX = 'knightcrawler-consumer';
|
||||||
const IMDB_ID_PREFIX = `${GLOBAL_KEY_PREFIX}|imdb_id`;
|
const IMDB_ID_PREFIX = `${GLOBAL_KEY_PREFIX}|imdb_id`;
|
||||||
@@ -17,7 +18,7 @@ const TRACKERS_TTL: number = 2 * 24 * 60 * 60; // 2 days
|
|||||||
|
|
||||||
export type CacheMethod = () => any;
|
export type CacheMethod = () => any;
|
||||||
|
|
||||||
class CacheService {
|
class CacheService implements ICacheService {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!configurationService.cacheConfig.NO_CACHE) {
|
if (!configurationService.cacheConfig.NO_CACHE) {
|
||||||
logger.info('Cache is disabled');
|
logger.info('Cache is disabled');
|
||||||
@@ -29,16 +30,16 @@ class CacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public cacheWrapImdbId = (key: string, method: CacheMethod): Promise<any> =>
|
public cacheWrapImdbId = (key: string, method: CacheMethod): Promise<any> =>
|
||||||
this.cacheWrap(CacheType.MongoDb, `${IMDB_ID_PREFIX}:${key}`, method, { ttl: GLOBAL_TTL });
|
this.cacheWrap(CacheType.MongoDb, `${IMDB_ID_PREFIX}:${key}`, method, {ttl: GLOBAL_TTL});
|
||||||
|
|
||||||
public cacheWrapKitsuId = (key: string, method: CacheMethod): Promise<any> =>
|
public cacheWrapKitsuId = (key: string, method: CacheMethod): Promise<any> =>
|
||||||
this.cacheWrap(CacheType.MongoDb, `${KITSU_ID_PREFIX}:${key}`, method, { ttl: GLOBAL_TTL });
|
this.cacheWrap(CacheType.MongoDb, `${KITSU_ID_PREFIX}:${key}`, method, {ttl: GLOBAL_TTL});
|
||||||
|
|
||||||
public cacheWrapMetadata = (id: string, method: CacheMethod): Promise<any> =>
|
public cacheWrapMetadata = (id: string, method: CacheMethod): Promise<any> =>
|
||||||
this.cacheWrap(CacheType.Memory, `${METADATA_PREFIX}:${id}`, method, { ttl: MEMORY_TTL });
|
this.cacheWrap(CacheType.Memory, `${METADATA_PREFIX}:${id}`, method, {ttl: MEMORY_TTL});
|
||||||
|
|
||||||
public cacheTrackers = (method: CacheMethod): Promise<any> =>
|
public cacheTrackers = (method: CacheMethod): Promise<any> =>
|
||||||
this.cacheWrap(CacheType.Memory, `${TRACKERS_KEY_PREFIX}`, method, { ttl: TRACKERS_TTL });
|
this.cacheWrap(CacheType.Memory, `${TRACKERS_KEY_PREFIX}`, method, {ttl: TRACKERS_TTL});
|
||||||
|
|
||||||
private initiateMemoryCache = () =>
|
private initiateMemoryCache = () =>
|
||||||
createCache(memoryStore(), {
|
createCache(memoryStore(), {
|
||||||
@@ -85,7 +86,7 @@ class CacheService {
|
|||||||
private readonly remoteCache: Cache;
|
private readonly remoteCache: Cache;
|
||||||
|
|
||||||
private cacheWrap = async (
|
private cacheWrap = async (
|
||||||
cacheType: CacheType, key: string, method: CacheMethod, options: CacheOptions): Promise<any> => {
|
cacheType: CacheType, key: string, method: CacheMethod, options: ICacheOptions): Promise<any> => {
|
||||||
const cache = this.getCacheType(cacheType);
|
const cache = this.getCacheType(cacheType);
|
||||||
|
|
||||||
if (configurationService.cacheConfig.NO_CACHE || !cache) {
|
if (configurationService.cacheConfig.NO_CACHE || !cache) {
|
||||||
|
|||||||
@@ -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 {Logger, pino} from "pino";
|
||||||
|
import {ILoggingService} from "../interfaces/logging_service";
|
||||||
|
|
||||||
class LoggingService {
|
class LoggingService implements ILoggingService {
|
||||||
public readonly logger: Logger = pino({
|
private readonly logger: Logger;
|
||||||
level: process.env.LOG_LEVEL || 'info'
|
|
||||||
});
|
constructor() {
|
||||||
|
this.logger = pino({
|
||||||
|
level: process.env.LOG_LEVEL || 'info'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public info(message: string, ...args: any[]): void {
|
public info(message: string, ...args: any[]): void {
|
||||||
this.logger.info(message);
|
this.logger.info(message, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public error(message: string, ...args: any[]): void {
|
public error(message: string, ...args: any[]): void {
|
||||||
this.logger.error(message);
|
this.logger.error(message, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public debug(message: string, ...args: any[]): void {
|
public debug(message: string, ...args: any[]): void {
|
||||||
this.logger.debug(message);
|
this.logger.debug(message, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public warn(message: string, ...args: any[]): void {
|
public warn(message: string, ...args: any[]): void {
|
||||||
this.logger.warn(message);
|
this.logger.warn(message, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logger = new LoggingService();
|
export const logger = new LoggingService();
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
import axios, {AxiosResponse} from 'axios';
|
import axios, {AxiosResponse} from 'axios';
|
||||||
import {search, ResultTypes} from 'google-sr';
|
import {ResultTypes, search} from 'google-sr';
|
||||||
import nameToImdb from 'name-to-imdb';
|
import nameToImdb from 'name-to-imdb';
|
||||||
import { cacheService } from './cache_service';
|
import {cacheService} from './cache_service';
|
||||||
import { TorrentType } from '../enums/torrent_types';
|
import {TorrentType} from '../enums/torrent_types';
|
||||||
import {MetadataResponse} from "../interfaces/metadata_response";
|
import {IMetadataResponse} from "../interfaces/metadata_response";
|
||||||
import {CinemetaJsonResponse} from "../interfaces/cinemeta_metadata";
|
import {ICinemetaJsonResponse} from "../interfaces/cinemeta_metadata";
|
||||||
import {CommonVideoMetadata} from "../interfaces/common_video_metadata";
|
import {ICommonVideoMetadata} from "../interfaces/common_video_metadata";
|
||||||
import {KitsuJsonResponse} from "../interfaces/kitsu_metadata";
|
import {IKitsuJsonResponse} from "../interfaces/kitsu_metadata";
|
||||||
import {MetaDataQuery} from "../interfaces/metadata_query";
|
import {IMetaDataQuery} from "../interfaces/metadata_query";
|
||||||
import {KitsuCatalogJsonResponse} from "../interfaces/kitsu_catalog_metadata";
|
import {IKitsuCatalogJsonResponse} from "../interfaces/kitsu_catalog_metadata";
|
||||||
|
import {IMetadataService} from "../interfaces/metadata_service";
|
||||||
|
|
||||||
const CINEMETA_URL = 'https://v3-cinemeta.strem.io';
|
const CINEMETA_URL = 'https://v3-cinemeta.strem.io';
|
||||||
const KITSU_URL = 'https://anime-kitsu.strem.fun';
|
const KITSU_URL = 'https://anime-kitsu.strem.fun';
|
||||||
const TIMEOUT = 20000;
|
const TIMEOUT = 20000;
|
||||||
|
|
||||||
class MetadataService {
|
class MetadataService implements IMetadataService {
|
||||||
public async getKitsuId(info: MetaDataQuery): Promise<string | Error> {
|
public async getKitsuId(info: IMetaDataQuery): Promise<string | Error> {
|
||||||
const title = this.escapeTitle(info.title.replace(/\s\|\s.*/, ''));
|
const title = this.escapeTitle(info.title.replace(/\s\|\s.*/, ''));
|
||||||
const year = info.year ? ` ${info.year}` : '';
|
const year = info.year ? ` ${info.year}` : '';
|
||||||
const season = info.season > 1 ? ` S${info.season}` : '';
|
const season = info.season > 1 ? ` S${info.season}` : '';
|
||||||
@@ -23,9 +24,9 @@ class MetadataService {
|
|||||||
const query = encodeURIComponent(key);
|
const query = encodeURIComponent(key);
|
||||||
|
|
||||||
return cacheService.cacheWrapKitsuId(key,
|
return cacheService.cacheWrapKitsuId(key,
|
||||||
() => axios.get(`${KITSU_URL}/catalog/series/kitsu-anime-list/search=${query}.json`, { timeout: 60000 })
|
() => axios.get(`${KITSU_URL}/catalog/series/kitsu-anime-list/search=${query}.json`, {timeout: 60000})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const body = response.data as KitsuCatalogJsonResponse;
|
const body = response.data as IKitsuCatalogJsonResponse;
|
||||||
if (body && body.metas && body.metas.length) {
|
if (body && body.metas && body.metas.length) {
|
||||||
return body.metas[0].id.replace('kitsu:', '');
|
return body.metas[0].id.replace('kitsu:', '');
|
||||||
} else {
|
} else {
|
||||||
@@ -34,7 +35,7 @@ class MetadataService {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getImdbId(info: MetaDataQuery): Promise<string | undefined> {
|
public async getImdbId(info: IMetaDataQuery): Promise<string | undefined> {
|
||||||
const name = this.escapeTitle(info.title);
|
const name = this.escapeTitle(info.title);
|
||||||
const year = info.year || (info.date && info.date.slice(0, 4));
|
const year = info.year || (info.date && info.date.slice(0, 4));
|
||||||
const key = `${name}_${year || 'NA'}_${info.type}`;
|
const key = `${name}_${year || 'NA'}_${info.type}`;
|
||||||
@@ -53,7 +54,7 @@ class MetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMetadata(query: MetaDataQuery): Promise<MetadataResponse | Error> {
|
public getMetadata(query: IMetaDataQuery): Promise<IMetadataResponse | Error> {
|
||||||
if (!query.id) {
|
if (!query.id) {
|
||||||
return Promise.reject("no valid id provided");
|
return Promise.reject("no valid id provided");
|
||||||
}
|
}
|
||||||
@@ -93,14 +94,14 @@ class MetadataService {
|
|||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async requestMetadata(url: string): Promise<MetadataResponse> {
|
private async requestMetadata(url: string): Promise<IMetadataResponse> {
|
||||||
let response: AxiosResponse<any, any> = await axios.get(url, {timeout: TIMEOUT});
|
let response: AxiosResponse<any, any> = await axios.get(url, {timeout: TIMEOUT});
|
||||||
let result: MetadataResponse;
|
let result: IMetadataResponse;
|
||||||
const body = response.data;
|
const body = response.data;
|
||||||
if ('kitsu_id' in body.meta) {
|
if ('kitsu_id' in body.meta) {
|
||||||
result = this.handleKitsuResponse(body as KitsuJsonResponse);
|
result = this.handleKitsuResponse(body as IKitsuJsonResponse);
|
||||||
} else if ('imdb_id' in body.meta) {
|
} else if ('imdb_id' in body.meta) {
|
||||||
result = this.handleCinemetaResponse(body as CinemetaJsonResponse);
|
result = this.handleCinemetaResponse(body as ICinemetaJsonResponse);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No valid metadata');
|
throw new Error('No valid metadata');
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ class MetadataService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleCinemetaResponse(body: CinemetaJsonResponse): MetadataResponse {
|
private handleCinemetaResponse(body: ICinemetaJsonResponse): IMetadataResponse {
|
||||||
return {
|
return {
|
||||||
imdbId: parseInt(body.meta.imdb_id),
|
imdbId: parseInt(body.meta.imdb_id),
|
||||||
type: body.meta.type,
|
type: body.meta.type,
|
||||||
@@ -137,7 +138,7 @@ class MetadataService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleKitsuResponse(body: KitsuJsonResponse): MetadataResponse {
|
private handleKitsuResponse(body: IKitsuJsonResponse): IMetadataResponse {
|
||||||
return {
|
return {
|
||||||
kitsuId: parseInt(body.meta.kitsu_id),
|
kitsuId: parseInt(body.meta.kitsu_id),
|
||||||
type: body.meta.type,
|
type: body.meta.type,
|
||||||
@@ -167,7 +168,7 @@ class MetadataService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEpisodeCount(videos: CommonVideoMetadata[]) {
|
private getEpisodeCount(videos: ICommonVideoMetadata[]) {
|
||||||
return Object.values(
|
return Object.values(
|
||||||
videos
|
videos
|
||||||
.filter(entry => entry.season !== 0 && entry.episode !== 0)
|
.filter(entry => entry.season !== 0 && entry.episode !== 0)
|
||||||
@@ -179,7 +180,7 @@ class MetadataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getIMDbIdFromNameToImdb(name: string, info: MetaDataQuery): Promise<string | Error> {
|
private getIMDbIdFromNameToImdb(name: string, info: IMetaDataQuery): Promise<string | Error> {
|
||||||
const year = info.year;
|
const year = info.year;
|
||||||
const type = info.type;
|
const type = info.type;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import {encode} from 'magnet-uri';
|
import {encode} from 'magnet-uri';
|
||||||
import torrentStream from 'torrent-stream';
|
import torrentStream from 'torrent-stream';
|
||||||
import {configurationService} from './configuration_service';
|
import {configurationService} from './configuration_service';
|
||||||
import {extensionService} from './extension_service';
|
import {ExtensionHelpers} from '../helpers/extension_helpers';
|
||||||
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||||
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
import {IParsedTorrent} from "../interfaces/parsed_torrent";
|
||||||
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
import {IFileAttributes} from "../../repository/interfaces/file_attributes";
|
||||||
import {SubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
|
import {ISubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
|
||||||
import {ContentAttributes} from "../../repository/interfaces/content_attributes";
|
import {IContentAttributes} from "../../repository/interfaces/content_attributes";
|
||||||
import {parse} from "parse-torrent-title";
|
import {parse} from "parse-torrent-title";
|
||||||
|
import {ITorrentDownloadService} from "../interfaces/torrent_download_service";
|
||||||
|
|
||||||
interface TorrentFile {
|
interface ITorrentFile {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
length: number;
|
length: number;
|
||||||
fileIndex: number;
|
fileIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TorrentDownloadService {
|
class TorrentDownloadService implements ITorrentDownloadService {
|
||||||
private engineOptions: TorrentStream.TorrentEngineOptions = {
|
private engineOptions: TorrentStream.TorrentEngineOptions = {
|
||||||
connections: configurationService.torrentConfig.MAX_CONNECTIONS_PER_TORRENT,
|
connections: configurationService.torrentConfig.MAX_CONNECTIONS_PER_TORRENT,
|
||||||
uploads: 0,
|
uploads: 0,
|
||||||
@@ -25,8 +26,8 @@ class TorrentDownloadService {
|
|||||||
tracker: true,
|
tracker: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
public async getTorrentFiles(torrent: ParsedTorrent, timeout: number = 30000): Promise<TorrentFileCollection> {
|
public async getTorrentFiles(torrent: IParsedTorrent, timeout: number = 30000): Promise<ITorrentFileCollection> {
|
||||||
const torrentFiles: TorrentFile[] = await this.filesFromTorrentStream(torrent, timeout);
|
const torrentFiles: ITorrentFile[] = await this.filesFromTorrentStream(torrent, timeout);
|
||||||
|
|
||||||
const videos = this.filterVideos(torrent, torrentFiles);
|
const videos = this.filterVideos(torrent, torrentFiles);
|
||||||
const subtitles = this.filterSubtitles(torrent, torrentFiles);
|
const subtitles = this.filterSubtitles(torrent, torrentFiles);
|
||||||
@@ -39,7 +40,7 @@ class TorrentDownloadService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async filesFromTorrentStream(torrent: ParsedTorrent, timeout: number): Promise<TorrentFile[]> {
|
private async filesFromTorrentStream(torrent: IParsedTorrent, timeout: number): Promise<ITorrentFile[]> {
|
||||||
if (!torrent.infoHash) {
|
if (!torrent.infoHash) {
|
||||||
return Promise.reject(new Error("No infoHash..."));
|
return Promise.reject(new Error("No infoHash..."));
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ class TorrentDownloadService {
|
|||||||
engine = torrentStream(magnet, this.engineOptions);
|
engine = torrentStream(magnet, this.engineOptions);
|
||||||
|
|
||||||
engine.on("ready", () => {
|
engine.on("ready", () => {
|
||||||
const files: TorrentFile[] = engine.files.map((file, fileId) => ({
|
const files: ITorrentFile[] = engine.files.map((file, fileId) => ({
|
||||||
...file,
|
...file,
|
||||||
fileIndex: fileId,
|
fileIndex: fileId,
|
||||||
size: file.length,
|
size: file.length,
|
||||||
@@ -73,22 +74,22 @@ class TorrentDownloadService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterVideos(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): FileAttributes[] {
|
private filterVideos(torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): IFileAttributes[] {
|
||||||
if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) {
|
if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) {
|
||||||
return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])];
|
return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])];
|
||||||
}
|
}
|
||||||
const videos = torrentFiles.filter(file => extensionService.isVideo(file.path || ''));
|
const videos = torrentFiles.filter(file => ExtensionHelpers.isVideo(file.path || ''));
|
||||||
const maxSize = Math.max(...videos.map((video: TorrentFile) => video.length));
|
const maxSize = Math.max(...videos.map((video: ITorrentFile) => video.length));
|
||||||
const minSampleRatio = videos.length <= 3 ? 3 : 10;
|
const minSampleRatio = videos.length <= 3 ? 3 : 10;
|
||||||
const minAnimeExtraRatio = 5;
|
const minAnimeExtraRatio = 5;
|
||||||
const minRedundantRatio = videos.length <= 3 ? 30 : Number.MAX_VALUE;
|
const minRedundantRatio = videos.length <= 3 ? 30 : Number.MAX_VALUE;
|
||||||
|
|
||||||
const isSample = (video: TorrentFile) => video.path?.match(/sample|bonus|promo/i) && maxSize / parseInt(video.path.toString()) > minSampleRatio;
|
const isSample = (video: ITorrentFile) => video.path?.match(/sample|bonus|promo/i) && maxSize / parseInt(video.path.toString()) > minSampleRatio;
|
||||||
const isRedundant = (video: TorrentFile) => maxSize / parseInt(video.path.toString()) > minRedundantRatio;
|
const isRedundant = (video: ITorrentFile) => maxSize / parseInt(video.path.toString()) > minRedundantRatio;
|
||||||
const isExtra = (video: TorrentFile) => video.path?.match(/extras?\//i);
|
const isExtra = (video: ITorrentFile) => video.path?.match(/extras?\//i);
|
||||||
const isAnimeExtra = (video: TorrentFile) => video.path?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i)
|
const isAnimeExtra = (video: ITorrentFile) => video.path?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i)
|
||||||
&& maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio;
|
&& maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio;
|
||||||
const isWatermark = (video: TorrentFile) => video.path?.match(/^[A-Z-]+(?:\.[A-Z]+)?\.\w{3,4}$/)
|
const isWatermark = (video: ITorrentFile) => video.path?.match(/^[A-Z-]+(?:\.[A-Z]+)?\.\w{3,4}$/)
|
||||||
&& maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio
|
&& maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio
|
||||||
|
|
||||||
return videos
|
return videos
|
||||||
@@ -100,17 +101,17 @@ class TorrentDownloadService {
|
|||||||
.map(video => this.mapTorrentFileToFileAttributes(torrent, video));
|
.map(video => this.mapTorrentFileToFileAttributes(torrent, video));
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterSubtitles(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): SubtitleAttributes[] {
|
private filterSubtitles(torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): ISubtitleAttributes[] {
|
||||||
return torrentFiles.filter(file => extensionService.isSubtitle(file.name || ''))
|
return torrentFiles.filter(file => ExtensionHelpers.isSubtitle(file.name || ''))
|
||||||
.map(file => this.mapTorrentFileToSubtitleAttributes(torrent, file));
|
.map(file => this.mapTorrentFileToSubtitleAttributes(torrent, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
private createContent(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): ContentAttributes[] {
|
private createContent(torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): IContentAttributes[] {
|
||||||
return torrentFiles.map(file => this.mapTorrentFileToContentAttributes(torrent, file));
|
return torrentFiles.map(file => this.mapTorrentFileToContentAttributes(torrent, file));
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTorrentFileToFileAttributes(torrent: ParsedTorrent, file: TorrentFile): FileAttributes {
|
private mapTorrentFileToFileAttributes(torrent: IParsedTorrent, file: ITorrentFile): IFileAttributes {
|
||||||
const videoFile: FileAttributes = {
|
const videoFile: IFileAttributes = {
|
||||||
title: file.name,
|
title: file.name,
|
||||||
size: file.length,
|
size: file.length,
|
||||||
fileIndex: file.fileIndex || 0,
|
fileIndex: file.fileIndex || 0,
|
||||||
@@ -125,7 +126,7 @@ class TorrentDownloadService {
|
|||||||
return {...videoFile, ...parse(file.name)};
|
return {...videoFile, ...parse(file.name)};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTorrentFileToSubtitleAttributes(torrent: ParsedTorrent, file: TorrentFile): SubtitleAttributes {
|
private mapTorrentFileToSubtitleAttributes(torrent: IParsedTorrent, file: ITorrentFile): ISubtitleAttributes {
|
||||||
return {
|
return {
|
||||||
title: file.name,
|
title: file.name,
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
@@ -135,7 +136,7 @@ class TorrentDownloadService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapTorrentFileToContentAttributes(torrent: ParsedTorrent, file: TorrentFile): ContentAttributes {
|
private mapTorrentFileToContentAttributes(torrent: IParsedTorrent, file: ITorrentFile): IContentAttributes {
|
||||||
return {
|
return {
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
fileIndex: file.fileIndex,
|
fileIndex: file.fileIndex,
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import {parse} from 'parse-torrent-title';
|
import {parse} from 'parse-torrent-title';
|
||||||
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
import {IParsedTorrent} from "../interfaces/parsed_torrent";
|
||||||
import {repository} from '../../repository/database_repository';
|
import {repository} from '../../repository/database_repository';
|
||||||
import {TorrentType} from '../enums/torrent_types';
|
import {TorrentType} from '../enums/torrent_types';
|
||||||
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||||
import {Torrent} from "../../repository/models/torrent";
|
import {Torrent} from "../../repository/models/torrent";
|
||||||
import {PromiseHelpers} from '../helpers/promises_helpers';
|
import {PromiseHelpers} from '../helpers/promises_helpers';
|
||||||
import {logger} from './logging_service';
|
import {logger} from './logging_service';
|
||||||
import {metadataService} from './metadata_service';
|
import {metadataService} from './metadata_service';
|
||||||
import {torrentFileService} from './torrent_file_service';
|
import {torrentFileService} from './torrent_file_service';
|
||||||
import {torrentSubtitleService} from './torrent_subtitle_service';
|
import {torrentSubtitleService} from './torrent_subtitle_service';
|
||||||
import {TorrentAttributes} from "../../repository/interfaces/torrent_attributes";
|
import {ITorrentAttributes} from "../../repository/interfaces/torrent_attributes";
|
||||||
import {File} from "../../repository/models/file";
|
import {File} from "../../repository/models/file";
|
||||||
import {Subtitle} from "../../repository/models/subtitle";
|
import {Subtitle} from "../../repository/models/subtitle";
|
||||||
|
import {ITorrentEntriesService} from "../interfaces/torrent_entries_service";
|
||||||
|
|
||||||
class TorrentEntriesService {
|
class TorrentEntriesService implements ITorrentEntriesService {
|
||||||
public async createTorrentEntry(torrent: ParsedTorrent, overwrite = false): Promise<void> {
|
public async createTorrentEntry(torrent: IParsedTorrent, overwrite = false): Promise<void> {
|
||||||
const titleInfo = parse(torrent.title);
|
const titleInfo = parse(torrent.title);
|
||||||
|
|
||||||
if (!torrent.imdbId && torrent.type !== TorrentType.Anime) {
|
if (!torrent.imdbId && torrent.type !== TorrentType.Anime) {
|
||||||
@@ -49,9 +50,9 @@ class TorrentEntriesService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileCollection: TorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
|
const fileCollection: ITorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
|
||||||
.then((torrentContents: TorrentFileCollection) => overwrite ? this.overwriteExistingFiles(torrent, torrentContents) : torrentContents)
|
.then((torrentContents: ITorrentFileCollection) => overwrite ? this.overwriteExistingFiles(torrent, torrentContents) : torrentContents)
|
||||||
.then((torrentContents: TorrentFileCollection) => torrentSubtitleService.assignSubtitles(torrentContents))
|
.then((torrentContents: ITorrentFileCollection) => torrentSubtitleService.assignSubtitles(torrentContents))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
logger.warn(`Failed getting files for ${torrent.title}`, error.message);
|
logger.warn(`Failed getting files for ${torrent.title}`, error.message);
|
||||||
return {};
|
return {};
|
||||||
@@ -86,8 +87,8 @@ class TorrentEntriesService {
|
|||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkAndUpdateTorrent(torrent: ParsedTorrent): Promise<boolean> {
|
public async checkAndUpdateTorrent(torrent: IParsedTorrent): Promise<boolean> {
|
||||||
const query: TorrentAttributes = {
|
const query: ITorrentAttributes = {
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
provider: torrent.provider,
|
provider: torrent.provider,
|
||||||
}
|
}
|
||||||
@@ -128,7 +129,7 @@ class TorrentEntriesService {
|
|||||||
const imdbId: string | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.imdbId));
|
const imdbId: string | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.imdbId));
|
||||||
const kitsuId: number | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.kitsuId));
|
const kitsuId: number | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.kitsuId));
|
||||||
|
|
||||||
const fileCollection: TorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
|
const fileCollection: ITorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
|
||||||
.then(torrentContents => notOpenedVideo ? torrentContents : {...torrentContents, videos: storedVideos})
|
.then(torrentContents => notOpenedVideo ? torrentContents : {...torrentContents, videos: storedVideos})
|
||||||
.then(torrentContents => torrentSubtitleService.assignSubtitles(torrentContents))
|
.then(torrentContents => torrentSubtitleService.assignSubtitles(torrentContents))
|
||||||
.then(torrentContents => this.assignMetaIds(torrentContents, imdbId, kitsuId))
|
.then(torrentContents => this.assignMetaIds(torrentContents, imdbId, kitsuId))
|
||||||
@@ -176,7 +177,7 @@ class TorrentEntriesService {
|
|||||||
.catch(error => logger.error(`Failed saving contents for [${torrent.infoHash}] ${torrent.title}`, error));
|
.catch(error => logger.error(`Failed saving contents for [${torrent.infoHash}] ${torrent.title}`, error));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateTorrentSeeders(torrent: TorrentAttributes) {
|
public async updateTorrentSeeders(torrent: ITorrentAttributes) {
|
||||||
if (!(torrent.infoHash || (torrent.provider && torrent.torrentId)) || !Number.isInteger(torrent.seeders)) {
|
if (!(torrent.infoHash || (torrent.provider && torrent.torrentId)) || !Number.isInteger(torrent.seeders)) {
|
||||||
return torrent;
|
return torrent;
|
||||||
}
|
}
|
||||||
@@ -188,7 +189,7 @@ class TorrentEntriesService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private assignMetaIds(fileCollection: TorrentFileCollection, imdbId: string, kitsuId: number): TorrentFileCollection {
|
private assignMetaIds(fileCollection: ITorrentFileCollection, imdbId: string, kitsuId: number): ITorrentFileCollection {
|
||||||
if (fileCollection.videos && fileCollection.videos.length) {
|
if (fileCollection.videos && fileCollection.videos.length) {
|
||||||
fileCollection.videos.forEach(video => {
|
fileCollection.videos.forEach(video => {
|
||||||
video.imdbId = imdbId;
|
video.imdbId = imdbId;
|
||||||
@@ -199,7 +200,7 @@ class TorrentEntriesService {
|
|||||||
return fileCollection;
|
return fileCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async overwriteExistingFiles(torrent: ParsedTorrent, torrentContents: TorrentFileCollection) {
|
private async overwriteExistingFiles(torrent: IParsedTorrent, torrentContents: ITorrentFileCollection) {
|
||||||
const videos = torrentContents && torrentContents.videos;
|
const videos = torrentContents && torrentContents.videos;
|
||||||
if (videos && videos.length) {
|
if (videos && videos.length) {
|
||||||
const existingFiles = await repository.getFiles(torrent.infoHash)
|
const existingFiles = await repository.getFiles(torrent.infoHash)
|
||||||
|
|||||||
@@ -4,30 +4,31 @@ import {parse} from 'parse-torrent-title';
|
|||||||
import {PromiseHelpers} from '../helpers/promises_helpers';
|
import {PromiseHelpers} from '../helpers/promises_helpers';
|
||||||
import {TorrentType} from '../enums/torrent_types';
|
import {TorrentType} from '../enums/torrent_types';
|
||||||
import {configurationService} from './configuration_service';
|
import {configurationService} from './configuration_service';
|
||||||
import {extensionService} from './extension_service';
|
import {ExtensionHelpers} from '../helpers/extension_helpers';
|
||||||
import {metadataService} from './metadata_service';
|
import {metadataService} from './metadata_service';
|
||||||
import {torrentDownloadService} from "./torrent_download_service";
|
import {torrentDownloadService} from "./torrent_download_service";
|
||||||
import {logger} from "./logging_service";
|
import {logger} from "./logging_service";
|
||||||
import {MetadataResponse} from "../interfaces/metadata_response";
|
import {IMetadataResponse} from "../interfaces/metadata_response";
|
||||||
import {MetaDataQuery} from "../interfaces/metadata_query";
|
import {IMetaDataQuery} from "../interfaces/metadata_query";
|
||||||
import {CommonVideoMetadata} from "../interfaces/common_video_metadata";
|
import {ICommonVideoMetadata} from "../interfaces/common_video_metadata";
|
||||||
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||||
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
import {IParsedTorrent} from "../interfaces/parsed_torrent";
|
||||||
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
import {IFileAttributes} from "../../repository/interfaces/file_attributes";
|
||||||
import {ContentAttributes} from "../../repository/interfaces/content_attributes";
|
import {IContentAttributes} from "../../repository/interfaces/content_attributes";
|
||||||
|
import {ITorrentFileService} from "../interfaces/torrent_file_service";
|
||||||
|
|
||||||
const MIN_SIZE: number = 5 * 1024 * 1024; // 5 MB
|
const MIN_SIZE: number = 5 * 1024 * 1024; // 5 MB
|
||||||
const MULTIPLE_FILES_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB
|
const MULTIPLE_FILES_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB
|
||||||
|
|
||||||
class TorrentFileService {
|
class TorrentFileService implements ITorrentFileService {
|
||||||
private readonly imdb_limiter: Bottleneck = new Bottleneck({
|
private readonly imdb_limiter: Bottleneck = new Bottleneck({
|
||||||
maxConcurrent: configurationService.metadataConfig.IMDB_CONCURRENT,
|
maxConcurrent: configurationService.metadataConfig.IMDB_CONCURRENT,
|
||||||
minTime: configurationService.metadataConfig.IMDB_INTERVAL_MS
|
minTime: configurationService.metadataConfig.IMDB_INTERVAL_MS
|
||||||
});
|
});
|
||||||
|
|
||||||
public async parseTorrentFiles(torrent: ParsedTorrent): Promise<TorrentFileCollection> {
|
public async parseTorrentFiles(torrent: IParsedTorrent): Promise<ITorrentFileCollection> {
|
||||||
const parsedTorrentName = parse(torrent.title);
|
const parsedTorrentName = parse(torrent.title);
|
||||||
const query: MetaDataQuery = {
|
const query: IMetaDataQuery = {
|
||||||
id: torrent.kitsuId || torrent.imdbId,
|
id: torrent.kitsuId || torrent.imdbId,
|
||||||
type: torrent.type || TorrentType.Movie,
|
type: torrent.type || TorrentType.Movie,
|
||||||
};
|
};
|
||||||
@@ -48,7 +49,7 @@ class TorrentFileService {
|
|||||||
return this.parseSeriesFiles(torrent, metadata)
|
return this.parseSeriesFiles(torrent, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
public isPackTorrent(torrent: ParsedTorrent): boolean {
|
public isPackTorrent(torrent: IParsedTorrent): boolean {
|
||||||
if (torrent.isPack) {
|
if (torrent.isPack) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -65,7 +66,7 @@ class TorrentFileService {
|
|||||||
return hasMultipleEpisodes && !hasSingleEpisode;
|
return hasMultipleEpisodes && !hasSingleEpisode;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseSeriesVideos(torrent: ParsedTorrent, videos: FileAttributes[]): FileAttributes[] {
|
private parseSeriesVideos(torrent: IParsedTorrent, videos: IFileAttributes[]): IFileAttributes[] {
|
||||||
const parsedTorrentName = parse(torrent.title);
|
const parsedTorrentName = parse(torrent.title);
|
||||||
const hasMovies = parsedTorrentName.complete || !!torrent.title.match(/movies?(?:\W|$)/i);
|
const hasMovies = parsedTorrentName.complete || !!torrent.title.match(/movies?(?:\W|$)/i);
|
||||||
const parsedVideos = videos.map(video => this.parseSeriesVideo(video));
|
const parsedVideos = videos.map(video => this.parseSeriesVideo(video));
|
||||||
@@ -73,8 +74,8 @@ class TorrentFileService {
|
|||||||
return parsedVideos.map(video => ({ ...video, isMovie: this.isMovieVideo(torrent, video, parsedVideos, hasMovies) }));
|
return parsedVideos.map(video => ({ ...video, isMovie: this.isMovieVideo(torrent, video, parsedVideos, hasMovies) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async parseMovieFiles(torrent: ParsedTorrent, metadata: MetadataResponse): Promise<TorrentFileCollection> {
|
private async parseMovieFiles(torrent: IParsedTorrent, metadata: IMetadataResponse): Promise<ITorrentFileCollection> {
|
||||||
const fileCollection: TorrentFileCollection = await this.getMoviesTorrentContent(torrent);
|
const fileCollection: ITorrentFileCollection = await this.getMoviesTorrentContent(torrent);
|
||||||
const filteredVideos = fileCollection.videos
|
const filteredVideos = fileCollection.videos
|
||||||
.filter(video => video.size > MIN_SIZE)
|
.filter(video => video.size > MIN_SIZE)
|
||||||
.filter(video => !this.isFeaturette(video));
|
.filter(video => !this.isFeaturette(video));
|
||||||
@@ -103,9 +104,9 @@ class TorrentFileService {
|
|||||||
return {...fileCollection, videos: parsedVideos};
|
return {...fileCollection, videos: parsedVideos};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async parseSeriesFiles(torrent: ParsedTorrent, metadata: MetadataResponse): Promise<TorrentFileCollection> {
|
private async parseSeriesFiles(torrent: IParsedTorrent, metadata: IMetadataResponse): Promise<ITorrentFileCollection> {
|
||||||
const fileCollection: TorrentFileCollection = await this.getSeriesTorrentContent(torrent);
|
const fileCollection: ITorrentFileCollection = await this.getSeriesTorrentContent(torrent);
|
||||||
const parsedVideos: FileAttributes[] = await Promise.resolve(fileCollection.videos)
|
const parsedVideos: IFileAttributes[] = await Promise.resolve(fileCollection.videos)
|
||||||
.then(videos => videos.filter(video => videos.length === 1 || video.size > MIN_SIZE))
|
.then(videos => videos.filter(video => videos.length === 1 || video.size > MIN_SIZE))
|
||||||
.then(videos => this.parseSeriesVideos(torrent, videos))
|
.then(videos => this.parseSeriesVideos(torrent, videos))
|
||||||
.then(videos => this.decomposeEpisodes(torrent, videos, metadata))
|
.then(videos => this.decomposeEpisodes(torrent, videos, metadata))
|
||||||
@@ -119,7 +120,7 @@ class TorrentFileService {
|
|||||||
return {...torrent.fileCollection, videos: parsedVideos};
|
return {...torrent.fileCollection, videos: parsedVideos};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMoviesTorrentContent(torrent: ParsedTorrent): Promise<TorrentFileCollection> {
|
private async getMoviesTorrentContent(torrent: IParsedTorrent): Promise<ITorrentFileCollection> {
|
||||||
const files = await torrentDownloadService.getTorrentFiles(torrent)
|
const files = await torrentDownloadService.getTorrentFiles(torrent)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (!this.isPackTorrent(torrent)) {
|
if (!this.isPackTorrent(torrent)) {
|
||||||
@@ -135,11 +136,11 @@ class TorrentFileService {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDefaultFileEntries(torrent: ParsedTorrent): FileAttributes[] {
|
private getDefaultFileEntries(torrent: IParsedTorrent): IFileAttributes[] {
|
||||||
return [{title: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}];
|
return [{title: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSeriesTorrentContent(torrent: ParsedTorrent): Promise<TorrentFileCollection> {
|
private async getSeriesTorrentContent(torrent: IParsedTorrent): Promise<ITorrentFileCollection> {
|
||||||
return torrentDownloadService.getTorrentFiles(torrent)
|
return torrentDownloadService.getTorrentFiles(torrent)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (!this.isPackTorrent(torrent)) {
|
if (!this.isPackTorrent(torrent)) {
|
||||||
@@ -149,7 +150,7 @@ class TorrentFileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async mapSeriesEpisode(torrent: ParsedTorrent, file: FileAttributes, files: FileAttributes[]) : Promise<FileAttributes[]> {
|
private async mapSeriesEpisode(torrent: IParsedTorrent, file: IFileAttributes, files: IFileAttributes[]) : Promise<IFileAttributes[]> {
|
||||||
if (!file.episodes && !file.episodes) {
|
if (!file.episodes && !file.episodes) {
|
||||||
if (files.length === 1 || files.some(f => f.episodes || f.episodes) || parse(torrent.title).seasons) {
|
if (files.length === 1 || files.some(f => f.episodes || f.episodes) || parse(torrent.title).seasons) {
|
||||||
return Promise.resolve([{
|
return Promise.resolve([{
|
||||||
@@ -179,7 +180,7 @@ class TorrentFileService {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async mapSeriesMovie(torrent: ParsedTorrent, file: FileAttributes): Promise<FileAttributes[]> {
|
private async mapSeriesMovie(torrent: IParsedTorrent, file: IFileAttributes): Promise<IFileAttributes[]> {
|
||||||
const kitsuId= torrent.type === TorrentType.Anime ? await this.findMovieKitsuId(file)
|
const kitsuId= torrent.type === TorrentType.Anime ? await this.findMovieKitsuId(file)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result instanceof Error) {
|
if (result instanceof Error) {
|
||||||
@@ -191,7 +192,7 @@ class TorrentFileService {
|
|||||||
|
|
||||||
const imdbId = !kitsuId ? await this.findMovieImdbId(file) : undefined;
|
const imdbId = !kitsuId ? await this.findMovieImdbId(file) : undefined;
|
||||||
|
|
||||||
const query: MetaDataQuery = {
|
const query: IMetaDataQuery = {
|
||||||
id: kitsuId || imdbId,
|
id: kitsuId || imdbId,
|
||||||
type: TorrentType.Movie
|
type: TorrentType.Movie
|
||||||
};
|
};
|
||||||
@@ -230,7 +231,7 @@ class TorrentFileService {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async decomposeEpisodes(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse = { episodeCount: [] }) {
|
private async decomposeEpisodes(torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse = { episodeCount: [] }) {
|
||||||
if (files.every(file => !file.episodes && !file.date)) {
|
if (files.every(file => !file.episodes && !file.date)) {
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
@@ -275,7 +276,7 @@ class TorrentFileService {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private preprocessEpisodes(files: FileAttributes[]) {
|
private preprocessEpisodes(files: IFileAttributes[]) {
|
||||||
// reverse special episode naming when they named with 0 episode, ie. S02E00
|
// reverse special episode naming when they named with 0 episode, ie. S02E00
|
||||||
files
|
files
|
||||||
.filter(file => Number.isInteger(file.season) && file.episode === 0)
|
.filter(file => Number.isInteger(file.season) && file.episode === 0)
|
||||||
@@ -286,7 +287,7 @@ class TorrentFileService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private isConcatSeasonAndEpisodeFiles(files: FileAttributes[], sortedEpisodes: number[], metadata: MetadataResponse) {
|
private isConcatSeasonAndEpisodeFiles(files: IFileAttributes[], sortedEpisodes: number[], metadata: IMetadataResponse) {
|
||||||
if (metadata.kitsuId !== undefined) {
|
if (metadata.kitsuId !== undefined) {
|
||||||
// anime does not use this naming scheme in 99% of cases;
|
// anime does not use this naming scheme in 99% of cases;
|
||||||
return false;
|
return false;
|
||||||
@@ -313,11 +314,11 @@ class TorrentFileService {
|
|||||||
|| concatAboveTotalEpisodeCount.length >= thresholdAbove;
|
|| concatAboveTotalEpisodeCount.length >= thresholdAbove;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isDateEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) {
|
private isDateEpisodeFiles(files: IFileAttributes[], metadata: IMetadataResponse) {
|
||||||
return files.every(file => (!file.season || !metadata.episodeCount[file.season - 1]) && file.date);
|
return files.every(file => (!file.season || !metadata.episodeCount[file.season - 1]) && file.date);
|
||||||
}
|
}
|
||||||
|
|
||||||
private isAbsoluteEpisodeFiles(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse) {
|
private isAbsoluteEpisodeFiles(torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse) {
|
||||||
const threshold = Math.ceil(files.length / 5);
|
const threshold = Math.ceil(files.length / 5);
|
||||||
const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId;
|
const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId;
|
||||||
const nonMovieEpisodes = files
|
const nonMovieEpisodes = files
|
||||||
@@ -330,7 +331,7 @@ class TorrentFileService {
|
|||||||
|| absoluteEpisodes.length >= threshold;
|
|| absoluteEpisodes.length >= threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isNewEpisodeNotInMetadata(torrent: ParsedTorrent, video: FileAttributes, metadata: MetadataResponse) {
|
private isNewEpisodeNotInMetadata(torrent: IParsedTorrent, video: IFileAttributes, metadata: IMetadataResponse) {
|
||||||
// new episode might not yet been indexed by cinemeta.
|
// new episode might not yet been indexed by cinemeta.
|
||||||
// detect this if episode number is larger than the last episode or season is larger than the last one
|
// detect this if episode number is larger than the last episode or season is larger than the last one
|
||||||
// only for non anime metas
|
// only for non anime metas
|
||||||
@@ -341,7 +342,7 @@ class TorrentFileService {
|
|||||||
&& video.episodes.every(ep => ep > (metadata.episodeCount[video.season - 1] || 0));
|
&& video.episodes.every(ep => ep > (metadata.episodeCount[video.season - 1] || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private decomposeConcatSeasonAndEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) {
|
private decomposeConcatSeasonAndEpisodeFiles(files: IFileAttributes[], metadata: IMetadataResponse) {
|
||||||
files
|
files
|
||||||
.filter(file => file.episodes && file.season !== 0 && file.episodes.every(ep => ep > 100))
|
.filter(file => file.episodes && file.season !== 0 && file.episodes.every(ep => ep > 100))
|
||||||
.filter(file => metadata.episodeCount[(file.season || this.div100(file.episodes[0])) - 1] < 100)
|
.filter(file => metadata.episodeCount[(file.season || this.div100(file.episodes[0])) - 1] < 100)
|
||||||
@@ -353,7 +354,7 @@ class TorrentFileService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private decomposeAbsoluteEpisodeFiles(torrent: ParsedTorrent, videos: FileAttributes[], metadata: MetadataResponse) {
|
private decomposeAbsoluteEpisodeFiles(torrent: IParsedTorrent, videos: IFileAttributes[], metadata: IMetadataResponse) {
|
||||||
if (metadata.episodeCount.length === 0) {
|
if (metadata.episodeCount.length === 0) {
|
||||||
videos
|
videos
|
||||||
.filter(file => !Number.isInteger(file.season) && file.episodes && !file.isMovie)
|
.filter(file => !Number.isInteger(file.season) && file.episodes && !file.isMovie)
|
||||||
@@ -377,7 +378,7 @@ class TorrentFileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private decomposeDateEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) {
|
private decomposeDateEpisodeFiles(files: IFileAttributes[], metadata: IMetadataResponse) {
|
||||||
if (!metadata || !metadata.videos || !metadata.videos.length) {
|
if (!metadata || !metadata.videos || !metadata.videos.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -411,7 +412,7 @@ class TorrentFileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private assignKitsuOrImdbEpisodes(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse) {
|
private assignKitsuOrImdbEpisodes(torrent: IParsedTorrent, files: IFileAttributes[], metadata: IMetadataResponse) {
|
||||||
if (!metadata || !metadata.videos || !metadata.videos.length) {
|
if (!metadata || !metadata.videos || !metadata.videos.length) {
|
||||||
if (torrent.type === TorrentType.Anime) {
|
if (torrent.type === TorrentType.Anime) {
|
||||||
// assign episodes as kitsu episodes for anime when no metadata available for imdb mapping
|
// assign episodes as kitsu episodes for anime when no metadata available for imdb mapping
|
||||||
@@ -429,7 +430,7 @@ class TorrentFileService {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
const seriesMapping: CommonVideoMetadata = metadata.videos
|
const seriesMapping: ICommonVideoMetadata = metadata.videos
|
||||||
.reduce((map, video) => {
|
.reduce((map, video) => {
|
||||||
const episodeMap = map[video.season] || {};
|
const episodeMap = map[video.season] || {};
|
||||||
episodeMap[video.episode] = video;
|
episodeMap[video.episode] = video;
|
||||||
@@ -464,13 +465,13 @@ class TorrentFileService {
|
|||||||
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode);
|
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode);
|
||||||
} else if (seriesMapping[file.season - 1]) {
|
} else if (seriesMapping[file.season - 1]) {
|
||||||
// sometimes a second season might be a continuation of the previous season
|
// sometimes a second season might be a continuation of the previous season
|
||||||
const seasonMapping = seriesMapping[file.season - 1] as CommonVideoMetadata;
|
const seasonMapping = seriesMapping[file.season - 1] as ICommonVideoMetadata;
|
||||||
const episodes = Object.values(seasonMapping);
|
const episodes = Object.values(seasonMapping);
|
||||||
const firstKitsuId = episodes.length && episodes[0];
|
const firstKitsuId = episodes.length && episodes[0];
|
||||||
const differentTitlesCount = new Set(episodes.map(ep => ep.id)).size
|
const differentTitlesCount = new Set(episodes.map(ep => ep.id)).size
|
||||||
const skippedCount = episodes.filter(ep => ep.id === firstKitsuId).length;
|
const skippedCount = episodes.filter(ep => ep.id === firstKitsuId).length;
|
||||||
const seasonEpisodes = files
|
const seasonEpisodes = files
|
||||||
.filter((otherFile: FileAttributes) => otherFile.season === file.season)
|
.filter((otherFile: IFileAttributes) => otherFile.season === file.season)
|
||||||
.reduce((a, b) => a.concat(b.episodes), []);
|
.reduce((a, b) => a.concat(b.episodes), []);
|
||||||
const isAbsoluteOrder = seasonEpisodes.every(ep => ep > skippedCount && ep <= episodes.length)
|
const isAbsoluteOrder = seasonEpisodes.every(ep => ep > skippedCount && ep <= episodes.length)
|
||||||
const isNormalOrder = seasonEpisodes.every(ep => ep + skippedCount <= episodes.length)
|
const isNormalOrder = seasonEpisodes.every(ep => ep + skippedCount <= episodes.length)
|
||||||
@@ -494,7 +495,7 @@ class TorrentFileService {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private needsCinemetaMetadataForAnime(files: FileAttributes[], metadata: MetadataResponse) {
|
private needsCinemetaMetadataForAnime(files: IFileAttributes[], metadata: IMetadataResponse) {
|
||||||
if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) {
|
if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -510,8 +511,8 @@ class TorrentFileService {
|
|||||||
.some(file => file.season < minSeason || file.season > maxSeason || file.episodes.every(ep => ep > total));
|
.some(file => file.season < minSeason || file.season > maxSeason || file.episodes.every(ep => ep > total));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateToCinemetaMetadata(metadata: MetadataResponse) {
|
private async updateToCinemetaMetadata(metadata: IMetadataResponse) {
|
||||||
const query: MetaDataQuery = {
|
const query: IMetaDataQuery = {
|
||||||
id: metadata.imdbId,
|
id: metadata.imdbId,
|
||||||
type: metadata.type
|
type: metadata.type
|
||||||
};
|
};
|
||||||
@@ -536,7 +537,7 @@ class TorrentFileService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private findMovieImdbId(title: FileAttributes | string) {
|
private findMovieImdbId(title: IFileAttributes | string) {
|
||||||
const parsedTitle = typeof title === 'string' ? parse(title) : title;
|
const parsedTitle = typeof title === 'string' ? parse(title) : title;
|
||||||
logger.debug(`Finding movie imdbId for ${title}`);
|
logger.debug(`Finding movie imdbId for ${title}`);
|
||||||
return this.imdb_limiter.schedule(async () => {
|
return this.imdb_limiter.schedule(async () => {
|
||||||
@@ -553,7 +554,7 @@ class TorrentFileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findMovieKitsuId(title: FileAttributes | string) {
|
private async findMovieKitsuId(title: IFileAttributes | string) {
|
||||||
const parsedTitle = typeof title === 'string' ? parse(title) : title;
|
const parsedTitle = typeof title === 'string' ? parse(title) : title;
|
||||||
const kitsuQuery = {
|
const kitsuQuery = {
|
||||||
title: parsedTitle.title,
|
title: parsedTitle.title,
|
||||||
@@ -568,22 +569,22 @@ class TorrentFileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private isDiskTorrent(contents: ContentAttributes[]) {
|
private isDiskTorrent(contents: IContentAttributes[]) {
|
||||||
return contents.some(content => extensionService.isDisk(content.path));
|
return contents.some(content => ExtensionHelpers.isDisk(content.path));
|
||||||
}
|
}
|
||||||
|
|
||||||
private isSingleMovie(videos: FileAttributes[]) {
|
private isSingleMovie(videos: IFileAttributes[]) {
|
||||||
return videos.length === 1 ||
|
return videos.length === 1 ||
|
||||||
(videos.length === 2 &&
|
(videos.length === 2 &&
|
||||||
videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?1\b|^0?1\.\w{2,4}$/i.test(v.path)) &&
|
videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?1\b|^0?1\.\w{2,4}$/i.test(v.path)) &&
|
||||||
videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?2\b|^0?2\.\w{2,4}$/i.test(v.path)));
|
videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?2\b|^0?2\.\w{2,4}$/i.test(v.path)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private isFeaturette(video: FileAttributes) {
|
private isFeaturette(video: IFileAttributes) {
|
||||||
return /featurettes?\/|extras-grym/i.test(video.path);
|
return /featurettes?\/|extras-grym/i.test(video.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseSeriesVideo(video: FileAttributes): FileAttributes {
|
private parseSeriesVideo(video: IFileAttributes): IFileAttributes {
|
||||||
const videoInfo = parse(video.title);
|
const videoInfo = parse(video.title);
|
||||||
// the episode may be in a folder containing season number
|
// the episode may be in a folder containing season number
|
||||||
if (!Number.isInteger(videoInfo.season) && video.path.includes('/')) {
|
if (!Number.isInteger(videoInfo.season) && video.path.includes('/')) {
|
||||||
@@ -629,7 +630,7 @@ class TorrentFileService {
|
|||||||
return { ...video, ...videoInfo };
|
return { ...video, ...videoInfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
private isMovieVideo(torrent: ParsedTorrent, video: FileAttributes, otherVideos: FileAttributes[], hasMovies: boolean): boolean {
|
private isMovieVideo(torrent: IParsedTorrent, video: IFileAttributes, otherVideos: IFileAttributes[], hasMovies: boolean): boolean {
|
||||||
if (Number.isInteger(torrent.season) && Array.isArray(torrent.episodes)) {
|
if (Number.isInteger(torrent.season) && Array.isArray(torrent.episodes)) {
|
||||||
// not movie if video has season
|
// not movie if video has season
|
||||||
return false;
|
return false;
|
||||||
@@ -653,7 +654,7 @@ class TorrentFileService {
|
|||||||
&& otherVideos.filter(other => other.title === video.title && other.year === video.year).length < 3;
|
&& otherVideos.filter(other => other.title === video.title && other.year === video.year).length < 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearInfoFields(video: FileAttributes) {
|
private clearInfoFields(video: IFileAttributes) {
|
||||||
video.imdbId = undefined;
|
video.imdbId = undefined;
|
||||||
video.imdbSeason = undefined;
|
video.imdbSeason = undefined;
|
||||||
video.imdbEpisode = undefined;
|
video.imdbEpisode = undefined;
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ import {TorrentType} from "../enums/torrent_types";
|
|||||||
import {logger} from "./logging_service";
|
import {logger} from "./logging_service";
|
||||||
import {trackerService} from "./tracker_service";
|
import {trackerService} from "./tracker_service";
|
||||||
import {torrentEntriesService} from "./torrent_entries_service";
|
import {torrentEntriesService} from "./torrent_entries_service";
|
||||||
import {IngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
|
import {IIngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
|
||||||
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
import {IParsedTorrent} from "../interfaces/parsed_torrent";
|
||||||
|
import {ITorrentProcessingService} from "../interfaces/torrent_processing_service";
|
||||||
|
|
||||||
class TorrentProcessingService {
|
class TorrentProcessingService implements ITorrentProcessingService {
|
||||||
public async processTorrentRecord(torrent: IngestedTorrentAttributes): Promise<void> {
|
public async processTorrentRecord(torrent: IIngestedTorrentAttributes): Promise<void> {
|
||||||
const { category } = torrent;
|
const {category} = torrent;
|
||||||
const type = category === 'tv' ? TorrentType.Series : TorrentType.Movie;
|
const type = category === 'tv' ? TorrentType.Series : TorrentType.Movie;
|
||||||
const torrentInfo: ParsedTorrent = await this.parseTorrent(torrent, type);
|
const torrentInfo: IParsedTorrent = await this.parseTorrent(torrent, type);
|
||||||
|
|
||||||
logger.info(`Processing torrent ${torrentInfo.title} with infoHash ${torrentInfo.infoHash}`);
|
logger.info(`Processing torrent ${torrentInfo.title} with infoHash ${torrentInfo.infoHash}`);
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ class TorrentProcessingService {
|
|||||||
return trackers.join(',');
|
return trackers.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async parseTorrent(torrent: IngestedTorrentAttributes, category: string): Promise<ParsedTorrent> {
|
private async parseTorrent(torrent: IIngestedTorrentAttributes, category: string): Promise<IParsedTorrent> {
|
||||||
const infoHash = torrent.info_hash?.trim().toLowerCase()
|
const infoHash = torrent.info_hash?.trim().toLowerCase()
|
||||||
return {
|
return {
|
||||||
title: torrent.name,
|
title: torrent.name,
|
||||||
@@ -41,7 +42,7 @@ class TorrentProcessingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseImdbId(torrent: IngestedTorrentAttributes): string | undefined {
|
private parseImdbId(torrent: IIngestedTorrentAttributes): string | undefined {
|
||||||
if (torrent.imdb === undefined || torrent.imdb === null) {
|
if (torrent.imdb === undefined || torrent.imdb === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
import { parse } from 'parse-torrent-title';
|
import {parse} from 'parse-torrent-title';
|
||||||
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
import {ITorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||||
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
import {IFileAttributes} from "../../repository/interfaces/file_attributes";
|
||||||
|
import {ITorrentSubtitleService} from "../interfaces/torrent_subtitle_service";
|
||||||
|
|
||||||
class TorrentSubtitleService {
|
class TorrentSubtitleService implements ITorrentSubtitleService {
|
||||||
public assignSubtitles(fileCollection: TorrentFileCollection) : TorrentFileCollection {
|
public assignSubtitles(fileCollection: ITorrentFileCollection): ITorrentFileCollection {
|
||||||
if (fileCollection.videos && fileCollection.videos.length && fileCollection.subtitles && fileCollection.subtitles.length) {
|
if (fileCollection.videos && fileCollection.videos.length && fileCollection.subtitles && fileCollection.subtitles.length) {
|
||||||
if (fileCollection.videos.length === 1) {
|
if (fileCollection.videos.length === 1) {
|
||||||
fileCollection.videos[0].subtitles = fileCollection.subtitles;
|
fileCollection.videos[0].subtitles = fileCollection.subtitles;
|
||||||
return { ...fileCollection, subtitles: [] };
|
return {...fileCollection, subtitles: []};
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedVideos = fileCollection.videos.map(video => this.parseVideo(video));
|
const parsedVideos = fileCollection.videos.map(video => this.parseVideo(video));
|
||||||
const assignedSubs = fileCollection.subtitles.map(subtitle => ({ subtitle, videos: this.mostProbableSubtitleVideos(subtitle, parsedVideos) }));
|
const assignedSubs = fileCollection.subtitles.map(subtitle => ({
|
||||||
|
subtitle,
|
||||||
|
videos: this.mostProbableSubtitleVideos(subtitle, parsedVideos)
|
||||||
|
}));
|
||||||
const unassignedSubs = assignedSubs.filter(assignedSub => !assignedSub.videos).map(assignedSub => assignedSub.subtitle);
|
const unassignedSubs = assignedSubs.filter(assignedSub => !assignedSub.videos).map(assignedSub => assignedSub.subtitle);
|
||||||
|
|
||||||
assignedSubs
|
assignedSubs
|
||||||
.filter(assignedSub => assignedSub.videos)
|
.filter(assignedSub => assignedSub.videos)
|
||||||
.forEach(assignedSub => assignedSub.videos.forEach(video => video.subtitles = (video.subtitles || []).concat(assignedSub.subtitle)));
|
.forEach(assignedSub => assignedSub.videos.forEach(video => video.subtitles = (video.subtitles || []).concat(assignedSub.subtitle)));
|
||||||
return { ...fileCollection, subtitles: unassignedSubs };
|
return {...fileCollection, subtitles: unassignedSubs};
|
||||||
}
|
}
|
||||||
return fileCollection;
|
return fileCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseVideo(video: FileAttributes) {
|
private parseVideo(video: IFileAttributes) {
|
||||||
const fileName = video.title.split('/').pop().replace(/\.(\w{2,4})$/, '');
|
const fileName = video.title.split('/').pop().replace(/\.(\w{2,4})$/, '');
|
||||||
const folderName = video.title.replace(/\/?[^/]+$/, '');
|
const folderName = video.title.replace(/\/?[^/]+$/, '');
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
import axios, {AxiosResponse} from 'axios';
|
||||||
import { cacheService } from "./cache_service";
|
import {cacheService} from "./cache_service";
|
||||||
import { configurationService } from './configuration_service';
|
import {configurationService} from './configuration_service';
|
||||||
import { logger } from "./logging_service";
|
import {logger} from "./logging_service";
|
||||||
|
import {ITrackerService} from "../interfaces/tracker_service";
|
||||||
|
|
||||||
class TrackerService {
|
class TrackerService implements ITrackerService {
|
||||||
public async getTrackers() : Promise<string[]> {
|
public async getTrackers(): Promise<string[]> {
|
||||||
return cacheService.cacheTrackers(this.downloadTrackers);
|
return cacheService.cacheTrackers(this.downloadTrackers);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {IngestedTorrent} from "./models/ingestedTorrent";
|
|||||||
import {Subtitle} from "./models/subtitle";
|
import {Subtitle} from "./models/subtitle";
|
||||||
import {Content} from "./models/content";
|
import {Content} from "./models/content";
|
||||||
import {SkipTorrent} from "./models/skipTorrent";
|
import {SkipTorrent} from "./models/skipTorrent";
|
||||||
import {FileAttributes} from "./interfaces/file_attributes";
|
import {IFileAttributes} from "./interfaces/file_attributes";
|
||||||
import {TorrentAttributes} from "./interfaces/torrent_attributes";
|
import {ITorrentAttributes} from "./interfaces/torrent_attributes";
|
||||||
import {IngestedPage} from "./models/ingestedPage";
|
import {IngestedPage} from "./models/ingestedPage";
|
||||||
import {logger} from "../lib/services/logging_service";
|
import {logger} from "../lib/services/logging_service";
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ class DatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTorrent(torrent: TorrentAttributes): Promise<Torrent | null> {
|
public async getTorrent(torrent: ITorrentAttributes): Promise<Torrent | null> {
|
||||||
const where = torrent.infoHash
|
const where = torrent.infoHash
|
||||||
? { infoHash: torrent.infoHash }
|
? { infoHash: torrent.infoHash }
|
||||||
: { provider: torrent.provider, torrentId: torrent.torrentId };
|
: { provider: torrent.provider, torrentId: torrent.torrentId };
|
||||||
@@ -61,11 +61,11 @@ class DatabaseRepository {
|
|||||||
return this.getTorrentsBasedOnQuery({ title: { [Op.regexp]: `${titleQuery}` }, type });
|
return this.getTorrentsBasedOnQuery({ title: { [Op.regexp]: `${titleQuery}` }, type });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTorrentsBasedOnQuery(where: WhereOptions<TorrentAttributes>): Promise<Torrent[]> {
|
public async getTorrentsBasedOnQuery(where: WhereOptions<ITorrentAttributes>): Promise<Torrent[]> {
|
||||||
return await Torrent.findAll({ where });
|
return await Torrent.findAll({ where });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFilesBasedOnQuery(where: WhereOptions<FileAttributes>): Promise<File[]> {
|
public async getFilesBasedOnQuery(where: WhereOptions<IFileAttributes>): Promise<File[]> {
|
||||||
return await File.findAll({ where });
|
return await File.findAll({ where });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ class DatabaseRepository {
|
|||||||
await this.createSubtitles(torrent.infoHash, torrent.subtitles);
|
await this.createSubtitles(torrent.infoHash, torrent.subtitles);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setTorrentSeeders(torrent: TorrentAttributes, seeders: number): Promise<[number]> {
|
public async setTorrentSeeders(torrent: ITorrentAttributes, seeders: number): Promise<[number]> {
|
||||||
const where = torrent.infoHash
|
const where = torrent.infoHash
|
||||||
? { infoHash: torrent.infoHash }
|
? { infoHash: torrent.infoHash }
|
||||||
: { provider: torrent.provider, torrentId: torrent.torrentId };
|
: { provider: torrent.provider, torrentId: torrent.torrentId };
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {Optional} from "sequelize";
|
import {Optional} from "sequelize";
|
||||||
|
|
||||||
export interface ContentAttributes {
|
export interface IContentAttributes {
|
||||||
infoHash: string;
|
infoHash: string;
|
||||||
fileIndex: number;
|
fileIndex: number;
|
||||||
path: string;
|
path: string;
|
||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentCreationAttributes extends Optional<ContentAttributes, 'fileIndex' | 'size'> {
|
export interface IContentCreationAttributes extends Optional<IContentAttributes, 'fileIndex' | 'size'> {
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import {Optional} from "sequelize";
|
import {Optional} from "sequelize";
|
||||||
import {SubtitleAttributes} from "./subtitle_attributes";
|
import {ISubtitleAttributes} from "./subtitle_attributes";
|
||||||
import {ParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result";
|
import {IParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result";
|
||||||
|
|
||||||
export interface FileAttributes extends ParseTorrentTitleResult {
|
export interface IFileAttributes extends IParseTorrentTitleResult {
|
||||||
id?: number;
|
id?: number;
|
||||||
infoHash?: string;
|
infoHash?: string;
|
||||||
fileIndex?: number;
|
fileIndex?: number;
|
||||||
@@ -13,10 +13,10 @@ export interface FileAttributes extends ParseTorrentTitleResult {
|
|||||||
imdbEpisode?: number;
|
imdbEpisode?: number;
|
||||||
kitsuId?: number;
|
kitsuId?: number;
|
||||||
kitsuEpisode?: number;
|
kitsuEpisode?: number;
|
||||||
subtitles?: SubtitleAttributes[];
|
subtitles?: ISubtitleAttributes[];
|
||||||
path?: string;
|
path?: string;
|
||||||
isMovie?: boolean;
|
isMovie?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileCreationAttributes extends Optional<FileAttributes, 'fileIndex' | 'size' | 'imdbId' | 'imdbSeason' | 'imdbEpisode' | 'kitsuId' | 'kitsuEpisode'> {
|
export interface IFileCreationAttributes extends Optional<IFileAttributes, 'fileIndex' | 'size' | 'imdbId' | 'imdbSeason' | 'imdbEpisode' | 'kitsuId' | 'kitsuEpisode'> {
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export interface IngestedPageAttributes {
|
export interface IIngestedPageAttributes {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IngestedPageCreationAttributes extends IngestedPageAttributes {
|
export interface IIngestedPageCreationAttributes extends IIngestedPageAttributes {
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Optional} from "sequelize";
|
import {Optional} from "sequelize";
|
||||||
|
|
||||||
export interface IngestedTorrentAttributes {
|
export interface IIngestedTorrentAttributes {
|
||||||
name: string;
|
name: string;
|
||||||
source: string;
|
source: string;
|
||||||
category: string;
|
category: string;
|
||||||
@@ -13,5 +13,5 @@ export interface IngestedTorrentAttributes {
|
|||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IngestedTorrentCreationAttributes extends Optional<IngestedTorrentAttributes, 'processed'> {
|
export interface IIngestedTorrentCreationAttributes extends Optional<IIngestedTorrentAttributes, 'processed'> {
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import {Optional} from "sequelize";
|
import {Optional} from "sequelize";
|
||||||
|
|
||||||
export interface ProviderAttributes {
|
export interface IProviderAttributes {
|
||||||
name: string;
|
name: string;
|
||||||
lastScraped: Date;
|
lastScraped: Date;
|
||||||
lastScrapedId: string;
|
lastScrapedId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderCreationAttributes extends Optional<ProviderAttributes, 'lastScraped' | 'lastScrapedId'> {
|
export interface IProviderCreationAttributes extends Optional<IProviderAttributes, 'lastScraped' | 'lastScrapedId'> {
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import {Optional} from "sequelize";
|
import {Optional} from "sequelize";
|
||||||
|
|
||||||
export interface SkipTorrentAttributes {
|
export interface ISkipTorrentAttributes {
|
||||||
infoHash: string;
|
infoHash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkipTorrentCreationAttributes extends Optional<SkipTorrentAttributes, never> {
|
export interface ISkipTorrentCreationAttributes extends Optional<ISkipTorrentAttributes, never> {
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Optional} from "sequelize";
|
import {Optional} from "sequelize";
|
||||||
|
|
||||||
export interface SubtitleAttributes {
|
export interface ISubtitleAttributes {
|
||||||
infoHash: string;
|
infoHash: string;
|
||||||
fileIndex: number;
|
fileIndex: number;
|
||||||
fileId?: number;
|
fileId?: number;
|
||||||
@@ -8,5 +8,5 @@ export interface SubtitleAttributes {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubtitleCreationAttributes extends Optional<SubtitleAttributes, 'fileId'> {
|
export interface ISubtitleCreationAttributes extends Optional<ISubtitleAttributes, 'fileId'> {
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import {Optional} from "sequelize";
|
import {Optional} from "sequelize";
|
||||||
import {ContentAttributes} from "./content_attributes";
|
import {IContentAttributes} from "./content_attributes";
|
||||||
import {SubtitleAttributes} from "./subtitle_attributes";
|
import {ISubtitleAttributes} from "./subtitle_attributes";
|
||||||
import {FileAttributes} from "./file_attributes";
|
import {IFileAttributes} from "./file_attributes";
|
||||||
|
|
||||||
export interface TorrentAttributes {
|
export interface ITorrentAttributes {
|
||||||
infoHash: string;
|
infoHash: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
torrentId?: string;
|
torrentId?: string;
|
||||||
@@ -17,10 +17,10 @@ export interface TorrentAttributes {
|
|||||||
resolution?: string;
|
resolution?: string;
|
||||||
reviewed?: boolean;
|
reviewed?: boolean;
|
||||||
opened?: boolean;
|
opened?: boolean;
|
||||||
contents?: ContentAttributes[];
|
contents?: IContentAttributes[];
|
||||||
files?: FileAttributes[];
|
files?: IFileAttributes[];
|
||||||
subtitles?: SubtitleAttributes[];
|
subtitles?: ISubtitleAttributes[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TorrentCreationAttributes extends Optional<TorrentAttributes, 'torrentId' | 'size' | 'seeders' | 'trackers' | 'languages' | 'resolution' | 'reviewed' | 'opened'> {
|
export interface ITorrentCreationAttributes extends Optional<ITorrentAttributes, 'torrentId' | 'size' | 'seeders' | 'trackers' | 'languages' | 'resolution' | 'reviewed' | 'opened'> {
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
|
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
|
||||||
import {ContentAttributes, ContentCreationAttributes} from "../interfaces/content_attributes";
|
import {IContentAttributes, IContentCreationAttributes} from "../interfaces/content_attributes";
|
||||||
import {Torrent} from "./torrent";
|
import {Torrent} from "./torrent";
|
||||||
|
|
||||||
@Table({modelName: 'content', timestamps: false})
|
@Table({modelName: 'content', timestamps: false})
|
||||||
export class Content extends Model<ContentAttributes, ContentCreationAttributes> {
|
export class Content extends Model<IContentAttributes, IContentCreationAttributes> {
|
||||||
@Column({ type: DataType.STRING(64), primaryKey: true, allowNull: false, onDelete: 'CASCADE' })
|
@Column({ type: DataType.STRING(64), primaryKey: true, allowNull: false, onDelete: 'CASCADE' })
|
||||||
@ForeignKey(() => Torrent)
|
@ForeignKey(() => Torrent)
|
||||||
declare infoHash: string;
|
declare infoHash: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
|
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
|
||||||
import {FileAttributes, FileCreationAttributes} from "../interfaces/file_attributes";
|
import {IFileAttributes, IFileCreationAttributes} from "../interfaces/file_attributes";
|
||||||
import {Torrent} from "./torrent";
|
import {Torrent} from "./torrent";
|
||||||
import {Subtitle} from "./subtitle";
|
import {Subtitle} from "./subtitle";
|
||||||
import {SubtitleAttributes} from "../interfaces/subtitle_attributes";
|
import {ISubtitleAttributes} from "../interfaces/subtitle_attributes";
|
||||||
|
|
||||||
const indexes = [
|
const indexes = [
|
||||||
{
|
{
|
||||||
@@ -23,7 +23,7 @@ const indexes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
@Table({modelName: 'file', timestamps: true, indexes: indexes })
|
@Table({modelName: 'file', timestamps: true, indexes: indexes })
|
||||||
export class File extends Model<FileAttributes, FileCreationAttributes> {
|
export class File extends Model<IFileAttributes, IFileCreationAttributes> {
|
||||||
@Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' })
|
@Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' })
|
||||||
@ForeignKey(() => Torrent)
|
@ForeignKey(() => Torrent)
|
||||||
declare infoHash: string;
|
declare infoHash: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
||||||
import {IngestedPageAttributes, IngestedPageCreationAttributes} from "../interfaces/ingested_page_attributes";
|
import {IIngestedPageAttributes, IIngestedPageCreationAttributes} from "../interfaces/ingested_page_attributes";
|
||||||
|
|
||||||
const indexes = [
|
const indexes = [
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,7 @@ const indexes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
@Table({modelName: 'ingested_page', timestamps: true, indexes: indexes})
|
@Table({modelName: 'ingested_page', timestamps: true, indexes: indexes})
|
||||||
export class IngestedPage extends Model<IngestedPageAttributes, IngestedPageCreationAttributes> {
|
export class IngestedPage extends Model<IIngestedPageAttributes, IIngestedPageCreationAttributes> {
|
||||||
@Column({ type: DataType.STRING(512), allowNull: false })
|
@Column({ type: DataType.STRING(512), allowNull: false })
|
||||||
declare url: string;
|
declare url: string;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
||||||
import {IngestedTorrentAttributes, IngestedTorrentCreationAttributes} from "../interfaces/ingested_torrent_attributes";
|
import {IIngestedTorrentAttributes, IIngestedTorrentCreationAttributes} from "../interfaces/ingested_torrent_attributes";
|
||||||
|
|
||||||
const indexes = [
|
const indexes = [
|
||||||
{
|
{
|
||||||
@@ -10,7 +10,7 @@ const indexes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
@Table({modelName: 'ingested_torrent', timestamps: true, indexes: indexes})
|
@Table({modelName: 'ingested_torrent', timestamps: true, indexes: indexes})
|
||||||
export class IngestedTorrent extends Model<IngestedTorrentAttributes, IngestedTorrentCreationAttributes> {
|
export class IngestedTorrent extends Model<IIngestedTorrentAttributes, IIngestedTorrentCreationAttributes> {
|
||||||
@Column({ type: DataType.STRING(512) })
|
@Column({ type: DataType.STRING(512) })
|
||||||
declare name: string;
|
declare name: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
||||||
import {ProviderAttributes, ProviderCreationAttributes} from "../interfaces/provider_attributes";
|
import {IProviderAttributes, IProviderCreationAttributes} from "../interfaces/provider_attributes";
|
||||||
|
|
||||||
@Table({modelName: 'provider', timestamps: false})
|
@Table({modelName: 'provider', timestamps: false})
|
||||||
export class Provider extends Model<ProviderAttributes, ProviderCreationAttributes> {
|
export class Provider extends Model<IProviderAttributes, IProviderCreationAttributes> {
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(32), primaryKey: true })
|
@Column({ type: DataType.STRING(32), primaryKey: true })
|
||||||
declare name: string;
|
declare name: string;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
||||||
import {SkipTorrentAttributes, SkipTorrentCreationAttributes} from "../interfaces/skip_torrent_attributes";
|
import {ISkipTorrentAttributes, ISkipTorrentCreationAttributes} from "../interfaces/skip_torrent_attributes";
|
||||||
|
|
||||||
|
|
||||||
@Table({modelName: 'skip_torrent', timestamps: false})
|
@Table({modelName: 'skip_torrent', timestamps: false})
|
||||||
export class SkipTorrent extends Model<SkipTorrentAttributes, SkipTorrentCreationAttributes> {
|
export class SkipTorrent extends Model<ISkipTorrentAttributes, ISkipTorrentCreationAttributes> {
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(64), primaryKey: true })
|
@Column({ type: DataType.STRING(64), primaryKey: true })
|
||||||
declare infoHash: string;
|
declare infoHash: string;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
|
import {Table, Column, Model, HasMany, DataType, BelongsTo, ForeignKey} from 'sequelize-typescript';
|
||||||
import {SubtitleAttributes, SubtitleCreationAttributes} from "../interfaces/subtitle_attributes";
|
import {ISubtitleAttributes, ISubtitleCreationAttributes} from "../interfaces/subtitle_attributes";
|
||||||
import {File} from "./file";
|
import {File} from "./file";
|
||||||
import {Torrent} from "./torrent";
|
import {Torrent} from "./torrent";
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ const indexes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
@Table({modelName: 'subtitle', timestamps: false, indexes: indexes})
|
@Table({modelName: 'subtitle', timestamps: false, indexes: indexes})
|
||||||
export class Subtitle extends Model<SubtitleAttributes, SubtitleCreationAttributes> {
|
export class Subtitle extends Model<ISubtitleAttributes, ISubtitleCreationAttributes> {
|
||||||
|
|
||||||
@Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' })
|
@Column({ type: DataType.STRING(64), allowNull: false, onDelete: 'CASCADE' })
|
||||||
declare infoHash: string;
|
declare infoHash: string;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
import { Table, Column, Model, HasMany, DataType } from 'sequelize-typescript';
|
||||||
import {TorrentAttributes, TorrentCreationAttributes} from "../interfaces/torrent_attributes";
|
import {ITorrentAttributes, ITorrentCreationAttributes} from "../interfaces/torrent_attributes";
|
||||||
import {Content} from "./content";
|
import {Content} from "./content";
|
||||||
import {File} from "./file";
|
import {File} from "./file";
|
||||||
import {Subtitle} from "./subtitle";
|
import {Subtitle} from "./subtitle";
|
||||||
|
|
||||||
@Table({modelName: 'torrent', timestamps: true})
|
@Table({modelName: 'torrent', timestamps: true})
|
||||||
|
|
||||||
export class Torrent extends Model<TorrentAttributes, TorrentCreationAttributes> {
|
export class Torrent extends Model<ITorrentAttributes, ITorrentCreationAttributes> {
|
||||||
@Column({type: DataType.STRING(64), primaryKey: true})
|
@Column({type: DataType.STRING(64), primaryKey: true})
|
||||||
declare infoHash: string;
|
declare infoHash: string;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user