mirror of
https://github.com/knightcrawler-stremio/knightcrawler.git
synced 2024-12-20 03:29:51 +00:00
meta data seems a bit iffy right now
Also gone back to torrent-stream WebTorrent seemed to be throwing the occasional engine crash
This commit is contained in:
3272
src/node/consumer/package-lock.json
generated
3272
src/node/consumer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,17 +29,14 @@
|
|||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"sequelize": "^6.36.0",
|
"sequelize": "^6.36.0",
|
||||||
"sequelize-typescript": "^2.1.6",
|
"sequelize-typescript": "^2.1.6",
|
||||||
"utp-native": "^2.5.3",
|
"torrent-stream": "^1.2.1"
|
||||||
"webtorrent": "^2.1.35"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"node-gyp": "^10.0.1",
|
|
||||||
"nodemon": "^3.0.3",
|
|
||||||
"@types/amqplib": "^0.10.4",
|
"@types/amqplib": "^0.10.4",
|
||||||
"@types/magnet-uri": "^5.1.5",
|
"@types/magnet-uri": "^5.1.5",
|
||||||
"@types/node": "^20.11.16",
|
"@types/node": "^20.11.16",
|
||||||
|
"@types/torrent-stream": "^0.0.9",
|
||||||
"@types/validator": "^13.11.8",
|
"@types/validator": "^13.11.8",
|
||||||
"@types/webtorrent": "^0.109.7",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
@@ -47,10 +44,12 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"eslint-plugin-import-helpers": "^1.3.1",
|
"eslint-plugin-import-helpers": "^1.3.1",
|
||||||
|
"node-gyp": "^10.0.1",
|
||||||
|
"nodemon": "^3.0.3",
|
||||||
"pino-pretty": "^10.3.1",
|
"pino-pretty": "^10.3.1",
|
||||||
"tsx": "^4.7.0",
|
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"tsx": "^4.7.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export interface ICinemetaMetaData {
|
|||||||
dvdRelease?: null;
|
dvdRelease?: null;
|
||||||
genre?: string[];
|
genre?: string[];
|
||||||
imdbRating?: string;
|
imdbRating?: string;
|
||||||
imdb_id?: string;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
popularity?: number;
|
popularity?: number;
|
||||||
poster?: string;
|
poster?: string;
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export interface ICommonVideoMetadata {
|
|||||||
title?: string;
|
title?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
imdb_id?: string;
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,6 @@ export interface IKitsuMeta {
|
|||||||
export interface IKitsuVideo extends ICommonVideoMetadata {
|
export interface IKitsuVideo extends ICommonVideoMetadata {
|
||||||
imdbEpisode?: number;
|
imdbEpisode?: number;
|
||||||
imdbSeason?: number;
|
imdbSeason?: number;
|
||||||
imdb_id?: string;
|
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export const torrentConfig = {
|
export const torrentConfig = {
|
||||||
MAX_CONNECTIONS_PER_TORRENT: parseInt(process.env.MAX_CONNECTIONS_PER_TORRENT || "20", 10),
|
MAX_CONNECTIONS_PER_TORRENT: parseInt(process.env.MAX_CONNECTIONS_PER_TORRENT || "20", 10),
|
||||||
MAX_CONNECTIONS_OVERALL: parseInt(process.env.MAX_CONNECTIONS_OVERALL || "100", 10),
|
|
||||||
TIMEOUT: parseInt(process.env.TORRENT_TIMEOUT || "30000", 10)
|
TIMEOUT: parseInt(process.env.TORRENT_TIMEOUT || "30000", 10)
|
||||||
};
|
};
|
||||||
@@ -8,7 +8,7 @@ import {IMetaDataQuery} from "@interfaces/metadata_query";
|
|||||||
import {IMetadataResponse} from "@interfaces/metadata_response";
|
import {IMetadataResponse} from "@interfaces/metadata_response";
|
||||||
import {IMetadataService} from "@interfaces/metadata_service";
|
import {IMetadataService} from "@interfaces/metadata_service";
|
||||||
import {IocTypes} from "@models/ioc_types";
|
import {IocTypes} from "@models/ioc_types";
|
||||||
import axios, {AxiosResponse} from 'axios';
|
import axios from 'axios';
|
||||||
import {ResultTypes, search} from 'google-sr';
|
import {ResultTypes, search} from 'google-sr';
|
||||||
import {inject, injectable} from "inversify";
|
import {inject, injectable} from "inversify";
|
||||||
import nameToImdb from 'name-to-imdb';
|
import nameToImdb from 'name-to-imdb';
|
||||||
@@ -70,12 +70,12 @@ export class MetadataService implements IMetadataService {
|
|||||||
|
|
||||||
const key = Number.isInteger(query.id) || query.id.toString().match(/^\d+$/) ? `kitsu:${query.id}` : query.id;
|
const key = Number.isInteger(query.id) || query.id.toString().match(/^\d+$/) ? `kitsu:${query.id}` : query.id;
|
||||||
const metaType = query.type === TorrentType.Movie ? TorrentType.Movie : TorrentType.Series;
|
const metaType = query.type === TorrentType.Movie ? TorrentType.Movie : TorrentType.Series;
|
||||||
return this.cacheService.cacheWrapMetadata(key.toString(), () => this.requestMetadata(`${KITSU_URL}/meta/${metaType}/${key}.json`)
|
return this.cacheService.cacheWrapMetadata(key.toString(), () => this.requestKitsuMetadata(`${KITSU_URL}/meta/${metaType}/${key}.json`)
|
||||||
.catch(() => this.requestMetadata(`${CINEMETA_URL}/meta/${metaType}/${key}.json`))
|
.catch(() => this.requestCinemetaMetadata(`${CINEMETA_URL}/meta/${metaType}/${key}.json`))
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// try different type in case there was a mismatch
|
// try different type in case there was a mismatch
|
||||||
const otherType = metaType === TorrentType.Movie ? TorrentType.Series : TorrentType.Movie;
|
const otherType = metaType === TorrentType.Movie ? TorrentType.Series : TorrentType.Movie;
|
||||||
return this.requestMetadata(`${CINEMETA_URL}/meta/${otherType}/${key}.json`)
|
return this.requestCinemetaMetadata(`${CINEMETA_URL}/meta/${otherType}/${key}.json`)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw new Error(`failed metadata query ${key} due: ${error.message}`);
|
throw new Error(`failed metadata query ${key} due: ${error.message}`);
|
||||||
@@ -101,23 +101,20 @@ export class MetadataService implements IMetadataService {
|
|||||||
.replace(/\s{2,}/, ' ') // replace multiple spaces
|
.replace(/\s{2,}/, ' ') // replace multiple spaces
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
private requestMetadata = async (url: string): Promise<IMetadataResponse> => {
|
private requestKitsuMetadata = async (url: string): Promise<IMetadataResponse> => {
|
||||||
const response: AxiosResponse = await axios.get(url, {timeout: TIMEOUT});
|
const response = await axios.get(url, {timeout: TIMEOUT});
|
||||||
let result: IMetadataResponse;
|
|
||||||
const body = response.data;
|
const body = response.data;
|
||||||
if ('kitsu_id' in body.meta) {
|
return this.handleKitsuResponse(body as IKitsuJsonResponse);
|
||||||
result = this.handleKitsuResponse(body as IKitsuJsonResponse);
|
};
|
||||||
} else if ('imdb_id' in body.meta) {
|
|
||||||
result = this.handleCinemetaResponse(body as ICinemetaJsonResponse);
|
|
||||||
} else {
|
|
||||||
throw new Error('No valid metadata');
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
private requestCinemetaMetadata = async (url: string): Promise<IMetadataResponse> => {
|
||||||
|
const response = await axios.get(url, {timeout: TIMEOUT});
|
||||||
|
const body = response.data;
|
||||||
|
return this.handleCinemetaResponse(body as ICinemetaJsonResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
private handleCinemetaResponse = (body: ICinemetaJsonResponse): IMetadataResponse => ({
|
private handleCinemetaResponse = (body: ICinemetaJsonResponse): IMetadataResponse => ({
|
||||||
imdbId: parseInt(body.meta?.imdb_id || '0'),
|
imdbId: parseInt(body.meta?.id || '0'),
|
||||||
type: body.meta?.type,
|
type: body.meta?.type,
|
||||||
title: body.meta?.name,
|
title: body.meta?.name,
|
||||||
year: parseInt(body.meta?.year || '0'),
|
year: parseInt(body.meta?.year || '0'),
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import {configurationService} from '@services/configuration_service';
|
|||||||
import {inject, injectable} from "inversify";
|
import {inject, injectable} from "inversify";
|
||||||
import {encode} from 'magnet-uri';
|
import {encode} from 'magnet-uri';
|
||||||
import {parse} from "parse-torrent-title";
|
import {parse} from "parse-torrent-title";
|
||||||
import WebTorrent from "webtorrent";
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import * as torrentStream from "torrent-stream";
|
||||||
|
import TorrentEngine = TorrentStream.TorrentEngine;
|
||||||
|
import TorrentEngineOptions = TorrentStream.TorrentEngineOptions;
|
||||||
|
|
||||||
interface ITorrentFile {
|
interface ITorrentFile {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -20,27 +23,19 @@ interface ITorrentFile {
|
|||||||
fileIndex: number;
|
fileIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientOptions : WebTorrent.Options = {
|
|
||||||
maxConns: configurationService.torrentConfig.MAX_CONNECTIONS_OVERALL,
|
|
||||||
utp: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
const torrentOptions: WebTorrent.TorrentOptions = {
|
|
||||||
skipVerify: true,
|
|
||||||
destroyStoreOnDestroy: true,
|
|
||||||
private: true,
|
|
||||||
maxWebConns: configurationService.torrentConfig.MAX_CONNECTIONS_PER_TORRENT,
|
|
||||||
}
|
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class TorrentDownloadService implements ITorrentDownloadService {
|
export class TorrentDownloadService implements ITorrentDownloadService {
|
||||||
private torrentClient: WebTorrent.Instance;
|
|
||||||
private logger: ILoggingService;
|
private logger: ILoggingService;
|
||||||
|
private engineOptions: TorrentEngineOptions = {
|
||||||
|
connections: configurationService.torrentConfig.MAX_CONNECTIONS_PER_TORRENT,
|
||||||
|
uploads: 0,
|
||||||
|
verify: false,
|
||||||
|
dht: false,
|
||||||
|
tracker: true,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(@inject(IocTypes.ILoggingService) logger: ILoggingService) {
|
constructor(@inject(IocTypes.ILoggingService) logger: ILoggingService) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.torrentClient = new WebTorrent(clientOptions);
|
|
||||||
this.torrentClient.on('error', errors => this.logClientErrors(errors));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTorrentFiles = async (torrent: IParsedTorrent, timeout: number = 30000): Promise<ITorrentFileCollection> => {
|
public getTorrentFiles = async (torrent: IParsedTorrent, timeout: number = 30000): Promise<ITorrentFileCollection> => {
|
||||||
@@ -64,42 +59,30 @@ export class TorrentDownloadService implements ITorrentDownloadService {
|
|||||||
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) => {
|
||||||
this.logger.debug(`Adding torrent with infoHash ${torrent.infoHash} to webtorrent client...`);
|
this.logger.debug(`Adding torrent with infoHash ${torrent.infoHash} to torrent engine...`);
|
||||||
|
|
||||||
const currentTorrent = this.torrentClient.add(magnet, torrentOptions);
|
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
this.removeTorrent(currentTorrent, torrent);
|
engine.destroy(() => {});
|
||||||
reject(new Error('No available connections for torrent!'));
|
reject(new Error('No available connections for torrent!'));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
currentTorrent.on('ready', () => {
|
const engine: TorrentEngine = torrentStream.default(magnet, this.engineOptions);
|
||||||
const files: ITorrentFile[] = currentTorrent.files.map((file, fileId) => ({
|
|
||||||
|
engine.on("ready", () => {
|
||||||
|
const files: ITorrentFile[] = engine.files.map((file, fileId) => ({
|
||||||
fileIndex: fileId,
|
fileIndex: fileId,
|
||||||
length: file.length,
|
length: file.length,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: file.path,
|
path: file.path,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.logger.debug(`Found ${files.length} files in torrent ${torrent.infoHash}`);
|
|
||||||
|
|
||||||
resolve(files);
|
resolve(files);
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
this.removeTorrent(currentTorrent, torrent);
|
engine.destroy(() => {});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private removeTorrent = (currentTorrent: WebTorrent.Torrent, torrent: IParsedTorrent): void => {
|
|
||||||
try {
|
|
||||||
this.torrentClient.remove(currentTorrent, {destroyStore: true}, () => {
|
|
||||||
this.logger.debug(`Removed torrent ${torrent.infoHash} from webtorrent client...`);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.logClientErrors(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private filterVideos = (torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): IFileAttributes[] => {
|
private filterVideos = (torrent: IParsedTorrent, torrentFiles: ITorrentFile[]): IFileAttributes[] => {
|
||||||
if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) {
|
if (torrentFiles.length === 1 && !Number.isInteger(torrentFiles[0].fileIndex)) {
|
||||||
return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])];
|
return [this.mapTorrentFileToFileAttributes(torrent, torrentFiles[0])];
|
||||||
@@ -179,9 +162,5 @@ export class TorrentDownloadService implements ITorrentDownloadService {
|
|||||||
path: file.path,
|
path: file.path,
|
||||||
size: file.length,
|
size: file.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
private logClientErrors(errors: Error | string | unknown): void {
|
|
||||||
this.logger.error(`Error in webtorrent client: ${errors}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ export class TorrentFileService implements ITorrentFileService {
|
|||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|
||||||
if (metadata === undefined || metadata instanceof Error) {
|
if (metadata === undefined || metadata instanceof Error) {
|
||||||
this.logger.warn(`Failed to retrieve metadata for torrent ${torrent.title}`);
|
|
||||||
this.logger.debug(`Metadata Error: ${torrent.title}`, metadata);
|
|
||||||
return Promise.reject(new Error('Failed to retrieve metadata'));
|
return Promise.reject(new Error('Failed to retrieve metadata'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user