Browse Source

Add torrent-search service

dev
David Ludwig 4 years ago
parent
commit
8e42868561
27 changed files with 2061 additions and 0 deletions
  1. +20
    -0
      api/torrent-search/package.json
  2. +32
    -0
      api/torrent-search/src/IpcClient.ts
  3. +4
    -0
      api/torrent-search/src/constants.ts
  4. +3
    -0
      api/torrent-search/src/index.ts
  5. +18
    -0
      api/torrent-search/src/schema.ts
  6. +7
    -0
      api/torrent-search/tsconfig.json
  7. +92
    -0
      api/torrent-search/yarn.lock
  8. +9
    -0
      docker-compose.dev.yml
  9. +6
    -0
      docker-compose.prod.yml
  10. +10
    -0
      docker-compose.yml
  11. +3
    -0
      services/torrent-search/README.md
  12. +9
    -0
      services/torrent-search/nodemon.json
  13. +31
    -0
      services/torrent-search/package.json
  14. +11
    -0
      services/torrent-search/src/index.ts
  15. +45
    -0
      services/torrent-search/src/services/IpcInterface.ts
  16. +61
    -0
      services/torrent-search/src/services/TorrentSearch.ts
  17. +7
    -0
      services/torrent-search/src/services/index.ts
  18. +63
    -0
      services/torrent-search/src/torrents/Torrent.ts
  19. +155
    -0
      services/torrent-search/src/torrents/parsing.ts
  20. +60
    -0
      services/torrent-search/src/torrents/providers/Provider.ts
  21. +14
    -0
      services/torrent-search/src/torrents/providers/index.ts
  22. +36
    -0
      services/torrent-search/src/torrents/providers/torrentgalaxy/index.ts
  23. +136
    -0
      services/torrent-search/src/torrents/providers/torrentgalaxy/search.ts
  24. +58
    -0
      services/torrent-search/src/torrents/ranking.ts
  25. +82
    -0
      services/torrent-search/src/torrents/util.ts
  26. +9
    -0
      services/torrent-search/tsconfig.json
  27. +1080
    -0
      services/torrent-search/yarn.lock

+ 20
- 0
api/torrent-search/package.json View File

@ -0,0 +1,20 @@
{
"name": "@autoplex-api/torrent-search",
"version": "0.0.0",
"main": "dist/lib/index.js",
"types": "dist/typings",
"license": "MIT",
"scripts": {
"build": "yarn run clean && tsc",
"clean": "rimraf ./dist"
},
"devDependencies": {
"@types/node": "^15.0.1",
"rimraf": "^3.0.2",
"typescript": "^4.2.4"
},
"dependencies": {
"@autoplex/ipc": "^0.0.0",
"@autoplex/microservice": "^0.0.0"
}
}

+ 32
- 0
api/torrent-search/src/IpcClient.ts View File

@ -0,0 +1,32 @@
import { IpcClientService } from "@autoplex/ipc";
import { Microservice } from "@autoplex/microservice";
import { SOCKET_PATH } from "./constants";
import { IMovieSearchInfo } from "./schema";
export class IpcClient<M extends Microservice = Microservice> extends IpcClientService<M>
{
/**
* The name of the service
*/
public readonly NAME = "Torrent Search";
/**
* The path to the socket file
*/
protected readonly SOCKET_PATH = SOCKET_PATH;
/**
* Add a torrent to the client
* @param torrent Magnet URI or file buffer
*/
public async searchMovie(title: string, imdbId?: string, year?: number, altTitles: string[] = [])
{
let response = await this.request("search_movie", <IMovieSearchInfo> {
title, imdbId, year, altTitles
});
if (response.error) {
throw new Error("Failed to search for movie torrent");
}
return <string>response.data;
}
}

+ 4
- 0
api/torrent-search/src/constants.ts View File

@ -0,0 +1,4 @@
/**
* The path to the socket file
*/
export const SOCKET_PATH = "/var/autoplex/ipc/torrent_search.sock";

+ 3
- 0
api/torrent-search/src/index.ts View File

@ -0,0 +1,3 @@
export * from "./constants";
export * from "./schema";
export * from "./IpcClient";

+ 18
- 0
api/torrent-search/src/schema.ts View File

@ -0,0 +1,18 @@
/**
* The movie search request structure
*/
export interface IMovieSearchInfo {
title : string,
altTitles : string[],
imdbId? : string,
year? : number,
torrentBlacklist: string[]
}
/**
* The generated torrent link structure
*/
export interface ITorrentLink {
link: string,
type: "magnet"|"file"
}

+ 7
- 0
api/torrent-search/tsconfig.json View File

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.package.json",
"compilerOptions": {
"outDir": "./dist/lib",
"declarationDir": "./dist/typings"
}
}

+ 92
- 0
api/torrent-search/yarn.lock View File

@ -0,0 +1,92 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/node@^15.0.1":
version "15.0.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.2.tgz#51e9c0920d1b45936ea04341aa3e2e58d339fb67"
integrity sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob@^7.1.3:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
typescript@^4.2.4:
version "4.2.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961"
integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

+ 9
- 0
docker-compose.dev.yml View File

@ -59,6 +59,15 @@ services:
- ./services/torrent-rest:/opt/app/services/torrent-rest
tty: true
torrent_search:
build:
target: dev
volumes:
- ./api:/opt/app/api
- ./packages:/opt/app/packages
- ./services/torrent-search:/opt/app/services/torrent-search
tty: true
torrent:
build:
target: dev


+ 6
- 0
docker-compose.prod.yml View File

@ -37,6 +37,12 @@ services:
environment:
NODE_ENV: production
torrent_search:
build:
target: prod
environment:
NODE_ENV: production
torrent:
build:
target: prod


+ 10
- 0
docker-compose.yml View File

@ -115,6 +115,16 @@ services:
volumes:
- var:/var/autoplex
torrent_search:
build:
context: .
args:
SERVICE: torrent-search
restart: unless-stopped
user: ${USER_ID}:${GROUP_ID}
volumes:
- var:/var/autoplex
torrent:
build:
context: .


+ 3
- 0
services/torrent-search/README.md View File

@ -0,0 +1,3 @@
# Autoplex Seeker
Seeker is a standalone container responsible for searching for requested content via direct search and monitoring RSS feeds of various torrent providers

+ 9
- 0
services/torrent-search/nodemon.json View File

@ -0,0 +1,9 @@
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --inspect=0.0.0.0:9230 -r ts-node/register src/index.ts",
"events": {
"start": "clear"
}
}

+ 31
- 0
services/torrent-search/package.json View File

@ -0,0 +1,31 @@
{
"name": "@autoplex-service/torrent-search",
"version": "1.0.0",
"main": "./dist/index.js",
"author": "David Ludwig <davidludwigii@gmail.com>",
"license": "MIT",
"scripts": {
"clean": "rimraf ./dist",
"build": "tsc",
"start": "NODE_ENV=production node .",
"start:dev": "nodemon"
},
"devDependencies": {
"@types/node": "^14.14.41",
"@types/node-ipc": "^9.1.3",
"@types/xml2js": "^0.4.8",
"nodemon": "^2.0.7",
"rimraf": "^3.0.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
},
"dependencies": {
"@autoplex-api/torrent": "^0.0.0",
"@autoplex-api/torrent-search": "^0.0.0",
"@autoplex/ipc": "^0.0.0",
"@autoplex/microservice": "^0.0.0",
"cheerio": "^1.0.0-rc.9",
"node-ipc": "^9.1.4",
"xml2js": "^0.4.23"
}
}

+ 11
- 0
services/torrent-search/src/index.ts View File

@ -0,0 +1,11 @@
import { Microservice } from "@autoplex/microservice";
import * as services from "./services";
// Create a new application instance
let app = new Microservice();
// Install the internal services
app.installServices(Object.values(services));
// Start the application
app.exec().then(process.exit);

+ 45
- 0
services/torrent-search/src/services/IpcInterface.ts View File

@ -0,0 +1,45 @@
import { IMovieSearchInfo, SOCKET_PATH } from "@autoplex-api/torrent-search";
import { Microservice } from "@autoplex/microservice";
import { IpcServerService } from "@autoplex/ipc";
import TorrentSearch from "./TorrentSearch";
export default class IpcInterface extends IpcServerService
{
/**
* The service name
*/
public readonly NAME = "IPC";
/**
* The path to the socket file
*/
public readonly SOCKET_PATH = SOCKET_PATH;
/**
* The torrent search service
*/
protected search!: TorrentSearch;
/**
* Link the IPC service to other required internal services
*/
public link(app: Microservice) {
this.search = app.service<TorrentSearch>("Torrent Search");
}
/**
* Install the the event handlers
*/
protected installMessageHandlers() {
this.addMessageHandler("search_movie", this.searchMovie);
}
// Interface Methods ---------------------------------------------------------------------------
/**
* Invoked when a new Movie ticket has been created
*/
protected async searchMovie(movie: IMovieSearchInfo) {
return await this.search.searchMovie(movie);
}
}

+ 61
- 0
services/torrent-search/src/services/TorrentSearch.ts View File

@ -0,0 +1,61 @@
import { IMovieSearchInfo, ITorrentLink } from "@autoplex-api/torrent-search/dist/typings";
import { InternalService } from "@autoplex/microservice";
import { MediaType, Provider, providers } from "../torrents/providers";
import { rankTorrents } from "../torrents/ranking";
import Torrent from "../torrents/Torrent";
export default class TorrentSearch extends InternalService
{
/**
* The name of the service
*/
public readonly NAME = "Torrent Search";
/**
* Store all providers
*/
protected providers!: Provider[];
/**
* Store a list of movie prodivers
*/
protected movieProviders!: Provider[];
/**
* Store a list of TV providers
*/
protected tvProviders!: Provider[];
/**
* Boot the torrent search sorvice
*/
public async boot() {
let providerClasses = Object.values(providers);
this.providers = providerClasses.map(ProviderClass => new ProviderClass());
this.movieProviders = this.providers.filter(provider => provider.PROVIDES & MediaType.Movies);
this.tvProviders = this.providers.filter(provider => provider.PROVIDES & MediaType.TvShows);
}
/**
* Search for a movie
*/
public async searchMovie(movie: IMovieSearchInfo) {
let results = await Promise.all(this.movieProviders.map(provider => provider.searchMovie(movie)));
let torrents = (<Torrent[]>[]).concat(...results);
if (torrents.length == 0) {
return null;
}
// Determine the preferred torrents
let preferredTorrents = rankTorrents(torrents);
if (preferredTorrents.length == 0) {
preferredTorrents = torrents;
}
// Return the selected torrent
this.log("Found movie torrent for", movie.title);
let link = await preferredTorrents[0].downloadLink();
return <ITorrentLink>{
link,
type: link.toLowerCase().startsWith("magnet:?xt=urn:btih:") ? "magnet" : "file"
};
}
}

+ 7
- 0
services/torrent-search/src/services/index.ts View File

@ -0,0 +1,7 @@
import IpcInterface from "./IpcInterface";
import TorrentSearch from "./TorrentSearch";
export {
IpcInterface,
TorrentSearch
}

+ 63
- 0
services/torrent-search/src/torrents/Torrent.ts View File

@ -0,0 +1,63 @@
import { IMovieSearchInfo } from "@autoplex-api/torrent-search/dist/typings";
import { ITorrentMetaInfo, parseMovieTorrentName } from "./parsing";
export default class Torrent
{
/**
* The name of the torrent
*/
public readonly name: string;
/**
* The size of the torrent in bytes (if available)
*/
public readonly size: number | null;
/**
* The number of seeders (if available)
*/
public readonly seeders: number;
/**
* Download link (if available)
*/
protected readonly link: string | null;
/**
* Metadata of the torrent
*/
public readonly metadata: ITorrentMetaInfo;
/**
* Create a new Torrent instance
*
* @param name The name of the torrent
* @param size The size of the torrent in bytes (if available)
* @param seeders The number of seeders (if available)
* @param link The number of seeders (if available)
*/
public constructor(movie: IMovieSearchInfo, name: string, size?: number, seeders?: number, link?: string) {
this.name = name.trim();
this.size = size ?? null;
this.seeders = seeders ?? 1;
this.link = link ?? null;
this.metadata = parseMovieTorrentName(name, movie);
}
/**
* Return a link to download (magnet or .torrent)
*/
public async downloadLink() {
if (this.link === null) {
throw Error("Magnet link does not exist");
}
return this.link;
}
/**
* Serialize this torrent into a string
*/
public toString() {
return `Name: ${this.name}; Size: ${this.size}; Seeders: ${this.seeders};`
}
}

+ 155
- 0
services/torrent-search/src/torrents/parsing.ts View File

@ -0,0 +1,155 @@
import { IMovieSearchInfo } from "@autoplex-api/torrent-search/dist/typings";
/**
* Video quality from lowest to highest
*/
export enum Resolution {
HD4k,
HD1080,
HD720,
SD384,
SD480,
SD360,
Unknown
}
// https://en.wikipedia.org/wiki/Pirated_movie_release_types#DVD_and_VOD_ripping
// https://en.wikipedia.org/wiki/Standard_(warez)#cite_note-txd2k9-13
/**
* Types of releases from lowest quality to highest
*/
export enum ReleaseType {
BluRay,
WebDl,
WebRip,
WebCap,
HDRip,
DVDR,
DVDRip,
Unknown, // Unknown is better than cam tbh
HDCAM,
CAM
}
export enum VideoCodec {
XviD,
x264,
x265,
}
export enum VideoCodecFlag {
REMUX,
HDR,
HEVC
}
export enum AudioCodec {
AC3,
DD51,
AAC71,
Atmos71,
TenBit
}
export interface ITorrentMetaInfo {
containsOtherLanguage: boolean,
resolution: Resolution,
releaseType: ReleaseType,
}
/**
* Determine meta-info from a torrent name
*/
export function parseMovieTorrentName(torrentName: string, movie: IMovieSearchInfo) {
// Split the meta info after the year if possible to make parsing more reliable
let split = torrentName.split(new RegExp(`${movie.year}|\\(${movie.year}\\)`));
let metaInfo = split[split.length - 1];
let title = split.length > 1 ? "" : movie.title; // No need to check title in parsing if split correctly
return <ITorrentMetaInfo>{
containsOtherLanguage: determineIfContainsOtherLanguages(torrentName, title),
resolution: determineResolution(metaInfo, title),
releaseType: determineReleaseType(metaInfo, title),
}
}
/**
* Examine the torrent name for language indicators
*/
function determineIfContainsOtherLanguages(torrentName: string, title: string) {
let matches = torrentName.match(/\b(?:Hindi|Telugu|Ita|Italian|Spanish|Latino|Russian|Arabic|Dual|Multi)\b/gi);
for (let match of matches ?? []) {
if (title.indexOf(match) == -1) {
return true;
}
}
return false;
}
/**
* Interpret the resolution string as an enum value
*/
function resolutionFromString(resolution: string) {
switch(resolution.toUpperCase()) {
case "4K":
case "UHD":
case "2160":
return Resolution.HD4k;
case "1080":
return Resolution.HD1080;
case "720":
return Resolution.HD720;
case "480":
return Resolution.SD480;
case "384":
return Resolution.SD384;
case "360":
return Resolution.SD360;
default:
return Resolution.Unknown;
}
}
/**
* Determine the video resolution of the torrent
*/
function determineResolution(torrentName: string, title: string) {
let matches = torrentName.match(/\b(?:2160|1080|720|480|384|360)p?|UltraHD|UHD|4K\b/gi);
if (matches == null) {
return Resolution.Unknown;
}
let resolution = matches[matches.length - 1];
// Make sure what was matched is not part of the title...
if (matches.length == 1 && title.indexOf(resolution) != -1) {
return Resolution.Unknown;
}
return resolutionFromString(resolution.replace(/p$/i, ""));
}
/**
* Determine the release type of the torrent
*/
function determineReleaseType(torrentName: string, title: string) {
let releaseTypeRegex: {[type: string]: RegExp} = {
[ReleaseType.BluRay]: /\b(?:BR|Blu-Ray|BluRay|BDRip|BRRip|BDMV|BDR|BD25|BD50|BD5|BD9)\b/i,
[ReleaseType.WebDl] : /\b(?:WEB.?DL|WEB-DLRip)\b/i,
[ReleaseType.WebRip]: /\b(?:WEB.?Rip|WEB)\b/i,
[ReleaseType.WebCap]: /\bWEB.?Cap\b/i,
[ReleaseType.HDRip] : /\b(?:HC|HD.?Rip)\b/i,
[ReleaseType.DVDR] : /\bDVD.?R|DVD-Full|Full-Rip|DVD.?5|DVD.?9\b/i,
[ReleaseType.DVDRip]: /\bDVD.?Rip|DVD.?Mux/i,
[ReleaseType.HDCAM] : /\b(?:TRUE|HD)CAM\b/i,
[ReleaseType.CAM] : /\bCAM.?Rip\b/i,
};
let matches: RegExpMatchArray | null;
for (let type in releaseTypeRegex) {
matches = torrentName.match(releaseTypeRegex[type]);
if (!matches) {
continue;
}
if (matches.length == 1 || title.indexOf(matches[matches.length - 1]) == -1) {
return <ReleaseType>parseInt(type);
}
}
return ReleaseType.Unknown;
}

+ 60
- 0
services/torrent-search/src/torrents/providers/Provider.ts View File

@ -0,0 +1,60 @@
import { IMovieSearchInfo } from "@autoplex-api/torrent-search";
import Torrent from "../Torrent";
/**
* Media type flags
*/
export enum MediaType {
None = 0x0,
Movies = 0x1,
TvShows = 0x2
}
export default abstract class Provider
{
/**
* Indicate what media types the provider supports
*/
public abstract readonly PROVIDES: MediaType;
/**
* Search for movies
*/
public abstract searchMovie(movie: IMovieSearchInfo): Promise<Torrent[]>;
// ---------------------------------------------------------------------------------------------
/**
* Indicate if the provider is currently searching for a torrent
*/
protected isSearching = false;
/**
* Maintain a queue of search requests
*/
protected searchQueue: (() => void)[] = [];
/**
* Lock the search
*/
protected async lock() {
if (this.isSearching) {
await new Promise<void>(resolve => this.searchQueue.push(resolve));
}
this.isSearching = true;
}
/**
* Unlock the search
*/
protected unlock(delay: number = 0) {
setTimeout(() => {
let next = this.searchQueue.splice(0, 1)[0];
if (next === undefined) {
this.isSearching = false;
return;
}
next();
}, delay);
}
}

+ 14
- 0
services/torrent-search/src/torrents/providers/index.ts View File

@ -0,0 +1,14 @@
import Provider, { MediaType } from "./Provider";
import TorrentGalaxy from "./torrentgalaxy";
/**
* Export all available torrent providers
*/
export const providers = {
TorrentGalaxy
};
/**
* Export the abstract torrent provider class
*/
export { Provider, MediaType }

+ 36
- 0
services/torrent-search/src/torrents/providers/torrentgalaxy/index.ts View File

@ -0,0 +1,36 @@
import { IMovieSearchInfo } from "@autoplex-api/torrent-search";
import Provider, { MediaType } from "../Provider";
import Torrent from "../../Torrent";
import { search, Sort } from "./search";
/**
* Throttle the torrent search
*/
const THROTTLE_SEARCH = 3000;
export default class TorrentGalaxy extends Provider
{
/**
* Indicate that this provider provides movies
*/
public readonly PROVIDES = MediaType.Movies;
/**
* Search for a movie
*/
public async searchMovie(movie: IMovieSearchInfo) {
if (movie.imdbId === undefined) {
return [];
}
await this.lock();
let torrents = await search(movie.imdbId, undefined, Sort.Seeders);
this.unlock(THROTTLE_SEARCH);
return torrents.torrents.map(torrent => new Torrent(
movie,
torrent.name,
torrent.size,
torrent.seeders,
torrent.magnet
));
}
}

+ 136
- 0
services/torrent-search/src/torrents/providers/torrentgalaxy/search.ts View File

@ -0,0 +1,136 @@
import type { Cheerio, CheerioAPI, Element } from "cheerio";
import cheerio from "cheerio";
import { request, convertToBytes } from "../../util";
const BASE_URL = "https://torrentgalaxy.mx/torrents.php?search=";
export enum LanguageId {
AllLanguages = 0,
English = 1,
French = 2,
German = 3,
Italian = 4,
Japanese = 5,
Spanish = 6,
Russian = 7,
Hindi = 8,
OtherMultiple = 9,
Korean = 10,
Danish = 11,
Norwegian = 12,
Dutch = 13,
Chinese = 14,
Portuguese = 15,
Bengali = 16,
Polish = 17,
Turkish = 18,
Telugu = 19,
Urdu = 20,
Arabic = 21,
Swedish = 22,
Romanian = 23,
Thai = 24
}
export enum Language {
AllLanguages ="AllLanguages",
English ="English",
French ="French",
German ="German",
Italian ="Italian",
Japanese ="Japanese",
Spanish ="Spanish",
Russian ="Russian",
Hindi ="Hindi",
OtherMultiple ="OtherMultiple",
Korean ="Korean",
Danish ="Danish",
Norwegian ="Norwegian",
Dutch ="Dutch",
Chinese ="Chinese",
Portuguese ="Portuguese",
Bengali ="Bengali",
Polish ="Polish",
Turkish ="Turkish",
Telugu ="Telugu",
Urdu ="Urdu",
Arabic ="Arabic",
Swedish ="Swedish",
Romanian ="Romanian",
Thai ="Thai"
}
export enum Category {
Documentaries = 9,
MoviesHD = 42,
MoviesSD = 1,
Movies4K = 3,
MoviesPacks = 4,
TVEpisodesHD = 41,
TVEPisodesSD = 5,
TVPacks = 6,
TVSports = 7
}
export enum Sort {
Date = "id",
Name = "name",
Size = "size",
Seeders = "seeders"
}
export enum SortOrder {
Asc = "asc",
Desc = "desc",
}
interface ITorrentGalaxyTorrent {
category: number,
language: Language,
name : string,
magnet : string,
size : number,
seeders : number,
leechers: number
}
interface ITorrentGalaxyResults {
torrents: ITorrentGalaxyTorrent[],
total_results: number
}
function scrapeRow($: CheerioAPI, row: Cheerio<Element>): ITorrentGalaxyTorrent {
let children = row.children();
let category = <string>$(children[0]).find("a").attr("href")?.split("cat=")[1];
let language = <Language>$(children[2]).find("img[title]").attr("title");
let name = $(children[3]).text();
let magnet = <string>$(children[4]).find("a[href^='magnet']").first().attr("href");
let [size, unit] = $(children[7]).text().split(" ");
let [seeders, leechers] = $(children[10]).text().slice(1, -1).split('/').map(v => parseInt(v));
return {
category: parseInt(category),
size: convertToBytes(parseFloat(size), unit),
language, name, magnet, seeders, leechers
}
}
function scrapeResults(response: string): ITorrentGalaxyResults {
let torrents: ITorrentGalaxyTorrent[] = [];
let $ = cheerio.load(response);
$(".tgxtable .tgxtablerow").each((_, elem) => {
torrents.push(scrapeRow($, $(elem)));
});
return {
torrents,
total_results: parseInt($("#filterbox2 > span").text())
};
}
/**
* Supports IMDb links too
*/
export async function search(query: string, language: LanguageId = LanguageId.AllLanguages, sort: Sort = Sort.Date, order: SortOrder = SortOrder.Desc) {
let res = await request(`${BASE_URL}${encodeURI(query)}&lang=${language}&sort=${sort}&order=${order}`);
let results = scrapeResults(res);
return results;
}

+ 58
- 0
services/torrent-search/src/torrents/ranking.ts View File

@ -0,0 +1,58 @@
import { Resolution } from "./parsing";
import Torrent from "./Torrent";
/**
* Rank a list of torrents from best to worst to download
*/
export function rankTorrents(torrents: Torrent[]) {
torrents.sort(sortCompare);
let preferred = torrents.filter(selectPreferredTorrents);
return preferred;
}
/**
* Filter out unwanted torrents
*/
function selectPreferredTorrents(torrent: Torrent) {
if (torrent.seeders == 0 || torrent.metadata.containsOtherLanguage) {
return false;
}
if (torrent.metadata.resolution == Resolution.HD4k) {
return torrent.size != null && torrent.size < 10*1024*1024*1024; // 10GB
}
return true;
}
/**
* A comparator for ranking torrents
*
* @param a Left side
* @param b Right side
*/
function sortCompare(a: Torrent, b: Torrent) {
// Languages
let languageCmp = <any>a.metadata.containsOtherLanguage - <any>b.metadata.containsOtherLanguage;
if (languageCmp !== 0) {
return languageCmp;
}
// Resolution
let resolutionCmp = a.metadata.resolution - b.metadata.resolution;
if (resolutionCmp !== 0) {
return resolutionCmp;
}
// If one has only a few seeds, don't worry about the other info. Prioritize seed count
if (a.seeders < 5 || b.seeders < 5) {
let seedersCmp = b.seeders - a.seeders;
if (seedersCmp != 0) {
return seedersCmp;
}
}
// Sort by the file size
let fileSizeCmp = (a.size ?? 0) - (b.size ?? 0);
if (fileSizeCmp !== 0) {
return fileSizeCmp;
}
return 0;
}

+ 82
- 0
services/torrent-search/src/torrents/util.ts View File

@ -0,0 +1,82 @@
import { parseString } from "xml2js";
import https from "https";
/**
* Perform an RSS/XML request
*/
export function rssRequest<T = any>(url: string) {
return new Promise<T>((resolve, reject) => {
https.get(url, { headers: { "User-Agent": "Node", "Accept": "application/rss+xml" } }, (response) => {
if (response.statusCode !== 200) {
reject("Status error: " + response.statusCode);
return;
}
response.setEncoding("utf-8");
let body = "";
response.on("data", (chunk) => body += chunk);
response.on("end", () => parseString(body, (err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
}));
});
});
}
/**
* Perform a generic GET request
*/
export function jsonRequest<T = any>(url: string) {
return new Promise<T>((resolve, reject) => {
https.get(url, { headers: { "User-Agent": "Node", "Accept": "*/*" } }, (response) => {
if (response.statusCode !== 200) {
reject("Status error: " + response.statusCode);
return;
}
response.setEncoding("utf-8");
let body = "";
response.on("data", (chunk) => body += chunk);
response.on("end", () => resolve(JSON.parse(body)));
});
});
}
/**
* Perform a generic GET request
*/
export function request(url: string, timeout: number = 10000) {
return new Promise<string>((resolve, reject) => {
https.get(url, { headers: { "User-Agent": "Node", "Accept": "*/*" }, timeout }, (response) => {
if (response.statusCode !== 200) {
reject("Status error: " + response.statusCode);
return;
}
response.setEncoding("utf-8");
let body = "";
response.on("data", (chunk) => body += chunk);
response.on("end", () => resolve(body));
}).on("timeout", () => reject("timeout"));
});
}
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export function convertToBytes(size: number, unit: string, throwUnknownUnit: boolean = true) {
switch(unit.toUpperCase()) {
case "GB":
return Math.ceil(size*1024*1024*1024);
case "MB":
return Math.ceil(size*1024*1024);
case "KB":
return Math.ceil(size*1024*1024);
default:
if (throwUnknownUnit) {
throw new Error("Unknown unit provided");
}
return Math.ceil(size);
}
}

+ 9
- 0
services/torrent-search/tsconfig.json View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"sourceMap": true, /* Generates corresponding '.map' file. */
"outDir": "./dist", /* Redirect output structure to the directory. */
"typeRoots": ["./src/typings"], /* List of folders to include type definitions from. */
"sourceRoot": "./src" /* Specify the location where debugger should locate TypeScript files instead of source locations. */
}
}

+ 1080
- 0
services/torrent-search/yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save