Mythus commited on
Commit
86f7ec6
·
verified ·
1 Parent(s): 9a006ec

Upload 5 files

Browse files
comet/main.py ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiohttp, asyncio, bencodepy, hashlib, re, base64, json, os, RTN, time
2
+
3
+ from fastapi import FastAPI, Request
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from fastapi.responses import RedirectResponse
6
+ from fastapi.templating import Jinja2Templates
7
+ from fastapi.staticfiles import StaticFiles
8
+ from contextlib import asynccontextmanager
9
+ from databases import Database
10
+
11
+ from .utils.logger import logger
12
+ from .utils.general import translate, isVideo, bytesToSize
13
+
14
+ database = Database(f"sqlite:///{os.getenv('DATABASE_PATH', 'database.db')}")
15
+
16
+ class BestOverallRanking(RTN.BaseRankingModel):
17
+ uhd: int = 100
18
+ fhd: int = 90
19
+ hd: int = 80
20
+ sd: int = 70
21
+ dolby_video: int = 100
22
+ hdr: int = 80
23
+ hdr10: int = 90
24
+ dts_x: int = 100
25
+ dts_hd: int = 80
26
+ dts_hd_ma: int = 90
27
+ atmos: int = 90
28
+ truehd: int = 60
29
+ ddplus: int = 40
30
+ aac: int = 30
31
+ ac3: int = 20
32
+ remux: int = 150
33
+ bluray: int = 120
34
+ webdl: int = 90
35
+
36
+ settings = RTN.SettingsModel()
37
+ ranking_model = BestOverallRanking()
38
+ rtn = RTN.RTN(settings=settings, ranking_model=ranking_model)
39
+
40
+ infoHashPattern = re.compile(r"\b([a-fA-F0-9]{40})\b")
41
+
42
+ @asynccontextmanager
43
+ async def lifespan(app: FastAPI):
44
+ await database.connect()
45
+ await database.execute("CREATE TABLE IF NOT EXISTS cache (cacheKey BLOB PRIMARY KEY, timestamp INTEGER, results TEXT)")
46
+ yield
47
+ await database.disconnect()
48
+
49
+ app = FastAPI(lifespan=lifespan, docs_url=None)
50
+
51
+ app.add_middleware(
52
+ CORSMiddleware,
53
+ allow_origins=["*"],
54
+ allow_credentials=True,
55
+ allow_methods=["*"],
56
+ allow_headers=["*"],
57
+ )
58
+
59
+ templates = Jinja2Templates("comet/templates")
60
+ app.mount("/static", StaticFiles(directory="comet/templates"), name="static")
61
+
62
+ @app.get("/")
63
+ async def root():
64
+ return RedirectResponse("/configure")
65
+
66
+ indexers = os.getenv("INDEXER_MANAGER_INDEXERS")
67
+ if "," in indexers:
68
+ indexers = indexers.split(",")
69
+ else:
70
+ indexers = [indexers]
71
+
72
+ webConfig = {
73
+ "indexers": indexers,
74
+ "languages": [indexer.replace(" ", "_") for indexer in RTN.patterns.language_code_mapping.keys()],
75
+ "resolutions": ["480p", "720p", "1080p", "1440p", "2160p", "2880p", "4320p"]
76
+ }
77
+
78
+ @app.get("/configure")
79
+ @app.get("/{b64config}/configure")
80
+ async def configure(request: Request):
81
+ return templates.TemplateResponse("index.html", {"request": request, "CUSTOM_HEADER_HTML": os.getenv("CUSTOM_HEADER_HTML", ""), "webConfig": webConfig})
82
+
83
+ def configChecking(b64config: str):
84
+ try:
85
+ config = json.loads(base64.b64decode(b64config).decode())
86
+
87
+ if not isinstance(config["debridService"], str) or config["debridService"] not in ["realdebrid"]:
88
+ return False
89
+
90
+ if not isinstance(config["debridApiKey"], str):
91
+ return False
92
+
93
+ if not isinstance(config["indexers"], list):
94
+ return False
95
+
96
+ if not isinstance(config["maxResults"], int) or config["maxResults"] < 0:
97
+ return False
98
+
99
+ if not isinstance(config["resolutions"], list) or len(config["resolutions"]) == 0:
100
+ return False
101
+
102
+ if not isinstance(config["languages"], list) or len(config["languages"]) == 0:
103
+ return False
104
+
105
+ return config
106
+ except:
107
+ return False
108
+
109
+ @app.get("/manifest.json")
110
+ @app.get("/{b64config}/manifest.json")
111
+ async def manifest():
112
+ return {
113
+ "id": "stremio.comet.fast",
114
+ "version": "1.0.0",
115
+ "name": "Comet",
116
+ "description": "Stremio's fastest torrent/debrid search add-on.",
117
+ "logo": "https://i.imgur.com/jmVoVMu.jpeg",
118
+ "background": "https://i.imgur.com/WwnXB3k.jpeg",
119
+ "resources": [
120
+ "stream"
121
+ ],
122
+ "types": [
123
+ "movie",
124
+ "series"
125
+ ],
126
+ "idPrefixes": [
127
+ "tt"
128
+ ],
129
+ "catalogs": [],
130
+ "behaviorHints": {
131
+ "configurable": True
132
+ }
133
+ }
134
+
135
+ async def getIndexerManager(session: aiohttp.ClientSession, indexerManagerType: str, indexers: list, query: str):
136
+ try:
137
+ timeout = aiohttp.ClientTimeout(total=int(os.getenv("INDEXER_MANAGER_TIMEOUT", 30)))
138
+ results = []
139
+
140
+ if indexerManagerType == "jackett":
141
+ response = await session.get(f"{os.getenv('INDEXER_MANAGER_URL', 'http://127.0.0.1:9117')}/api/v2.0/indexers/all/results?apikey={os.getenv('INDEXER_MANAGER_API_KEY')}&Query={query}&Tracker[]={'&Tracker[]='.join(indexer for indexer in indexers)}", timeout=timeout)
142
+ response = await response.json()
143
+
144
+ for result in response["Results"]:
145
+ results.append(result)
146
+
147
+ if indexerManagerType == "prowlarr":
148
+ getIndexers = await session.get(f"{os.getenv('INDEXER_MANAGER_URL', 'http://127.0.0.1:9696')}/api/v1/indexer", headers={
149
+ "X-Api-Key": os.getenv("INDEXER_MANAGER_API_KEY")
150
+ })
151
+ getIndexers = await getIndexers.json()
152
+
153
+ indexersId = []
154
+ for indexer in getIndexers:
155
+ if indexer["definitionName"] in indexers:
156
+ indexersId.append(indexer["id"])
157
+
158
+ response = await session.get(f"{os.getenv('INDEXER_MANAGER_URL', 'http://127.0.0.1:9696')}/api/v1/search?query={query}&indexerIds={'&indexerIds='.join(str(indexerId) for indexerId in indexersId)}&type=search", headers={
159
+ "X-Api-Key": os.getenv("INDEXER_MANAGER_API_KEY")
160
+ })
161
+ response = await response.json()
162
+
163
+ for result in response:
164
+ results.append(result)
165
+
166
+ return results
167
+ except Exception as e:
168
+ logger.warning(f"Exception while getting {indexerManagerType} results for {query} with {indexers}: {e}")
169
+
170
+ async def getTorrentHash(session: aiohttp.ClientSession, indexerManagerType: str, torrent: dict):
171
+ if "InfoHash" in torrent and torrent["InfoHash"] != None:
172
+ return torrent["InfoHash"]
173
+
174
+ if "infoHash" in torrent:
175
+ return torrent["infoHash"]
176
+
177
+ url = torrent["Link"] if indexerManagerType == "jackett" else torrent["downloadUrl"]
178
+
179
+ try:
180
+ timeout = aiohttp.ClientTimeout(total=int(os.getenv("GET_TORRENT_TIMEOUT", 5)))
181
+ response = await session.get(url, allow_redirects=False, timeout=timeout)
182
+ if response.status == 200:
183
+ torrentData = await response.read()
184
+ torrentDict = bencodepy.decode(torrentData)
185
+ info = bencodepy.encode(torrentDict[b"info"])
186
+ hash = hashlib.sha1(info).hexdigest()
187
+ else:
188
+ location = response.headers.get("Location", "")
189
+ if not location:
190
+ return
191
+
192
+ match = infoHashPattern.search(location)
193
+ if not match:
194
+ return
195
+
196
+ hash = match.group(1).upper()
197
+
198
+ return hash
199
+ except Exception as e:
200
+ logger.warning(f"Exception while getting torrent info hash for {torrent['indexer'] if 'indexer' in torrent else (torrent['Tracker'] if 'Tracker' in torrent else '')}|{url}: {e}")
201
+ # logger.warning(f"Exception while getting torrent info hash for {jackettIndexerPattern.findall(url)[0]}|{jackettNamePattern.search(url)[0]}: {e}")
202
+
203
+ @app.get("/stream/{type}/{id}.json")
204
+ @app.get("/{b64config}/stream/{type}/{id}.json")
205
+ async def stream(request: Request, b64config: str, type: str, id: str):
206
+ config = configChecking(b64config)
207
+ if not config:
208
+ return {
209
+ "streams": [
210
+ {
211
+ "name": "[⚠️] Comet",
212
+ "title": "Invalid Comet config.",
213
+ "url": "https://comet.fast"
214
+ }
215
+ ]
216
+ }
217
+
218
+ async with aiohttp.ClientSession() as session:
219
+ checkDebrid = await session.get("https://api.real-debrid.com/rest/1.0/user", headers={
220
+ "Authorization": f"Bearer {config['debridApiKey']}"
221
+ })
222
+ checkDebrid = await checkDebrid.text()
223
+ if not '"type": "premium"' in checkDebrid:
224
+ return {
225
+ "streams": [
226
+ {
227
+ "name": "[⚠️] Comet",
228
+ "title": "Invalid Real-Debrid account.",
229
+ "url": "https://comet.fast"
230
+ }
231
+ ]
232
+ }
233
+
234
+ season = None
235
+ episode = None
236
+ if type == "series":
237
+ info = id.split(":")
238
+
239
+ id = info[0]
240
+ season = int(info[1])
241
+ episode = int(info[2])
242
+
243
+ getMetadata = await session.get(f"https://v3.sg.media-imdb.com/suggestion/a/{id}.json")
244
+ metadata = await getMetadata.json()
245
+
246
+ name = metadata["d"][0]["l"]
247
+ name = translate(name)
248
+
249
+ cacheKey = hashlib.md5(json.dumps({"debridService": config["debridService"], "name": name, "season": season, "episode": episode, "indexers": config["indexers"], "resolutions": config["resolutions"], "languages": config["languages"]}).encode("utf-8")).hexdigest()
250
+ cached = await database.fetch_one(f"SELECT EXISTS (SELECT 1 FROM cache WHERE cacheKey = '{cacheKey}')")
251
+ if cached[0] != 0:
252
+ logger.info(f"Cache found for {name}")
253
+
254
+ timestamp = await database.fetch_one(f"SELECT timestamp FROM cache WHERE cacheKey = '{cacheKey}'")
255
+ if timestamp[0] + int(os.getenv("CACHE_TTL", 86400)) < time.time():
256
+ await database.execute(f"DELETE FROM cache WHERE cacheKey = '{cacheKey}'")
257
+
258
+ logger.info(f"Cache expired for {name}")
259
+ else:
260
+ sortedRankedFiles = await database.fetch_one(f"SELECT results FROM cache WHERE cacheKey = '{cacheKey}'")
261
+ sortedRankedFiles = json.loads(sortedRankedFiles[0])
262
+
263
+ results = []
264
+ for hash in sortedRankedFiles:
265
+ results.append({
266
+ "name": f"[RD⚡] Comet {sortedRankedFiles[hash]['data']['resolution'][0] if len(sortedRankedFiles[hash]['data']['resolution']) > 0 else 'Unknown'}",
267
+ "title": f"{sortedRankedFiles[hash]['data']['title']}\n💾 {bytesToSize(sortedRankedFiles[hash]['data']['size'])}",
268
+ "url": f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{sortedRankedFiles[hash]['data']['index']}"
269
+ })
270
+
271
+ return {"streams": results}
272
+ else:
273
+ logger.info(f"No cache found for {name} with user configuration")
274
+
275
+ indexerManagerType = os.getenv("INDEXER_MANAGER_TYPE", "jackett")
276
+
277
+ logger.info(f"Start of {indexerManagerType} search for {name} with indexers {config['indexers']}")
278
+
279
+ tasks = []
280
+ tasks.append(getIndexerManager(session, indexerManagerType, config["indexers"], name))
281
+ if type == "series":
282
+ tasks.append(getIndexerManager(session, indexerManagerType, config["indexers"], f"{name} S0{season}E0{episode}"))
283
+ searchResponses = await asyncio.gather(*tasks)
284
+
285
+ torrents = []
286
+ for results in searchResponses:
287
+ if results == None:
288
+ continue
289
+
290
+ for result in results:
291
+ torrents.append(result)
292
+
293
+ logger.info(f"{len(torrents)} torrents found for {name}")
294
+
295
+ if len(torrents) == 0:
296
+ return {"streams": []}
297
+
298
+ tasks = []
299
+ filtered = 0
300
+ for torrent in torrents:
301
+ parsedTorrent = RTN.parse(torrent["Title"]) if indexerManagerType == "jackett" else RTN.parse(torrent["title"])
302
+ if not "All" in config["resolutions"] and len(parsedTorrent.resolution) > 0 and parsedTorrent.resolution[0] not in config["resolutions"]:
303
+ filtered += 1
304
+
305
+ continue
306
+ if not "All" in config["languages"] and not parsedTorrent.is_multi_audio and not any(language in parsedTorrent.language for language in config["languages"]):
307
+ filtered += 1
308
+
309
+ continue
310
+
311
+ tasks.append(getTorrentHash(session, indexerManagerType, torrent))
312
+
313
+ torrentHashes = await asyncio.gather(*tasks)
314
+ torrentHashes = list(set([hash for hash in torrentHashes if hash]))
315
+
316
+ logger.info(f"{len(torrentHashes)} info hashes found for {name}")
317
+
318
+ if len(torrentHashes) == 0:
319
+ return {"streams": []}
320
+
321
+ getAvailability = await session.get(f"https://api.real-debrid.com/rest/1.0/torrents/instantAvailability/{'/'.join(torrentHashes)}", headers={
322
+ "Authorization": f"Bearer {config['debridApiKey']}"
323
+ })
324
+
325
+ files = {}
326
+
327
+ availability = await getAvailability.json()
328
+ for hash, details in availability.items():
329
+ if not "rd" in details:
330
+ continue
331
+
332
+ if type == "series":
333
+ for variants in details["rd"]:
334
+ for index, file in variants.items():
335
+ filename = file["filename"].lower()
336
+
337
+ if not isVideo(filename):
338
+ continue
339
+
340
+ filenameParsed = RTN.parse(file["filename"])
341
+ if season in filenameParsed.season and episode in filenameParsed.episode:
342
+ files[hash] = {
343
+ "index": index,
344
+ "title": file["filename"],
345
+ "size": file["filesize"]
346
+ }
347
+
348
+ continue
349
+
350
+ for variants in details["rd"]:
351
+ for index, file in variants.items():
352
+ filename = file["filename"].lower()
353
+
354
+ if not isVideo(filename):
355
+ continue
356
+
357
+ files[hash] = {
358
+ "index": index,
359
+ "title": file["filename"],
360
+ "size": file["filesize"]
361
+ }
362
+
363
+ rankedFiles = set()
364
+ for hash in files:
365
+ # try:
366
+ rankedFile = rtn.rank(files[hash]["title"], hash) # , remove_trash=True, correct_title=name - removed because it's not working great
367
+ rankedFiles.add(rankedFile)
368
+ # except:
369
+ # continue
370
+
371
+ sortedRankedFiles = RTN.sort_torrents(rankedFiles)
372
+
373
+ logger.info(f"{len(sortedRankedFiles)} cached files found on Real-Debrid for {name}")
374
+
375
+ if len(sortedRankedFiles) == 0:
376
+ return {"streams": []}
377
+
378
+ sortedRankedFiles = {
379
+ key: (value.model_dump() if isinstance(value, RTN.Torrent) else value)
380
+ for key, value in sortedRankedFiles.items()
381
+ }
382
+ for hash in sortedRankedFiles: # needed for caching
383
+ sortedRankedFiles[hash]["data"]["title"] = files[hash]["title"]
384
+ sortedRankedFiles[hash]["data"]["size"] = files[hash]["size"]
385
+ sortedRankedFiles[hash]["data"]["index"] = files[hash]["index"]
386
+
387
+ jsonData = json.dumps(sortedRankedFiles).replace("'", "''")
388
+ await database.execute(f"INSERT INTO cache (cacheKey, results, timestamp) VALUES ('{cacheKey}', '{jsonData}', {time.time()})")
389
+ logger.info(f"Results have been cached for {name}")
390
+
391
+ results = []
392
+ for hash in sortedRankedFiles:
393
+ results.append({
394
+ "name": f"[RD⚡] Comet {sortedRankedFiles[hash]['data']['resolution'][0] if len(sortedRankedFiles[hash]['data']['resolution']) > 0 else 'Unknown'}",
395
+ "title": f"{sortedRankedFiles[hash]['data']['title']}\n💾 {bytesToSize(sortedRankedFiles[hash]['data']['size'])}",
396
+ "url": f"{request.url.scheme}://{request.url.netloc}/{b64config}/playback/{hash}/{sortedRankedFiles[hash]['data']['index']}"
397
+ })
398
+
399
+ return {
400
+ "streams": results
401
+ }
402
+
403
+ async def generateDownloadLink(debridApiKey: str, hash: str, index: str):
404
+ try:
405
+ async with aiohttp.ClientSession() as session:
406
+ checkBlacklisted = await session.get("https://real-debrid.com/vpn")
407
+ checkBlacklisted = await checkBlacklisted.text()
408
+
409
+ proxy = None
410
+ if "Your ISP or VPN provider IP address is currently blocked on our website" in checkBlacklisted:
411
+ proxy = os.getenv("DEBRID_PROXY_URL", "http://127.0.0.1:1080")
412
+
413
+ logger.warning(f"Real-Debrid blacklisted server's IP. Switching to proxy {proxy} for {hash}|{index}")
414
+
415
+ addMagnet = await session.post(f"https://api.real-debrid.com/rest/1.0/torrents/addMagnet", headers={
416
+ "Authorization": f"Bearer {debridApiKey}"
417
+ }, data={
418
+ "magnet": f"magnet:?xt=urn:btih:{hash}"
419
+ }, proxy=proxy)
420
+ addMagnet = await addMagnet.json()
421
+
422
+ getMagnetInfo = await session.get(addMagnet["uri"], headers={
423
+ "Authorization": f"Bearer {debridApiKey}"
424
+ }, proxy=proxy)
425
+ getMagnetInfo = await getMagnetInfo.json()
426
+
427
+ selectFile = await session.post(f"https://api.real-debrid.com/rest/1.0/torrents/selectFiles/{addMagnet['id']}", headers={
428
+ "Authorization": f"Bearer {debridApiKey}"
429
+ }, data={
430
+ "files": index
431
+ }, proxy=proxy)
432
+
433
+ getMagnetInfo = await session.get(addMagnet["uri"], headers={
434
+ "Authorization": f"Bearer {debridApiKey}"
435
+ }, proxy=proxy)
436
+ getMagnetInfo = await getMagnetInfo.json()
437
+
438
+ unrestrictLink = await session.post(f"https://api.real-debrid.com/rest/1.0/unrestrict/link", headers={
439
+ "Authorization": f"Bearer {debridApiKey}"
440
+ }, data={
441
+ "link": getMagnetInfo["links"][0]
442
+ }, proxy=proxy)
443
+ unrestrictLink = await unrestrictLink.json()
444
+
445
+ return unrestrictLink["download"]
446
+ except Exception as e:
447
+ logger.warning(f"Exception while getting download link from Real Debrid for {hash}|{index}: {e}")
448
+
449
+ return "https://comet.fast"
450
+
451
+ @app.head("/{b64config}/playback/{hash}/{index}")
452
+ async def stream(b64config: str, hash: str, index: str):
453
+ config = configChecking(b64config)
454
+ if not config:
455
+ return
456
+
457
+ downloadLink = await generateDownloadLink(config["debridApiKey"], hash, index)
458
+
459
+ return RedirectResponse(downloadLink, status_code=302)
460
+
461
+ @app.get("/{b64config}/playback/{hash}/{index}")
462
+ async def stream(b64config: str, hash: str, index: str):
463
+ config = configChecking(b64config)
464
+ if not config:
465
+ return
466
+
467
+ downloadLink = await generateDownloadLink(config["debridApiKey"], hash, index)
468
+
469
+ return RedirectResponse(downloadLink, status_code=302)
comet/templates/index.html ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html class="sl-theme-dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
+ <meta content="Comet" property="og:title" />
7
+ <meta content="Stremio's fastest torrent/debrid search add-on." property="og:description" />
8
+ <meta content="https://comet.fast" property="og:url" />
9
+ <meta content="https://i.imgur.com/jmVoVMu.jpeg" property="og:image" />
10
+ <meta content="#6b6ef8" data-react-helmet="true" name="theme-color" />
11
+
12
+ <title>Comet - Stremio's fastest torrent/debrid search add-on.</title>
13
+ <link id="favicon" rel="icon" type="image/x-icon" href="https://i.imgur.com/jmVoVMu.jpeg">
14
+
15
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/themes/dark.css" />
16
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.15.1/cdn/shoelace-autoloader.js"></script>
17
+
18
+ <style>
19
+ :not(:defined) {
20
+ visibility: hidden;
21
+ }
22
+
23
+ body {
24
+ display: flex;
25
+ flex-direction: column;
26
+ justify-content: center;
27
+ align-items: center;
28
+ min-height: 100vh;
29
+ margin: 0;
30
+ background: radial-gradient(ellipse at bottom, #25292c 0%, #0c0d13 100%);
31
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
32
+ font-size: 1rem;
33
+ font-weight: 400;
34
+ }
35
+
36
+ ::-webkit-scrollbar {
37
+ overflow: hidden;
38
+ }
39
+
40
+ .header {
41
+ text-align: center;
42
+ width: 40%;
43
+ margin-bottom: 20px;
44
+ }
45
+
46
+ .comet-text {
47
+ font-size: calc(1.375rem + 1.5vw);
48
+ font-weight: 500;
49
+ margin-bottom: 0;
50
+ }
51
+
52
+ .form-container {
53
+ background-color: #1a1d20;
54
+ padding: 2rem;
55
+ border-radius: 0.375rem;
56
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
57
+ width: 50%;
58
+ margin-bottom: 50px;
59
+ }
60
+
61
+ .form-item {
62
+ margin-bottom: 0.75rem;
63
+ }
64
+
65
+ .centered-item {
66
+ display: flex;
67
+ justify-content: center;
68
+ }
69
+
70
+ .stars {
71
+ position: fixed;
72
+ top: 0;
73
+ left: 0;
74
+ width: 100%;
75
+ height: 120%;
76
+ transform: rotate(-45deg);
77
+ z-index: -1;
78
+ }
79
+ .star {
80
+ --star-color: var(--primary-color);
81
+ --star-tail-length: 6em;
82
+ --star-tail-height: 2px;
83
+ --star-width: calc(var(--star-tail-length) / 6);
84
+ --fall-duration: 9s;
85
+ --tail-fade-duration: var(--fall-duration);
86
+ position: absolute;
87
+ top: var(--top-offset);
88
+ left: 0;
89
+ width: var(--star-tail-length);
90
+ height: var(--star-tail-height);
91
+ color: var(--star-color);
92
+ background: linear-gradient(45deg, currentColor, transparent);
93
+ border-radius: 50%;
94
+ filter: drop-shadow(0 0 6px currentColor);
95
+ transform: translate3d(104em, 0, 0);
96
+ animation: fall var(--fall-duration) var(--fall-delay) linear infinite, tail-fade var(--tail-fade-duration) var(--fall-delay) ease-out infinite;
97
+ }
98
+ @media screen and (max-width: 750px) {
99
+ .star {
100
+ animation: fall var(--fall-duration) var(--fall-delay) linear infinite;
101
+ }
102
+ }
103
+ .star:nth-child(1) {
104
+ --star-tail-length: 6.14em;
105
+ --top-offset: 3.64vh;
106
+ --fall-duration: 10.878s;
107
+ --fall-delay: 0.034s;
108
+ }
109
+ .star:nth-child(2) {
110
+ --star-tail-length: 5.08em;
111
+ --top-offset: 69.69vh;
112
+ --fall-duration: 11.372s;
113
+ --fall-delay: 1.679s;
114
+ }
115
+ .star:nth-child(3) {
116
+ --star-tail-length: 6.1em;
117
+ --top-offset: 64.3vh;
118
+ --fall-duration: 7.088s;
119
+ --fall-delay: 3.382s;
120
+ }
121
+ .star:nth-child(4) {
122
+ --star-tail-length: 6.66em;
123
+ --top-offset: 34.91vh;
124
+ --fall-duration: 6.184s;
125
+ --fall-delay: 9.61s;
126
+ }
127
+ .star:nth-child(5) {
128
+ --star-tail-length: 6.89em;
129
+ --top-offset: 24.92vh;
130
+ --fall-duration: 9.465s;
131
+ --fall-delay: 9.12s;
132
+ }
133
+ .star:nth-child(6) {
134
+ --star-tail-length: 6.5em;
135
+ --top-offset: 51.9vh;
136
+ --fall-duration: 10.52s;
137
+ --fall-delay: 0.214s;
138
+ }
139
+ .star:nth-child(7) {
140
+ --star-tail-length: 5.58em;
141
+ --top-offset: 24.15vh;
142
+ --fall-duration: 8.9s;
143
+ --fall-delay: 0.499s;
144
+ }
145
+ .star:nth-child(8) {
146
+ --star-tail-length: 6.26em;
147
+ --top-offset: 59.4vh;
148
+ --fall-duration: 7.671s;
149
+ --fall-delay: 1.694s;
150
+ }
151
+ .star:nth-child(9) {
152
+ --star-tail-length: 6.46em;
153
+ --top-offset: 54.52vh;
154
+ --fall-duration: 6.484s;
155
+ --fall-delay: 4.616s;
156
+ }
157
+ .star:nth-child(10) {
158
+ --star-tail-length: 5.34em;
159
+ --top-offset: 74.03vh;
160
+ --fall-duration: 8.565s;
161
+ --fall-delay: 1.159s;
162
+ }
163
+ .star:nth-child(11) {
164
+ --star-tail-length: 6.84em;
165
+ --top-offset: 90.94vh;
166
+ --fall-duration: 10.133s;
167
+ --fall-delay: 6.108s;
168
+ }
169
+ .star:nth-child(12) {
170
+ --star-tail-length: 5.48em;
171
+ --top-offset: 97.27vh;
172
+ --fall-duration: 10.248s;
173
+ --fall-delay: 4.186s;
174
+ }
175
+ .star:nth-child(13) {
176
+ --star-tail-length: 7.21em;
177
+ --top-offset: 17.75vh;
178
+ --fall-duration: 10.549s;
179
+ --fall-delay: 1.868s;
180
+ }
181
+ .star:nth-child(14) {
182
+ --star-tail-length: 6.35em;
183
+ --top-offset: 94.98vh;
184
+ --fall-duration: 9.682s;
185
+ --fall-delay: 9.327s;
186
+ }
187
+ .star:nth-child(15) {
188
+ --star-tail-length: 5.18em;
189
+ --top-offset: 52.87vh;
190
+ --fall-duration: 9.934s;
191
+ --fall-delay: 8.919s;
192
+ }
193
+ .star:nth-child(16) {
194
+ --star-tail-length: 5.07em;
195
+ --top-offset: 50.26vh;
196
+ --fall-duration: 11.826s;
197
+ --fall-delay: 8.478s;
198
+ }
199
+ .star:nth-child(17) {
200
+ --star-tail-length: 7.01em;
201
+ --top-offset: 85.73vh;
202
+ --fall-duration: 9.87s;
203
+ --fall-delay: 1.206s;
204
+ }
205
+ .star:nth-child(18) {
206
+ --star-tail-length: 6.39em;
207
+ --top-offset: 2.81vh;
208
+ --fall-duration: 10.292s;
209
+ --fall-delay: 4.394s;
210
+ }
211
+ .star:nth-child(19) {
212
+ --star-tail-length: 5.27em;
213
+ --top-offset: 56.83vh;
214
+ --fall-duration: 8.944s;
215
+ --fall-delay: 8.779s;
216
+ }
217
+ .star:nth-child(20) {
218
+ --star-tail-length: 6.07em;
219
+ --top-offset: 54.55vh;
220
+ --fall-duration: 9.324s;
221
+ --fall-delay: 7.375s;
222
+ }
223
+ .star:nth-child(21) {
224
+ --star-tail-length: 6.1em;
225
+ --top-offset: 87.67vh;
226
+ --fall-duration: 11.943s;
227
+ --fall-delay: 6.919s;
228
+ }
229
+ .star:nth-child(22) {
230
+ --star-tail-length: 5.98em;
231
+ --top-offset: 70.04vh;
232
+ --fall-duration: 9.995s;
233
+ --fall-delay: 4.472s;
234
+ }
235
+ .star:nth-child(23) {
236
+ --star-tail-length: 6.34em;
237
+ --top-offset: 77.19vh;
238
+ --fall-duration: 10.073s;
239
+ --fall-delay: 8.354s;
240
+ }
241
+ .star:nth-child(24) {
242
+ --star-tail-length: 6.95em;
243
+ --top-offset: 14.48vh;
244
+ --fall-duration: 9.028s;
245
+ --fall-delay: 7.638s;
246
+ }
247
+ .star:nth-child(25) {
248
+ --star-tail-length: 6.23em;
249
+ --top-offset: 8.48vh;
250
+ --fall-duration: 7.427s;
251
+ --fall-delay: 0.915s;
252
+ }
253
+ .star:nth-child(26) {
254
+ --star-tail-length: 5.09em;
255
+ --top-offset: 6.56vh;
256
+ --fall-duration: 7.706s;
257
+ --fall-delay: 2.841s;
258
+ }
259
+ .star:nth-child(27) {
260
+ --star-tail-length: 7.01em;
261
+ --top-offset: 92.85vh;
262
+ --fall-duration: 7.359s;
263
+ --fall-delay: 7.229s;
264
+ }
265
+ .star:nth-child(28) {
266
+ --star-tail-length: 5.49em;
267
+ --top-offset: 27.89vh;
268
+ --fall-duration: 10.344s;
269
+ --fall-delay: 2.346s;
270
+ }
271
+ .star:nth-child(29) {
272
+ --star-tail-length: 5.82em;
273
+ --top-offset: 56.08vh;
274
+ --fall-duration: 10.911s;
275
+ --fall-delay: 4.231s;
276
+ }
277
+ .star:nth-child(30) {
278
+ --star-tail-length: 7.24em;
279
+ --top-offset: 22.54vh;
280
+ --fall-duration: 9.344s;
281
+ --fall-delay: 2.112s;
282
+ }
283
+ .star:nth-child(31) {
284
+ --star-tail-length: 6.8em;
285
+ --top-offset: 59.49vh;
286
+ --fall-duration: 7.059s;
287
+ --fall-delay: 0.924s;
288
+ }
289
+ .star:nth-child(32) {
290
+ --star-tail-length: 5.22em;
291
+ --top-offset: 44.01vh;
292
+ --fall-duration: 10.121s;
293
+ --fall-delay: 0.591s;
294
+ }
295
+ .star:nth-child(33) {
296
+ --star-tail-length: 6.1em;
297
+ --top-offset: 78.61vh;
298
+ --fall-duration: 8.306s;
299
+ --fall-delay: 4.403s;
300
+ }
301
+ .star:nth-child(34) {
302
+ --star-tail-length: 7.26em;
303
+ --top-offset: 85.76vh;
304
+ --fall-duration: 7.058s;
305
+ --fall-delay: 6.772s;
306
+ }
307
+ .star:nth-child(35) {
308
+ --star-tail-length: 7.01em;
309
+ --top-offset: 77.17vh;
310
+ --fall-duration: 6.29s;
311
+ --fall-delay: 1.468s;
312
+ }
313
+ .star:nth-child(36) {
314
+ --star-tail-length: 5.17em;
315
+ --top-offset: 13.63vh;
316
+ --fall-duration: 6.739s;
317
+ --fall-delay: 0.019s;
318
+ }
319
+ .star:nth-child(37) {
320
+ --star-tail-length: 6.41em;
321
+ --top-offset: 70.18vh;
322
+ --fall-duration: 6.177s;
323
+ --fall-delay: 8.148s;
324
+ }
325
+ .star:nth-child(38) {
326
+ --star-tail-length: 5.32em;
327
+ --top-offset: 62.65vh;
328
+ --fall-duration: 10.476s;
329
+ --fall-delay: 0.98s;
330
+ }
331
+ .star:nth-child(39) {
332
+ --star-tail-length: 7.24em;
333
+ --top-offset: 66.12vh;
334
+ --fall-duration: 8.449s;
335
+ --fall-delay: 4.255s;
336
+ }
337
+ .star:nth-child(40) {
338
+ --star-tail-length: 6.73em;
339
+ --top-offset: 14.73vh;
340
+ --fall-duration: 9.857s;
341
+ --fall-delay: 6.867s;
342
+ }
343
+ .star:nth-child(41) {
344
+ --star-tail-length: 5.25em;
345
+ --top-offset: 45.23vh;
346
+ --fall-duration: 7.898s;
347
+ --fall-delay: 4.966s;
348
+ }
349
+ .star:nth-child(42) {
350
+ --star-tail-length: 6.73em;
351
+ --top-offset: 36.17vh;
352
+ --fall-duration: 7.32s;
353
+ --fall-delay: 3.93s;
354
+ }
355
+ .star:nth-child(43) {
356
+ --star-tail-length: 7.38em;
357
+ --top-offset: 83.09vh;
358
+ --fall-duration: 7.394s;
359
+ --fall-delay: 5.388s;
360
+ }
361
+ .star:nth-child(44) {
362
+ --star-tail-length: 5.18em;
363
+ --top-offset: 98.36vh;
364
+ --fall-duration: 6.905s;
365
+ --fall-delay: 2.771s;
366
+ }
367
+ .star:nth-child(45) {
368
+ --star-tail-length: 6.66em;
369
+ --top-offset: 27.99vh;
370
+ --fall-duration: 7.62s;
371
+ --fall-delay: 3.624s;
372
+ }
373
+ .star:nth-child(46) {
374
+ --star-tail-length: 5.19em;
375
+ --top-offset: 92vh;
376
+ --fall-duration: 9.158s;
377
+ --fall-delay: 1.984s;
378
+ }
379
+ .star:nth-child(47) {
380
+ --star-tail-length: 6.16em;
381
+ --top-offset: 2.87vh;
382
+ --fall-duration: 9.266s;
383
+ --fall-delay: 4.04s;
384
+ }
385
+ .star:nth-child(48) {
386
+ --star-tail-length: 6.34em;
387
+ --top-offset: 19.39vh;
388
+ --fall-duration: 7.503s;
389
+ --fall-delay: 0.045s;
390
+ }
391
+ .star:nth-child(49) {
392
+ --star-tail-length: 6.85em;
393
+ --top-offset: 79.92vh;
394
+ --fall-duration: 7.472s;
395
+ --fall-delay: 1.514s;
396
+ }
397
+ .star:nth-child(50) {
398
+ --star-tail-length: 7.35em;
399
+ --top-offset: 63.71vh;
400
+ --fall-duration: 8.117s;
401
+ --fall-delay: 4.46s;
402
+ }
403
+ .star::before, .star::after {
404
+ position: absolute;
405
+ content: "";
406
+ top: 0;
407
+ left: calc(var(--star-width) / -2);
408
+ width: var(--star-width);
409
+ height: 100%;
410
+ background: linear-gradient(45deg, transparent, currentColor, transparent);
411
+ border-radius: inherit;
412
+ animation: blink 2s linear infinite;
413
+ }
414
+ .star::before {
415
+ transform: rotate(45deg);
416
+ }
417
+ .star::after {
418
+ transform: rotate(-45deg);
419
+ }
420
+
421
+ @keyframes fall {
422
+ to {
423
+ transform: translate3d(-30em, 0, 0);
424
+ }
425
+ }
426
+ @keyframes tail-fade {
427
+ 0%, 50% {
428
+ width: var(--star-tail-length);
429
+ opacity: 1;
430
+ }
431
+ 70%, 80% {
432
+ width: 0;
433
+ opacity: 0.4;
434
+ }
435
+ 100% {
436
+ width: 0;
437
+ opacity: 0;
438
+ }
439
+ }
440
+ @keyframes blink {
441
+ 50% {
442
+ opacity: 0.6;
443
+ }
444
+ }
445
+ </style>
446
+ </head>
447
+
448
+ <body>
449
+ <div class="stars"></div>
450
+
451
+ <script>
452
+ document.addEventListener("DOMContentLoaded", function() {
453
+ const starsContainer = document.querySelector('.stars');
454
+ const isMobile = window.innerWidth <= 750;
455
+ const starCount = isMobile ? 30 : 30; // Reduce the number of stars on mobile because of performance issues (currently disabled because we need to find a better workarround)
456
+
457
+ for (let i = 0; i < starCount; i++) {
458
+ const star = document.createElement("div");
459
+ star.classList.add("star");
460
+ starsContainer.appendChild(star);
461
+ }
462
+ });
463
+ </script>
464
+
465
+ <div class="header">
466
+ <p class="comet-text">🚀 Comet - <a href="https://github.com/g0ldyy/comet">GitHub</a></p>
467
+ {{CUSTOM_HEADER_HTML|safe}}
468
+ </div>
469
+
470
+ <div class="form-container">
471
+ <div class="form-item">
472
+ <sl-select id="indexers" multiple clearable label="Indexers" placeholder="Select indexers"></sl-select>
473
+ </div>
474
+
475
+ <div class="form-item">
476
+ <sl-select id="languages" multiple clearable label="Languages" placeholder="Select languages"></sl-select>
477
+ </div>
478
+
479
+ <div class="form-item">
480
+ <sl-select id="resolutions" multiple clearable label="Resolutions" placeholder="Select resolutions"></sl-select>
481
+ </div>
482
+
483
+ <div class="form-item">
484
+ <sl-input id="maxResults" type="number" min=0 value=0 label="Max Results" placeholder="Enter max results"></sl-input>
485
+ </div>
486
+
487
+ <div class="form-item">
488
+ <sl-select id="debridService" value="realdebrid" label="Debrid Service" placeholder="Select debrid service">
489
+ <sl-option value="realdebrid">Real-Debrid</sl-option>
490
+ </sl-select>
491
+ </div>
492
+
493
+ <div class="form-item">
494
+ <sl-input id="debridApiKey" label="Debrid API Key" placeholder="Enter API key"></sl-input>
495
+ </div>
496
+
497
+ <div class="centered-item">
498
+ <sl-button id="install" variant="neutral">Install</sl-button>
499
+
500
+ <sl-alert variant="neutral" duration="3000" closable>
501
+ <sl-icon slot="icon" name="clipboard2-check"></sl-icon>
502
+ <strong>The Stremio addon link has been automatically copied.</strong>
503
+ </sl-alert>
504
+
505
+ <script type="module">
506
+ let defaultLanguages = [];
507
+ let defaultResolutions = [];
508
+
509
+ document.addEventListener("DOMContentLoaded", function() {
510
+ fetch("/static/config.json")
511
+ .then(response => response.json())
512
+ .then(data => {
513
+ populateSelect("indexers", data.indexers);
514
+ populateSelect("languages", data.languages);
515
+ populateSelect("resolutions", data.resolutions);
516
+
517
+ defaultLanguages = data.languages;
518
+ defaultResolutions = data.resolutions;
519
+ });
520
+ });
521
+
522
+ function populateSelect(selectId, options) {
523
+ const selectElement = document.getElementById(selectId);
524
+ options.forEach(option => {
525
+ const optionElement = document.createElement("sl-option");
526
+ optionElement.value = option;
527
+ optionElement.textContent = option;
528
+ selectElement.appendChild(optionElement);
529
+ });
530
+ selectElement.value = options;
531
+ }
532
+
533
+ const button = document.querySelector("sl-button");
534
+ const alert = document.querySelector('sl-alert[variant="neutral"]');
535
+ button.addEventListener("click", () => {
536
+ const debridService = document.getElementById("debridService").value;
537
+ const debridApiKey = document.getElementById("debridApiKey").value;
538
+ const indexers = Array.from(document.getElementById("indexers").selectedOptions).map(option => option.value);
539
+ const languages = Array.from(document.getElementById("languages").selectedOptions).map(option => option.value);
540
+ const resolutions = Array.from(document.getElementById("resolutions").selectedOptions).map(option => option.value);
541
+ const maxResults = document.getElementById("maxResults").value;
542
+
543
+ const selectedLanguages = languages.length === defaultLanguages.length && languages.every((val, index) => val === defaultLanguages[index]) ? ["All"] : languages;
544
+ const selectedResolutions = resolutions.length === defaultResolutions.length && resolutions.every((val, index) => val === defaultResolutions[index]) ? ["All"] : resolutions;
545
+
546
+ const settings = {
547
+ debridService: debridService,
548
+ debridApiKey: debridApiKey,
549
+ indexers: indexers,
550
+ maxResults: parseInt(maxResults),
551
+ resolutions: selectedResolutions,
552
+ languages: selectedLanguages
553
+ };
554
+
555
+ navigator.clipboard.writeText(`${window.location.origin}/${btoa(JSON.stringify(settings))}/manifest.json`).then(() => {
556
+ alert.toast();
557
+ });
558
+ });
559
+ </script>
560
+ </div>
561
+ </div>
562
+ </body>
563
+ </html>
comet/utils/__init__.py ADDED
File without changes
comet/utils/general.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ translationTable = {
4
+ 'ā': 'a', 'ă': 'a', 'ą': 'a', 'ć': 'c', 'č': 'c', 'ç': 'c',
5
+ 'ĉ': 'c', 'ċ': 'c', 'ď': 'd', 'đ': 'd', 'è': 'e', 'é': 'e',
6
+ 'ê': 'e', 'ë': 'e', 'ē': 'e', 'ĕ': 'e', 'ę': 'e', 'ě': 'e',
7
+ 'ĝ': 'g', 'ğ': 'g', 'ġ': 'g', 'ģ': 'g', 'ĥ': 'h', 'î': 'i',
8
+ 'ï': 'i', 'ì': 'i', 'í': 'i', 'ī': 'i', 'ĩ': 'i', 'ĭ': 'i',
9
+ 'ı': 'i', 'ĵ': 'j', 'ķ': 'k', 'ĺ': 'l', 'ļ': 'l', 'ł': 'l',
10
+ 'ń': 'n', 'ň': 'n', 'ñ': 'n', 'ņ': 'n', 'ʼn': 'n', 'ó': 'o',
11
+ 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ø': 'o', 'ō': 'o', 'ő': 'o',
12
+ 'œ': 'oe', 'ŕ': 'r', 'ř': 'r', 'ŗ': 'r', 'š': 's', 'ş': 's',
13
+ 'ś': 's', 'ș': 's', 'ß': 'ss', 'ť': 't', 'ţ': 't', 'ū': 'u',
14
+ 'ŭ': 'u', 'ũ': 'u', 'û': 'u', 'ü': 'u', 'ù': 'u', 'ú': 'u',
15
+ 'ų': 'u', 'ű': 'u', 'ŵ': 'w', 'ý': 'y', 'ÿ': 'y', 'ŷ': 'y',
16
+ 'ž': 'z', 'ż': 'z', 'ź': 'z', 'æ': 'ae', 'ǎ': 'a', 'ǧ': 'g',
17
+ 'ə': 'e', 'ƒ': 'f', 'ǐ': 'i', 'ǒ': 'o', 'ǔ': 'u', 'ǚ': 'u',
18
+ 'ǜ': 'u', 'ǹ': 'n', 'ǻ': 'a', 'ǽ': 'ae', 'ǿ': 'o',
19
+ }
20
+ translationTable = str.maketrans(translationTable)
21
+
22
+ def translate(title: str):
23
+ return title.translate(translationTable)
24
+
25
+ def isVideo(title: str):
26
+ return title.endswith(tuple([".mkv", ".mp4", ".avi", ".mov", ".flv", ".wmv", ".webm", ".mpg", ".mpeg", ".m4v", ".3gp", ".3g2", ".ogv", ".ogg", ".drc", ".gif", ".gifv", ".mng", ".avi", ".mov", ".qt", ".wmv", ".yuv", ".rm", ".rmvb", ".asf", ".amv", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".mpg", ".mpeg", ".m2v", ".m4v", ".svi", ".3gp", ".3g2", ".mxf", ".roq", ".nsv", ".flv", ".f4v", ".f4p", ".f4a", ".f4b"]))
27
+
28
+ def bytesToSize(bytes: int):
29
+ sizes = ["Bytes", "KB", "MB", "GB", "TB"]
30
+
31
+ if bytes == 0:
32
+ return "0 Byte"
33
+
34
+ i = int(math.floor(math.log(bytes, 1024)))
35
+
36
+ return f"{round(bytes / math.pow(1024, i), 2)} {sizes[i]}"
comet/utils/logger.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ from loguru import logger
3
+
4
+ def setupLogger(level: str):
5
+ logger.level("INFO", icon="📰", color="<fg #FC5F39>")
6
+ logger.level("DEBUG", icon="🕸️", color="<fg #DC5F00>")
7
+ logger.level("WARNING", icon="⚠️", color="<fg #DC5F00>")
8
+
9
+ log_format = (
10
+ "<white>{time:YYYY-MM-DD}</white> <magenta>{time:HH:mm:ss}</magenta> | "
11
+ "<level>{level.icon}</level> <level>{level}</level> | "
12
+ "<cyan>{module}</cyan>.<cyan>{function}</cyan> - <level>{message}</level>"
13
+ )
14
+
15
+ logger.configure(handlers=[
16
+ {
17
+ "sink": sys.stderr,
18
+ "level": level,
19
+ "format": log_format,
20
+ "backtrace": False,
21
+ "diagnose": False,
22
+ "enqueue": True,
23
+ }
24
+ ])
25
+
26
+ setupLogger("DEBUG")