no1b4me commited on
Commit
c237e22
·
verified ·
1 Parent(s): 8732f46

Upload 25 files

Browse files
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
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
+ }