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 {
|
||||
contents?: ParsableTorrentFile[];
|
||||
videos?: ParsableTorrentFile[];
|
||||
subtitles?: ParsableTorrentFile[];
|
||||
contents?: ContentAttributes[];
|
||||
videos?: FileAttributes[];
|
||||
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 { configurationService } from './configuration_service';
|
||||
import {configurationService} from './configuration_service';
|
||||
import {extensionService} from './extension_service';
|
||||
import {TorrentInfo} from "../interfaces/torrent_info";
|
||||
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 {
|
||||
private engineOptions: TorrentStream.TorrentEngineOptions = {
|
||||
@@ -14,73 +24,124 @@ class TorrentDownloadService {
|
||||
dht: false,
|
||||
tracker: true,
|
||||
};
|
||||
|
||||
public async getTorrentFiles(torrent: TorrentInfo, timeout: number = 30000): Promise<TorrentFileCollection> {
|
||||
return this.filesFromTorrentStream(torrent, timeout)
|
||||
.then((files: Array<ParsableTorrentFile>) => ({
|
||||
contents: files,
|
||||
videos: this.filterVideos(files),
|
||||
subtitles: this.filterSubtitles(files)
|
||||
}));
|
||||
|
||||
public async getTorrentFiles(torrent: ParsedTorrent, timeout: number = 30000): Promise<TorrentFileCollection> {
|
||||
const torrentFiles: TorrentFile[] = await this.filesFromTorrentStream(torrent, timeout);
|
||||
|
||||
const videos = this.filterVideos(torrent, torrentFiles);
|
||||
const subtitles = this.filterSubtitles(torrent, torrentFiles);
|
||||
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) {
|
||||
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) => {
|
||||
let engine: TorrentStream.TorrentEngine;
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
engine.destroy(() => {});
|
||||
engine.destroy(() => {
|
||||
});
|
||||
reject(new Error('No available connections for torrent!'));
|
||||
}, timeout);
|
||||
|
||||
engine = torrentStream(magnet, this.engineOptions);
|
||||
|
||||
engine.on("ready", () => {
|
||||
const files: ParsableTorrentFile[] = engine.files.map((file, fileId) => ({
|
||||
const files: TorrentFile[] = engine.files.map((file, fileId) => ({
|
||||
...file,
|
||||
fileIndex: fileId,
|
||||
size: file.length,
|
||||
title: file.name}));
|
||||
|
||||
title: file.name
|
||||
}));
|
||||
|
||||
resolve(files);
|
||||
|
||||
engine.destroy(() => {});
|
||||
engine.destroy(() => {
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private filterVideos(files: Array<ParsableTorrentFile>): Array<ParsableTorrentFile> {
|
||||
if (files.length === 1 && !Number.isInteger(files[0].fileIndex)) {
|
||||
return files;
|
||||
private filterVideos(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): FileAttributes[] {
|
||||
if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) {
|
||||
return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])];
|
||||
}
|
||||
const videos = files.filter(file => extensionService.isVideo(file.path || ''));
|
||||
const maxSize = Math.max(...videos.map((video: ParsableTorrentFile) => video.length));
|
||||
const videos = torrentFiles.filter(file => extensionService.isVideo(file.path || ''));
|
||||
const maxSize = Math.max(...videos.map((video: TorrentFile) => video.length));
|
||||
const minSampleRatio = videos.length <= 3 ? 3 : 10;
|
||||
const minAnimeExtraRatio = 5;
|
||||
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 isExtra = (video: ParsableTorrentFile) => video.path?.match(/extras?\//i);
|
||||
const isAnimeExtra = (video: ParsableTorrentFile) => video.path?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i)
|
||||
|
||||
const isSample = (video: TorrentFile) => video.path?.match(/sample|bonus|promo/i) && maxSize / parseInt(video.path.toString()) > minSampleRatio;
|
||||
const isRedundant = (video: TorrentFile) => maxSize / parseInt(video.path.toString()) > minRedundantRatio;
|
||||
const isExtra = (video: TorrentFile) => video.path?.match(/extras?\//i);
|
||||
const isAnimeExtra = (video: TorrentFile) => video.path?.match(/(?:\b|_)(?:NC)?(?:ED|OP|PV)(?:v?\d\d?)?(?:\b|_)/i)
|
||||
&& 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
|
||||
|
||||
return videos
|
||||
.filter(video => !isSample(video))
|
||||
.filter(video => !isExtra(video))
|
||||
.filter(video => !isAnimeExtra(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> {
|
||||
return files.filter(file => extensionService.isSubtitle(file.path || ''));
|
||||
private filterSubtitles(torrent: ParsedTorrent, torrentFiles: TorrentFile[]): SubtitleAttributes[] {
|
||||
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 moment from 'moment';
|
||||
import { parse } from 'parse-torrent-title';
|
||||
import {parse} from 'parse-torrent-title';
|
||||
import {PromiseHelpers} from '../helpers/promises_helpers';
|
||||
import { TorrentType } from '../enums/torrent_types';
|
||||
import {TorrentInfo} from "../interfaces/torrent_info";
|
||||
import { configurationService } from './configuration_service';
|
||||
import { extensionService } from './extension_service';
|
||||
import { metadataService } from './metadata_service';
|
||||
import { parsingService } from './parsing_service';
|
||||
import { torrentDownloadService } from "./torrent_download_service";
|
||||
import {TorrentType} from '../enums/torrent_types';
|
||||
import {configurationService} from './configuration_service';
|
||||
import {extensionService} from './extension_service';
|
||||
import {metadataService} from './metadata_service';
|
||||
import {torrentDownloadService} from "./torrent_download_service";
|
||||
import {logger} from "./logging_service";
|
||||
import {MetadataResponse} from "../interfaces/metadata_response";
|
||||
import {ParseTorrentTitleResult} from "../interfaces/parse_torrent_title_result";
|
||||
import {MetaDataQuery} from "../interfaces/metadata_query";
|
||||
import {ParsableTorrentFile} from "../interfaces/parsable_torrent_file";
|
||||
import {CommonVideoMetadata} from "../interfaces/common_video_metadata";
|
||||
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
||||
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
||||
import {ContentAttributes} from "../../repository/interfaces/content_attributes";
|
||||
|
||||
class TorrentFileService {
|
||||
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({
|
||||
maxConcurrent: configurationService.metadataConfig.IMDB_CONCURRENT,
|
||||
minTime: configurationService.metadataConfig.IMDB_INTERVAL_MS
|
||||
});
|
||||
|
||||
public async parseTorrentFiles(torrent: TorrentInfo) {
|
||||
public async parseTorrentFiles(torrent: ParsedTorrent): Promise<TorrentFileCollection> {
|
||||
const parsedTorrentName = parse(torrent.title);
|
||||
const query: MetaDataQuery = {
|
||||
id: torrent.kitsuId || torrent.imdbId,
|
||||
@@ -47,7 +49,32 @@ class TorrentFileService {
|
||||
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 filteredVideos = videos
|
||||
.filter(video => video.size > this.MIN_SIZE)
|
||||
@@ -66,7 +93,7 @@ class TorrentFileService {
|
||||
|
||||
const parsedVideos = await PromiseHelpers.sequence(filteredVideos.map(video => () => this.isFeaturette(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 => ({
|
||||
infoHash: torrent.infoHash,
|
||||
fileIndex: video.fileIndex,
|
||||
@@ -77,53 +104,58 @@ class TorrentFileService {
|
||||
return {contents, videos: parsedVideos, subtitles};
|
||||
}
|
||||
|
||||
private async parseSeriesFiles(torrent: TorrentInfo, metadata: MetadataResponse) {
|
||||
const {contents, videos, subtitles} = await this.getSeriesTorrentContent(torrent);
|
||||
const parsedVideos = await Promise.resolve(videos)
|
||||
private async parseSeriesFiles(torrent: ParsedTorrent, metadata: MetadataResponse): TorrentFileCollection {
|
||||
const fileCollection: TorrentFileCollection = await this.getSeriesTorrentContent(torrent);
|
||||
const parsedVideos: FileAttributes[] = await Promise.resolve(fileCollection.videos)
|
||||
.then(videos => videos.filter(video => videos.length === 1 || video.size > this.MIN_SIZE))
|
||||
.then(videos => parsingService.parseSeriesVideos(torrent, videos))
|
||||
.then(videos => this.decomposeEpisodes(torrent, videos, metadata))
|
||||
.then(videos => this.assignKitsuOrImdbEpisodes(torrent, videos, metadata))
|
||||
.then(videos => this.parseSeriesVideos(torrent))
|
||||
.then(videos => this.decomposeEpisodes(torrent, metadata))
|
||||
.then(videos => this.assignKitsuOrImdbEpisodes(torrent, fileCollection.videos, metadata))
|
||||
.then(videos => Promise.all(videos.map(video => video.isMovie
|
||||
? this.mapSeriesMovie(video, torrent)
|
||||
: this.mapSeriesEpisode(video, torrent, videos))))
|
||||
.then(videos => videos
|
||||
.map((video: ParsableTorrentFile) => this.isFeaturette(video) ? this.clearInfoFields(video) : video))
|
||||
return {contents, videos: parsedVideos, subtitles};
|
||||
.map((video: ParsedTorrent) => this.isFeaturette(video) ? this.clearInfoFields(video) : video))
|
||||
return {...torrent.fileCollection, videos: parsedVideos};
|
||||
}
|
||||
|
||||
private async getMoviesTorrentContent(torrent: TorrentInfo) {
|
||||
private async getMoviesTorrentContent(torrent: ParsedTorrent) {
|
||||
const files = await torrentDownloadService.getTorrentFiles(torrent)
|
||||
.catch(error => {
|
||||
if (!parsingService.isPackTorrent(torrent)) {
|
||||
return { videos: [{name: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}], contents:[], subtitles: []}
|
||||
if (!this.isPackTorrent(torrent)) {
|
||||
const entries = [{name: torrent.title, path: torrent.title, size: torrent.size, fileIndex: null}];
|
||||
return { videos: entries, contents:[], subtitles: [], files: entries}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
.catch(error => {
|
||||
if (!parsingService.isPackTorrent(torrent)) {
|
||||
return { videos: [{ name: torrent.title, path: torrent.title, size: torrent.size }], subtitles: [], contents: [] }
|
||||
if (!this.isPackTorrent(torrent)) {
|
||||
return { videos: this.getDefaultFileEntries(torrent), subtitles: [], contents: [] }
|
||||
}
|
||||
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 (files.length === 1 || files.some(f => f.episodes || f.episodes) || parse(torrent.title).seasons) {
|
||||
return Promise.resolve({
|
||||
infoHash: torrent.infoHash,
|
||||
fileIndex: file.fileIndex,
|
||||
title: file.path || file.name,
|
||||
title: file.path || file.title,
|
||||
size: file.size,
|
||||
imdbId: torrent.imdbId || file.imdbId,
|
||||
});
|
||||
@@ -134,17 +166,20 @@ class TorrentFileService {
|
||||
return Promise.resolve(episodeIndexes.map((index) => ({
|
||||
infoHash: torrent.infoHash,
|
||||
fileIndex: file.fileIndex,
|
||||
title: file.path || file.name,
|
||||
title: file.path || file.title,
|
||||
size: file.size,
|
||||
imdbId: file.imdbId || torrent.imdbId,
|
||||
imdbId: file.imdbId.toString() || torrent.imdbId.toString(),
|
||||
imdbSeason: file.season,
|
||||
season: file.season,
|
||||
imdbEpisode: file.episodes && file.episodes[index],
|
||||
episode: file.episodes && file.episodes[index],
|
||||
kitsuEpisode: file.episodes && file.episodes[index],
|
||||
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)
|
||||
.then(result => {
|
||||
if (result instanceof Error) {
|
||||
@@ -168,10 +203,10 @@ class TorrentFileService {
|
||||
return [{
|
||||
infoHash: torrent.infoHash,
|
||||
fileIndex: file.fileIndex,
|
||||
title: file.path || file.name,
|
||||
title: file.path || file.title,
|
||||
size: file.size,
|
||||
imdbId: imdbId,
|
||||
kitsuId: kitsuId,
|
||||
kitsuId: parseInt(kitsuId),
|
||||
episodes: undefined,
|
||||
imdbSeason: undefined,
|
||||
imdbEpisode: undefined,
|
||||
@@ -185,16 +220,17 @@ class TorrentFileService {
|
||||
return [{
|
||||
infoHash: torrent.infoHash,
|
||||
fileIndex: file.fileIndex,
|
||||
title: file.path || file.name,
|
||||
title: file.path || file.title,
|
||||
size: file.size,
|
||||
imdbId: metadata.imdbId || imdbId,
|
||||
kitsuId: metadata.kitsuId || kitsuId,
|
||||
season: episodeVideo && metadata.imdbId ? episodeVideo.season : undefined,
|
||||
episode: episodeVideo && metadata.imdbId | metadata.kitsuId ? episodeVideo.episode || episodeVideo.episode : undefined,
|
||||
imdbId: metadata.imdbId.toString() || imdbId,
|
||||
kitsuId: parseInt(metadata.kitsuId.toString() || kitsuId),
|
||||
imdbSeason: episodeVideo && metadata.imdbId ? episodeVideo.season : 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)) {
|
||||
return files;
|
||||
}
|
||||
@@ -239,7 +275,7 @@ class TorrentFileService {
|
||||
return files;
|
||||
}
|
||||
|
||||
private preprocessEpisodes(files: ParsableTorrentFile[]) {
|
||||
private preprocessEpisodes(files: FileAttributes[]) {
|
||||
// reverse special episode naming when they named with 0 episode, ie. S02E00
|
||||
files
|
||||
.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) {
|
||||
// anime does not use this naming scheme in 99% of cases;
|
||||
return false;
|
||||
@@ -277,11 +313,11 @@ class TorrentFileService {
|
||||
|| 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);
|
||||
}
|
||||
|
||||
private isAbsoluteEpisodeFiles(torrent: TorrentInfo, files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
||||
private isAbsoluteEpisodeFiles(torrent: ParsedTorrent, files: FileAttributes[], metadata: MetadataResponse) {
|
||||
const threshold = Math.ceil(files.length / 5);
|
||||
const isAnime = torrent.type === TorrentType.Anime && torrent.kitsuId;
|
||||
const nonMovieEpisodes = files
|
||||
@@ -294,18 +330,18 @@ class TorrentFileService {
|
||||
|| 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.
|
||||
// detect this if episode number is larger than the last episode or season is larger than the last one
|
||||
// only for non anime metas
|
||||
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)
|
||||
&& file.season >= metadata.episodeCount.length
|
||||
&& file.episodes.every(ep => ep > (metadata.episodeCount[file.season - 1] || 0));
|
||||
&& video.season >= metadata.episodeCount.length
|
||||
&& video.episodes.every(ep => ep > (metadata.episodeCount[video.season - 1] || 0));
|
||||
}
|
||||
|
||||
private decomposeConcatSeasonAndEpisodeFiles(files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
||||
private decomposeConcatSeasonAndEpisodeFiles(files: FileAttributes[], metadata: MetadataResponse) {
|
||||
files
|
||||
.filter(file => file.episodes && file.season !== 0 && file.episodes.every(ep => ep > 100))
|
||||
.filter(file => metadata.episodeCount[(file.season || this.div100(file.episodes[0])) - 1] < 100)
|
||||
@@ -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) {
|
||||
files
|
||||
videos
|
||||
.filter(file => !Number.isInteger(file.season) && file.episodes && !file.isMovie)
|
||||
.forEach(file => {
|
||||
file.season = 1;
|
||||
});
|
||||
return;
|
||||
}
|
||||
files
|
||||
videos
|
||||
.filter(file => file.episodes && !file.isMovie && file.season !== 0)
|
||||
.filter(file => !this.isNewEpisodeNotInMetadata(torrent, file, metadata))
|
||||
.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) {
|
||||
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 (torrent.type === TorrentType.Anime) {
|
||||
// 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)) {
|
||||
// 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;
|
||||
@@ -402,14 +438,14 @@ class TorrentFileService {
|
||||
}, {});
|
||||
|
||||
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 => {
|
||||
const seasonMapping = seriesMapping[file.season];
|
||||
const episodeMapping = seasonMapping && seasonMapping[file.episodes[0]];
|
||||
if (episodeMapping && Number.isInteger(episodeMapping.imdbSeason)) {
|
||||
file.imdbId = metadata.imdbId;
|
||||
file.season = episodeMapping.imdbSeason;
|
||||
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].imdbEpisode);
|
||||
if (episodeMapping && Number.isInteger(episodeMapping.season)) {
|
||||
file.imdbId = metadata.imdbId.toString();
|
||||
file.season = episodeMapping.season;
|
||||
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].episode);
|
||||
} else {
|
||||
// no imdb mapping available for episode
|
||||
file.season = undefined;
|
||||
@@ -423,7 +459,7 @@ class TorrentFileService {
|
||||
.forEach(file => {
|
||||
if (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.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode);
|
||||
} else if (seriesMapping[file.season - 1]) {
|
||||
@@ -434,12 +470,12 @@ class TorrentFileService {
|
||||
const differentTitlesCount = new Set(episodes.map(ep => ep.id)).size
|
||||
const skippedCount = episodes.filter(ep => ep.id === firstKitsuId).length;
|
||||
const seasonEpisodes = files
|
||||
.filter(otherFile => otherFile.season === file.season)
|
||||
.filter((otherFile: FileAttributes) => otherFile.season === file.season)
|
||||
.reduce((a, b) => a.episodes.concat(b.episodes), []);
|
||||
const isAbsoluteOrder = seasonEpisodes.episodes.every(ep => ep > skippedCount && ep <= episodes.length)
|
||||
const isNormalOrder = seasonEpisodes.episodes.every(ep => ep + skippedCount <= episodes.length)
|
||||
if (differentTitlesCount >= 1 && (isAbsoluteOrder || isNormalOrder)) {
|
||||
file.imdbId = metadata.imdbId;
|
||||
file.imdbId = metadata.imdbId.toString();
|
||||
file.season = file.season - 1;
|
||||
file.episodes = file.episodes.map(ep => isAbsoluteOrder ? ep : ep + skippedCount);
|
||||
file.kitsuId = seasonMapping[file.episodes[0]].kitsuId;
|
||||
@@ -448,7 +484,7 @@ class TorrentFileService {
|
||||
} 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
|
||||
const seasonMapping = seriesMapping[1];
|
||||
file.imdbId = metadata.imdbId;
|
||||
file.imdbId = metadata.imdbId.toString();
|
||||
file.season = 1;
|
||||
file.kitsuId = seasonMapping[file.episodes[0]].kitsuId;
|
||||
file.episodes = file.episodes.map(ep => seasonMapping[ep] && seasonMapping[ep].kitsuEpisode);
|
||||
@@ -458,7 +494,7 @@ class TorrentFileService {
|
||||
return files;
|
||||
}
|
||||
|
||||
private needsCinemetaMetadataForAnime(files: ParsableTorrentFile[], metadata: MetadataResponse) {
|
||||
private needsCinemetaMetadataForAnime(files: FileAttributes[], metadata: MetadataResponse) {
|
||||
if (!metadata || !metadata.imdbId || !metadata.videos || !metadata.videos.length) {
|
||||
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;
|
||||
logger.debug(`Finding movie imdbId for ${title}`);
|
||||
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 kitsuQuery = {
|
||||
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));
|
||||
}
|
||||
|
||||
private isSingleMovie(videos: ParsableTorrentFile[]) {
|
||||
private isSingleMovie(videos: FileAttributes[]) {
|
||||
return videos.length === 1 ||
|
||||
(videos.length === 2 &&
|
||||
videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?1\b|^0?1\.\w{2,4}$/i.test(v.path)) &&
|
||||
videos.find(v => /\b(?:part|disc|cd)[ ._-]?0?2\b|^0?2\.\w{2,4}$/i.test(v.path)));
|
||||
}
|
||||
|
||||
private isFeaturette(video: ParsableTorrentFile) {
|
||||
private isFeaturette(video: FileAttributes) {
|
||||
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.season = undefined;
|
||||
video.episode = undefined;
|
||||
video.imdbSeason = undefined;
|
||||
video.imdbEpisode = undefined;
|
||||
video.kitsuId = undefined;
|
||||
video.kitsuEpisode = undefined;
|
||||
return video;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import {TorrentInfo} from "../interfaces/torrent_info";
|
||||
import {TorrentType} from "../enums/torrent_types";
|
||||
import {logger} from "./logging_service";
|
||||
import {checkAndUpdateTorrent, createTorrentEntry} from "../torrentEntries.js";
|
||||
import {trackerService} from "./tracker_service";
|
||||
import {torrentEntriesService} from "./torrent_entries_service";
|
||||
import {IngestedTorrentAttributes} from "../../repository/interfaces/ingested_torrent_attributes";
|
||||
import {ParsedTorrent} from "../interfaces/parsed_torrent";
|
||||
|
||||
class TorrentProcessingService {
|
||||
public async processTorrentRecord(torrent: IngestedTorrentAttributes): Promise<void> {
|
||||
const { category } = torrent;
|
||||
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}`);
|
||||
|
||||
if (await checkAndUpdateTorrent(torrentInfo)) {
|
||||
if (await torrentEntriesService.checkAndUpdateTorrent(torrentInfo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return createTorrentEntry(torrentInfo);
|
||||
return torrentEntriesService.createTorrentEntry(torrentInfo);
|
||||
}
|
||||
|
||||
private async assignTorrentTrackers(): Promise<string> {
|
||||
@@ -25,14 +25,14 @@ class TorrentProcessingService {
|
||||
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()
|
||||
return {
|
||||
title: torrent.name,
|
||||
torrentId: `${torrent.name}_${infoHash}`,
|
||||
infoHash: infoHash,
|
||||
seeders: 100,
|
||||
size: torrent.size,
|
||||
size: parseInt(torrent.size),
|
||||
uploadDate: torrent.createdAt,
|
||||
imdbId: this.parseImdbId(torrent),
|
||||
type: category,
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { parse } from 'parse-torrent-title';
|
||||
import {TorrentFileCollection} from "../interfaces/torrent_file_collection";
|
||||
import {FileAttributes} from "../../repository/interfaces/file_attributes";
|
||||
|
||||
class TorrentSubtitleService {
|
||||
public assignSubtitles(contents: any, videos: any[], subtitles: any[]) {
|
||||
if (videos && videos.length && subtitles && subtitles.length) {
|
||||
if (videos.length === 1) {
|
||||
videos[0].subtitles = subtitles;
|
||||
return { contents, videos, subtitles: [] };
|
||||
public assignSubtitles(fileCollection: TorrentFileCollection) : TorrentFileCollection {
|
||||
if (fileCollection.videos && fileCollection.videos.length && fileCollection.subtitles && fileCollection.subtitles.length) {
|
||||
if (fileCollection.videos.length === 1) {
|
||||
fileCollection.videos[0].subtitles = fileCollection.subtitles;
|
||||
return { ...fileCollection, subtitles: [] };
|
||||
}
|
||||
|
||||
const parsedVideos = videos.map(video => this.parseVideo(video));
|
||||
const assignedSubs = subtitles.map(subtitle => ({ subtitle, videos: this.mostProbableSubtitleVideos(subtitle, parsedVideos) }));
|
||||
const parsedVideos = fileCollection.videos.map(video => this.parseVideo(video));
|
||||
const assignedSubs = fileCollection.subtitles.map(subtitle => ({ subtitle, videos: this.mostProbableSubtitleVideos(subtitle, parsedVideos) }));
|
||||
const unassignedSubs = assignedSubs.filter(assignedSub => !assignedSub.videos).map(assignedSub => assignedSub.subtitle);
|
||||
|
||||
assignedSubs
|
||||
.filter(assignedSub => assignedSub.videos)
|
||||
.forEach(assignedSub => assignedSub.videos.forEach(video => video.subtitles = (video.subtitles || []).concat(assignedSub.subtitle)));
|
||||
return { 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 folderName = video.title.replace(/\/?[^/]+$/, '');
|
||||
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
|
||||
? { infoHash: torrent.infoHash }
|
||||
: { provider: torrent.provider, torrentId: torrent.torrentId };
|
||||
@@ -69,25 +69,6 @@ class DatabaseRepository {
|
||||
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[]> {
|
||||
return await Torrent.findAll({
|
||||
where: literal(
|
||||
@@ -148,8 +129,8 @@ class DatabaseRepository {
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteTorrent(torrent: TorrentAttributes): Promise<number> {
|
||||
return await Torrent.destroy({ where: { infoHash: torrent.infoHash } });
|
||||
public async deleteTorrent(infoHash: string): Promise<number> {
|
||||
return await Torrent.destroy({ where: { infoHash: infoHash } });
|
||||
}
|
||||
|
||||
public async createFile(file: File): Promise<void> {
|
||||
@@ -171,16 +152,16 @@ class DatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async getFiles(torrent: Torrent): Promise<File[]> {
|
||||
return File.findAll({ where: { infoHash: torrent.infoHash } });
|
||||
public async getFiles(infoHash: string): Promise<File[]> {
|
||||
return File.findAll({ where: { infoHash: infoHash } });
|
||||
}
|
||||
|
||||
public async getFilesBasedOnTitle(titleQuery: string): Promise<File[]> {
|
||||
return File.findAll({ where: { title: { [Op.regexp]: `${titleQuery}` } } });
|
||||
}
|
||||
|
||||
public async deleteFile(file: File): Promise<number> {
|
||||
return File.destroy({ where: { id: file.id } });
|
||||
public async deleteFile(id: number): Promise<number> {
|
||||
return File.destroy({ where: { id: id } });
|
||||
}
|
||||
|
||||
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[]> {
|
||||
return Subtitle.findAll({ where: { infoHash: torrent.infoHash } });
|
||||
public async getSubtitles(infoHash: string): Promise<Subtitle[]> {
|
||||
return Subtitle.findAll({ where: { infoHash: infoHash } });
|
||||
}
|
||||
|
||||
public async getUnassignedSubtitles(): Promise<Subtitle[]> {
|
||||
@@ -224,14 +205,14 @@ class DatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public async getContents(torrent: Torrent): Promise<Content[]> {
|
||||
return Content.findAll({ where: { infoHash: torrent.infoHash } });
|
||||
public async getContents(infoHash: string): Promise<Content[]> {
|
||||
return Content.findAll({ where: { infoHash: infoHash } });
|
||||
}
|
||||
|
||||
public async getSkipTorrent(torrent: Torrent): Promise<SkipTorrent> {
|
||||
const result = await SkipTorrent.findByPk(torrent.infoHash);
|
||||
public async getSkipTorrent(infoHash: string): Promise<SkipTorrent> {
|
||||
const result = await SkipTorrent.findByPk(infoHash);
|
||||
if (!result) {
|
||||
throw new Error(`torrent not found: ${torrent.infoHash}`);
|
||||
throw new Error(`torrent not found: ${infoHash}`);
|
||||
}
|
||||
return result.dataValues as SkipTorrent;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import {Optional} from "sequelize";
|
||||
import {SubtitleAttributes} from "./subtitle_attributes";
|
||||
import {ParseTorrentTitleResult} from "../../lib/interfaces/parse_torrent_title_result";
|
||||
|
||||
export interface FileAttributes {
|
||||
export interface FileAttributes extends ParseTorrentTitleResult {
|
||||
id?: number;
|
||||
infoHash: string;
|
||||
fileIndex: number;
|
||||
title: string;
|
||||
size: number;
|
||||
imdbId: string;
|
||||
imdbSeason: number;
|
||||
imdbEpisode: number;
|
||||
kitsuId: number;
|
||||
kitsuEpisode: number;
|
||||
infoHash?: string;
|
||||
fileIndex?: number;
|
||||
title?: string;
|
||||
size?: number;
|
||||
imdbId?: string;
|
||||
imdbSeason?: number;
|
||||
imdbEpisode?: number;
|
||||
kitsuId?: number;
|
||||
kitsuEpisode?: number;
|
||||
subtitles?: SubtitleAttributes[];
|
||||
path?: string;
|
||||
isMovie?: boolean;
|
||||
}
|
||||
|
||||
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 {
|
||||
infoHash: string;
|
||||
provider: string;
|
||||
torrentId: string;
|
||||
title: string;
|
||||
size: number;
|
||||
type: string;
|
||||
uploadDate: Date;
|
||||
seeders: number;
|
||||
trackers: string;
|
||||
languages: string;
|
||||
resolution: string;
|
||||
reviewed: boolean;
|
||||
opened: boolean;
|
||||
contents: ContentAttributes[];
|
||||
files: FileAttributes[];
|
||||
provider?: string;
|
||||
torrentId?: string;
|
||||
title?: string;
|
||||
size?: number;
|
||||
type?: string;
|
||||
uploadDate?: Date;
|
||||
seeders?: number;
|
||||
trackers?: string;
|
||||
languages?: string;
|
||||
resolution?: string;
|
||||
reviewed?: boolean;
|
||||
opened?: boolean;
|
||||
contents?: ContentAttributes[];
|
||||
files?: FileAttributes[];
|
||||
subtitles?: SubtitleAttributes[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user