wip torrent_file_service, not completed yet
This commit is contained in:
@@ -1,12 +0,0 @@
|
|||||||
import {ParseTorrentTitleResult} from "./parse_torrent_title_result";
|
|
||||||
|
|
||||||
export interface ParsableTorrentFile extends ParseTorrentTitleResult {
|
|
||||||
name?: string;
|
|
||||||
path?: string;
|
|
||||||
size?: number;
|
|
||||||
length?: number;
|
|
||||||
fileIndex?: number;
|
|
||||||
isMovie?: boolean;
|
|
||||||
imdbId?: string | number;
|
|
||||||
kitsuId?: number | string;
|
|
||||||
}
|
|
||||||
18
src/node/consumer/src/lib/interfaces/parsed_torrent.ts
Normal file
18
src/node/consumer/src/lib/interfaces/parsed_torrent.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import {ParseTorrentTitleResult} from "./parse_torrent_title_result";
|
||||||
|
import {TorrentType} from "../enums/torrent_types";
|
||||||
|
import {TorrentFileCollection} from "./torrent_file_collection";
|
||||||
|
|
||||||
|
export interface ParsedTorrent extends ParseTorrentTitleResult {
|
||||||
|
size?: number;
|
||||||
|
isPack?: boolean;
|
||||||
|
imdbId?: string | number;
|
||||||
|
kitsuId?: string | number;
|
||||||
|
trackers?: string;
|
||||||
|
provider?: string | null;
|
||||||
|
infoHash: string | null;
|
||||||
|
type: string | TorrentType;
|
||||||
|
uploadDate?: Date;
|
||||||
|
seeders?: number;
|
||||||
|
torrentId?: string;
|
||||||
|
fileCollection?: TorrentFileCollection;
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import {ParsableTorrentFile} from "./parsable_torrent_file";
|
import {ContentAttributes} from "../../repository/interfaces/content_attributes";
|
||||||
|
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
||||||
|
import {SubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
|
||||||
|
|
||||||
export interface TorrentFileCollection {
|
export interface TorrentFileCollection {
|
||||||
contents?: ParsableTorrentFile[];
|
contents?: ContentAttributes[];
|
||||||
videos?: ParsableTorrentFile[];
|
videos?: FileAttributes[];
|
||||||
subtitles?: ParsableTorrentFile[];
|
subtitles?: SubtitleAttributes[];
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import {TorrentType} from "../enums/torrent_types";
|
|
||||||
|
|
||||||
export interface TorrentInfo {
|
|
||||||
title: string | null;
|
|
||||||
torrentId: string;
|
|
||||||
infoHash: string | null;
|
|
||||||
seeders: number;
|
|
||||||
uploadDate: Date;
|
|
||||||
imdbId?: string;
|
|
||||||
kitsuId?: string;
|
|
||||||
type: TorrentType;
|
|
||||||
provider?: string | null;
|
|
||||||
trackers: string;
|
|
||||||
size?: number;
|
|
||||||
pack?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { parse } from 'parse-torrent-title';
|
|
||||||
import { TorrentType } from '../enums/torrent_types';
|
|
||||||
import {ParseTorrentTitleResult} from "../interfaces/parse_torrent_title_result";
|
|
||||||
import {ParsableTorrentFile} from "../interfaces/parsable_torrent_file";
|
|
||||||
import {TorrentInfo} from "../interfaces/torrent_info";
|
|
||||||
|
|
||||||
class ParsingService {
|
|
||||||
private readonly MULTIPLE_FILES_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB
|
|
||||||
|
|
||||||
public parseSeriesVideos(torrent: TorrentInfo, videos: ParsableTorrentFile[]): ParsableTorrentFile[] {
|
|
||||||
const parsedTorrentName = parse(torrent.title);
|
|
||||||
const hasMovies = parsedTorrentName.complete || !!torrent.title.match(/movies?(?:\W|$)/i);
|
|
||||||
const parsedVideos = videos.map(video => this.parseSeriesVideo(video, parsedTorrentName));
|
|
||||||
return parsedVideos.map(video => ({ ...video, isMovie: this.isMovieVideo(video, parsedVideos, torrent.type, hasMovies) }));
|
|
||||||
}
|
|
||||||
|
|
||||||
public isPackTorrent(torrent: TorrentInfo): boolean {
|
|
||||||
if (torrent.pack) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const parsedInfo = parse(torrent.title);
|
|
||||||
if (torrent.type === TorrentType.Movie) {
|
|
||||||
return parsedInfo.complete || typeof parsedInfo.year === 'string' || /movies/i.test(torrent.title);
|
|
||||||
}
|
|
||||||
const hasMultipleEpisodes = parsedInfo.complete ||
|
|
||||||
torrent.size > this.MULTIPLE_FILES_SIZE ||
|
|
||||||
(parsedInfo.seasons && parsedInfo.seasons.length > 1) ||
|
|
||||||
(parsedInfo.episodes && parsedInfo.episodes.length > 1) ||
|
|
||||||
(parsedInfo.seasons && !parsedInfo.episodes);
|
|
||||||
const hasSingleEpisode = Number.isInteger(parsedInfo.episode) || (!parsedInfo.episodes && parsedInfo.date);
|
|
||||||
return hasMultipleEpisodes && !hasSingleEpisode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private parseSeriesVideo(video: ParsableTorrentFile, parsedTorrentName: ParseTorrentTitleResult): ParseTorrentTitleResult {
|
|
||||||
const videoInfo = parse(video.name);
|
|
||||||
// the episode may be in a folder containing season number
|
|
||||||
if (!Number.isInteger(videoInfo.season) && video.path.includes('/')) {
|
|
||||||
const folders = video.path.split('/');
|
|
||||||
const pathInfo = parse(folders[folders.length - 2]);
|
|
||||||
videoInfo.season = pathInfo.season;
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(videoInfo.season) && parsedTorrentName.season) {
|
|
||||||
videoInfo.season = parsedTorrentName.season;
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(videoInfo.season) && videoInfo.seasons && videoInfo.seasons.length > 1) {
|
|
||||||
// in case single file was interpreted as having multiple seasons
|
|
||||||
videoInfo.season = videoInfo.seasons[0];
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(videoInfo.season) && video.path.includes('/') && parsedTorrentName.seasons
|
|
||||||
&& parsedTorrentName.seasons.length > 1) {
|
|
||||||
// russian season are usually named with 'series name-2` i.e. Улицы разбитых фонарей-6/22. Одиночный выстрел.mkv
|
|
||||||
const folderPathSeasonMatch = video.path.match(/[\u0400-\u04ff]-(\d{1,2})(?=.*\/)/);
|
|
||||||
videoInfo.season = folderPathSeasonMatch && parseInt(folderPathSeasonMatch[1], 10) || undefined;
|
|
||||||
}
|
|
||||||
// sometimes video file does not have correct date format as in torrent title
|
|
||||||
if (!videoInfo.episodes && !videoInfo.date && parsedTorrentName.date) {
|
|
||||||
videoInfo.date = parsedTorrentName.date;
|
|
||||||
}
|
|
||||||
// limit number of episodes in case of incorrect parsing
|
|
||||||
if (videoInfo.episodes && videoInfo.episodes.length > 20) {
|
|
||||||
videoInfo.episodes = [videoInfo.episodes[0]];
|
|
||||||
videoInfo.episode = videoInfo.episodes[0];
|
|
||||||
}
|
|
||||||
// force episode to any found number if it was not parsed
|
|
||||||
if (!videoInfo.episodes && !videoInfo.date) {
|
|
||||||
const epMatcher = videoInfo.title.match(
|
|
||||||
/(?<!season\W*|disk\W*|movie\W*|film\W*)(?:^|\W|_)(\d{1,4})(?:a|b|c|v\d)?(?:_|\W|$)(?!disk|movie|film)/i);
|
|
||||||
videoInfo.episodes = epMatcher && [parseInt(epMatcher[1], 10)];
|
|
||||||
videoInfo.episode = videoInfo.episodes && videoInfo.episodes[0];
|
|
||||||
}
|
|
||||||
if (!videoInfo.episodes && !videoInfo.date) {
|
|
||||||
const epMatcher = video.name.match(new RegExp(`(?:\\(${videoInfo.year}\\)|part)[._ ]?(\\d{1,3})(?:\\b|_)`, "i"));
|
|
||||||
videoInfo.episodes = epMatcher && [parseInt(epMatcher[1], 10)];
|
|
||||||
videoInfo.episode = videoInfo.episodes && videoInfo.episodes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...video, ...videoInfo };
|
|
||||||
}
|
|
||||||
|
|
||||||
private isMovieVideo(video: ParseTorrentTitleResult, otherVideos: ParseTorrentTitleResult[], type: TorrentType, hasMovies: boolean): boolean {
|
|
||||||
if (Number.isInteger(video.season) && Array.isArray(video.episodes)) {
|
|
||||||
// not movie if video has season
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (video.title.match(/\b(?:\d+[ .]movie|movie[ .]\d+)\b/i)) {
|
|
||||||
// movie if video explicitly has numbered movie keyword in the name, ie. 1 Movie or Movie 1
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!hasMovies && type !== TorrentType.Anime) {
|
|
||||||
// not movie if torrent name does not contain movies keyword or is not a pack torrent and is not anime
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!video.episodes) {
|
|
||||||
// movie if there's no episode info it could be a movie
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// movie if contains year info and there aren't more than 3 video with same title and year
|
|
||||||
// as some series titles might contain year in it.
|
|
||||||
return !!video.year
|
|
||||||
&& otherVideos.length > 3
|
|
||||||
&& otherVideos.filter(other => other.title === video.title && other.year === video.year).length < 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parsingService = new ParsingService();
|
|
||||||
|
|
||||||
@@ -1,10 +1,20 @@
|
|||||||
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 {extensionService} from './extension_service';
|
||||||
import {TorrentInfo} from "../interfaces/torrent_info";
|
|
||||||
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||||
import {ParsableTorrentFile} from "../interfaces/parsable_torrent_file";
|
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
||||||
|
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
||||||
|
import {SubtitleAttributes} from "../../repository/interfaces/subtitle_attributes";
|
||||||
|
import {ContentAttributes} from "../../repository/interfaces/content_attributes";
|
||||||
|
import {parse} from "parse-torrent-title";
|
||||||
|
|
||||||
|
interface TorrentFile {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
length: number;
|
||||||
|
fileIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
class TorrentDownloadService {
|
class TorrentDownloadService {
|
||||||
private engineOptions: TorrentStream.TorrentEngineOptions = {
|
private engineOptions: TorrentStream.TorrentEngineOptions = {
|
||||||
@@ -14,73 +24,124 @@ class TorrentDownloadService {
|
|||||||
dht: false,
|
dht: false,
|
||||||
tracker: true,
|
tracker: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
public async getTorrentFiles(torrent: TorrentInfo, timeout: number = 30000): Promise<TorrentFileCollection> {
|
public async getTorrentFiles(torrent: ParsedTorrent, timeout: number = 30000): Promise<TorrentFileCollection> {
|
||||||
return this.filesFromTorrentStream(torrent, timeout)
|
const torrentFiles: TorrentFile[] = await this.filesFromTorrentStream(torrent, timeout);
|
||||||
.then((files: Array<ParsableTorrentFile>) => ({
|
|
||||||
contents: files,
|
const videos = this.filterVideos(torrent, torrentFiles);
|
||||||
videos: this.filterVideos(files),
|
const subtitles = this.filterSubtitles(torrent, torrentFiles);
|
||||||
subtitles: this.filterSubtitles(files)
|
const contents = this.createContent(torrent, torrentFiles);
|
||||||
}));
|
|
||||||
|
return {
|
||||||
|
contents: contents,
|
||||||
|
videos: videos,
|
||||||
|
subtitles: subtitles,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async filesFromTorrentStream(torrent: TorrentInfo, timeout: number): Promise<Array<ParsableTorrentFile>> {
|
private async filesFromTorrentStream(torrent: ParsedTorrent, timeout: number): Promise<TorrentFile[]> {
|
||||||
if (!torrent.infoHash) {
|
if (!torrent.infoHash) {
|
||||||
return Promise.reject(new Error("No infoHash..."));
|
return Promise.reject(new Error("No infoHash..."));
|
||||||
}
|
}
|
||||||
const magnet = encode({ infoHash: torrent.infoHash, announce: torrent.trackers.split(',') });
|
const magnet = encode({infoHash: torrent.infoHash, announce: torrent.trackers.split(',')});
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let engine: TorrentStream.TorrentEngine;
|
let engine: TorrentStream.TorrentEngine;
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
engine.destroy(() => {});
|
engine.destroy(() => {
|
||||||
|
});
|
||||||
reject(new Error('No available connections for torrent!'));
|
reject(new Error('No available connections for torrent!'));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
engine = torrentStream(magnet, this.engineOptions);
|
engine = torrentStream(magnet, this.engineOptions);
|
||||||
|
|
||||||
engine.on("ready", () => {
|
engine.on("ready", () => {
|
||||||
const files: ParsableTorrentFile[] = engine.files.map((file, fileId) => ({
|
const files: TorrentFile[] = engine.files.map((file, fileId) => ({
|
||||||
...file,
|
...file,
|
||||||
fileIndex: fileId,
|
fileIndex: fileId,
|
||||||
size: file.length,
|
size: file.length,
|
||||||
title: file.name}));
|
title: file.name
|
||||||
|
}));
|
||||||
|
|
||||||
resolve(files);
|
resolve(files);
|
||||||
|
|
||||||
engine.destroy(() => {});
|
engine.destroy(() => {
|
||||||
|
});
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterVideos(files: Array<ParsableTorrentFile>): Array<ParsableTorrentFile> {
|
private filterVideos(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): FileAttributes[] {
|
||||||
if (files.length === 1 && !Number.isInteger(files[0].fileIndex)) {
|
if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) {
|
||||||
return files;
|
return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])];
|
||||||
}
|
}
|
||||||
const videos = files.filter(file => extensionService.isVideo(file.path || ''));
|
const videos = torrentFiles.filter(file => extensionService.isVideo(file.path || ''));
|
||||||
const maxSize = Math.max(...videos.map((video: ParsableTorrentFile) => video.length));
|
const maxSize = Math.max(...videos.map((video: TorrentFile) => 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: ParsableTorrentFile) => video.path?.match(/sample|bonus|promo/i) && maxSize / parseInt(video.length.toString()) > minSampleRatio;
|
|
||||||
const isRedundant = (video: ParsableTorrentFile) => maxSize / parseInt(video.length.toString()) > minRedundantRatio;
|
const isSample = (video: TorrentFile) => video.path?.match(/sample|bonus|promo/i) && maxSize / parseInt(video.path.toString()) > minSampleRatio;
|
||||||
const isExtra = (video: ParsableTorrentFile) => video.path?.match(/extras?\//i);
|
const isRedundant = (video: TorrentFile) => maxSize / parseInt(video.path.toString()) > minRedundantRatio;
|
||||||
const isAnimeExtra = (video: ParsableTorrentFile) => video.path?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i)
|
const isExtra = (video: TorrentFile) => video.path?.match(/extras?\//i);
|
||||||
|
const isAnimeExtra = (video: TorrentFile) => video.path?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i)
|
||||||
&& maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio;
|
&& maxSize / parseInt(video.length.toString()) > minAnimeExtraRatio;
|
||||||
const isWatermark = (video: ParsableTorrentFile) => video.path?.match(/^[A-Z-]+(?:\.[A-Z]+)?\.\w{3,4}$/)
|
const isWatermark = (video: TorrentFile) => 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
|
||||||
.filter(video => !isSample(video))
|
.filter(video => !isSample(video))
|
||||||
.filter(video => !isExtra(video))
|
.filter(video => !isExtra(video))
|
||||||
.filter(video => !isAnimeExtra(video))
|
.filter(video => !isAnimeExtra(video))
|
||||||
.filter(video => !isRedundant(video))
|
.filter(video => !isRedundant(video))
|
||||||
.filter(video => !isWatermark(video));
|
.filter(video => !isWatermark(video))
|
||||||
|
.map(video => this.mapTorrentFileToFileAttributes(torrent, video));
|
||||||
}
|
}
|
||||||
|
|
||||||
private filterSubtitles(files: Array<ParsableTorrentFile>): Array<ParsableTorrentFile> {
|
private filterSubtitles(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): SubtitleAttributes[] {
|
||||||
return files.filter(file => extensionService.isSubtitle(file.path || ''));
|
return torrentFiles.filter(file => extensionService.isSubtitle(file.name || ''))
|
||||||
|
.map(file => this.mapTorrentFileToSubtitleAttributes(torrent, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
private createContent(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): ContentAttributes[] {
|
||||||
|
return torrentFiles.map(file => this.mapTorrentFileToContentAttributes(torrent, file));
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapTorrentFileToFileAttributes(torrent: ParsedTorrent, file: TorrentFile): FileAttributes {
|
||||||
|
const videoFile: FileAttributes = {
|
||||||
|
title: file.name,
|
||||||
|
size: file.length,
|
||||||
|
fileIndex: file.fileIndex || 0,
|
||||||
|
infoHash: torrent.infoHash,
|
||||||
|
imdbId: torrent.imdbId.toString(),
|
||||||
|
imdbSeason: torrent.season || 0,
|
||||||
|
imdbEpisode: torrent.episode || 0,
|
||||||
|
kitsuId: parseInt(torrent.kitsuId.toString()) || 0,
|
||||||
|
kitsuEpisode: torrent.episode || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return {...videoFile, ...parse(file.name)};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapTorrentFileToSubtitleAttributes(torrent: ParsedTorrent, file: TorrentFile): SubtitleAttributes {
|
||||||
|
return {
|
||||||
|
title: file.name,
|
||||||
|
infoHash: torrent.infoHash,
|
||||||
|
fileIndex: file.fileIndex,
|
||||||
|
fileId: file.fileIndex,
|
||||||
|
path: file.path,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapTorrentFileToContentAttributes(torrent: ParsedTorrent, file: TorrentFile): ContentAttributes {
|
||||||
|
return {
|
||||||
|
infoHash: torrent.infoHash,
|
||||||
|
fileIndex: file.fileIndex,
|
||||||
|
path: file.path,
|
||||||
|
size: file.length,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
233
src/node/consumer/src/lib/services/torrent_entries_service.ts
Normal file
233
src/node/consumer/src/lib/services/torrent_entries_service.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import {parse} from 'parse-torrent-title';
|
||||||
|
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
||||||
|
import {repository} from '../../repository/database_repository';
|
||||||
|
import {TorrentType} from '../enums/torrent_types';
|
||||||
|
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||||
|
import {Torrent} from "../../repository/models/torrent";
|
||||||
|
import {PromiseHelpers} from '../helpers/promises_helpers';
|
||||||
|
import {logger} from './logging_service';
|
||||||
|
import {metadataService} from './metadata_service';
|
||||||
|
import {torrentFileService} from './torrent_file_service';
|
||||||
|
import {torrentSubtitleService} from './torrent_subtitle_service';
|
||||||
|
import {TorrentAttributes} from "../../repository/interfaces/torrent_attributes";
|
||||||
|
import {File} from "../../repository/models/file";
|
||||||
|
import {Subtitle} from "../../repository/models/subtitle";
|
||||||
|
|
||||||
|
class TorrentEntriesService {
|
||||||
|
public async createTorrentEntry(torrent: ParsedTorrent, overwrite = false): Promise<void> {
|
||||||
|
const titleInfo = parse(torrent.title);
|
||||||
|
|
||||||
|
if (!torrent.imdbId && torrent.type !== TorrentType.Anime) {
|
||||||
|
const imdbQuery = {
|
||||||
|
title: titleInfo.title,
|
||||||
|
year: titleInfo.year,
|
||||||
|
type: torrent.type
|
||||||
|
};
|
||||||
|
torrent.imdbId = await metadataService.getImdbId(imdbQuery)
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
if (torrent.imdbId && torrent.imdbId.toString().length < 9) {
|
||||||
|
// pad zeros to imdbId if missing
|
||||||
|
torrent.imdbId = 'tt' + torrent.imdbId.toString().replace('tt', '').padStart(7, '0');
|
||||||
|
}
|
||||||
|
if (torrent.imdbId && torrent.imdbId.toString().length > 9 && torrent.imdbId.toString().startsWith('tt0')) {
|
||||||
|
// sanitize imdbId from redundant zeros
|
||||||
|
torrent.imdbId = torrent.imdbId.toString().replace(/tt0+([0-9]{7,})$/, 'tt$1');
|
||||||
|
}
|
||||||
|
if (!torrent.kitsuId && torrent.type === TorrentType.Anime) {
|
||||||
|
const kitsuQuery = {
|
||||||
|
title: titleInfo.title,
|
||||||
|
year: titleInfo.year,
|
||||||
|
season: titleInfo.season,
|
||||||
|
};
|
||||||
|
torrent.kitsuId = await metadataService.getKitsuId(kitsuQuery)
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!torrent.imdbId && !torrent.kitsuId && !torrentFileService.isPackTorrent(torrent)) {
|
||||||
|
logger.warn(`imdbId or kitsuId not found: ${torrent.provider} ${torrent.title}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileCollection: TorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
|
||||||
|
.then((torrentContents: TorrentFileCollection) => overwrite ? this.overwriteExistingFiles(torrent, torrentContents) : torrentContents)
|
||||||
|
.then((torrentContents: TorrentFileCollection) => torrentSubtitleService.assignSubtitles(torrentContents))
|
||||||
|
.catch(error => {
|
||||||
|
logger.warn(`Failed getting files for ${torrent.title}`, error.message);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fileCollection.videos || !fileCollection.videos.length) {
|
||||||
|
logger.warn(`no video files found for ${torrent.provider} [${torrent.infoHash}] ${torrent.title}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTorrent: Torrent = Torrent.build({
|
||||||
|
...torrent,
|
||||||
|
contents: fileCollection.contents,
|
||||||
|
subtitles: fileCollection.subtitles
|
||||||
|
});
|
||||||
|
|
||||||
|
return repository.createTorrent(newTorrent)
|
||||||
|
.then(() => PromiseHelpers.sequence(fileCollection.videos.map(video => () => {
|
||||||
|
const newVideo = File.build(video);
|
||||||
|
return repository.createFile(newVideo)
|
||||||
|
})))
|
||||||
|
.then(() => logger.info(`Created ${torrent.provider} entry for [${torrent.infoHash}] ${torrent.title}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createSkipTorrentEntry(torrent: Torrent) {
|
||||||
|
return repository.createSkipTorrent(torrent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getStoredTorrentEntry(torrent: Torrent) {
|
||||||
|
return repository.getSkipTorrent(torrent.infoHash)
|
||||||
|
.catch(() => repository.getTorrent(torrent))
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async checkAndUpdateTorrent(torrent: ParsedTorrent): Promise<boolean> {
|
||||||
|
const query: TorrentAttributes = {
|
||||||
|
infoHash: torrent.infoHash,
|
||||||
|
provider: torrent.provider,
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingTorrent = await repository.getTorrent(query).catch(() => undefined);
|
||||||
|
|
||||||
|
if (!existingTorrent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTorrent.provider === 'RARBG') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (existingTorrent.provider === 'KickassTorrents' && torrent.provider) {
|
||||||
|
existingTorrent.provider = torrent.provider;
|
||||||
|
existingTorrent.torrentId = torrent.torrentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingTorrent.languages && torrent.languages && existingTorrent.provider !== 'RARBG') {
|
||||||
|
existingTorrent.languages = torrent.languages;
|
||||||
|
await existingTorrent.save();
|
||||||
|
logger.debug(`Updated [${existingTorrent.infoHash}] ${existingTorrent.title} language to ${torrent.languages}`);
|
||||||
|
}
|
||||||
|
return this.createTorrentContents(existingTorrent)
|
||||||
|
.then(() => this.updateTorrentSeeders(existingTorrent));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createTorrentContents(torrent: Torrent) {
|
||||||
|
if (torrent.opened) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedVideos: File[] = await repository.getFiles(torrent.infoHash).catch(() => []);
|
||||||
|
if (!storedVideos || !storedVideos.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notOpenedVideo = storedVideos.length === 1 && !Number.isInteger(storedVideos[0].fileIndex);
|
||||||
|
const imdbId: string | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.imdbId));
|
||||||
|
const kitsuId: number | undefined = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.kitsuId));
|
||||||
|
|
||||||
|
const fileCollection: TorrentFileCollection = await torrentFileService.parseTorrentFiles(torrent)
|
||||||
|
.then(torrentContents => notOpenedVideo ? torrentContents : {...torrentContents, videos: storedVideos})
|
||||||
|
.then(torrentContents => torrentSubtitleService.assignSubtitles(torrentContents))
|
||||||
|
.then(torrentContents => this.assignMetaIds(torrentContents, imdbId, kitsuId))
|
||||||
|
.catch(error => {
|
||||||
|
logger.warn(`Failed getting contents for [${torrent.infoHash}] ${torrent.title}`, error.message);
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.assignMetaIds(fileCollection, imdbId, kitsuId);
|
||||||
|
|
||||||
|
if (!fileCollection.contents || !fileCollection.contents.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notOpenedVideo && fileCollection.videos.length === 1) {
|
||||||
|
// if both have a single video and stored one was not opened, update stored one to true metadata and use that
|
||||||
|
storedVideos[0].fileIndex = fileCollection.videos[0].fileIndex;
|
||||||
|
storedVideos[0].title = fileCollection.videos[0].title;
|
||||||
|
storedVideos[0].size = fileCollection.videos[0].size;
|
||||||
|
storedVideos[0].subtitles = fileCollection.videos[0].subtitles.map(subtitle => Subtitle.build(subtitle));
|
||||||
|
fileCollection.videos[0] = storedVideos[0];
|
||||||
|
}
|
||||||
|
// no videos available or more than one new videos were in the torrent
|
||||||
|
const shouldDeleteOld = notOpenedVideo && fileCollection.videos.every(video => !video.id);
|
||||||
|
|
||||||
|
const newTorrent: Torrent = Torrent.build({
|
||||||
|
...torrent,
|
||||||
|
contents: fileCollection.contents,
|
||||||
|
subtitles: fileCollection.subtitles
|
||||||
|
});
|
||||||
|
|
||||||
|
return repository.createTorrent(newTorrent)
|
||||||
|
.then(() => {
|
||||||
|
if (shouldDeleteOld) {
|
||||||
|
logger.debug(`Deleting old video for [${torrent.infoHash}] ${torrent.title}`)
|
||||||
|
return storedVideos[0].destroy();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
})
|
||||||
|
.then(() => PromiseHelpers.sequence(fileCollection.videos.map(video => () => {
|
||||||
|
const newVideo = File.build(video);
|
||||||
|
return repository.createFile(newVideo)
|
||||||
|
})))
|
||||||
|
.then(() => logger.info(`Created contents for ${torrent.provider} [${torrent.infoHash}] ${torrent.title}`))
|
||||||
|
.catch(error => logger.error(`Failed saving contents for [${torrent.infoHash}] ${torrent.title}`, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateTorrentSeeders(torrent: TorrentAttributes) {
|
||||||
|
if (!(torrent.infoHash || (torrent.provider && torrent.torrentId)) || !Number.isInteger(torrent.seeders)) {
|
||||||
|
return torrent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return repository.setTorrentSeeders(torrent, torrent.seeders)
|
||||||
|
.catch(error => {
|
||||||
|
logger.warn('Failed updating seeders:', error);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private assignMetaIds(fileCollection: TorrentFileCollection, imdbId: string, kitsuId: number): TorrentFileCollection {
|
||||||
|
if (fileCollection.videos && fileCollection.videos.length) {
|
||||||
|
fileCollection.videos.forEach(video => {
|
||||||
|
video.imdbId = imdbId;
|
||||||
|
video.kitsuId = kitsuId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async overwriteExistingFiles(torrent: ParsedTorrent, torrentContents: TorrentFileCollection) {
|
||||||
|
const videos = torrentContents && torrentContents.videos;
|
||||||
|
if (videos && videos.length) {
|
||||||
|
const existingFiles = await repository.getFiles(torrent.infoHash)
|
||||||
|
.then((existing) => existing
|
||||||
|
.reduce((map, next) => {
|
||||||
|
const fileIndex = next.fileIndex !== undefined ? next.fileIndex : null;
|
||||||
|
map[fileIndex] = (map[fileIndex] || []).concat(next);
|
||||||
|
return map;
|
||||||
|
}, {}))
|
||||||
|
.catch(() => undefined);
|
||||||
|
if (existingFiles && Object.keys(existingFiles).length) {
|
||||||
|
const overwrittenVideos = videos
|
||||||
|
.map(file => {
|
||||||
|
const mapping = videos.length === 1 && Object.keys(existingFiles).length === 1
|
||||||
|
? Object.values(existingFiles)[0]
|
||||||
|
: existingFiles[file.fileIndex !== undefined ? file.fileIndex : null];
|
||||||
|
if (mapping) {
|
||||||
|
const originalFile = mapping.shift();
|
||||||
|
return {id: originalFile.id, ...file};
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
return {...torrentContents, videos: overwrittenVideos};
|
||||||
|
}
|
||||||
|
return torrentContents;
|
||||||
|
}
|
||||||
|
return Promise.reject(`No video files found for: ${torrent.title}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const torrentEntriesService = new TorrentEntriesService();
|
||||||
@@ -1,30 +1,32 @@
|
|||||||
import Bottleneck from 'bottleneck';
|
import Bottleneck from 'bottleneck';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { parse } from 'parse-torrent-title';
|
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 {TorrentInfo} from "../interfaces/torrent_info";
|
import {configurationService} from './configuration_service';
|
||||||
import { configurationService } from './configuration_service';
|
import {extensionService} from './extension_service';
|
||||||
import { extensionService } from './extension_service';
|
import {metadataService} from './metadata_service';
|
||||||
import { metadataService } from './metadata_service';
|
import {torrentDownloadService} from "./torrent_download_service";
|
||||||
import { parsingService } from './parsing_service';
|
|
||||||
import { torrentDownloadService } from "./torrent_download_service";
|
|
||||||
import {logger} from "./logging_service";
|
import {logger} from "./logging_service";
|
||||||
import {MetadataResponse} from "../interfaces/metadata_response";
|
import {MetadataResponse} from "../interfaces/metadata_response";
|
||||||
import {ParseTorrentTitleResult} from "../interfaces/parse_torrent_title_result";
|
|
||||||
import {MetaDataQuery} from "../interfaces/metadata_query";
|
import {MetaDataQuery} from "../interfaces/metadata_query";
|
||||||
import {ParsableTorrentFile} from "../interfaces/parsable_torrent_file";
|
|
||||||
import {CommonVideoMetadata} from "../interfaces/common_video_metadata";
|
import {CommonVideoMetadata} from "../interfaces/common_video_metadata";
|
||||||
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||||
|
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
||||||
|
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
||||||
|
import {ContentAttributes} from "../../repository/interfaces/content_attributes";
|
||||||
|
|
||||||
class TorrentFileService {
|
class TorrentFileService {
|
||||||
private readonly MIN_SIZE: number = 5 * 1024 * 1024; // 5 MB
|
private readonly MIN_SIZE: number = 5 * 1024 * 1024; // 5 MB
|
||||||
|
|
||||||
|
private readonly MULTIPLE_FILES_SIZE = 4 * 1024 * 1024 * 1024; // 4 GB
|
||||||
|
|
||||||
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: TorrentInfo) {
|
public async parseTorrentFiles(torrent: ParsedTorrent): Promise<TorrentFileCollection> {
|
||||||
const parsedTorrentName = parse(torrent.title);
|
const parsedTorrentName = parse(torrent.title);
|
||||||
const query: MetaDataQuery = {
|
const query: MetaDataQuery = {
|
||||||
id: torrent.kitsuId || torrent.imdbId,
|
id: torrent.kitsuId || torrent.imdbId,
|
||||||
@@ -47,7 +49,32 @@ class TorrentFileService {
|
|||||||
return this.parseSeriesFiles(torrent, metadata)
|
return this.parseSeriesFiles(torrent, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async parseMovieFiles(torrent: TorrentInfo, metadata: MetadataResponse): Promise<TorrentFileCollection> {
|
public isPackTorrent(torrent: ParsedTorrent): boolean {
|
||||||
|
if (torrent.isPack) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const parsedInfo = parse(torrent.title);
|
||||||
|
if (torrent.type === TorrentType.Movie) {
|
||||||
|
return parsedInfo.complete || typeof parsedInfo.year === 'string' || /movies/i.test(torrent.title);
|
||||||
|
}
|
||||||
|
const hasMultipleEpisodes = parsedInfo.complete ||
|
||||||
|
torrent.size > this.MULTIPLE_FILES_SIZE ||
|
||||||
|
(parsedInfo.seasons && parsedInfo.seasons.length > 1) ||
|
||||||
|
(parsedInfo.episodes && parsedInfo.episodes.length > 1) ||
|
||||||
|
(parsedInfo.seasons && !parsedInfo.episodes);
|
||||||
|
const hasSingleEpisode = Number.isInteger(parsedInfo.episode) || (!parsedInfo.episodes && parsedInfo.date);
|
||||||
|
return hasMultipleEpisodes && !hasSingleEpisode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseSeriesVideos(torrent: ParsedTorrent): FileAttributes[] {
|
||||||
|
const parsedTorrentName = parse(torrent.title);
|
||||||
|
const hasMovies = parsedTorrentName.complete || !!torrent.title.match(/movies?(?:\W|$)/i);
|
||||||
|
const parsedVideos = torrent.fileCollection.videos.map(video => this.parseSeriesVideo(video));
|
||||||
|
|
||||||
|
return parsedVideos.map(video => ({ ...video, isMovie: this.isMovieVideo(torrent, video, parsedVideos, hasMovies) }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async parseMovieFiles(torrent: ParsedTorrent, metadata: MetadataResponse): Promise<TorrentFileCollection> {
|
||||||
const {contents, videos, subtitles} = await this.getMoviesTorrentContent(torrent);
|
const {contents, videos, subtitles} = await this.getMoviesTorrentContent(torrent);
|
||||||
const filteredVideos = videos
|
const filteredVideos = videos
|
||||||
.filter(video => video.size > this.MIN_SIZE)
|
.filter(video => video.size > this.MIN_SIZE)
|
||||||
@@ -66,7 +93,7 @@ class TorrentFileService {
|
|||||||
|
|
||||||
const parsedVideos = await PromiseHelpers.sequence(filteredVideos.map(video => () => this.isFeaturette(video)
|
const parsedVideos = await PromiseHelpers.sequence(filteredVideos.map(video => () => this.isFeaturette(video)
|
||||||
? Promise.resolve(video)
|
? Promise.resolve(video)
|
||||||
: this.findMovieImdbId(video.name).then(imdbId => ({...video, imdbId}))))
|
: this.findMovieImdbId(video.title).then(imdbId => ({...video, imdbId}))))
|
||||||
.then(videos => videos.map(video => ({
|
.then(videos => videos.map(video => ({
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
fileIndex: video.fileIndex,
|
fileIndex: video.fileIndex,
|
||||||
@@ -77,53 +104,58 @@ class TorrentFileService {
|
|||||||
return {contents, videos: parsedVideos, subtitles};
|
return {contents, videos: parsedVideos, subtitles};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async parseSeriesFiles(torrent: TorrentInfo, metadata: MetadataResponse) {
|
private async parseSeriesFiles(torrent: ParsedTorrent, metadata: MetadataResponse): TorrentFileCollection {
|
||||||
const {contents, videos, subtitles} = await this.getSeriesTorrentContent(torrent);
|
const fileCollection: TorrentFileCollection = await this.getSeriesTorrentContent(torrent);
|
||||||
const parsedVideos = await Promise.resolve(videos)
|
const parsedVideos: FileAttributes[] = await Promise.resolve(fileCollection.videos)
|
||||||
.then(videos => videos.filter(video => videos.length === 1 || video.size > this.MIN_SIZE))
|
.then(videos => videos.filter(video => videos.length === 1 || video.size > this.MIN_SIZE))
|
||||||
.then(videos => parsingService.parseSeriesVideos(torrent, videos))
|
.then(videos => this.parseSeriesVideos(torrent))
|
||||||
.then(videos => this.decomposeEpisodes(torrent, videos, metadata))
|
.then(videos => this.decomposeEpisodes(torrent, metadata))
|
||||||
.then(videos => this.assignKitsuOrImdbEpisodes(torrent, videos, metadata))
|
.then(videos => this.assignKitsuOrImdbEpisodes(torrent, fileCollection.videos, metadata))
|
||||||
.then(videos => Promise.all(videos.map(video => video.isMovie
|
.then(videos => Promise.all(videos.map(video => video.isMovie
|
||||||
? this.mapSeriesMovie(video, torrent)
|
? this.mapSeriesMovie(video, torrent)
|
||||||
: this.mapSeriesEpisode(video, torrent, videos))))
|
: this.mapSeriesEpisode(video, torrent, videos))))
|
||||||
.then(videos => videos
|
.then(videos => videos
|
||||||
.map((video: ParsableTorrentFile) => this.isFeaturette(video) ? this.clearInfoFields(video) : video))
|
.map((video: ParsedTorrent) => this.isFeaturette(video) ? this.clearInfoFields(video) : video))
|
||||||
return {contents, videos: parsedVideos, subtitles};
|
return {...torrent.fileCollection, videos: parsedVideos};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getMoviesTorrentContent(torrent: TorrentInfo) {
|
private async getMoviesTorrentContent(torrent: ParsedTorrent) {
|
||||||
const files = await torrentDownloadService.getTorrentFiles(torrent)
|
const files = await torrentDownloadService.getTorrentFiles(torrent)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (!parsingService.isPackTorrent(torrent)) {
|
if (!this.isPackTorrent(torrent)) {
|
||||||
return { videos: [{name: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}], contents:[], subtitles: []}
|
const entries = [{name: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}];
|
||||||
|
return { videos: entries, contents:[], subtitles: [], files: entries}
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (files.contents && files.contents.length && !files.videos.length && this.isDiskTorrent(files.contents)) {
|
if (files.contents && files.contents.length && !files.videos.length && this.isDiskTorrent(files.contents)) {
|
||||||
files.videos = [{name: torrent.title, path: torrent.title, size: torrent.size}];
|
files.videos = [{name: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}];
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDefaultFileEntries(torrent: ParsedTorrent): FileAttributes[] {
|
||||||
|
return [{title: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}];
|
||||||
|
}
|
||||||
|
|
||||||
private async getSeriesTorrentContent(torrent: TorrentInfo) {
|
private async getSeriesTorrentContent(torrent: ParsedTorrent): Promise<TorrentFileCollection> {
|
||||||
return torrentDownloadService.getTorrentFiles(torrent)
|
return torrentDownloadService.getTorrentFiles(torrent)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (!parsingService.isPackTorrent(torrent)) {
|
if (!this.isPackTorrent(torrent)) {
|
||||||
return { videos: [{ name: torrent.title, path: torrent.title, size: torrent.size }], subtitles: [], contents: [] }
|
return { videos: this.getDefaultFileEntries(torrent), subtitles: [], contents: [] }
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async mapSeriesEpisode(file: ParsableTorrentFile, torrent: TorrentInfo, files: ParsableTorrentFile[]) : Promise<ParsableTorrentFile> {
|
private async mapSeriesEpisode(file: FileAttributes, torrent: ParsedTorrent, files: ParsedTorrent[]) : Promise<FileAttributes[]> {
|
||||||
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({
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
fileIndex: file.fileIndex,
|
fileIndex: file.fileIndex,
|
||||||
title: file.path || file.name,
|
title: file.path || file.title,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
imdbId: torrent.imdbId || file.imdbId,
|
imdbId: torrent.imdbId || file.imdbId,
|
||||||
});
|
});
|
||||||
@@ -134,17 +166,20 @@ class TorrentFileService {
|
|||||||
return Promise.resolve(episodeIndexes.map((index) => ({
|
return Promise.resolve(episodeIndexes.map((index) => ({
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
fileIndex: file.fileIndex,
|
fileIndex: file.fileIndex,
|
||||||
title: file.path || file.name,
|
title: file.path || file.title,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
imdbId: file.imdbId || torrent.imdbId,
|
imdbId: file.imdbId.toString() || torrent.imdbId.toString(),
|
||||||
|
imdbSeason: file.season,
|
||||||
season: file.season,
|
season: file.season,
|
||||||
|
imdbEpisode: file.episodes && file.episodes[index],
|
||||||
episode: file.episodes && file.episodes[index],
|
episode: file.episodes && file.episodes[index],
|
||||||
|
kitsuEpisode: file.episodes && file.episodes[index],
|
||||||
episodes: file.episodes,
|
episodes: file.episodes,
|
||||||
kitsuId: file.kitsuId || torrent.kitsuId,
|
kitsuId: parseInt(file.kitsuId.toString() || torrent.kitsuId.toString()),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
private async mapSeriesMovie(file: ParsableTorrentFile, torrent: TorrentInfo): Promise<ParsableTorrentFile> {
|
private async mapSeriesMovie(torrent: ParsedTorrent, file: FileAttributes): Promise<FileAttributes[]> {
|
||||||
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) {
|
||||||
@@ -168,10 +203,10 @@ class TorrentFileService {
|
|||||||
return [{
|
return [{
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
fileIndex: file.fileIndex,
|
fileIndex: file.fileIndex,
|
||||||
title: file.path || file.name,
|
title: file.path || file.title,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
imdbId: imdbId,
|
imdbId: imdbId,
|
||||||
kitsuId: kitsuId,
|
kitsuId: parseInt(kitsuId),
|
||||||
episodes: undefined,
|
episodes: undefined,
|
||||||
imdbSeason: undefined,
|
imdbSeason: undefined,
|
||||||
imdbEpisode: undefined,
|
imdbEpisode: undefined,
|
||||||
@@ -185,16 +220,17 @@ class TorrentFileService {
|
|||||||
return [{
|
return [{
|
||||||
infoHash: torrent.infoHash,
|
infoHash: torrent.infoHash,
|
||||||
fileIndex: file.fileIndex,
|
fileIndex: file.fileIndex,
|
||||||
title: file.path || file.name,
|
title: file.path || file.title,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
imdbId: metadata.imdbId || imdbId,
|
imdbId: metadata.imdbId.toString() || imdbId,
|
||||||
kitsuId: metadata.kitsuId || kitsuId,
|
kitsuId: parseInt(metadata.kitsuId.toString() || kitsuId),
|
||||||
season: episodeVideo && metadata.imdbId ? episodeVideo.season : undefined,
|
imdbSeason: episodeVideo && metadata.imdbId ? episodeVideo.season : undefined,
|
||||||
episode: episodeVideo && metadata.imdbId | metadata.kitsuId ? episodeVideo.episode || episodeVideo.episode : undefined,
|
imdbEpisode: episodeVideo && metadata.imdbId | metadata.kitsuId ? episodeVideo.episode || episodeVideo.episode : undefined,
|
||||||
|
kitsuEpisode: episodeVideo && metadata.imdbId | metadata.kitsuId ? episodeVideo.episode || episodeVideo.episode : undefined,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async decomposeEpisodes(torrent: TorrentInfo, files: ParsableTorrentFile[], metadata: MetadataResponse = { episodeCount: [] }) {
|
private async decomposeEpisodes(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse = { episodeCount: [] }) {
|
||||||
if (files.every(file => !file.episodes && !file.date)) {
|
if (files.every(file => !file.episodes && !file.date)) {
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
@@ -239,7 +275,7 @@ class TorrentFileService {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private preprocessEpisodes(files: ParsableTorrentFile[]) {
|
private preprocessEpisodes(files: FileAttributes[]) {
|
||||||
// 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)
|
||||||
@@ -250,7 +286,7 @@ class TorrentFileService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private isConcatSeasonAndEpisodeFiles(files: ParsableTorrentFile[], sortedEpisodes: number[], metadata: MetadataResponse) {
|
private isConcatSeasonAndEpisodeFiles(files: FileAttributes[], sortedEpisodes: number[], metadata: MetadataResponse) {
|
||||||
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;
|
||||||
@@ -277,11 +313,11 @@ class TorrentFileService {
|
|||||||
|| concatAboveTotalEpisodeCount.length >= thresholdAbove;
|
|| concatAboveTotalEpisodeCount.length >= thresholdAbove;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isDateEpisodeFiles(files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
private isDateEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) {
|
||||||
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: TorrentInfo, files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
private isAbsoluteEpisodeFiles(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse) {
|
||||||
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
|
||||||
@@ -294,18 +330,18 @@ class TorrentFileService {
|
|||||||
|| absoluteEpisodes.length >= threshold;
|
|| absoluteEpisodes.length >= threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isNewEpisodeNotInMetadata(torrent: TorrentInfo, file: ParsableTorrentFile, metadata: MetadataResponse) {
|
private isNewEpisodeNotInMetadata(torrent: ParsedTorrent, video: FileAttributes, metadata: MetadataResponse) {
|
||||||
// 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
|
||||||
const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId;
|
const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId;
|
||||||
return !isAnime && !file.isMovie && file.episodes && file.season !== 1
|
return !isAnime && !video.isMovie && video.episodes && video.season !== 1
|
||||||
&& /continuing|current/i.test(metadata.status)
|
&& /continuing|current/i.test(metadata.status)
|
||||||
&& file.season >= metadata.episodeCount.length
|
&& video.season >= metadata.episodeCount.length
|
||||||
&& file.episodes.every(ep => ep > (metadata.episodeCount[file.season - 1] || 0));
|
&& video.episodes.every(ep => ep > (metadata.episodeCount[video.season - 1] || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private decomposeConcatSeasonAndEpisodeFiles(files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
private decomposeConcatSeasonAndEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) {
|
||||||
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)
|
||||||
@@ -317,16 +353,16 @@ class TorrentFileService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private decomposeAbsoluteEpisodeFiles(torrent: TorrentInfo, files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
private decomposeAbsoluteEpisodeFiles(torrent: ParsedTorrent, videos: FileAttributes[], metadata: MetadataResponse) {
|
||||||
if (metadata.episodeCount.length === 0) {
|
if (metadata.episodeCount.length === 0) {
|
||||||
files
|
videos
|
||||||
.filter(file => !Number.isInteger(file.season) && file.episodes && !file.isMovie)
|
.filter(file => !Number.isInteger(file.season) && file.episodes && !file.isMovie)
|
||||||
.forEach(file => {
|
.forEach(file => {
|
||||||
file.season = 1;
|
file.season = 1;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
files
|
videos
|
||||||
.filter(file => file.episodes && !file.isMovie && file.season !== 0)
|
.filter(file => file.episodes && !file.isMovie && file.season !== 0)
|
||||||
.filter(file => !this.isNewEpisodeNotInMetadata(torrent, file, metadata))
|
.filter(file => !this.isNewEpisodeNotInMetadata(torrent, file, metadata))
|
||||||
.filter(file => !file.season || (metadata.episodeCount[file.season - 1] || 0) < file.episodes[0])
|
.filter(file => !file.season || (metadata.episodeCount[file.season - 1] || 0) < file.episodes[0])
|
||||||
@@ -341,7 +377,7 @@ class TorrentFileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private decomposeDateEpisodeFiles(files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
private decomposeDateEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) {
|
||||||
if (!metadata || !metadata.videos || !metadata.videos.length) {
|
if (!metadata || !metadata.videos || !metadata.videos.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -375,7 +411,7 @@ class TorrentFileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private assignKitsuOrImdbEpisodes(torrent: TorrentInfo, files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
private assignKitsuOrImdbEpisodes(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse) {
|
||||||
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
|
||||||
@@ -387,7 +423,7 @@ class TorrentFileService {
|
|||||||
})
|
})
|
||||||
if (metadata.type === TorrentType.Movie && files.every(file => !file.imdbId)) {
|
if (metadata.type === TorrentType.Movie && files.every(file => !file.imdbId)) {
|
||||||
// sometimes a movie has episode naming, thus not recognized as a movie and imdbId not assigned
|
// sometimes a movie has episode naming, thus not recognized as a movie and imdbId not assigned
|
||||||
files.forEach(file => file.imdbId = metadata.imdbId);
|
files.forEach(file => file.imdbId = metadata.imdbId.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return files;
|
return files;
|
||||||
@@ -402,14 +438,14 @@ class TorrentFileService {
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
if (metadata.videos.some(video => Number.isInteger(video.season)) || !metadata.imdbId) {
|
if (metadata.videos.some(video => Number.isInteger(video.season)) || !metadata.imdbId) {
|
||||||
files.filter((file => Number.isInteger(file.season) && file.episodes))
|
files.filter((file => Number.isInteger(torrent.season) && torrent.episodes))
|
||||||
.map(file => {
|
.map(file => {
|
||||||
const seasonMapping = seriesMapping[file.season];
|
const seasonMapping = seriesMapping[file.season];
|
||||||
const episodeMapping = seasonMapping && seasonMapping[file.episodes[0]];
|
const episodeMapping = seasonMapping && seasonMapping[file.episodes[0]];
|
||||||
if (episodeMapping && Number.isInteger(episodeMapping.imdbSeason)) {
|
if (episodeMapping && Number.isInteger(episodeMapping.season)) {
|
||||||
file.imdbId = metadata.imdbId;
|
file.imdbId = metadata.imdbId.toString();
|
||||||
file.season = episodeMapping.imdbSeason;
|
file.season = episodeMapping.season;
|
||||||
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].imdbEpisode);
|
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].episode);
|
||||||
} else {
|
} else {
|
||||||
// no imdb mapping available for episode
|
// no imdb mapping available for episode
|
||||||
file.season = undefined;
|
file.season = undefined;
|
||||||
@@ -423,7 +459,7 @@ class TorrentFileService {
|
|||||||
.forEach(file => {
|
.forEach(file => {
|
||||||
if (seriesMapping[file.season]) {
|
if (seriesMapping[file.season]) {
|
||||||
const seasonMapping = seriesMapping[file.season];
|
const seasonMapping = seriesMapping[file.season];
|
||||||
file.imdbId = metadata.imdbId;
|
file.imdbId = metadata.imdbId.toString();
|
||||||
file.kitsuId = seasonMapping[file.episodes[0]] && seasonMapping[file.episodes[0]].kitsuId;
|
file.kitsuId = seasonMapping[file.episodes[0]] && seasonMapping[file.episodes[0]].kitsuId;
|
||||||
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]) {
|
||||||
@@ -434,12 +470,12 @@ class TorrentFileService {
|
|||||||
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 => otherFile.season === file.season)
|
.filter((otherFile: FileAttributes) => otherFile.season === file.season)
|
||||||
.reduce((a, b) => a.episodes.concat(b.episodes), []);
|
.reduce((a, b) => a.episodes.concat(b.episodes), []);
|
||||||
const isAbsoluteOrder = seasonEpisodes.episodes.every(ep => ep > skippedCount && ep <= episodes.length)
|
const isAbsoluteOrder = seasonEpisodes.episodes.every(ep => ep > skippedCount && ep <= episodes.length)
|
||||||
const isNormalOrder = seasonEpisodes.episodes.every(ep => ep + skippedCount <= episodes.length)
|
const isNormalOrder = seasonEpisodes.episodes.every(ep => ep + skippedCount <= episodes.length)
|
||||||
if (differentTitlesCount >= 1 && (isAbsoluteOrder || isNormalOrder)) {
|
if (differentTitlesCount >= 1 && (isAbsoluteOrder || isNormalOrder)) {
|
||||||
file.imdbId = metadata.imdbId;
|
file.imdbId = metadata.imdbId.toString();
|
||||||
file.season = file.season - 1;
|
file.season = file.season - 1;
|
||||||
file.episodes = file.episodes.map(ep => isAbsoluteOrder ? ep : ep + skippedCount);
|
file.episodes = file.episodes.map(ep => isAbsoluteOrder ? ep : ep + skippedCount);
|
||||||
file.kitsuId = seasonMapping[file.episodes[0]].kitsuId;
|
file.kitsuId = seasonMapping[file.episodes[0]].kitsuId;
|
||||||
@@ -448,7 +484,7 @@ class TorrentFileService {
|
|||||||
} else if (Object.values(seriesMapping).length === 1 && seriesMapping[1]) {
|
} else if (Object.values(seriesMapping).length === 1 && seriesMapping[1]) {
|
||||||
// sometimes series might be named with sequel season but it's not a season on imdb and a new title
|
// sometimes series might be named with sequel season but it's not a season on imdb and a new title
|
||||||
const seasonMapping = seriesMapping[1];
|
const seasonMapping = seriesMapping[1];
|
||||||
file.imdbId = metadata.imdbId;
|
file.imdbId = metadata.imdbId.toString();
|
||||||
file.season = 1;
|
file.season = 1;
|
||||||
file.kitsuId = seasonMapping[file.episodes[0]].kitsuId;
|
file.kitsuId = seasonMapping[file.episodes[0]].kitsuId;
|
||||||
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode);
|
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode);
|
||||||
@@ -458,7 +494,7 @@ class TorrentFileService {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
private needsCinemetaMetadataForAnime(files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
private needsCinemetaMetadataForAnime(files: FileAttributes[], metadata: MetadataResponse) {
|
||||||
if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) {
|
if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -500,7 +536,7 @@ class TorrentFileService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private findMovieImdbId(title: ParseTorrentTitleResult | string) {
|
private findMovieImdbId(title: FileAttributes | 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 () => {
|
||||||
@@ -517,7 +553,7 @@ class TorrentFileService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async findMovieKitsuId(title: ParseTorrentTitleResult | string) {
|
private async findMovieKitsuId(title: FileAttributes | 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,
|
||||||
@@ -532,26 +568,97 @@ class TorrentFileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private isDiskTorrent(contents: ParsableTorrentFile[]) {
|
private isDiskTorrent(contents: ContentAttributes[]) {
|
||||||
return contents.some(content => extensionService.isDisk(content.path));
|
return contents.some(content => extensionService.isDisk(content.path));
|
||||||
}
|
}
|
||||||
|
|
||||||
private isSingleMovie(videos: ParsableTorrentFile[]) {
|
private isSingleMovie(videos: FileAttributes[]) {
|
||||||
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: ParsableTorrentFile) {
|
private isFeaturette(video: FileAttributes) {
|
||||||
return /featurettes?\/|extras-grym/i.test(video.path);
|
return /featurettes?\/|extras-grym/i.test(video.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearInfoFields(video: ParsableTorrentFile) {
|
private parseSeriesVideo(video: FileAttributes): FileAttributes {
|
||||||
|
const videoInfo = parse(video.title);
|
||||||
|
// the episode may be in a folder containing season number
|
||||||
|
if (!Number.isInteger(videoInfo.season) && video.path.includes('/')) {
|
||||||
|
const folders = video.path.split('/');
|
||||||
|
const pathInfo = parse(folders[folders.length - 2]);
|
||||||
|
videoInfo.season = pathInfo.season;
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(videoInfo.season) && video.season) {
|
||||||
|
videoInfo.season = video.season;
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(videoInfo.season) && videoInfo.seasons && videoInfo.seasons.length > 1) {
|
||||||
|
// in case single file was interpreted as having multiple seasons
|
||||||
|
videoInfo.season = videoInfo.seasons[0];
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(videoInfo.season) && video.path.includes('/') && video.seasons
|
||||||
|
&& video.seasons.length > 1) {
|
||||||
|
// russian season are usually named with 'series name-2` i.e. Улицы разбитых фонарей-6/22. Одиночный выстрел.mkv
|
||||||
|
const folderPathSeasonMatch = video.path.match(/[\u0400-\u04ff]-(\d{1,2})(?=.*\/)/);
|
||||||
|
videoInfo.season = folderPathSeasonMatch && parseInt(folderPathSeasonMatch[1], 10) || undefined;
|
||||||
|
}
|
||||||
|
// sometimes video file does not have correct date format as in torrent title
|
||||||
|
if (!videoInfo.episodes && !videoInfo.date && video.date) {
|
||||||
|
videoInfo.date = video.date;
|
||||||
|
}
|
||||||
|
// limit number of episodes in case of incorrect parsing
|
||||||
|
if (videoInfo.episodes && videoInfo.episodes.length > 20) {
|
||||||
|
videoInfo.episodes = [videoInfo.episodes[0]];
|
||||||
|
videoInfo.episode = videoInfo.episodes[0];
|
||||||
|
}
|
||||||
|
// force episode to any found number if it was not parsed
|
||||||
|
if (!videoInfo.episodes && !videoInfo.date) {
|
||||||
|
const epMatcher = videoInfo.title.match(
|
||||||
|
/(?<!season\W*|disk\W*|movie\W*|film\W*)(?:^|\W|_)(\d{1,4})(?:a|b|c|v\d)?(?:_|\W|$)(?!disk|movie|film)/i);
|
||||||
|
videoInfo.episodes = epMatcher && [parseInt(epMatcher[1], 10)];
|
||||||
|
videoInfo.episode = videoInfo.episodes && videoInfo.episodes[0];
|
||||||
|
}
|
||||||
|
if (!videoInfo.episodes && !videoInfo.date) {
|
||||||
|
const epMatcher = video.title.match(new RegExp(`(?:\\(${videoInfo.year}\\)|part)[._ ]?(\\d{1,3})(?:\\b|_)`, "i"));
|
||||||
|
videoInfo.episodes = epMatcher && [parseInt(epMatcher[1], 10)];
|
||||||
|
videoInfo.episode = videoInfo.episodes && videoInfo.episodes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...video, ...videoInfo };
|
||||||
|
}
|
||||||
|
|
||||||
|
private isMovieVideo(torrent: ParsedTorrent, video: FileAttributes, otherVideos: FileAttributes[], hasMovies: boolean): boolean {
|
||||||
|
if (Number.isInteger(torrent.season) && Array.isArray(torrent.episodes)) {
|
||||||
|
// not movie if video has season
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (torrent.title.match(/\b(?:\d+[ .]movie|movie[ .]\d+)\b/i)) {
|
||||||
|
// movie if video explicitly has numbered movie keyword in the name, ie. 1 Movie or Movie 1
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!hasMovies && torrent.type !== TorrentType.Anime) {
|
||||||
|
// not movie if torrent name does not contain movies keyword or is not a pack torrent and is not anime
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!torrent.episodes) {
|
||||||
|
// movie if there's no episode info it could be a movie
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// movie if contains year info and there aren't more than 3 video with same title and year
|
||||||
|
// as some series titles might contain year in it.
|
||||||
|
return !!torrent.year
|
||||||
|
&& otherVideos.length > 3
|
||||||
|
&& otherVideos.filter(other => other.title === video.title && other.year === video.year).length < 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearInfoFields(video: FileAttributes) {
|
||||||
video.imdbId = undefined;
|
video.imdbId = undefined;
|
||||||
video.season = undefined;
|
video.imdbSeason = undefined;
|
||||||
video.episode = undefined;
|
video.imdbEpisode = undefined;
|
||||||
video.kitsuId = undefined;
|
video.kitsuId = undefined;
|
||||||
|
video.kitsuEpisode = undefined;
|
||||||
return video;
|
return video;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import {TorrentInfo} from "../interfaces/torrent_info";
|
|
||||||
import {TorrentType} from "../enums/torrent_types";
|
import {TorrentType} from "../enums/torrent_types";
|
||||||
import {logger} from "./logging_service";
|
import {logger} from "./logging_service";
|
||||||
import {checkAndUpdateTorrent, createTorrentEntry} from "../torrentEntries.js";
|
|
||||||
import {trackerService} from "./tracker_service";
|
import {trackerService} from "./tracker_service";
|
||||||
|
import {torrentEntriesService} from "./torrent_entries_service";
|
||||||
import {IngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
|
import {IngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
|
||||||
|
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
||||||
|
|
||||||
class TorrentProcessingService {
|
class TorrentProcessingService {
|
||||||
public async processTorrentRecord(torrent: IngestedTorrentAttributes): Promise<void> {
|
public async processTorrentRecord(torrent: IngestedTorrentAttributes): 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: TorrentInfo = await this.parseTorrent(torrent, type);
|
const torrentInfo: ParsedTorrent = 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}`);
|
||||||
|
|
||||||
if (await checkAndUpdateTorrent(torrentInfo)) {
|
if (await torrentEntriesService.checkAndUpdateTorrent(torrentInfo)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createTorrentEntry(torrentInfo);
|
return torrentEntriesService.createTorrentEntry(torrentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async assignTorrentTrackers(): Promise<string> {
|
private async assignTorrentTrackers(): Promise<string> {
|
||||||
@@ -25,14 +25,14 @@ class TorrentProcessingService {
|
|||||||
return trackers.join(',');
|
return trackers.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async parseTorrent(torrent: IngestedTorrentAttributes, category: string): Promise<TorrentInfo> {
|
private async parseTorrent(torrent: IngestedTorrentAttributes, category: string): Promise<ParsedTorrent> {
|
||||||
const infoHash = torrent.info_hash?.trim().toLowerCase()
|
const infoHash = torrent.info_hash?.trim().toLowerCase()
|
||||||
return {
|
return {
|
||||||
title: torrent.name,
|
title: torrent.name,
|
||||||
torrentId: `${torrent.name}_${infoHash}`,
|
torrentId: `${torrent.name}_${infoHash}`,
|
||||||
infoHash: infoHash,
|
infoHash: infoHash,
|
||||||
seeders: 100,
|
seeders: 100,
|
||||||
size: torrent.size,
|
size: parseInt(torrent.size),
|
||||||
uploadDate: torrent.createdAt,
|
uploadDate: torrent.createdAt,
|
||||||
imdbId: this.parseImdbId(torrent),
|
imdbId: this.parseImdbId(torrent),
|
||||||
type: category,
|
type: category,
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import { parse } from 'parse-torrent-title';
|
import { parse } from 'parse-torrent-title';
|
||||||
|
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||||
|
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
||||||
|
|
||||||
class TorrentSubtitleService {
|
class TorrentSubtitleService {
|
||||||
public assignSubtitles(contents: any, videos: any[], subtitles: any[]) {
|
public assignSubtitles(fileCollection: TorrentFileCollection) : TorrentFileCollection {
|
||||||
if (videos && videos.length && subtitles && subtitles.length) {
|
if (fileCollection.videos && fileCollection.videos.length && fileCollection.subtitles && fileCollection.subtitles.length) {
|
||||||
if (videos.length === 1) {
|
if (fileCollection.videos.length === 1) {
|
||||||
videos[0].subtitles = subtitles;
|
fileCollection.videos[0].subtitles = fileCollection.subtitles;
|
||||||
return { contents, videos, subtitles: [] };
|
return { ...fileCollection, subtitles: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedVideos = videos.map(video => this.parseVideo(video));
|
const parsedVideos = fileCollection.videos.map(video => this.parseVideo(video));
|
||||||
const assignedSubs = 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 { contents, videos, subtitles: unassignedSubs };
|
return { ...fileCollection, subtitles: unassignedSubs };
|
||||||
}
|
}
|
||||||
return { contents, videos, subtitles };
|
return fileCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseVideo(video: any) {
|
private parseVideo(video: FileAttributes) {
|
||||||
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,183 +0,0 @@
|
|||||||
import { parse } from 'parse-torrent-title';
|
|
||||||
import { repository } from '../repository/database_repository';
|
|
||||||
import { TorrentType } from './enums/torrent_types';
|
|
||||||
import { PromiseHelpers } from './helpers/promises_helpers';
|
|
||||||
import { logger } from './services/logging_service';
|
|
||||||
import { metadataService } from './services/metadata_service';
|
|
||||||
import { parsingService } from './services/parsing_service';
|
|
||||||
import { torrentFileService } from './services/torrent_file_service';
|
|
||||||
import { torrentSubtitleService } from './services/torrent_subtitle_service';
|
|
||||||
|
|
||||||
export async function createTorrentEntry(torrent, overwrite = false) {
|
|
||||||
const titleInfo = parse(torrent.title);
|
|
||||||
|
|
||||||
if (!torrent.imdbId && torrent.type !== TorrentType.Anime) {
|
|
||||||
const imdbQuery = {
|
|
||||||
title: titleInfo.title,
|
|
||||||
year: titleInfo.year,
|
|
||||||
type: torrent.type
|
|
||||||
};
|
|
||||||
torrent.imdbId = await metadataService.getImdbId(imdbQuery)
|
|
||||||
.catch(() => undefined);
|
|
||||||
}
|
|
||||||
if (torrent.imdbId && torrent.imdbId.length < 9) {
|
|
||||||
// pad zeros to imdbId if missing
|
|
||||||
torrent.imdbId = 'tt' + torrent.imdbId.replace('tt', '').padStart(7, '0');
|
|
||||||
}
|
|
||||||
if (torrent.imdbId && torrent.imdbId.length > 9 && torrent.imdbId.startsWith('tt0')) {
|
|
||||||
// sanitize imdbId from redundant zeros
|
|
||||||
torrent.imdbId = torrent.imdbId.replace(/tt0+([0-9]{7,})$/, 'tt$1');
|
|
||||||
}
|
|
||||||
if (!torrent.kitsuId && torrent.type === TorrentType.Anime) {
|
|
||||||
const kitsuQuery = {
|
|
||||||
title: titleInfo.title,
|
|
||||||
year: titleInfo.year,
|
|
||||||
season: titleInfo.season,
|
|
||||||
};
|
|
||||||
torrent.kitsuId = await metadataService.getKitsuId(kitsuQuery)
|
|
||||||
.catch(() => undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!torrent.imdbId && !torrent.kitsuId && !parsingService.isPackTorrent(torrent)) {
|
|
||||||
logger.warn(`imdbId or kitsuId not found: ${torrent.provider} ${torrent.title}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { contents, videos, subtitles } = await torrentFileService.parseTorrentFiles(torrent)
|
|
||||||
.then(torrentContents => overwrite ? overwriteExistingFiles(torrent, torrentContents) : torrentContents)
|
|
||||||
.then(torrentContents => torrentSubtitleService.assignSubtitles(torrentContents))
|
|
||||||
.catch(error => {
|
|
||||||
logger.warn(`Failed getting files for ${torrent.title}`, error.message);
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
if (!videos || !videos.length) {
|
|
||||||
logger.warn(`no video files found for ${torrent.provider} [${torrent.infoHash}] ${torrent.title}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return repository.createTorrent({ ...torrent, contents, subtitles })
|
|
||||||
.then(() => PromiseHelpers.sequence(videos.map(video => () => repository.createFile(video))))
|
|
||||||
.then(() => logger.info(`Created ${torrent.provider} entry for [${torrent.infoHash}] ${torrent.title}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function overwriteExistingFiles(torrent, torrentContents) {
|
|
||||||
const videos = torrentContents && torrentContents.videos;
|
|
||||||
if (videos && videos.length) {
|
|
||||||
const existingFiles = await repository.getFiles({ infoHash: videos[0].infoHash })
|
|
||||||
.then((existing) => existing
|
|
||||||
.reduce((map, next) => {
|
|
||||||
const fileIndex = next.fileIndex !== undefined ? next.fileIndex : null;
|
|
||||||
map[fileIndex] = (map[fileIndex] || []).concat(next);
|
|
||||||
return map;
|
|
||||||
}, {}))
|
|
||||||
.catch(() => undefined);
|
|
||||||
if (existingFiles && Object.keys(existingFiles).length) {
|
|
||||||
const overwrittenVideos = videos
|
|
||||||
.map(file => {
|
|
||||||
const mapping = videos.length === 1 && Object.keys(existingFiles).length === 1
|
|
||||||
? Object.values(existingFiles)[0]
|
|
||||||
: existingFiles[file.fileIndex !== undefined ? file.fileIndex : null];
|
|
||||||
if (mapping) {
|
|
||||||
const originalFile = mapping.shift();
|
|
||||||
return { id: originalFile.id, ...file };
|
|
||||||
}
|
|
||||||
return file;
|
|
||||||
});
|
|
||||||
return { ...torrentContents, videos: overwrittenVideos };
|
|
||||||
}
|
|
||||||
return torrentContents;
|
|
||||||
}
|
|
||||||
return Promise.reject(`No video files found for: ${torrent.title}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSkipTorrentEntry(torrent) {
|
|
||||||
return repository.createSkipTorrent(torrent);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStoredTorrentEntry(torrent) {
|
|
||||||
return repository.getSkipTorrent(torrent)
|
|
||||||
.catch(() => repository.getTorrent(torrent))
|
|
||||||
.catch(() => undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkAndUpdateTorrent(torrent) {
|
|
||||||
const storedTorrent = torrent.dataValues
|
|
||||||
? torrent
|
|
||||||
: await repository.getTorrent(torrent).catch(() => undefined);
|
|
||||||
if (!storedTorrent) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (storedTorrent.provider === 'RARBG') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (storedTorrent.provider === 'KickassTorrents' && torrent.provider) {
|
|
||||||
storedTorrent.provider = torrent.provider;
|
|
||||||
storedTorrent.torrentId = torrent.torrentId;
|
|
||||||
}
|
|
||||||
if (!storedTorrent.languages && torrent.languages && storedTorrent.provider !== 'RARBG') {
|
|
||||||
storedTorrent.languages = torrent.languages;
|
|
||||||
await storedTorrent.save();
|
|
||||||
logger.debug(`Updated [${storedTorrent.infoHash}] ${storedTorrent.title} language to ${torrent.languages}`);
|
|
||||||
}
|
|
||||||
return createTorrentContents({ ...storedTorrent.get(), torrentLink: torrent.torrentLink })
|
|
||||||
.then(() => updateTorrentSeeders(torrent));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createTorrentContents(torrent) {
|
|
||||||
if (torrent.opened) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const storedVideos = await repository.getFiles(torrent).catch(() => []);
|
|
||||||
if (!storedVideos || !storedVideos.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const notOpenedVideo = storedVideos.length === 1 && !Number.isInteger(storedVideos[0].fileIndex);
|
|
||||||
const imdbId = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.imdbId));
|
|
||||||
const kitsuId = PromiseHelpers.mostCommonValue(storedVideos.map(stored => stored.kitsuId));
|
|
||||||
|
|
||||||
const { contents, videos, subtitles } = await torrentFileService.parseTorrentFiles({ ...torrent, imdbId, kitsuId })
|
|
||||||
.then(torrentContents => notOpenedVideo ? torrentContents : { ...torrentContents, videos: storedVideos })
|
|
||||||
.then(torrentContents => torrentSubtitleService.assignSubtitles(torrentContents))
|
|
||||||
.catch(error => {
|
|
||||||
logger.warn(`Failed getting contents for [${torrent.infoHash}] ${torrent.title}`, error.message);
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!contents || !contents.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (notOpenedVideo && videos.length === 1) {
|
|
||||||
// if both have a single video and stored one was not opened, update stored one to true metadata and use that
|
|
||||||
storedVideos[0].fileIndex = videos[0].fileIndex;
|
|
||||||
storedVideos[0].title = videos[0].title;
|
|
||||||
storedVideos[0].size = videos[0].size;
|
|
||||||
storedVideos[0].subtitles = videos[0].subtitles;
|
|
||||||
videos[0] = storedVideos[0];
|
|
||||||
}
|
|
||||||
// no videos available or more than one new videos were in the torrent
|
|
||||||
const shouldDeleteOld = notOpenedVideo && videos.every(video => !video.id);
|
|
||||||
|
|
||||||
return repository.createTorrent({ ...torrent, contents, subtitles })
|
|
||||||
.then(() => {
|
|
||||||
if (shouldDeleteOld) {
|
|
||||||
logger.debug(`Deleting old video for [${torrent.infoHash}] ${torrent.title}`)
|
|
||||||
return storedVideos[0].destroy();
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
})
|
|
||||||
.then(() => PromiseHelpers.sequence(videos.map(video => () => repository.createFile(video))))
|
|
||||||
.then(() => logger.info(`Created contents for ${torrent.provider} [${torrent.infoHash}] ${torrent.title}`))
|
|
||||||
.catch(error => logger.error(`Failed saving contents for [${torrent.infoHash}] ${torrent.title}`, error));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateTorrentSeeders(torrent) {
|
|
||||||
if (!(torrent.infoHash || (torrent.provider && torrent.torrentId)) || !Number.isInteger(torrent.seeders)) {
|
|
||||||
return torrent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return repository.setTorrentSeeders(torrent, torrent.seeders)
|
|
||||||
.catch(error => {
|
|
||||||
logger.warn('Failed updating seeders:', error);
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -50,7 +50,7 @@ class DatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTorrent(torrent: Torrent): Promise<Torrent | null> {
|
public async getTorrent(torrent: TorrentAttributes): 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 };
|
||||||
@@ -69,25 +69,6 @@ class DatabaseRepository {
|
|||||||
return await File.findAll({ where });
|
return await File.findAll({ where });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUnprocessedIngestedTorrents(): Promise<IngestedTorrent[]> {
|
|
||||||
return await IngestedTorrent.findAll({
|
|
||||||
where: {
|
|
||||||
processed: false,
|
|
||||||
category: {
|
|
||||||
[Op.or]: ['tv', 'movies']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setIngestedTorrentsProcessed(ingestedTorrents: IngestedTorrent[]): Promise<void> {
|
|
||||||
await PromiseHelpers.sequence(ingestedTorrents
|
|
||||||
.map(ingestedTorrent => async () => {
|
|
||||||
ingestedTorrent.processed = true;
|
|
||||||
await ingestedTorrent.save();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getTorrentsWithoutSize(): Promise<Torrent[]> {
|
public async getTorrentsWithoutSize(): Promise<Torrent[]> {
|
||||||
return await Torrent.findAll({
|
return await Torrent.findAll({
|
||||||
where: literal(
|
where: literal(
|
||||||
@@ -148,8 +129,8 @@ class DatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteTorrent(torrent: TorrentAttributes): Promise<number> {
|
public async deleteTorrent(infoHash: string): Promise<number> {
|
||||||
return await Torrent.destroy({ where: { infoHash: torrent.infoHash } });
|
return await Torrent.destroy({ where: { infoHash: infoHash } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createFile(file: File): Promise<void> {
|
public async createFile(file: File): Promise<void> {
|
||||||
@@ -171,16 +152,16 @@ class DatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFiles(torrent: Torrent): Promise<File[]> {
|
public async getFiles(infoHash: string): Promise<File[]> {
|
||||||
return File.findAll({ where: { infoHash: torrent.infoHash } });
|
return File.findAll({ where: { infoHash: infoHash } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFilesBasedOnTitle(titleQuery: string): Promise<File[]> {
|
public async getFilesBasedOnTitle(titleQuery: string): Promise<File[]> {
|
||||||
return File.findAll({ where: { title: { [Op.regexp]: `${titleQuery}` } } });
|
return File.findAll({ where: { title: { [Op.regexp]: `${titleQuery}` } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteFile(file: File): Promise<number> {
|
public async deleteFile(id: number): Promise<number> {
|
||||||
return File.destroy({ where: { id: file.id } });
|
return File.destroy({ where: { id: id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createSubtitles(infoHash: string, subtitles: Subtitle[]): Promise<void | Model<any, any>[]> {
|
public async createSubtitles(infoHash: string, subtitles: Subtitle[]): Promise<void | Model<any, any>[]> {
|
||||||
@@ -209,8 +190,8 @@ class DatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSubtitles(torrent: Torrent): Promise<Subtitle[]> {
|
public async getSubtitles(infoHash: string): Promise<Subtitle[]> {
|
||||||
return Subtitle.findAll({ where: { infoHash: torrent.infoHash } });
|
return Subtitle.findAll({ where: { infoHash: infoHash } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUnassignedSubtitles(): Promise<Subtitle[]> {
|
public async getUnassignedSubtitles(): Promise<Subtitle[]> {
|
||||||
@@ -224,14 +205,14 @@ class DatabaseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getContents(torrent: Torrent): Promise<Content[]> {
|
public async getContents(infoHash: string): Promise<Content[]> {
|
||||||
return Content.findAll({ where: { infoHash: torrent.infoHash } });
|
return Content.findAll({ where: { infoHash: infoHash } });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSkipTorrent(torrent: Torrent): Promise<SkipTorrent> {
|
public async getSkipTorrent(infoHash: string): Promise<SkipTorrent> {
|
||||||
const result = await SkipTorrent.findByPk(torrent.infoHash);
|
const result = await SkipTorrent.findByPk(infoHash);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error(`torrent not found: ${torrent.infoHash}`);
|
throw new Error(`torrent not found: ${infoHash}`);
|
||||||
}
|
}
|
||||||
return result.dataValues as SkipTorrent;
|
return result.dataValues as SkipTorrent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import {Optional} from "sequelize";
|
import {Optional} from "sequelize";
|
||||||
import {SubtitleAttributes} from "./subtitle_attributes";
|
import {SubtitleAttributes} from "./subtitle_attributes";
|
||||||
|
import {ParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result";
|
||||||
|
|
||||||
export interface FileAttributes {
|
export interface FileAttributes extends ParseTorrentTitleResult {
|
||||||
id?: number;
|
id?: number;
|
||||||
infoHash: string;
|
infoHash?: string;
|
||||||
fileIndex: number;
|
fileIndex?: number;
|
||||||
title: string;
|
title?: string;
|
||||||
size: number;
|
size?: number;
|
||||||
imdbId: string;
|
imdbId?: string;
|
||||||
imdbSeason: number;
|
imdbSeason?: number;
|
||||||
imdbEpisode: number;
|
imdbEpisode?: number;
|
||||||
kitsuId: number;
|
kitsuId?: number;
|
||||||
kitsuEpisode: number;
|
kitsuEpisode?: number;
|
||||||
subtitles?: SubtitleAttributes[];
|
subtitles?: SubtitleAttributes[];
|
||||||
|
path?: string;
|
||||||
|
isMovie?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileCreationAttributes extends Optional<FileAttributes, 'fileIndex' | 'size' | 'imdbId' | 'imdbSeason' | 'imdbEpisode' | 'kitsuId' | 'kitsuEpisode'> {
|
export interface FileCreationAttributes extends Optional<FileAttributes, 'fileIndex' | 'size' | 'imdbId' | 'imdbSeason' | 'imdbEpisode' | 'kitsuId' | 'kitsuEpisode'> {
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ import {FileAttributes} from "./file_attributes";
|
|||||||
|
|
||||||
export interface TorrentAttributes {
|
export interface TorrentAttributes {
|
||||||
infoHash: string;
|
infoHash: string;
|
||||||
provider: string;
|
provider?: string;
|
||||||
torrentId: string;
|
torrentId?: string;
|
||||||
title: string;
|
title?: string;
|
||||||
size: number;
|
size?: number;
|
||||||
type: string;
|
type?: string;
|
||||||
uploadDate: Date;
|
uploadDate?: Date;
|
||||||
seeders: number;
|
seeders?: number;
|
||||||
trackers: string;
|
trackers?: string;
|
||||||
languages: string;
|
languages?: string;
|
||||||
resolution: string;
|
resolution?: string;
|
||||||
reviewed: boolean;
|
reviewed?: boolean;
|
||||||
opened: boolean;
|
opened?: boolean;
|
||||||
contents: ContentAttributes[];
|
contents?: ContentAttributes[];
|
||||||
files: FileAttributes[];
|
files?: FileAttributes[];
|
||||||
subtitles?: SubtitleAttributes[];
|
subtitles?: SubtitleAttributes[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user