Spaces:
Running
Running
Upload 25 files
Browse files- Dockerfile +51 -0
- package.json +48 -0
- patches/[email protected] +57 -0
- pnpm-lock.yaml +0 -0
- src/addon/manifest.ts +116 -0
- src/addon/server.ts +23 -0
- src/addon/streams.ts +247 -0
- src/index.ts +17 -0
- src/router.ts +112 -0
- src/torrent/eztv.ts +91 -0
- src/torrent/insane.ts +154 -0
- src/torrent/itorrent.ts +130 -0
- src/torrent/jackett.ts +40 -0
- src/torrent/ncore.ts +146 -0
- src/torrent/search.ts +176 -0
- src/torrent/webtorrent.ts +214 -0
- src/torrent/yts.ts +58 -0
- src/utils/dotenv.ts +3 -0
- src/utils/file.ts +47 -0
- src/utils/https.ts +45 -0
- src/utils/imdb.ts +37 -0
- src/utils/language.ts +16 -0
- src/utils/quality.ts +83 -0
- src/utils/shows.ts +56 -0
- tsconfig.json +12 -0
Dockerfile
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
ARG NODE_VERSION=20.11.1
|
2 |
+
ARG PNPM_VERSION=8.15.4
|
3 |
+
ARG TS_VERSION=5.3.3
|
4 |
+
|
5 |
+
# Builder stage
|
6 |
+
FROM node:${NODE_VERSION} as build
|
7 |
+
|
8 |
+
WORKDIR /usr/src/app
|
9 |
+
|
10 |
+
RUN npm install -g typescript@${TS_VERSION}
|
11 |
+
RUN --mount=type=cache,target=/root/.npm \
|
12 |
+
npm install -g pnpm@${PNPM_VERSION}
|
13 |
+
RUN --mount=type=bind,source=package.json,target=package.json \
|
14 |
+
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
15 |
+
--mount=type=bind,source=patches,target=patches \
|
16 |
+
--mount=type=cache,target=/root/.local/share/pnpm/store \
|
17 |
+
pnpm install --frozen-lockfile
|
18 |
+
|
19 |
+
COPY . .
|
20 |
+
|
21 |
+
RUN pnpm run build
|
22 |
+
RUN pnpm prune --prod
|
23 |
+
|
24 |
+
# Runner stage
|
25 |
+
FROM node:${NODE_VERSION}-slim as final
|
26 |
+
|
27 |
+
COPY package.json .
|
28 |
+
COPY --from=build /usr/src/app/node_modules ./node_modules
|
29 |
+
COPY --from=build /usr/src/app/dist ./dist
|
30 |
+
|
31 |
+
ENV NODE_ENV production
|
32 |
+
ENV HTTPS_METHOD local-ip.medicmobile.org
|
33 |
+
ENV DOWNLOAD_DIR /data
|
34 |
+
ENV KEEP_DOWNLOADED_FILES false
|
35 |
+
ENV MAX_CONNS_PER_TORRENT 50
|
36 |
+
ENV DOWNLOAD_SPEED_LIMIT 20971520
|
37 |
+
ENV UPLOAD_SPEED_LIMIT 1048576
|
38 |
+
ENV SEED_TIME 60000
|
39 |
+
ENV TORRENT_TIMEOUT 5000
|
40 |
+
|
41 |
+
VOLUME /data
|
42 |
+
|
43 |
+
RUN mkdir -p /data
|
44 |
+
RUN chown -R node /data
|
45 |
+
|
46 |
+
USER node
|
47 |
+
|
48 |
+
EXPOSE 58827
|
49 |
+
EXPOSE 58828
|
50 |
+
|
51 |
+
CMD npm start
|
package.json
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "stremio-torrent-stream",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"description": "",
|
5 |
+
"main": "index.js",
|
6 |
+
"type": "module",
|
7 |
+
"scripts": {
|
8 |
+
"build": "tsc",
|
9 |
+
"start": "node --no-warnings dist/index.js",
|
10 |
+
"dev": "esrun --node-no-warnings --watch src/index.ts"
|
11 |
+
},
|
12 |
+
"keywords": [],
|
13 |
+
"author": "",
|
14 |
+
"license": "ISC",
|
15 |
+
"devDependencies": {
|
16 |
+
"@types/bytes": "^3.1.4",
|
17 |
+
"@types/express": "^4.17.21",
|
18 |
+
"@types/fs-extra": "^11.0.4",
|
19 |
+
"@types/localtunnel": "^2.0.4",
|
20 |
+
"@types/node": "^20.11.20",
|
21 |
+
"@types/stremio-addon-sdk": "^1.6.11",
|
22 |
+
"@types/tough-cookie": "^4.0.5",
|
23 |
+
"@types/webtorrent": "~0.109.8",
|
24 |
+
"esrun": "^3.2.26"
|
25 |
+
},
|
26 |
+
"dependencies": {
|
27 |
+
"axios": "^1.6.5",
|
28 |
+
"axios-cookiejar-support": "^4.0.7",
|
29 |
+
"cheerio": "1.0.0-rc.12",
|
30 |
+
"dotenv": "^16.4.5",
|
31 |
+
"express": "^4.19.2",
|
32 |
+
"eztv-crawler": "^1.3.6",
|
33 |
+
"fs-extra": "^11.2.0",
|
34 |
+
"localtunnel": "^2.0.2",
|
35 |
+
"memory-chunk-store": "^1.3.5",
|
36 |
+
"mime": "^4.0.1",
|
37 |
+
"stremio-addon-sdk": "^1.6.10",
|
38 |
+
"tough-cookie": "^4.1.3",
|
39 |
+
"ts-jackett-api": "^1.0.0",
|
40 |
+
"webtorrent": "^2.4.2",
|
41 |
+
"yts-api-node": "^1.1.3"
|
42 |
+
},
|
43 |
+
"pnpm": {
|
44 |
+
"patchedDependencies": {
|
45 |
+
"[email protected]": "patches/[email protected]"
|
46 |
+
}
|
47 |
+
}
|
48 |
+
}
|
patches/[email protected]
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
diff --git a/src/builder.js b/src/builder.js
|
2 |
+
index 55f32d13d7f610aac4609a7e62dffc916794fb90..f74567b316cdd8a7e212d5231db86e97e64e1314 100644
|
3 |
+
--- a/src/builder.js
|
4 |
+
+++ b/src/builder.js
|
5 |
+
@@ -79,7 +79,7 @@ function AddonBuilder(manifest) {
|
6 |
+
|
7 |
+
function AddonInterface(manifest, handlers) {
|
8 |
+
this.manifest = Object.freeze(Object.assign({}, manifest))
|
9 |
+
- this.get = (resource, type, id, extra = {}, config = {}) => {
|
10 |
+
+ this.get = (resource, type, id, extra = {}, config = {}, req) => {
|
11 |
+
const handler = handlers[resource]
|
12 |
+
if (!handler) {
|
13 |
+
return Promise.reject({
|
14 |
+
@@ -87,7 +87,7 @@ function AddonInterface(manifest, handlers) {
|
15 |
+
noHandler: true
|
16 |
+
})
|
17 |
+
}
|
18 |
+
- return handler({ type, id, extra, config })
|
19 |
+
+ return handler({ type, id, extra, config, req })
|
20 |
+
}
|
21 |
+
return this
|
22 |
+
}
|
23 |
+
diff --git a/src/getRouter.js b/src/getRouter.js
|
24 |
+
index 4c6269d65c851203811dab877cd8543238653378..c18f2f1c5f32703aa3ae22ea4388174b2c485337 100644
|
25 |
+
--- a/src/getRouter.js
|
26 |
+
+++ b/src/getRouter.js
|
27 |
+
@@ -61,7 +61,7 @@ function getRouter({ manifest , get }) {
|
28 |
+
}
|
29 |
+
}
|
30 |
+
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
31 |
+
- get(resource, type, id, extra, config)
|
32 |
+
+ get(resource, type, id, extra, config, req)
|
33 |
+
.then(resp => {
|
34 |
+
|
35 |
+
let cacheHeaders = {
|
36 |
+
diff --git a/src/serveHTTP.js b/src/serveHTTP.js
|
37 |
+
index db94ae3ef5874162bf4573ffa6372901c636a483..9a99fe597ebf50b2167bee11772af52952bce6bd 100644
|
38 |
+
--- a/src/serveHTTP.js
|
39 |
+
+++ b/src/serveHTTP.js
|
40 |
+
@@ -53,7 +53,7 @@ function serveHTTP(addonInterface, opts = {}) {
|
41 |
+
return new Promise(function(resolve, reject) {
|
42 |
+
server.on('listening', function() {
|
43 |
+
const url = `http://127.0.0.1:${server.address().port}/manifest.json`
|
44 |
+
- console.log('HTTP addon accessible at:', url)
|
45 |
+
+ console.log(`HTTP addon listening on port ${server.address().port}`)
|
46 |
+
if (process.argv.includes('--launch')) {
|
47 |
+
const base = 'https://staging.strem.io#'
|
48 |
+
//const base = 'https://app.strem.io/shell-v4.4#'
|
49 |
+
@@ -63,7 +63,7 @@ function serveHTTP(addonInterface, opts = {}) {
|
50 |
+
if (process.argv.includes('--install')) {
|
51 |
+
opn(url.replace('http://', 'stremio://'))
|
52 |
+
}
|
53 |
+
- resolve({ url, server })
|
54 |
+
+ resolve({ url, server, app })
|
55 |
+
})
|
56 |
+
server.on('error', reject)
|
57 |
+
})
|
pnpm-lock.yaml
ADDED
The diff for this file is too large to render.
See raw diff
|
|
src/addon/manifest.ts
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Manifest } from "stremio-addon-sdk";
|
2 |
+
|
3 |
+
export const manifest: Manifest = {
|
4 |
+
id: "community.torrent-stream",
|
5 |
+
version: "1.0.0",
|
6 |
+
catalogs: [],
|
7 |
+
resources: ["stream"],
|
8 |
+
types: ["movie", "series"],
|
9 |
+
name: "Torrent Stream",
|
10 |
+
logo: "https://upload.wikimedia.org/wikipedia/en/7/79/WebTorrent_logo.png",
|
11 |
+
background:
|
12 |
+
"https://i.etsystatic.com/35367581/r/il/53bf97/4463935832/il_fullxfull.4463935832_3k3g.jpg",
|
13 |
+
description:
|
14 |
+
"This addon enables Stremio to stream movies and shows from torrents",
|
15 |
+
idPrefixes: ["tt"],
|
16 |
+
behaviorHints: {
|
17 |
+
// @ts-ignore
|
18 |
+
configurable: true,
|
19 |
+
configurationRequired: true,
|
20 |
+
},
|
21 |
+
config: [
|
22 |
+
{
|
23 |
+
title: "Enable Jackett search",
|
24 |
+
key: "enableJackett",
|
25 |
+
type: "checkbox",
|
26 |
+
},
|
27 |
+
{
|
28 |
+
title: "Jackett API URL",
|
29 |
+
key: "jackettUrl",
|
30 |
+
type: "text",
|
31 |
+
},
|
32 |
+
{
|
33 |
+
title: "Jackett API Key",
|
34 |
+
key: "jackettKey",
|
35 |
+
type: "password",
|
36 |
+
},
|
37 |
+
{
|
38 |
+
title: "Enable nCore search",
|
39 |
+
key: "enableNcore",
|
40 |
+
type: "checkbox",
|
41 |
+
default: "checked",
|
42 |
+
},
|
43 |
+
{
|
44 |
+
title: "nCore username",
|
45 |
+
key: "nCoreUser",
|
46 |
+
type: "text",
|
47 |
+
},
|
48 |
+
{
|
49 |
+
title: "nCore password",
|
50 |
+
key: "nCorePassword",
|
51 |
+
type: "password",
|
52 |
+
},
|
53 |
+
{
|
54 |
+
title: "Enable iNSANE search",
|
55 |
+
key: "enableInsane",
|
56 |
+
type: "checkbox",
|
57 |
+
},
|
58 |
+
{
|
59 |
+
title: "iNSANE username",
|
60 |
+
key: "insaneUser",
|
61 |
+
type: "text",
|
62 |
+
},
|
63 |
+
{
|
64 |
+
title: "iNSANE password",
|
65 |
+
key: "insanePassword",
|
66 |
+
type: "password",
|
67 |
+
},
|
68 |
+
{
|
69 |
+
title: "Enable iTorrent search",
|
70 |
+
key: "enableItorrent",
|
71 |
+
type: "checkbox",
|
72 |
+
},
|
73 |
+
{
|
74 |
+
title: "Enable YTS search",
|
75 |
+
key: "enableYts",
|
76 |
+
type: "checkbox",
|
77 |
+
},
|
78 |
+
{
|
79 |
+
title: "Enable EZTV search",
|
80 |
+
key: "enableEztv",
|
81 |
+
type: "checkbox",
|
82 |
+
},
|
83 |
+
{
|
84 |
+
title: "Use titles for torrent search",
|
85 |
+
key: "searchByTitle",
|
86 |
+
type: "checkbox",
|
87 |
+
},
|
88 |
+
{
|
89 |
+
title: "Do not show HDR results",
|
90 |
+
key: "disableHdr",
|
91 |
+
type: "checkbox",
|
92 |
+
},
|
93 |
+
{
|
94 |
+
title: "Do not show HEVC results",
|
95 |
+
key: "disableHevc",
|
96 |
+
type: "checkbox",
|
97 |
+
},
|
98 |
+
{
|
99 |
+
title: "Do not show 4K results",
|
100 |
+
key: "disable4k",
|
101 |
+
type: "checkbox",
|
102 |
+
},
|
103 |
+
{
|
104 |
+
title: "Do not show CAM results",
|
105 |
+
key: "disableCam",
|
106 |
+
type: "checkbox",
|
107 |
+
default: "checked",
|
108 |
+
},
|
109 |
+
{
|
110 |
+
title: "Do not show 3D results",
|
111 |
+
key: "disable3d",
|
112 |
+
type: "checkbox",
|
113 |
+
default: "checked",
|
114 |
+
},
|
115 |
+
],
|
116 |
+
};
|
src/addon/server.ts
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Application } from "express";
|
2 |
+
import stremio from "stremio-addon-sdk";
|
3 |
+
import { streamHandler } from "./streams.js";
|
4 |
+
import { manifest } from "./manifest.js";
|
5 |
+
import { Server } from "http";
|
6 |
+
|
7 |
+
export const serveHTTP = async (port: number) => {
|
8 |
+
const builder = new stremio.addonBuilder(manifest);
|
9 |
+
|
10 |
+
// @ts-ignore
|
11 |
+
builder.defineStreamHandler(streamHandler);
|
12 |
+
const addonInterface = builder.getInterface();
|
13 |
+
|
14 |
+
// @ts-ignore
|
15 |
+
const {
|
16 |
+
url,
|
17 |
+
server,
|
18 |
+
app,
|
19 |
+
}: { url: string; server: Server; app: Application } =
|
20 |
+
await stremio.serveHTTP(addonInterface, { port });
|
21 |
+
|
22 |
+
return app;
|
23 |
+
};
|
src/addon/streams.ts
ADDED
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Request } from "express";
|
2 |
+
import Stremio from "stremio-addon-sdk";
|
3 |
+
import {
|
4 |
+
TorrentCategory,
|
5 |
+
TorrentSearchResult,
|
6 |
+
TorrentSource,
|
7 |
+
searchTorrents,
|
8 |
+
} from "../torrent/search.js";
|
9 |
+
import { getTorrentInfo } from "../torrent/webtorrent.js";
|
10 |
+
import { getReadableSize, isSubtitleFile, isVideoFile } from "../utils/file.js";
|
11 |
+
import { getTitles } from "../utils/imdb.js";
|
12 |
+
import { guessLanguage } from "../utils/language.js";
|
13 |
+
import { guessQuality } from "../utils/quality.js";
|
14 |
+
import { isFileNameMatch, isTorrentNameMatch } from "../utils/shows.js";
|
15 |
+
|
16 |
+
interface HandlerArgs {
|
17 |
+
type: string;
|
18 |
+
id: string;
|
19 |
+
config?: {
|
20 |
+
enableJackett: string;
|
21 |
+
jackettUrl: string;
|
22 |
+
jackettKey: string;
|
23 |
+
enableNcore: string;
|
24 |
+
nCoreUser: string;
|
25 |
+
nCorePassword: string;
|
26 |
+
enableInsane: string;
|
27 |
+
insaneUser: string;
|
28 |
+
insanePassword: string;
|
29 |
+
enableItorrent: string;
|
30 |
+
enableYts: string;
|
31 |
+
enableEztv: string;
|
32 |
+
searchByTitle: string;
|
33 |
+
disableHdr: string;
|
34 |
+
disableHevc: string;
|
35 |
+
disable4k: string;
|
36 |
+
disableCam: string;
|
37 |
+
disable3d: string;
|
38 |
+
};
|
39 |
+
req: Request;
|
40 |
+
}
|
41 |
+
|
42 |
+
export const streamHandler = async ({ type, id, config, req }: HandlerArgs) => {
|
43 |
+
let torrents: TorrentSearchResult[] = [];
|
44 |
+
|
45 |
+
const categories: TorrentCategory[] = [];
|
46 |
+
if (type === "movie") categories.push("movie");
|
47 |
+
if (type === "series") categories.push("show");
|
48 |
+
|
49 |
+
const sources: TorrentSource[] = [];
|
50 |
+
if (config.enableJackett === "on") sources.push("jackett");
|
51 |
+
if (config.enableNcore === "on") sources.push("ncore");
|
52 |
+
if (config.enableInsane === "on") sources.push("insane");
|
53 |
+
if (config.enableItorrent === "on") sources.push("itorrent");
|
54 |
+
if (config.enableYts === "on") sources.push("yts");
|
55 |
+
if (config.enableEztv === "on") sources.push("eztv");
|
56 |
+
|
57 |
+
const [imdbId, season, episode] = id.split(":");
|
58 |
+
|
59 |
+
const queries = [imdbId];
|
60 |
+
if (config.searchByTitle === "on") queries.push(...(await getTitles(imdbId)));
|
61 |
+
|
62 |
+
torrents = (
|
63 |
+
await Promise.all(
|
64 |
+
queries.map((query) =>
|
65 |
+
searchTorrents(query, {
|
66 |
+
categories,
|
67 |
+
sources,
|
68 |
+
jackett: {
|
69 |
+
url: config.jackettUrl,
|
70 |
+
apiKey: config.jackettKey,
|
71 |
+
},
|
72 |
+
ncore: {
|
73 |
+
user: config.nCoreUser,
|
74 |
+
password: config.nCorePassword,
|
75 |
+
},
|
76 |
+
insane: {
|
77 |
+
user: config.insaneUser,
|
78 |
+
password: config.insanePassword,
|
79 |
+
},
|
80 |
+
})
|
81 |
+
)
|
82 |
+
)
|
83 |
+
).flat();
|
84 |
+
|
85 |
+
torrents = dedupeTorrents(torrents);
|
86 |
+
|
87 |
+
torrents = torrents.filter((torrent) => {
|
88 |
+
if (!torrent.seeds) return false;
|
89 |
+
if (torrent.category?.includes("DVD")) return false;
|
90 |
+
if (!isAllowedFormat(config, torrent.name)) return false;
|
91 |
+
if (!isAllowedQuality(config, guessQuality(torrent.name).quality))
|
92 |
+
return false;
|
93 |
+
|
94 |
+
if (
|
95 |
+
season &&
|
96 |
+
episode &&
|
97 |
+
!isTorrentNameMatch(torrent.name, Number(season), Number(episode))
|
98 |
+
)
|
99 |
+
return false;
|
100 |
+
|
101 |
+
return true;
|
102 |
+
});
|
103 |
+
|
104 |
+
let streams = (
|
105 |
+
await Promise.all(
|
106 |
+
torrents.map((torrent) =>
|
107 |
+
getStreamsFromTorrent(req, torrent, season, episode)
|
108 |
+
)
|
109 |
+
)
|
110 |
+
).flat();
|
111 |
+
|
112 |
+
streams = streams.filter((stream) => {
|
113 |
+
if (!isAllowedFormat(config, stream.fileName)) return false;
|
114 |
+
if (!isAllowedQuality(config, stream.quality)) return false;
|
115 |
+
return true;
|
116 |
+
});
|
117 |
+
|
118 |
+
streams.sort((a, b) => b.score - a.score);
|
119 |
+
|
120 |
+
return { streams: streams.map((stream) => stream.stream) };
|
121 |
+
};
|
122 |
+
|
123 |
+
const dedupeTorrents = (torrents: TorrentSearchResult[]) => {
|
124 |
+
const map = new Map(
|
125 |
+
torrents.map((torrent) => [`${torrent.tracker}:${torrent.name}`, torrent])
|
126 |
+
);
|
127 |
+
|
128 |
+
return [...map.values()];
|
129 |
+
};
|
130 |
+
|
131 |
+
export const getStreamsFromTorrent = async (
|
132 |
+
req: Request,
|
133 |
+
torrent: TorrentSearchResult,
|
134 |
+
season?: string,
|
135 |
+
episode?: string
|
136 |
+
): Promise<
|
137 |
+
{
|
138 |
+
stream: Stremio.Stream;
|
139 |
+
torrentName: string;
|
140 |
+
fileName: string;
|
141 |
+
quality: string;
|
142 |
+
score: number;
|
143 |
+
}[]
|
144 |
+
> => {
|
145 |
+
const uri = torrent.torrent || torrent.magnet;
|
146 |
+
if (!uri) return [];
|
147 |
+
|
148 |
+
const torrentInfo = await getTorrentInfo(uri);
|
149 |
+
if (!torrentInfo) return [];
|
150 |
+
|
151 |
+
let videos = torrentInfo.files.filter((file) => isVideoFile(file.name));
|
152 |
+
|
153 |
+
if (season && episode) {
|
154 |
+
videos = videos.filter((file) =>
|
155 |
+
isFileNameMatch(file.name, Number(season), Number(episode))
|
156 |
+
);
|
157 |
+
}
|
158 |
+
|
159 |
+
const videosSize = videos.reduce((acc, file) => acc + file.size, 0);
|
160 |
+
|
161 |
+
videos = videos.filter(
|
162 |
+
(file) => file.size > videosSize / (videos.length + 1)
|
163 |
+
);
|
164 |
+
|
165 |
+
const subs = torrentInfo.files.filter((file) => isSubtitleFile(file.name));
|
166 |
+
|
167 |
+
const torrentQuality = guessQuality(torrent.name);
|
168 |
+
const language = guessLanguage(torrent.name, torrent.category);
|
169 |
+
|
170 |
+
// @ts-ignore
|
171 |
+
return videos.map((file) => {
|
172 |
+
const fileQuality = guessQuality(file.name);
|
173 |
+
|
174 |
+
const { quality, score } =
|
175 |
+
fileQuality.score > torrentQuality.score ? fileQuality : torrentQuality;
|
176 |
+
|
177 |
+
const description = [
|
178 |
+
...(season && episode ? [torrent.name, file.name] : [torrent.name]),
|
179 |
+
[
|
180 |
+
`💾 ${getReadableSize(file.size)}`,
|
181 |
+
`⬆️ ${torrent.seeds}`,
|
182 |
+
`⬇️ ${torrent.peers}`,
|
183 |
+
].join(" "),
|
184 |
+
[`🔊 ${language}`, `⚙️ ${torrent.tracker}`].join(" "),
|
185 |
+
].join("\n");
|
186 |
+
|
187 |
+
const streamEndpoint = `${req.protocol}://${req.get("host")}/stream`;
|
188 |
+
|
189 |
+
const url = [
|
190 |
+
streamEndpoint,
|
191 |
+
encodeURIComponent(uri),
|
192 |
+
encodeURIComponent(file.path),
|
193 |
+
].join("/");
|
194 |
+
|
195 |
+
const subtitles = subs.map((sub, index) => ({
|
196 |
+
id: index.toString(),
|
197 |
+
url: [
|
198 |
+
streamEndpoint,
|
199 |
+
encodeURIComponent(uri),
|
200 |
+
encodeURIComponent(sub.path),
|
201 |
+
].join("/"),
|
202 |
+
lang: sub.name,
|
203 |
+
}));
|
204 |
+
|
205 |
+
return {
|
206 |
+
stream: {
|
207 |
+
name: quality,
|
208 |
+
description,
|
209 |
+
url,
|
210 |
+
subtitles,
|
211 |
+
behaviorHints: {
|
212 |
+
bingeGroup: torrent.name,
|
213 |
+
},
|
214 |
+
},
|
215 |
+
torrentName: torrent.name,
|
216 |
+
fileName: file.name,
|
217 |
+
quality,
|
218 |
+
score,
|
219 |
+
};
|
220 |
+
});
|
221 |
+
};
|
222 |
+
|
223 |
+
const isAllowedQuality = (config: HandlerArgs["config"], quality: string) => {
|
224 |
+
if (config?.disable4k === "on" && quality.includes("4K")) return false;
|
225 |
+
|
226 |
+
if (config?.disableCam === "on" && quality.includes("CAM")) return false;
|
227 |
+
|
228 |
+
if (
|
229 |
+
config?.disableHdr === "on" &&
|
230 |
+
(quality.includes("HDR") || quality.includes("Dolby Vision"))
|
231 |
+
)
|
232 |
+
return false;
|
233 |
+
|
234 |
+
if (config?.disable3d === "on" && quality.includes("3D")) return false;
|
235 |
+
|
236 |
+
return true;
|
237 |
+
};
|
238 |
+
|
239 |
+
const isAllowedFormat = (config: HandlerArgs["config"], name: string) => {
|
240 |
+
if (config?.disableHevc === "on") {
|
241 |
+
const str = name.replace(/\W/g, "").toLowerCase();
|
242 |
+
if (str.includes("x265") || str.includes("h265") || str.includes("hevc"))
|
243 |
+
return false;
|
244 |
+
}
|
245 |
+
|
246 |
+
return true;
|
247 |
+
};
|
src/index.ts
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import "./utils/dotenv.js";
|
2 |
+
|
3 |
+
import express from "express";
|
4 |
+
import { serveHTTP } from "./addon/server.js";
|
5 |
+
import { router } from "./router.js";
|
6 |
+
import { serveHTTPS } from "./utils/https.js";
|
7 |
+
|
8 |
+
const PORT = Number(process.env.PORT) || 58827;
|
9 |
+
const HTTPS_PORT = Number(process.env.HTTPS_PORT) || 58828;
|
10 |
+
|
11 |
+
const main = async () => {
|
12 |
+
const app = await serveHTTP(PORT);
|
13 |
+
app.use(express.json()).use(router);
|
14 |
+
await serveHTTPS(app, HTTPS_PORT);
|
15 |
+
};
|
16 |
+
|
17 |
+
main();
|
src/router.ts
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Router } from "express";
|
2 |
+
import { searchTorrents } from "./torrent/search.js";
|
3 |
+
import {
|
4 |
+
getFile,
|
5 |
+
getOrAddTorrent,
|
6 |
+
getStats,
|
7 |
+
getTorrentInfo,
|
8 |
+
streamClosed,
|
9 |
+
streamOpened,
|
10 |
+
} from "./torrent/webtorrent.js";
|
11 |
+
import { getStreamingMimeType } from "./utils/file.js";
|
12 |
+
|
13 |
+
export const router = Router();
|
14 |
+
|
15 |
+
router.get("/stats", (req, res) => {
|
16 |
+
const stats = getStats();
|
17 |
+
res.json(stats);
|
18 |
+
});
|
19 |
+
|
20 |
+
router.get("/torrents/:query", async (req, res) => {
|
21 |
+
const { query } = req.params;
|
22 |
+
const torrents = await searchTorrents(query);
|
23 |
+
res.json(torrents);
|
24 |
+
});
|
25 |
+
|
26 |
+
router.post("/torrents/:query", async (req, res) => {
|
27 |
+
const { query } = req.params;
|
28 |
+
const options = req.body;
|
29 |
+
const torrents = await searchTorrents(query, options);
|
30 |
+
res.json(torrents);
|
31 |
+
});
|
32 |
+
|
33 |
+
router.get("/torrent/:torrentUri", async (req, res) => {
|
34 |
+
const { torrentUri } = req.params;
|
35 |
+
|
36 |
+
const torrent = await getTorrentInfo(torrentUri);
|
37 |
+
if (!torrent) return res.status(500).send("Failed to get torrent");
|
38 |
+
|
39 |
+
torrent.files.forEach((file) => {
|
40 |
+
file.url = [
|
41 |
+
`${req.protocol}://${req.get("host")}`,
|
42 |
+
"stream",
|
43 |
+
encodeURIComponent(torrentUri),
|
44 |
+
encodeURIComponent(file.path),
|
45 |
+
].join("/");
|
46 |
+
});
|
47 |
+
|
48 |
+
res.json(torrent);
|
49 |
+
});
|
50 |
+
|
51 |
+
router.get("/stream/:torrentUri/:filePath", async (req, res) => {
|
52 |
+
const { torrentUri, filePath } = req.params;
|
53 |
+
|
54 |
+
const torrent = await getOrAddTorrent(torrentUri);
|
55 |
+
if (!torrent) return res.status(500).send("Failed to add torrent");
|
56 |
+
|
57 |
+
const file = getFile(torrent, filePath);
|
58 |
+
if (!file) return res.status(404).send("File not found");
|
59 |
+
|
60 |
+
const { range } = req.headers;
|
61 |
+
const positions = (range || "").replace(/bytes=/, "").split("-");
|
62 |
+
const start = Number(positions[0]);
|
63 |
+
const end = Number(positions[1]) || file.length - 1;
|
64 |
+
|
65 |
+
if (start >= file.length || end >= file.length) {
|
66 |
+
res.writeHead(416, {
|
67 |
+
"Content-Range": `bytes */${file.length}`,
|
68 |
+
});
|
69 |
+
return res.end();
|
70 |
+
}
|
71 |
+
|
72 |
+
const headers = {
|
73 |
+
"Content-Range": `bytes ${start}-${end}/${file.length}`,
|
74 |
+
"Accept-Ranges": "bytes",
|
75 |
+
"Content-Length": end - start + 1,
|
76 |
+
"Content-Type": getStreamingMimeType(file.name),
|
77 |
+
};
|
78 |
+
|
79 |
+
res.writeHead(206, headers);
|
80 |
+
|
81 |
+
try {
|
82 |
+
const noDataTimeout = setTimeout(() => {
|
83 |
+
res.status(500).end();
|
84 |
+
}, 10000);
|
85 |
+
|
86 |
+
const noReadTimeout = setTimeout(() => {
|
87 |
+
res.status(200).end();
|
88 |
+
}, 60000);
|
89 |
+
|
90 |
+
const videoStream = file.createReadStream({ start, end });
|
91 |
+
|
92 |
+
videoStream.on("data", () => {
|
93 |
+
clearTimeout(noDataTimeout);
|
94 |
+
});
|
95 |
+
|
96 |
+
videoStream.on("readable", () => {
|
97 |
+
noReadTimeout.refresh();
|
98 |
+
});
|
99 |
+
|
100 |
+
videoStream.on("error", (error) => {});
|
101 |
+
|
102 |
+
videoStream.pipe(res);
|
103 |
+
|
104 |
+
streamOpened(torrent.infoHash, file.name);
|
105 |
+
|
106 |
+
res.on("close", () => {
|
107 |
+
streamClosed(torrent.infoHash, file.name);
|
108 |
+
});
|
109 |
+
} catch (error) {
|
110 |
+
res.status(500).end();
|
111 |
+
}
|
112 |
+
});
|
src/torrent/eztv.ts
ADDED
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from "axios";
|
2 |
+
import * as cheerio from "cheerio";
|
3 |
+
import { getTorrentsByImdbId } from "eztv-crawler";
|
4 |
+
import { TorrentSearchResult } from "./search.js";
|
5 |
+
import { isImdbId } from "../utils/imdb.js";
|
6 |
+
|
7 |
+
export const searchEztv = async (
|
8 |
+
searchQuery: string
|
9 |
+
): Promise<TorrentSearchResult[]> => {
|
10 |
+
try {
|
11 |
+
if (isImdbId(searchQuery)) {
|
12 |
+
const res = await getTorrentsByImdbId(searchQuery);
|
13 |
+
|
14 |
+
return res.torrents.map((torrent) => ({
|
15 |
+
name: torrent.title.replace("EZTV", "").trim(),
|
16 |
+
tracker: "EZTV",
|
17 |
+
category: parseCategory(torrent.title),
|
18 |
+
size: Number(torrent.size_bytes),
|
19 |
+
seeds: torrent.seeds,
|
20 |
+
peers: torrent.peers,
|
21 |
+
torrent: torrent.torrent_url,
|
22 |
+
magnet: torrent.magnet_url,
|
23 |
+
}));
|
24 |
+
} else {
|
25 |
+
const formData = new FormData();
|
26 |
+
formData.append("layout", "def_wlinks");
|
27 |
+
|
28 |
+
const eztvPage = await axios.post(
|
29 |
+
`https://eztv.wf/search/${encodeURIComponent(searchQuery)}`,
|
30 |
+
formData
|
31 |
+
);
|
32 |
+
const $ = cheerio.load(eztvPage.data);
|
33 |
+
|
34 |
+
const results = $('[name="hover"]').toArray();
|
35 |
+
|
36 |
+
return results.map((res) => {
|
37 |
+
const title = $(res).find("td:nth-child(2)").text()?.replace(/\n/g, "");
|
38 |
+
const size = $(res).find("td:nth-child(4)").text();
|
39 |
+
const seeds = $(res).find("td:nth-child(6)").text();
|
40 |
+
|
41 |
+
const torrent = $(res)
|
42 |
+
.find("td:nth-child(3) .download_1")
|
43 |
+
.attr("href")
|
44 |
+
?.replace(/\n/g, "");
|
45 |
+
|
46 |
+
const magnet = $(res)
|
47 |
+
.find("td:nth-child(3) .magnet")
|
48 |
+
.attr("href")
|
49 |
+
?.replace(/\n/g, "");
|
50 |
+
|
51 |
+
return {
|
52 |
+
name: title.replace("[eztv]", "").trim(),
|
53 |
+
tracker: "EZTV",
|
54 |
+
category: parseCategory(title),
|
55 |
+
size: parseSize(size),
|
56 |
+
seeds: Number(seeds) || 0,
|
57 |
+
peers: 0,
|
58 |
+
torrent,
|
59 |
+
magnet,
|
60 |
+
};
|
61 |
+
});
|
62 |
+
}
|
63 |
+
} catch (error) {
|
64 |
+
return [];
|
65 |
+
}
|
66 |
+
};
|
67 |
+
|
68 |
+
const parseCategory = (title: string) => {
|
69 |
+
let quality = "SD";
|
70 |
+
if (title.includes("720p")) quality = "720p";
|
71 |
+
if (title.includes("1080p")) quality = "1080p";
|
72 |
+
if (title.includes("2160p")) quality = "2160p";
|
73 |
+
return `TV/${quality}`;
|
74 |
+
};
|
75 |
+
|
76 |
+
const parseSize = (size: string) => {
|
77 |
+
const units: Record<string, number> = {
|
78 |
+
TB: 1024 ** 4,
|
79 |
+
GB: 1024 ** 3,
|
80 |
+
MB: 1024 ** 2,
|
81 |
+
KB: 1024,
|
82 |
+
B: 1,
|
83 |
+
};
|
84 |
+
|
85 |
+
const [sizeStr, unit] = size.split(" ");
|
86 |
+
const sizeNum = Number(sizeStr);
|
87 |
+
|
88 |
+
if (!sizeNum || !units[unit]) return 0;
|
89 |
+
|
90 |
+
return Math.ceil(sizeNum * units[unit]);
|
91 |
+
};
|
src/torrent/insane.ts
ADDED
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from "axios";
|
2 |
+
import { wrapper } from "axios-cookiejar-support";
|
3 |
+
import * as cheerio from "cheerio";
|
4 |
+
import { CookieJar } from "tough-cookie";
|
5 |
+
import { TorrentSearchResult } from "./search.js";
|
6 |
+
|
7 |
+
const INSANE_USER = process.env.INSANE_USER;
|
8 |
+
const INSANE_PASSWORD = process.env.INSANE_PASSWORD;
|
9 |
+
|
10 |
+
const USER_AGENT =
|
11 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0";
|
12 |
+
|
13 |
+
export enum InsaneCategory {
|
14 |
+
Film_Hun_SD = 41,
|
15 |
+
Film_Hun_HD = 27,
|
16 |
+
Film_Hun_UHD = 44,
|
17 |
+
Film_Eng_SD = 42,
|
18 |
+
Film_Eng_HD = 25,
|
19 |
+
Film_Eng_UHD = 45,
|
20 |
+
Sorozat_Hun = 8,
|
21 |
+
Sorozat_Hun_HD = 40,
|
22 |
+
Sorozat_Hun_UHD = 47,
|
23 |
+
Sorozat_Eng = 7,
|
24 |
+
Sorozat_Eng_HD = 39,
|
25 |
+
Sorozat_Eng_UHD = 46,
|
26 |
+
}
|
27 |
+
|
28 |
+
export const searchInsane = async (
|
29 |
+
searchQuery: string,
|
30 |
+
categories: InsaneCategory[],
|
31 |
+
insaneUser?: string,
|
32 |
+
insanePassword?: string
|
33 |
+
): Promise<TorrentSearchResult[]> => {
|
34 |
+
try {
|
35 |
+
const user = insaneUser || INSANE_USER;
|
36 |
+
const password = insanePassword || INSANE_PASSWORD;
|
37 |
+
|
38 |
+
if (!user || !password) return [];
|
39 |
+
|
40 |
+
const jar = new CookieJar();
|
41 |
+
|
42 |
+
const client = wrapper(
|
43 |
+
// @ts-ignore
|
44 |
+
axios.create({
|
45 |
+
// @ts-ignore
|
46 |
+
jar,
|
47 |
+
baseURL: "https://newinsane.info",
|
48 |
+
headers: { "User-Agent": USER_AGENT },
|
49 |
+
})
|
50 |
+
);
|
51 |
+
|
52 |
+
const formData = new FormData();
|
53 |
+
formData.append("username", user);
|
54 |
+
formData.append("password", password);
|
55 |
+
await client.post("/login.php", formData);
|
56 |
+
|
57 |
+
const torrents: TorrentSearchResult[] = [];
|
58 |
+
|
59 |
+
let page = 0;
|
60 |
+
|
61 |
+
while (page <= 5) {
|
62 |
+
try {
|
63 |
+
let torrentsOnPage = 0;
|
64 |
+
|
65 |
+
let params = new URLSearchParams({
|
66 |
+
page: page.toString(),
|
67 |
+
search: searchQuery,
|
68 |
+
searchsort: "normal",
|
69 |
+
searchtype: "desc",
|
70 |
+
torart: "tor",
|
71 |
+
});
|
72 |
+
|
73 |
+
for (const category of categories) {
|
74 |
+
params.append("cat[]", category.toString());
|
75 |
+
}
|
76 |
+
|
77 |
+
const link = `/browse.php?${params.toString()}}`;
|
78 |
+
const torrentsPage = await client.get(link);
|
79 |
+
const $ = cheerio.load(torrentsPage.data);
|
80 |
+
|
81 |
+
for (const el of $("tr.torrentrow")) {
|
82 |
+
torrentsOnPage++;
|
83 |
+
|
84 |
+
const tracker = "iNSANE";
|
85 |
+
const name = $(el).find("a.torrentname").attr("title");
|
86 |
+
const category = parseCategory(
|
87 |
+
$(el).find("td.caticon > a > img").attr("title")
|
88 |
+
);
|
89 |
+
const size = parseSize($(el).find("td.size").text());
|
90 |
+
const seeds = Number($(el).find("td.data > a:nth-of-type(1)").text());
|
91 |
+
const peers = Number($(el).find("td.data > a:nth-of-type(2)").text());
|
92 |
+
const torrent = $(el).find("a.downloadicon").attr("href");
|
93 |
+
|
94 |
+
if (!name || !torrent) continue;
|
95 |
+
|
96 |
+
torrents.push({
|
97 |
+
name,
|
98 |
+
tracker,
|
99 |
+
category,
|
100 |
+
size,
|
101 |
+
seeds,
|
102 |
+
peers,
|
103 |
+
torrent,
|
104 |
+
});
|
105 |
+
}
|
106 |
+
|
107 |
+
if (torrentsOnPage < 25) break;
|
108 |
+
|
109 |
+
page++;
|
110 |
+
} catch {
|
111 |
+
continue;
|
112 |
+
}
|
113 |
+
}
|
114 |
+
|
115 |
+
return torrents;
|
116 |
+
} catch (error) {
|
117 |
+
return [];
|
118 |
+
}
|
119 |
+
};
|
120 |
+
|
121 |
+
const parseCategory = (category: string | undefined) => {
|
122 |
+
const categories: Record<string, string> = {
|
123 |
+
"Film/Hun/SD": "Movies/SD/HU",
|
124 |
+
"Film/Hun/HD": "Movies/HD/HU",
|
125 |
+
"Film/Hun/UHD": "Movies/UHD/HU",
|
126 |
+
"Film/Eng/SD": "Movies/SD/EN",
|
127 |
+
"Film/Eng/HD": "Movies/HD/EN",
|
128 |
+
"Film/Eng/UHD": "Movies/UHD/EN",
|
129 |
+
"Sorozat/Hun": "TV/SD/HU",
|
130 |
+
"Sorozat/Hun/HD": "TV/HD/HU",
|
131 |
+
"Sorozat/Hun/UHD": "TV/UHD/HU",
|
132 |
+
"Sorozat/Eng": "TV/SD/EN",
|
133 |
+
"Sorozat/Eng/HD": "TV/HD/EN",
|
134 |
+
"Sorozat/Eng/UHD": "TV/UHD/EN",
|
135 |
+
};
|
136 |
+
|
137 |
+
return categories[category as string];
|
138 |
+
};
|
139 |
+
|
140 |
+
const parseSize = (sizeStr: string) => {
|
141 |
+
const size = sizeStr.replace(",", ".").trim();
|
142 |
+
let bytes = 0;
|
143 |
+
if (size.endsWith("TiB"))
|
144 |
+
bytes = (Number(size.replace("TiB", "")) || 0) * 1024 ** 4;
|
145 |
+
else if (size.endsWith("GiB"))
|
146 |
+
bytes = (Number(size.replace("GiB", "")) || 0) * 1024 ** 3;
|
147 |
+
else if (size.endsWith("MiB"))
|
148 |
+
bytes = (Number(size.replace("MiB", "")) || 0) * 1024 ** 2;
|
149 |
+
else if (size.endsWith("KiB"))
|
150 |
+
bytes = (Number(size.replace("KiB", "")) || 0) * 1024;
|
151 |
+
else if (size.endsWith("B")) bytes = Number(size.replace("B", "")) || 0;
|
152 |
+
|
153 |
+
return Math.ceil(bytes);
|
154 |
+
};
|
src/torrent/itorrent.ts
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from "axios";
|
2 |
+
import * as cheerio from "cheerio";
|
3 |
+
import { TorrentSearchResult } from "./search.js";
|
4 |
+
|
5 |
+
export enum ItorrentCategory {
|
6 |
+
Film = 3,
|
7 |
+
Sorozat = 4,
|
8 |
+
}
|
9 |
+
|
10 |
+
export enum ItorrentQuality {
|
11 |
+
SD = "sd",
|
12 |
+
HD = "hd",
|
13 |
+
CAM = "cam",
|
14 |
+
}
|
15 |
+
|
16 |
+
export const searchItorrent = async (
|
17 |
+
searchQuery: string,
|
18 |
+
categories: ItorrentCategory[],
|
19 |
+
qualities: ItorrentQuality[]
|
20 |
+
): Promise<TorrentSearchResult[]> => {
|
21 |
+
const torrents: TorrentSearchResult[] = [];
|
22 |
+
const quality = qualities.join(",");
|
23 |
+
|
24 |
+
await Promise.all(
|
25 |
+
categories.map(async (category) => {
|
26 |
+
let page = 0;
|
27 |
+
|
28 |
+
while (page <= 5) {
|
29 |
+
try {
|
30 |
+
page++;
|
31 |
+
|
32 |
+
let torrentsOnPage = 0;
|
33 |
+
|
34 |
+
const link = `https://itorrent.ws/torrentek/category/${category}/title/${searchQuery}/qualities[]/${quality}/page/${page}/`;
|
35 |
+
const torrentsPage = await axios.get(link);
|
36 |
+
const $ = cheerio.load(torrentsPage.data);
|
37 |
+
|
38 |
+
await Promise.all(
|
39 |
+
[...$("tr.gradeX")].map(async (el) => {
|
40 |
+
torrentsOnPage++;
|
41 |
+
|
42 |
+
const tracker = "iTorrent";
|
43 |
+
const torrentAnchor = $(el).find("td.ellipse > a");
|
44 |
+
const torrentHref = torrentAnchor.attr("href");
|
45 |
+
const name = torrentAnchor.text().trim();
|
46 |
+
|
47 |
+
if (!torrentHref || !name) return;
|
48 |
+
|
49 |
+
const category = parseCategory($(el).find("i.zqf").attr("title"));
|
50 |
+
const size = parseSize(
|
51 |
+
$(el).find("td:nth-child(5)").text().trim()
|
52 |
+
);
|
53 |
+
const seeds = Number($(el).find("td:nth-child(7)").text());
|
54 |
+
const peers = Number($(el).find("td:nth-child(8)").text());
|
55 |
+
|
56 |
+
const torrentPageLink = `https://itorrent.ws${torrentHref}`;
|
57 |
+
|
58 |
+
let torrent: string | undefined;
|
59 |
+
let magnet: string | undefined;
|
60 |
+
|
61 |
+
try {
|
62 |
+
const torrentPage = await axios.get(torrentPageLink);
|
63 |
+
const $ = cheerio.load(torrentPage.data);
|
64 |
+
|
65 |
+
const torrentFileHref = $("a.btn-primary.seed-warning").attr(
|
66 |
+
"href"
|
67 |
+
);
|
68 |
+
|
69 |
+
if (torrentFileHref)
|
70 |
+
torrent = `https://itorrent.ws${torrentFileHref}`;
|
71 |
+
|
72 |
+
magnet = $("a.btn-success.seed-warning").attr("href");
|
73 |
+
|
74 |
+
if (!torrent && !magnet) return;
|
75 |
+
} catch {
|
76 |
+
return;
|
77 |
+
}
|
78 |
+
|
79 |
+
torrents.push({
|
80 |
+
name,
|
81 |
+
tracker,
|
82 |
+
category,
|
83 |
+
size,
|
84 |
+
seeds,
|
85 |
+
peers,
|
86 |
+
torrent,
|
87 |
+
magnet,
|
88 |
+
});
|
89 |
+
})
|
90 |
+
);
|
91 |
+
|
92 |
+
if (torrentsOnPage < 48) break;
|
93 |
+
} catch {
|
94 |
+
continue;
|
95 |
+
}
|
96 |
+
}
|
97 |
+
})
|
98 |
+
);
|
99 |
+
|
100 |
+
return torrents;
|
101 |
+
};
|
102 |
+
|
103 |
+
const parseCategory = (category: string | undefined) => {
|
104 |
+
const categories: Record<string, string> = {
|
105 |
+
"Film/HU/CAM": "Movies/CAM/HU",
|
106 |
+
"Film/HU/SD": "Movies/SD/HU",
|
107 |
+
"Film/HU/HD": "Movies/HD/HU",
|
108 |
+
"Sorozat/HU/SD": "TV/SD/HU",
|
109 |
+
"Sorozat/HU/HD": "TV/HD/HU",
|
110 |
+
};
|
111 |
+
|
112 |
+
return categories[category as string];
|
113 |
+
};
|
114 |
+
|
115 |
+
const parseSize = (size: string) => {
|
116 |
+
const units: Record<string, number> = {
|
117 |
+
TB: 1024 ** 4,
|
118 |
+
GB: 1024 ** 3,
|
119 |
+
MB: 1024 ** 2,
|
120 |
+
KB: 1024,
|
121 |
+
B: 1,
|
122 |
+
};
|
123 |
+
|
124 |
+
const [sizeStr, unit] = size.split(" ");
|
125 |
+
const sizeNum = Number(sizeStr);
|
126 |
+
|
127 |
+
if (!sizeNum || !units[unit]) return 0;
|
128 |
+
|
129 |
+
return Math.ceil(sizeNum * units[unit]);
|
130 |
+
};
|
src/torrent/jackett.ts
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { JackettApi } from "ts-jackett-api";
|
2 |
+
import { JackettCategory } from "ts-jackett-api/lib/types/JackettCategory.js";
|
3 |
+
import { TorrentSearchResult } from "./search.js";
|
4 |
+
|
5 |
+
const JACKETT_URL = process.env.JACKETT_URL;
|
6 |
+
const JACKETT_KEY = process.env.JACKETT_KEY;
|
7 |
+
|
8 |
+
export const searchJackett = async (
|
9 |
+
searchQuery: string,
|
10 |
+
categories: JackettCategory[],
|
11 |
+
jackettUrl?: string,
|
12 |
+
jackettKey?: string
|
13 |
+
): Promise<TorrentSearchResult[]> => {
|
14 |
+
try {
|
15 |
+
const url = jackettUrl || JACKETT_URL;
|
16 |
+
const key = jackettKey || JACKETT_KEY;
|
17 |
+
|
18 |
+
if (!url || !key) return [];
|
19 |
+
|
20 |
+
const client = new JackettApi(url, key);
|
21 |
+
|
22 |
+
const res = await client.search({
|
23 |
+
query: searchQuery,
|
24 |
+
category: categories,
|
25 |
+
});
|
26 |
+
|
27 |
+
return res.Results.map((result) => ({
|
28 |
+
name: result.Title,
|
29 |
+
tracker: result.Tracker,
|
30 |
+
category: result.CategoryDesc || undefined,
|
31 |
+
size: result.Size,
|
32 |
+
seeds: result.Seeders,
|
33 |
+
peers: result.Peers,
|
34 |
+
torrent: result.Link || undefined,
|
35 |
+
magnet: result.MagnetUri || undefined,
|
36 |
+
}));
|
37 |
+
} catch (error) {
|
38 |
+
return [];
|
39 |
+
}
|
40 |
+
};
|
src/torrent/ncore.ts
ADDED
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from "axios";
|
2 |
+
import { wrapper } from "axios-cookiejar-support";
|
3 |
+
import * as cheerio from "cheerio";
|
4 |
+
import { CookieJar } from "tough-cookie";
|
5 |
+
import { TorrentSearchResult } from "./search.js";
|
6 |
+
import { isImdbId } from "../utils/imdb.js";
|
7 |
+
|
8 |
+
const NCORE_USER = process.env.NCORE_USER;
|
9 |
+
const NCORE_PASSWORD = process.env.NCORE_PASSWORD;
|
10 |
+
|
11 |
+
export enum NcoreCategory {
|
12 |
+
Film_SD_HU = "xvid_hun",
|
13 |
+
Film_SD_EN = "xvid",
|
14 |
+
Film_HD_HU = "hd_hun",
|
15 |
+
Film_HD_EN = "hd",
|
16 |
+
Sorozat_SD_HU = "xvidser_hun",
|
17 |
+
Sorozat_SD_EN = "xvidser",
|
18 |
+
Sorozat_HD_HU = "hdser_hun",
|
19 |
+
Sorozat_HD_EN = "hdser",
|
20 |
+
}
|
21 |
+
|
22 |
+
export const searchNcore = async (
|
23 |
+
searchQuery: string,
|
24 |
+
categories: NcoreCategory[],
|
25 |
+
ncoreUser?: string,
|
26 |
+
ncorePassword?: string
|
27 |
+
): Promise<TorrentSearchResult[]> => {
|
28 |
+
try {
|
29 |
+
const user = ncoreUser || NCORE_USER;
|
30 |
+
const password = ncorePassword || NCORE_PASSWORD;
|
31 |
+
|
32 |
+
if (!user || !password) return [];
|
33 |
+
|
34 |
+
const jar = new CookieJar();
|
35 |
+
// @ts-ignore
|
36 |
+
const client = wrapper(axios.create({ jar, baseURL: "https://ncore.pro" }));
|
37 |
+
|
38 |
+
const formData = new FormData();
|
39 |
+
formData.append("nev", user);
|
40 |
+
formData.append("pass", password);
|
41 |
+
formData.append("set_lang", "hu");
|
42 |
+
formData.append("submitted", "1");
|
43 |
+
await client.post("/login.php", formData);
|
44 |
+
|
45 |
+
const torrents: TorrentSearchResult[] = [];
|
46 |
+
|
47 |
+
let page = 0;
|
48 |
+
|
49 |
+
while (page <= 5) {
|
50 |
+
try {
|
51 |
+
page++;
|
52 |
+
|
53 |
+
let torrentsOnPage = 0;
|
54 |
+
|
55 |
+
let params = new URLSearchParams({
|
56 |
+
oldal: page.toString(),
|
57 |
+
tipus: "kivalasztottak_kozott",
|
58 |
+
kivalasztott_tipus: categories.join(","),
|
59 |
+
mire: searchQuery,
|
60 |
+
miben: isImdbId(searchQuery) ? "imdb" : "name",
|
61 |
+
miszerint: "ctime",
|
62 |
+
hogyan: "DESC",
|
63 |
+
});
|
64 |
+
|
65 |
+
const link = `/torrents.php?${params.toString()}}`;
|
66 |
+
const torrentsPage = await client.get(link);
|
67 |
+
const $ = cheerio.load(torrentsPage.data);
|
68 |
+
|
69 |
+
const rssUrl = $("link[rel=alternate]").attr("href");
|
70 |
+
const downloadKey = rssUrl?.split("=")[1];
|
71 |
+
if (!downloadKey) return torrents;
|
72 |
+
|
73 |
+
for (const el of $("div.box_torrent")) {
|
74 |
+
torrentsOnPage++;
|
75 |
+
|
76 |
+
const name = $(el).find("div.torrent_txt > a").attr("title");
|
77 |
+
|
78 |
+
const categoryHref = $(el)
|
79 |
+
.find("a > img.categ_link")
|
80 |
+
.parent()
|
81 |
+
.attr("href");
|
82 |
+
|
83 |
+
const tracker = "nCore";
|
84 |
+
const category = parseCategory(categoryHref?.split("=")[1]);
|
85 |
+
const size = parseSize($(el).find("div.box_meret2").text());
|
86 |
+
const seeds = Number($(el).find("div.box_s2").text());
|
87 |
+
const peers = Number($(el).find("div.box_l2").text());
|
88 |
+
const torrentId = $(el).next().next().attr("id");
|
89 |
+
const torrent = `https://ncore.pro/torrents.php?action=download&id=${torrentId}&key=${downloadKey}`;
|
90 |
+
|
91 |
+
if (!name || !torrentId) continue;
|
92 |
+
|
93 |
+
torrents.push({
|
94 |
+
name,
|
95 |
+
tracker,
|
96 |
+
category,
|
97 |
+
size,
|
98 |
+
seeds,
|
99 |
+
peers,
|
100 |
+
torrent,
|
101 |
+
});
|
102 |
+
}
|
103 |
+
|
104 |
+
if (torrentsOnPage < 50) break;
|
105 |
+
} catch {
|
106 |
+
continue;
|
107 |
+
}
|
108 |
+
}
|
109 |
+
|
110 |
+
return torrents;
|
111 |
+
} catch (error) {
|
112 |
+
return [];
|
113 |
+
}
|
114 |
+
};
|
115 |
+
|
116 |
+
const parseCategory = (category: string | undefined) => {
|
117 |
+
const categories: Record<NcoreCategory, string> = {
|
118 |
+
[NcoreCategory.Film_SD_HU]: "Movies/SD/HU",
|
119 |
+
[NcoreCategory.Film_SD_EN]: "Movies/SD/EN",
|
120 |
+
[NcoreCategory.Film_HD_HU]: "Movies/HD/HU",
|
121 |
+
[NcoreCategory.Film_HD_EN]: "Movies/HD/EN",
|
122 |
+
[NcoreCategory.Sorozat_SD_HU]: "TV/SD/HU",
|
123 |
+
[NcoreCategory.Sorozat_SD_EN]: "TV/SD/EN",
|
124 |
+
[NcoreCategory.Sorozat_HD_HU]: "TV/HD/HU",
|
125 |
+
[NcoreCategory.Sorozat_HD_EN]: "TV/HD/EN",
|
126 |
+
};
|
127 |
+
|
128 |
+
return categories[category as NcoreCategory];
|
129 |
+
};
|
130 |
+
|
131 |
+
const parseSize = (size: string) => {
|
132 |
+
const units: Record<string, number> = {
|
133 |
+
TiB: 1024 ** 4,
|
134 |
+
GiB: 1024 ** 3,
|
135 |
+
MiB: 1024 ** 2,
|
136 |
+
KiB: 1024,
|
137 |
+
B: 1,
|
138 |
+
};
|
139 |
+
|
140 |
+
const [sizeStr, unit] = size.split(" ");
|
141 |
+
const sizeNum = Number(sizeStr);
|
142 |
+
|
143 |
+
if (!sizeNum || !units[unit]) return 0;
|
144 |
+
|
145 |
+
return Math.ceil(sizeNum * units[unit]);
|
146 |
+
};
|
src/torrent/search.ts
ADDED
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { JackettCategory } from "ts-jackett-api/lib/types/JackettCategory.js";
|
2 |
+
import { searchEztv } from "./eztv.js";
|
3 |
+
import {
|
4 |
+
ItorrentCategory,
|
5 |
+
ItorrentQuality,
|
6 |
+
searchItorrent,
|
7 |
+
} from "./itorrent.js";
|
8 |
+
import { searchJackett } from "./jackett.js";
|
9 |
+
import { NcoreCategory, searchNcore } from "./ncore.js";
|
10 |
+
import { searchYts } from "./yts.js";
|
11 |
+
import { InsaneCategory, searchInsane } from "./insane.js";
|
12 |
+
|
13 |
+
export type TorrentCategory = "movie" | "show";
|
14 |
+
|
15 |
+
export type TorrentSource =
|
16 |
+
| "jackett"
|
17 |
+
| "ncore"
|
18 |
+
| "insane"
|
19 |
+
| "itorrent"
|
20 |
+
| "yts"
|
21 |
+
| "eztv";
|
22 |
+
|
23 |
+
export interface TorrentSearchOptions {
|
24 |
+
categories?: TorrentCategory[];
|
25 |
+
sources?: TorrentSource[];
|
26 |
+
jackett?: {
|
27 |
+
url?: string;
|
28 |
+
apiKey?: string;
|
29 |
+
};
|
30 |
+
ncore?: {
|
31 |
+
user?: string;
|
32 |
+
password?: string;
|
33 |
+
};
|
34 |
+
insane?: {
|
35 |
+
user?: string;
|
36 |
+
password?: string;
|
37 |
+
};
|
38 |
+
}
|
39 |
+
|
40 |
+
export interface TorrentSearchResult {
|
41 |
+
name: string;
|
42 |
+
tracker: string;
|
43 |
+
category?: string;
|
44 |
+
size?: number;
|
45 |
+
seeds?: number;
|
46 |
+
peers?: number;
|
47 |
+
torrent?: string;
|
48 |
+
magnet?: string;
|
49 |
+
}
|
50 |
+
|
51 |
+
export const searchTorrents = async (
|
52 |
+
query: string,
|
53 |
+
options?: TorrentSearchOptions
|
54 |
+
) => {
|
55 |
+
const searchAllCategories = !options?.categories?.length;
|
56 |
+
const searchAllSources = !options?.sources?.length;
|
57 |
+
|
58 |
+
const promises: Promise<TorrentSearchResult[]>[] = [];
|
59 |
+
|
60 |
+
if (options?.sources?.includes("jackett") || searchAllSources) {
|
61 |
+
const categories = new Set<JackettCategory>();
|
62 |
+
|
63 |
+
if (options?.categories?.includes("movie") || searchAllCategories) {
|
64 |
+
categories.add(JackettCategory.Movies);
|
65 |
+
}
|
66 |
+
|
67 |
+
if (options?.categories?.includes("show") || searchAllCategories) {
|
68 |
+
categories.add(JackettCategory.TV);
|
69 |
+
}
|
70 |
+
|
71 |
+
promises.push(
|
72 |
+
searchJackett(
|
73 |
+
query,
|
74 |
+
Array.from(categories),
|
75 |
+
options?.jackett?.url,
|
76 |
+
options?.jackett?.apiKey
|
77 |
+
)
|
78 |
+
);
|
79 |
+
}
|
80 |
+
|
81 |
+
if (options?.sources?.includes("ncore") || searchAllSources) {
|
82 |
+
const categories = new Set<NcoreCategory>();
|
83 |
+
|
84 |
+
if (options?.categories?.includes("movie") || searchAllCategories) {
|
85 |
+
categories.add(NcoreCategory.Film_HD_HU);
|
86 |
+
categories.add(NcoreCategory.Film_HD_EN);
|
87 |
+
categories.add(NcoreCategory.Film_SD_HU);
|
88 |
+
categories.add(NcoreCategory.Film_SD_EN);
|
89 |
+
}
|
90 |
+
|
91 |
+
if (options?.categories?.includes("show") || searchAllCategories) {
|
92 |
+
categories.add(NcoreCategory.Sorozat_HD_HU);
|
93 |
+
categories.add(NcoreCategory.Sorozat_HD_EN);
|
94 |
+
categories.add(NcoreCategory.Sorozat_SD_HU);
|
95 |
+
categories.add(NcoreCategory.Sorozat_SD_EN);
|
96 |
+
}
|
97 |
+
|
98 |
+
promises.push(
|
99 |
+
searchNcore(
|
100 |
+
query,
|
101 |
+
Array.from(categories),
|
102 |
+
options?.ncore?.user,
|
103 |
+
options?.ncore?.password
|
104 |
+
)
|
105 |
+
);
|
106 |
+
}
|
107 |
+
|
108 |
+
if (options?.sources?.includes("insane") || searchAllSources) {
|
109 |
+
const categories = new Set<InsaneCategory>();
|
110 |
+
|
111 |
+
if (options?.categories?.includes("movie") || searchAllCategories) {
|
112 |
+
categories.add(InsaneCategory.Film_Hun_SD);
|
113 |
+
categories.add(InsaneCategory.Film_Hun_HD);
|
114 |
+
categories.add(InsaneCategory.Film_Hun_UHD);
|
115 |
+
categories.add(InsaneCategory.Film_Eng_SD);
|
116 |
+
categories.add(InsaneCategory.Film_Eng_HD);
|
117 |
+
categories.add(InsaneCategory.Film_Eng_UHD);
|
118 |
+
}
|
119 |
+
|
120 |
+
if (options?.categories?.includes("show") || searchAllCategories) {
|
121 |
+
categories.add(InsaneCategory.Sorozat_Hun);
|
122 |
+
categories.add(InsaneCategory.Sorozat_Hun_HD);
|
123 |
+
categories.add(InsaneCategory.Sorozat_Hun_UHD);
|
124 |
+
categories.add(InsaneCategory.Sorozat_Eng);
|
125 |
+
categories.add(InsaneCategory.Sorozat_Eng_HD);
|
126 |
+
categories.add(InsaneCategory.Sorozat_Eng_UHD);
|
127 |
+
}
|
128 |
+
|
129 |
+
promises.push(
|
130 |
+
searchInsane(
|
131 |
+
query,
|
132 |
+
Array.from(categories),
|
133 |
+
options?.insane?.user,
|
134 |
+
options?.insane?.password
|
135 |
+
)
|
136 |
+
);
|
137 |
+
}
|
138 |
+
|
139 |
+
if (options?.sources?.includes("itorrent") || searchAllSources) {
|
140 |
+
const categories = new Set<ItorrentCategory>();
|
141 |
+
|
142 |
+
if (options?.categories?.includes("movie") || searchAllCategories) {
|
143 |
+
categories.add(ItorrentCategory.Film);
|
144 |
+
}
|
145 |
+
|
146 |
+
if (options?.categories?.includes("show") || searchAllCategories) {
|
147 |
+
categories.add(ItorrentCategory.Sorozat);
|
148 |
+
}
|
149 |
+
|
150 |
+
const qualities = [
|
151 |
+
ItorrentQuality.HD,
|
152 |
+
ItorrentQuality.SD,
|
153 |
+
ItorrentQuality.CAM,
|
154 |
+
];
|
155 |
+
|
156 |
+
promises.push(searchItorrent(query, Array.from(categories), qualities));
|
157 |
+
}
|
158 |
+
|
159 |
+
if (options?.sources?.includes("yts") || searchAllSources) {
|
160 |
+
if (options?.categories?.includes("movie") || searchAllCategories) {
|
161 |
+
promises.push(searchYts(query));
|
162 |
+
}
|
163 |
+
}
|
164 |
+
|
165 |
+
if (options?.sources?.includes("eztv") || searchAllSources) {
|
166 |
+
if (options?.categories?.includes("show") || searchAllCategories) {
|
167 |
+
promises.push(searchEztv(query));
|
168 |
+
}
|
169 |
+
}
|
170 |
+
|
171 |
+
const results = (await Promise.all(promises)).flat();
|
172 |
+
|
173 |
+
console.log(`Search: got ${results.length} results for ${query}`);
|
174 |
+
|
175 |
+
return results;
|
176 |
+
};
|
src/torrent/webtorrent.ts
ADDED
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import fs from "fs-extra";
|
2 |
+
import MemoryStore from "memory-chunk-store";
|
3 |
+
import os from "os";
|
4 |
+
import path from "path";
|
5 |
+
import WebTorrent, { Torrent } from "webtorrent";
|
6 |
+
import { getReadableDuration } from "../utils/file.js";
|
7 |
+
|
8 |
+
interface FileInfo {
|
9 |
+
name: string;
|
10 |
+
path: string;
|
11 |
+
size: number;
|
12 |
+
url?: string;
|
13 |
+
}
|
14 |
+
|
15 |
+
interface ActiveFileInfo extends FileInfo {
|
16 |
+
progress: number;
|
17 |
+
downloaded: number;
|
18 |
+
}
|
19 |
+
|
20 |
+
export interface TorrentInfo {
|
21 |
+
name: string;
|
22 |
+
infoHash: string;
|
23 |
+
size: number;
|
24 |
+
files: FileInfo[];
|
25 |
+
}
|
26 |
+
|
27 |
+
interface ActiveTorrentInfo extends TorrentInfo {
|
28 |
+
progress: number;
|
29 |
+
downloaded: number;
|
30 |
+
uploaded: number;
|
31 |
+
downloadSpeed: number;
|
32 |
+
uploadSpeed: number;
|
33 |
+
peers: number;
|
34 |
+
openStreams: number;
|
35 |
+
files: ActiveFileInfo[];
|
36 |
+
}
|
37 |
+
|
38 |
+
// Directory to store downloaded files (default OS temp directory)
|
39 |
+
const DOWNLOAD_DIR =
|
40 |
+
process.env.DOWNLOAD_DIR || path.join(os.tmpdir(), "torrent-stream-server");
|
41 |
+
|
42 |
+
// Keep downloaded files after all streams are closed (default false)
|
43 |
+
const KEEP_DOWNLOADED_FILES = process.env.KEEP_DOWNLOADED_FILES
|
44 |
+
? process.env.KEEP_DOWNLOADED_FILES === "true"
|
45 |
+
: false;
|
46 |
+
|
47 |
+
if (!KEEP_DOWNLOADED_FILES) fs.emptyDirSync(DOWNLOAD_DIR);
|
48 |
+
|
49 |
+
// Maximum number of connections per torrent (default 50)
|
50 |
+
const MAX_CONNS_PER_TORRENT = Number(process.env.MAX_CONNS_PER_TORRENT) || 50;
|
51 |
+
|
52 |
+
// Max download speed (bytes/s) over all torrents (default 20MB/s)
|
53 |
+
const DOWNLOAD_SPEED_LIMIT =
|
54 |
+
Number(process.env.DOWNLOAD_SPEED_LIMIT) || 20 * 1024 * 1024;
|
55 |
+
|
56 |
+
// Max upload speed (bytes/s) over all torrents (default 1MB/s)
|
57 |
+
const UPLOAD_SPEED_LIMIT =
|
58 |
+
Number(process.env.UPLOAD_SPEED_LIMIT) || 1 * 1024 * 1024;
|
59 |
+
|
60 |
+
// Time (ms) to seed torrents after all streams are closed (default 1 minute)
|
61 |
+
const SEED_TIME = Number(process.env.SEED_TIME) || 60 * 1000;
|
62 |
+
|
63 |
+
// Timeout (ms) when adding torrents if no metadata is received (default 5 seconds)
|
64 |
+
const TORRENT_TIMEOUT = Number(process.env.TORRENT_TIMEOUT) || 5 * 1000;
|
65 |
+
|
66 |
+
const infoClient = new WebTorrent();
|
67 |
+
const streamClient = new WebTorrent({
|
68 |
+
// @ts-ignore
|
69 |
+
downloadLimit: DOWNLOAD_SPEED_LIMIT,
|
70 |
+
uploadLimit: UPLOAD_SPEED_LIMIT,
|
71 |
+
maxConns: MAX_CONNS_PER_TORRENT,
|
72 |
+
});
|
73 |
+
|
74 |
+
streamClient.on("torrent", (torrent) => {
|
75 |
+
console.log(`Added torrent: ${torrent.name}`);
|
76 |
+
});
|
77 |
+
|
78 |
+
streamClient.on("error", (error) => {
|
79 |
+
if (typeof error === "string") {
|
80 |
+
console.error(`Error: ${error}`);
|
81 |
+
} else {
|
82 |
+
if (error.message.startsWith("Cannot add duplicate torrent")) return;
|
83 |
+
console.error(`Error: ${error.message}`);
|
84 |
+
}
|
85 |
+
});
|
86 |
+
|
87 |
+
infoClient.on("error", () => {});
|
88 |
+
|
89 |
+
const launchTime = Date.now();
|
90 |
+
|
91 |
+
export const getStats = () => ({
|
92 |
+
uptime: getReadableDuration(Date.now() - launchTime),
|
93 |
+
openStreams: [...openStreams.values()].reduce((a, b) => a + b, 0),
|
94 |
+
downloadSpeed: streamClient.downloadSpeed,
|
95 |
+
uploadSpeed: streamClient.uploadSpeed,
|
96 |
+
activeTorrents: streamClient.torrents.map<ActiveTorrentInfo>((torrent) => ({
|
97 |
+
name: torrent.name,
|
98 |
+
infoHash: torrent.infoHash,
|
99 |
+
size: torrent.length,
|
100 |
+
progress: torrent.progress,
|
101 |
+
downloaded: torrent.downloaded,
|
102 |
+
uploaded: torrent.uploaded,
|
103 |
+
downloadSpeed: torrent.downloadSpeed,
|
104 |
+
uploadSpeed: torrent.uploadSpeed,
|
105 |
+
peers: torrent.numPeers,
|
106 |
+
openStreams: openStreams.get(torrent.infoHash) || 0,
|
107 |
+
files: torrent.files.map((file) => ({
|
108 |
+
name: file.name,
|
109 |
+
path: file.path,
|
110 |
+
size: file.length,
|
111 |
+
progress: file.progress,
|
112 |
+
downloaded: file.downloaded,
|
113 |
+
})),
|
114 |
+
})),
|
115 |
+
});
|
116 |
+
|
117 |
+
export const getOrAddTorrent = (uri: string) =>
|
118 |
+
new Promise<Torrent | undefined>((resolve) => {
|
119 |
+
const torrent = streamClient.add(
|
120 |
+
uri,
|
121 |
+
{
|
122 |
+
path: DOWNLOAD_DIR,
|
123 |
+
destroyStoreOnDestroy: !KEEP_DOWNLOADED_FILES,
|
124 |
+
// @ts-ignore
|
125 |
+
deselect: true,
|
126 |
+
},
|
127 |
+
(torrent) => {
|
128 |
+
clearTimeout(timeout);
|
129 |
+
resolve(torrent);
|
130 |
+
}
|
131 |
+
);
|
132 |
+
|
133 |
+
const timeout = setTimeout(() => {
|
134 |
+
torrent.destroy();
|
135 |
+
resolve(undefined);
|
136 |
+
}, TORRENT_TIMEOUT);
|
137 |
+
});
|
138 |
+
|
139 |
+
export const getFile = (torrent: Torrent, path: string) =>
|
140 |
+
torrent.files.find((file) => file.path === path);
|
141 |
+
|
142 |
+
export const getTorrentInfo = async (uri: string) => {
|
143 |
+
const getInfo = (torrent: Torrent): TorrentInfo => ({
|
144 |
+
name: torrent.name,
|
145 |
+
infoHash: torrent.infoHash,
|
146 |
+
size: torrent.length,
|
147 |
+
files: torrent.files.map((file) => ({
|
148 |
+
name: file.name,
|
149 |
+
path: file.path,
|
150 |
+
size: file.length,
|
151 |
+
})),
|
152 |
+
});
|
153 |
+
|
154 |
+
return await new Promise<TorrentInfo | undefined>((resolve) => {
|
155 |
+
const torrent = infoClient.add(
|
156 |
+
uri,
|
157 |
+
{ store: MemoryStore, destroyStoreOnDestroy: true },
|
158 |
+
(torrent) => {
|
159 |
+
clearTimeout(timeout);
|
160 |
+
const info = getInfo(torrent);
|
161 |
+
console.log(`Fetched info: ${info.name}`);
|
162 |
+
torrent.destroy();
|
163 |
+
resolve(info);
|
164 |
+
}
|
165 |
+
);
|
166 |
+
|
167 |
+
const timeout = setTimeout(() => {
|
168 |
+
torrent.destroy();
|
169 |
+
resolve(undefined);
|
170 |
+
}, TORRENT_TIMEOUT);
|
171 |
+
});
|
172 |
+
};
|
173 |
+
|
174 |
+
const timeouts = new Map<string, NodeJS.Timeout>();
|
175 |
+
const openStreams = new Map<string, number>();
|
176 |
+
|
177 |
+
export const streamOpened = (hash: string, fileName: string) => {
|
178 |
+
console.log(`Stream opened: ${fileName}`);
|
179 |
+
|
180 |
+
const count = openStreams.get(hash) || 0;
|
181 |
+
openStreams.set(hash, count + 1);
|
182 |
+
|
183 |
+
const timeout = timeouts.get(hash);
|
184 |
+
|
185 |
+
if (timeout) {
|
186 |
+
clearTimeout(timeout);
|
187 |
+
timeouts.delete(hash);
|
188 |
+
}
|
189 |
+
};
|
190 |
+
|
191 |
+
export const streamClosed = (hash: string, fileName: string) => {
|
192 |
+
console.log(`Stream closed: ${fileName}`);
|
193 |
+
|
194 |
+
const count = openStreams.get(hash) || 1;
|
195 |
+
openStreams.set(hash, count - 1);
|
196 |
+
|
197 |
+
if (count > 1) return;
|
198 |
+
|
199 |
+
openStreams.delete(hash);
|
200 |
+
|
201 |
+
let timeout = timeouts.get(hash);
|
202 |
+
if (timeout) return;
|
203 |
+
|
204 |
+
timeout = setTimeout(async () => {
|
205 |
+
const torrent = await streamClient.get(hash);
|
206 |
+
// @ts-ignore
|
207 |
+
torrent?.destroy(undefined, () => {
|
208 |
+
console.log(`Removed torrent: ${torrent.name}`);
|
209 |
+
timeouts.delete(hash);
|
210 |
+
});
|
211 |
+
}, SEED_TIME);
|
212 |
+
|
213 |
+
timeouts.set(hash, timeout);
|
214 |
+
};
|
src/torrent/yts.ts
ADDED
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { details, search } from "yts-api-node";
|
2 |
+
import { TorrentSearchResult } from "./search.js";
|
3 |
+
import { isImdbId } from "../utils/imdb.js";
|
4 |
+
|
5 |
+
const trackers = [
|
6 |
+
"udp://glotorrents.pw:6969/announce",
|
7 |
+
"udp://tracker.opentrackr.org:1337/announce",
|
8 |
+
"udp://torrent.gresille.org:80/announce",
|
9 |
+
"udp://tracker.openbittorrent.com:80",
|
10 |
+
"udp://tracker.coppersurfer.tk:6969",
|
11 |
+
"udp://tracker.leechers-paradise.org:6969",
|
12 |
+
"udp://p4p.arenabg.ch:1337",
|
13 |
+
"udp://tracker.internetwarriors.net:1337",
|
14 |
+
];
|
15 |
+
|
16 |
+
const trackersString = "&tr=" + trackers.join("&tr=");
|
17 |
+
|
18 |
+
export const searchYts = async (
|
19 |
+
searchQuery: string
|
20 |
+
): Promise<TorrentSearchResult[]> => {
|
21 |
+
try {
|
22 |
+
if (isImdbId(searchQuery)) {
|
23 |
+
const res = await details({ movie_id: searchQuery });
|
24 |
+
|
25 |
+
return res.data.movie.torrents.map((torrent) => ({
|
26 |
+
name: `${res.data.movie.title_long} ${torrent.quality} ${torrent.type
|
27 |
+
.replace("bluray", "BluRay")
|
28 |
+
.replace("web", "WEB")}`,
|
29 |
+
tracker: "YTS",
|
30 |
+
category: `Movies/${torrent.quality}`,
|
31 |
+
size: torrent.size_bytes,
|
32 |
+
seeds: torrent.seeds,
|
33 |
+
peers: torrent.peers,
|
34 |
+
torrent: torrent.url,
|
35 |
+
magnet: `magnet:?xt=urn:btih:${torrent.hash}${trackersString}`,
|
36 |
+
}));
|
37 |
+
} else {
|
38 |
+
const res = await search({ query_term: searchQuery });
|
39 |
+
|
40 |
+
return res.data.movies.flatMap((movie) =>
|
41 |
+
movie.torrents.map((torrent) => ({
|
42 |
+
name: `${movie.title_long} ${torrent.quality} ${torrent.type
|
43 |
+
.replace("bluray", "BluRay")
|
44 |
+
.replace("web", "WEB")}`,
|
45 |
+
tracker: "YTS",
|
46 |
+
category: `Movies/${torrent.quality}`,
|
47 |
+
size: torrent.size_bytes,
|
48 |
+
seeds: torrent.seeds,
|
49 |
+
peers: torrent.peers,
|
50 |
+
torrent: torrent.url,
|
51 |
+
magnet: `magnet:?xt=urn:btih:${torrent.hash}${trackersString}`,
|
52 |
+
}))
|
53 |
+
);
|
54 |
+
}
|
55 |
+
} catch (error) {
|
56 |
+
return [];
|
57 |
+
}
|
58 |
+
};
|
src/utils/dotenv.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
import dotenv from "dotenv";
|
2 |
+
|
3 |
+
dotenv.config();
|
src/utils/file.ts
ADDED
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import mime from "mime";
|
2 |
+
|
3 |
+
export const isVideoFile = (filename: string) =>
|
4 |
+
mime.getType(filename)?.startsWith("video") || false;
|
5 |
+
|
6 |
+
export const isSubtitleFile = (filename: string) =>
|
7 |
+
filename.toLowerCase().endsWith(".srt") ||
|
8 |
+
filename.toLowerCase().endsWith(".sub") ||
|
9 |
+
filename.toLowerCase().endsWith(".vtt") ||
|
10 |
+
filename.toLowerCase().endsWith(".smi") ||
|
11 |
+
filename.toLowerCase().endsWith(".ssa") ||
|
12 |
+
filename.toLowerCase().endsWith(".ass") ||
|
13 |
+
filename.toLowerCase().endsWith(".txt");
|
14 |
+
|
15 |
+
export const getStreamingMimeType = (filename: string) => {
|
16 |
+
const mimeType = mime.getType(filename);
|
17 |
+
return mimeType?.startsWith("video")
|
18 |
+
? "video/mp4"
|
19 |
+
: mimeType || "application/unknown";
|
20 |
+
};
|
21 |
+
|
22 |
+
export const getReadableSize = (bytes: number) => {
|
23 |
+
if (bytes == 0) {
|
24 |
+
return "0.00 B";
|
25 |
+
}
|
26 |
+
var e = Math.floor(Math.log(bytes) / Math.log(1024));
|
27 |
+
return (
|
28 |
+
(bytes / Math.pow(1024, e)).toFixed(2) + " " + " KMGTP".charAt(e) + "B"
|
29 |
+
);
|
30 |
+
};
|
31 |
+
|
32 |
+
export const getReadableDuration = (millisecs: number) => {
|
33 |
+
var seconds = (millisecs / 1000).toFixed(1);
|
34 |
+
var minutes = (millisecs / (1000 * 60)).toFixed(1);
|
35 |
+
var hours = (millisecs / (1000 * 60 * 60)).toFixed(1);
|
36 |
+
var days = (millisecs / (1000 * 60 * 60 * 24)).toFixed(1);
|
37 |
+
|
38 |
+
if (Number(seconds) < 60) {
|
39 |
+
return seconds + " seconds";
|
40 |
+
} else if (Number(minutes) < 60) {
|
41 |
+
return minutes + " minutes";
|
42 |
+
} else if (Number(hours) < 24) {
|
43 |
+
return hours + " hours";
|
44 |
+
} else {
|
45 |
+
return days + " days";
|
46 |
+
}
|
47 |
+
};
|
src/utils/https.ts
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from "axios";
|
2 |
+
import { RequestListener } from "http";
|
3 |
+
import https from "https";
|
4 |
+
import localtunnel from "localtunnel";
|
5 |
+
|
6 |
+
enum HttpsMethod {
|
7 |
+
None = "none",
|
8 |
+
LocalIpMedic = "local-ip.medicmobile.org",
|
9 |
+
LocalIpCo = "local-ip.co",
|
10 |
+
Localtunnel = "localtunnel",
|
11 |
+
}
|
12 |
+
|
13 |
+
const HTTPS_METHOD = process.env.HTTPS_METHOD || HttpsMethod.None;
|
14 |
+
|
15 |
+
export const serveHTTPS = async (app: RequestListener, port: number) => {
|
16 |
+
if (HTTPS_METHOD === HttpsMethod.LocalIpMedic) {
|
17 |
+
const json = (await axios.get("https://local-ip.medicmobile.org/keys"))
|
18 |
+
.data;
|
19 |
+
const cert = `${json.cert}\n${json.chain}`;
|
20 |
+
const httpsServer = https.createServer({ key: json.privkey, cert }, app);
|
21 |
+
httpsServer.listen(port);
|
22 |
+
console.log(`HTTPS addon listening on port ${port}`);
|
23 |
+
return httpsServer;
|
24 |
+
}
|
25 |
+
|
26 |
+
if (HTTPS_METHOD === HttpsMethod.LocalIpCo) {
|
27 |
+
const key = (await axios.get("http://local-ip.co/cert/server.key")).data;
|
28 |
+
const serverPem = (await axios.get("http://local-ip.co/cert/server.pem"))
|
29 |
+
.data;
|
30 |
+
const chainPem = (await axios.get("http://local-ip.co/cert/chain.pem"))
|
31 |
+
.data;
|
32 |
+
const cert = `${serverPem}\n${chainPem}`;
|
33 |
+
const httpsServer = https.createServer({ key, cert }, app);
|
34 |
+
httpsServer.listen(port);
|
35 |
+
console.log(`HTTPS addon listening on port ${port}`);
|
36 |
+
return httpsServer;
|
37 |
+
}
|
38 |
+
|
39 |
+
if (HTTPS_METHOD === HttpsMethod.Localtunnel) {
|
40 |
+
const tunnel = await localtunnel({ port: Number(process.env.PORT) });
|
41 |
+
console.log(`Tunnel accessible at: ${tunnel.url}`);
|
42 |
+
const tunnelPassword = await axios.get("https://loca.lt/mytunnelpassword");
|
43 |
+
console.log(`Tunnel password: ${tunnelPassword.data}`);
|
44 |
+
}
|
45 |
+
};
|
src/utils/imdb.ts
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from "axios";
|
2 |
+
|
3 |
+
export const getTitle = async (imdbId: string, language?: string) => {
|
4 |
+
try {
|
5 |
+
const data = (
|
6 |
+
await axios.get(`https://www.imdb.com/title/${imdbId}`, {
|
7 |
+
headers: {
|
8 |
+
"Accept-Language": language,
|
9 |
+
"Accept-Encoding": "gzip,deflate,compress",
|
10 |
+
"User-Agent":
|
11 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
12 |
+
},
|
13 |
+
})
|
14 |
+
).data;
|
15 |
+
|
16 |
+
const title = data.match(/<title>(.*?)<\/title>/)[1] as string;
|
17 |
+
if (!title) return undefined;
|
18 |
+
return title.split(" (")[0];
|
19 |
+
} catch {
|
20 |
+
return undefined;
|
21 |
+
}
|
22 |
+
};
|
23 |
+
|
24 |
+
export const getTitles = async (imdbId: string) => {
|
25 |
+
const titles = new Set<string>();
|
26 |
+
|
27 |
+
(await Promise.all([getTitle(imdbId), getTitle(imdbId, "en")])).forEach(
|
28 |
+
(title) => {
|
29 |
+
if (title) titles.add(title);
|
30 |
+
}
|
31 |
+
);
|
32 |
+
|
33 |
+
return [...titles];
|
34 |
+
};
|
35 |
+
|
36 |
+
export const isImdbId = (str: string) =>
|
37 |
+
/ev\d{7}\/\d{4}(-\d)?|(ch|co|ev|nm|tt)\d{7}/.test(str);
|
src/utils/language.ts
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const guessLanguage = (name: string, category?: string) => {
|
2 |
+
if (category?.includes("HU")) return "Hungarian";
|
3 |
+
|
4 |
+
const split = name
|
5 |
+
.toLowerCase()
|
6 |
+
.replace(/\W/g, " ")
|
7 |
+
.replace("x", " ")
|
8 |
+
.split(" ");
|
9 |
+
|
10 |
+
if (split.includes("hun") || split.includes("hungarian")) return "Hungarian";
|
11 |
+
if (split.includes("ger") || split.includes("german")) return "German";
|
12 |
+
if (split.includes("fre") || split.includes("french")) return "French";
|
13 |
+
if (split.includes("ita") || split.includes("italian")) return "Italian";
|
14 |
+
|
15 |
+
return "English";
|
16 |
+
};
|
src/utils/quality.ts
ADDED
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const guessQuality = (name: string) => {
|
2 |
+
const split = name.replace(/\W/g, " ").toLowerCase().split(" ");
|
3 |
+
|
4 |
+
let score = 0;
|
5 |
+
const parts = [];
|
6 |
+
|
7 |
+
if (split.includes("2160p")) {
|
8 |
+
parts.push("4K");
|
9 |
+
score += 3000;
|
10 |
+
} else if (split.includes("1080p")) {
|
11 |
+
parts.push("1080p");
|
12 |
+
score += 2000;
|
13 |
+
} else if (split.includes("720p")) {
|
14 |
+
parts.push("720p");
|
15 |
+
score += 1000;
|
16 |
+
}
|
17 |
+
|
18 |
+
if (
|
19 |
+
(split.includes("dolby") && split.includes("vision")) ||
|
20 |
+
split.includes("dovi") ||
|
21 |
+
split.includes("dv")
|
22 |
+
) {
|
23 |
+
parts.push("Dolby Vision");
|
24 |
+
score += 20;
|
25 |
+
} else if (split.includes("hdr")) {
|
26 |
+
parts.push("HDR");
|
27 |
+
score += 10;
|
28 |
+
}
|
29 |
+
|
30 |
+
if (
|
31 |
+
split.includes("bluray") ||
|
32 |
+
(split.includes("blu") && split.includes("ray")) ||
|
33 |
+
split.includes("bdrip") ||
|
34 |
+
split.includes("brrip")
|
35 |
+
) {
|
36 |
+
parts.push("BluRay");
|
37 |
+
score += 500;
|
38 |
+
|
39 |
+
if (split.includes("remux")) {
|
40 |
+
parts.push("Remux");
|
41 |
+
score += 100;
|
42 |
+
}
|
43 |
+
} else if (
|
44 |
+
split.includes("webrip") ||
|
45 |
+
split.includes("webdl") ||
|
46 |
+
split.includes("web")
|
47 |
+
) {
|
48 |
+
parts.push("WEB");
|
49 |
+
score += 400;
|
50 |
+
} else if (split.includes("dvdrip")) {
|
51 |
+
parts.push("DVD");
|
52 |
+
score += 300;
|
53 |
+
} else if (split.includes("hdtv")) {
|
54 |
+
parts.push("HDTV");
|
55 |
+
score += 200;
|
56 |
+
} else if (split.includes("sdtv")) {
|
57 |
+
parts.push("sdtv");
|
58 |
+
score += 100;
|
59 |
+
} else if (
|
60 |
+
split.includes("camrip") ||
|
61 |
+
split.includes("cam") ||
|
62 |
+
split.includes("hdcam") ||
|
63 |
+
split.includes("ts") ||
|
64 |
+
split.includes("hdts") ||
|
65 |
+
split.includes("tc") ||
|
66 |
+
split.includes("hdtc")
|
67 |
+
) {
|
68 |
+
parts.push("CAM");
|
69 |
+
score -= 5000;
|
70 |
+
}
|
71 |
+
|
72 |
+
if (split.includes("3d")) {
|
73 |
+
parts.push("3D");
|
74 |
+
score -= 1;
|
75 |
+
}
|
76 |
+
|
77 |
+
if (parts.length === 0) {
|
78 |
+
parts.push("Unknown");
|
79 |
+
score = -Infinity;
|
80 |
+
}
|
81 |
+
|
82 |
+
return { quality: parts.join(" "), score };
|
83 |
+
};
|
src/utils/shows.ts
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const guessSeasonEpisode = (name: string) => {
|
2 |
+
const str = name.replace(/\W/g, " ").toLowerCase();
|
3 |
+
|
4 |
+
const seasonMatch = [...str.matchAll(/[s](?<season>\d+)/g)];
|
5 |
+
const episodeMatch = str.match(/[e](?<episode>\d+)/);
|
6 |
+
|
7 |
+
if (seasonMatch.length === 0 && str.includes("complete")) {
|
8 |
+
return { completeSeries: true };
|
9 |
+
} else if (seasonMatch.length === 1 && !episodeMatch) {
|
10 |
+
const season = Number(seasonMatch[0].groups?.season) || 0;
|
11 |
+
return { seasons: [season] };
|
12 |
+
} else if (seasonMatch.length > 1) {
|
13 |
+
const firstSeason = Number(seasonMatch[0].groups?.season) || 0;
|
14 |
+
const lastSeason =
|
15 |
+
Number(seasonMatch[seasonMatch.length - 1].groups?.season) || 0;
|
16 |
+
const seasons = [];
|
17 |
+
for (let i = firstSeason; i <= lastSeason; i++) seasons.push(i);
|
18 |
+
return { seasons };
|
19 |
+
} else if (seasonMatch[0] || episodeMatch) {
|
20 |
+
const season = Number(seasonMatch[0]?.groups?.season) || undefined;
|
21 |
+
const episode = Number(episodeMatch?.groups?.episode) || undefined;
|
22 |
+
return { season, episode };
|
23 |
+
} else {
|
24 |
+
const seasonEpisodeMatch = str.match(/(?<season>\d+)x(?<episode>\d+)/);
|
25 |
+
const season = Number(seasonEpisodeMatch?.groups?.season) || undefined;
|
26 |
+
const episode = Number(seasonEpisodeMatch?.groups?.episode) || undefined;
|
27 |
+
return { season, episode };
|
28 |
+
}
|
29 |
+
};
|
30 |
+
|
31 |
+
export const isTorrentNameMatch = (
|
32 |
+
name: string,
|
33 |
+
season: number,
|
34 |
+
episode: number
|
35 |
+
) => {
|
36 |
+
const guess = guessSeasonEpisode(name);
|
37 |
+
if (guess.completeSeries) return true;
|
38 |
+
if (guess.seasons?.includes(season)) return true;
|
39 |
+
if (guess.season === season && guess.episode === episode) return true;
|
40 |
+
if (season === 0) {
|
41 |
+
if (name.toLowerCase().includes("special")) return true;
|
42 |
+
if (guess.season === undefined && guess.seasons === undefined) return true;
|
43 |
+
}
|
44 |
+
return false;
|
45 |
+
};
|
46 |
+
|
47 |
+
export const isFileNameMatch = (
|
48 |
+
name: string,
|
49 |
+
season: number,
|
50 |
+
episode: number
|
51 |
+
) => {
|
52 |
+
const guess = guessSeasonEpisode(name);
|
53 |
+
if (guess.season === season && guess.episode === episode) return true;
|
54 |
+
if (season === 0) return true;
|
55 |
+
return false;
|
56 |
+
};
|
tsconfig.json
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"compilerOptions": {
|
3 |
+
"module": "NodeNext",
|
4 |
+
"moduleResolution": "NodeNext",
|
5 |
+
"target": "ESNext",
|
6 |
+
"sourceMap": true,
|
7 |
+
"rootDir": "src",
|
8 |
+
"outDir": "dist",
|
9 |
+
"esModuleInterop": true,
|
10 |
+
"skipLibCheck": true
|
11 |
+
}
|
12 |
+
}
|