quasarr 0.1.6__py3-none-any.whl → 1.23.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of quasarr might be problematic. Click here for more details.

Files changed (77) hide show
  1. quasarr/__init__.py +316 -42
  2. quasarr/api/__init__.py +187 -0
  3. quasarr/api/arr/__init__.py +387 -0
  4. quasarr/api/captcha/__init__.py +1189 -0
  5. quasarr/api/config/__init__.py +23 -0
  6. quasarr/api/sponsors_helper/__init__.py +166 -0
  7. quasarr/api/statistics/__init__.py +196 -0
  8. quasarr/downloads/__init__.py +319 -256
  9. quasarr/downloads/linkcrypters/__init__.py +0 -0
  10. quasarr/downloads/linkcrypters/al.py +237 -0
  11. quasarr/downloads/linkcrypters/filecrypt.py +444 -0
  12. quasarr/downloads/linkcrypters/hide.py +123 -0
  13. quasarr/downloads/packages/__init__.py +476 -0
  14. quasarr/downloads/sources/al.py +697 -0
  15. quasarr/downloads/sources/by.py +106 -0
  16. quasarr/downloads/sources/dd.py +76 -0
  17. quasarr/downloads/sources/dj.py +7 -0
  18. quasarr/downloads/sources/dl.py +199 -0
  19. quasarr/downloads/sources/dt.py +66 -0
  20. quasarr/downloads/sources/dw.py +14 -7
  21. quasarr/downloads/sources/he.py +112 -0
  22. quasarr/downloads/sources/mb.py +47 -0
  23. quasarr/downloads/sources/nk.py +54 -0
  24. quasarr/downloads/sources/nx.py +42 -83
  25. quasarr/downloads/sources/sf.py +159 -0
  26. quasarr/downloads/sources/sj.py +7 -0
  27. quasarr/downloads/sources/sl.py +90 -0
  28. quasarr/downloads/sources/wd.py +110 -0
  29. quasarr/downloads/sources/wx.py +127 -0
  30. quasarr/providers/cloudflare.py +204 -0
  31. quasarr/providers/html_images.py +22 -0
  32. quasarr/providers/html_templates.py +211 -104
  33. quasarr/providers/imdb_metadata.py +108 -3
  34. quasarr/providers/log.py +19 -0
  35. quasarr/providers/myjd_api.py +201 -40
  36. quasarr/providers/notifications.py +99 -11
  37. quasarr/providers/obfuscated.py +65 -0
  38. quasarr/providers/sessions/__init__.py +0 -0
  39. quasarr/providers/sessions/al.py +286 -0
  40. quasarr/providers/sessions/dd.py +78 -0
  41. quasarr/providers/sessions/dl.py +175 -0
  42. quasarr/providers/sessions/nx.py +76 -0
  43. quasarr/providers/shared_state.py +656 -79
  44. quasarr/providers/statistics.py +154 -0
  45. quasarr/providers/version.py +60 -1
  46. quasarr/providers/web_server.py +1 -1
  47. quasarr/search/__init__.py +144 -15
  48. quasarr/search/sources/al.py +448 -0
  49. quasarr/search/sources/by.py +204 -0
  50. quasarr/search/sources/dd.py +135 -0
  51. quasarr/search/sources/dj.py +213 -0
  52. quasarr/search/sources/dl.py +354 -0
  53. quasarr/search/sources/dt.py +265 -0
  54. quasarr/search/sources/dw.py +94 -67
  55. quasarr/search/sources/fx.py +89 -33
  56. quasarr/search/sources/he.py +196 -0
  57. quasarr/search/sources/mb.py +195 -0
  58. quasarr/search/sources/nk.py +188 -0
  59. quasarr/search/sources/nx.py +75 -21
  60. quasarr/search/sources/sf.py +374 -0
  61. quasarr/search/sources/sj.py +213 -0
  62. quasarr/search/sources/sl.py +246 -0
  63. quasarr/search/sources/wd.py +208 -0
  64. quasarr/search/sources/wx.py +337 -0
  65. quasarr/storage/config.py +39 -10
  66. quasarr/storage/setup.py +269 -97
  67. quasarr/storage/sqlite_database.py +6 -1
  68. quasarr-1.23.0.dist-info/METADATA +306 -0
  69. quasarr-1.23.0.dist-info/RECORD +77 -0
  70. {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/WHEEL +1 -1
  71. quasarr/arr/__init__.py +0 -423
  72. quasarr/captcha_solver/__init__.py +0 -284
  73. quasarr-0.1.6.dist-info/METADATA +0 -81
  74. quasarr-0.1.6.dist-info/RECORD +0 -31
  75. {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/entry_points.txt +0 -0
  76. {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info/licenses}/LICENSE +0 -0
  77. {quasarr-0.1.6.dist-info → quasarr-1.23.0.dist-info}/top_level.txt +0 -0
quasarr/arr/__init__.py DELETED
@@ -1,423 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # Quasarr
3
- # Project by https://github.com/rix1337
4
-
5
- import json
6
- import re
7
- import traceback
8
- from base64 import urlsafe_b64decode
9
- from datetime import datetime
10
- from xml.etree import ElementTree as ET
11
-
12
- import requests
13
- from bottle import Bottle, request, response
14
-
15
- from quasarr.captcha_solver import get_filecrypt_links
16
- from quasarr.downloads import download_package, delete_package, get_packages
17
- from quasarr.providers import shared_state
18
- from quasarr.providers.html_templates import render_button, render_centered_html
19
- from quasarr.providers.obfuscated import captcha_js, captcha_values
20
- from quasarr.providers.web_server import Server
21
- from quasarr.search import get_search_results
22
-
23
-
24
- def api(shared_state_dict, shared_state_lock):
25
- shared_state.set_state(shared_state_dict, shared_state_lock)
26
-
27
- app = Bottle()
28
-
29
- @app.get('/captcha')
30
- def serve_captcha():
31
- try:
32
- device = shared_state.values["device"]
33
- except KeyError:
34
- device = None
35
- if not device:
36
- return render_centered_html(f'''<h1>Quasarr</h1>
37
- <p>JDownloader connection not established.</p>''')
38
-
39
- protected = shared_state.get_db("protected").retrieve_all_titles()
40
- if not protected:
41
- return render_centered_html(f'''<h1>Quasarr</h1>
42
- <p>No protected packages found! CAPTCHA not needed.</p>''')
43
- else:
44
- package = protected[0]
45
- package_id = package[0]
46
- data = json.loads(package[1])
47
- title = data["title"]
48
- links = data["links"]
49
- password = data["password"]
50
-
51
- link_options = ""
52
- if len(links) > 1:
53
- for link in links:
54
- if "filecrypt." in link[0]:
55
- link_options += f'<option value="{link[0]}">{link[1]}</option>'
56
- link_select = f'''<div id="mirrors-select">
57
- <label for="link-select">Mirror:</label>
58
- <select id="link-select">
59
- {link_options}
60
- </select>
61
- </div>
62
- <script>
63
- document.getElementById("link-select").addEventListener("change", function() {{
64
- var selectedLink = this.value;
65
- document.getElementById("link-hidden").value = selectedLink;
66
- }});
67
- </script>
68
- '''
69
- else:
70
- link_select = f'<div id="mirrors-select">Mirror: <b>{links[0][1]}</b></div>'
71
-
72
- content = render_centered_html(r'''
73
- <script type="text/javascript">
74
- var api_key = "''' + captcha_values()["api_key"] + r'''";
75
- var endpoint = '/' + window.location.pathname.split('/')[1] + '/' + api_key + '.html';
76
- function handleToken(token) {
77
- document.getElementById("puzzle-captcha").remove();
78
- document.getElementById("mirrors-select").remove();
79
- document.getElementById("captcha-key").innerText = 'Using result "' + token + '" to decrypt links...';
80
- var link = document.getElementById("link-hidden").value;
81
- fetch('/decrypt-filecrypt', {
82
- method: 'POST',
83
- headers: {
84
- 'Content-Type': 'application/json',
85
- },
86
- body: JSON.stringify({
87
- token: token,
88
- ''' + f'''package_id: '{package_id}',
89
- title: '{title}',
90
- link: link,
91
- password: '{password}'
92
- ''' + '''})
93
- })
94
- .then(response => response.json())
95
- .then(data => {
96
- if (data.success) {
97
- document.getElementById("captcha-key").insertAdjacentHTML('afterend',
98
- '<p>Successful for: ' + data.title + '</p>');
99
- } else {
100
- document.getElementById("captcha-key").insertAdjacentHTML('afterend',
101
- '<p>Failed. Check console for details!</p>');
102
- }
103
- document.getElementById("reload-button").style.display = "block";
104
- });
105
- }
106
- ''' + captcha_js() + f'''</script>
107
- <div>
108
- <h1>Quasarr</h1>
109
- <div id="captcha-key"></div>
110
- {link_select}<br><br>
111
- <input type="hidden" id="link-hidden" value="{links[0][0]}" />
112
- <div id="puzzle-captcha" aria-style="mobile">
113
- <strong>Your adblocker prevents the captcha from loading. Disable it!</strong>
114
- </div>
115
- <div id="reload-button" style="display: none;">
116
- {render_button("Solve another CAPTCHA", "secondary", {
117
- "onclick": "location.reload()",
118
- })}</div>
119
- </div>
120
- </html>''')
121
-
122
- return content
123
-
124
- @app.post('/decrypt-filecrypt')
125
- def submit_token():
126
- protected = shared_state.get_db("protected").retrieve_all_titles()
127
- if not protected:
128
- return {"success": False, "title": "No protected packages found! CAPTCHA not needed."}
129
-
130
- download_links = []
131
-
132
- try:
133
- data = request.json
134
- token = data.get('token')
135
- package_id = data.get('package_id')
136
- title = data.get('title')
137
- link = data.get('link')
138
- password = data.get('password')
139
- if token:
140
- print(f"Received token: {token}")
141
- print(f"Decrypting links for {title}")
142
- download_links = get_filecrypt_links(shared_state, token, title, link, password)
143
-
144
- print(f"Decrypted {len(download_links)} download links for {title}")
145
-
146
- shared_state.download_package(download_links, title, password, package_id)
147
-
148
- shared_state.get_db("protected").delete(package_id)
149
-
150
- except Exception as e:
151
- print(f"Error decrypting: {e}")
152
-
153
- return {"success": bool(download_links), "title": title}
154
-
155
- @app.post('/captcha/<captcha_id>.html')
156
- def proxy(captcha_id):
157
- target_url = f"{captcha_values()["url"]}/captcha/{captcha_id}.html"
158
-
159
- headers = {key: value for key, value in request.headers.items() if key != 'Host'}
160
- data = request.body.read()
161
- resp = requests.post(target_url, headers=headers, data=data)
162
-
163
- response.content_type = resp.headers.get('Content-Type')
164
-
165
- content = resp.text
166
- content = re.sub(r'<script src="/(.*?)"></script>',
167
- f'<script src="{captcha_values()["url"]}/\\1"></script>', content)
168
- response.content_type = 'text/html'
169
- return content
170
-
171
- @app.post('/captcha/<captcha_id>.json')
172
- def specific_proxy(captcha_id):
173
- target_url = f"{captcha_values()["url"]}/captcha/{captcha_id}.json"
174
-
175
- headers = {key: value for key, value in request.headers.items() if key != 'Host'}
176
- data = request.body.read()
177
- resp = requests.post(target_url, headers=headers, data=data)
178
-
179
- response.content_type = resp.headers.get('Content-Type')
180
- return resp.content
181
-
182
- @app.get('/captcha/<captcha_id>/<uuid>/<filename>')
183
- def captcha_proxy(captcha_id, uuid, filename):
184
- new_url = f"{captcha_values()["url"]}/captcha/{captcha_id}/{uuid}/{filename}"
185
-
186
- try:
187
- external_response = requests.get(new_url, stream=True)
188
- external_response.raise_for_status()
189
- response.content_type = 'image/png'
190
- response.headers['Content-Disposition'] = f'inline; filename="{filename}"'
191
- return external_response.iter_content(chunk_size=8192)
192
-
193
- except requests.RequestException as e:
194
- response.status = 502
195
- return f"Error fetching resource: {e}"
196
-
197
- @app.post('/captcha/<captcha_id>/check')
198
- def captcha_check_proxy(captcha_id):
199
- new_url = f"{captcha_values()["url"]}/captcha/{captcha_id}/check"
200
- headers = {key: value for key, value in request.headers.items()}
201
-
202
- data = request.body.read()
203
- resp = requests.post(new_url, headers=headers, data=data)
204
-
205
- response.status = resp.status_code
206
- for header in resp.headers:
207
- if header.lower() not in ['content-encoding', 'transfer-encoding', 'content-length', 'connection']:
208
- response.set_header(header, resp.headers[header])
209
- return resp.content
210
-
211
- @app.get('/')
212
- def index():
213
- protected = shared_state.get_db("protected").retrieve_all_titles()
214
- captcha_hint = ""
215
- if protected:
216
- package_count = len(protected)
217
- package_text = f"Package{'s' if package_count > 1 else ''} waiting for CAPTCHA"
218
- amount_info = f": {package_count}" if package_count > 1 else ""
219
- button_text = f"Solve CAPTCHA{'s' if package_count > 1 else ''} here to decrypt links!"
220
-
221
- captcha_hint = f'''
222
- <h2>Links protected by CAPTCHA</h2>
223
- <p>{package_text}{amount_info}</p>
224
- <p>{render_button(button_text, "primary", {"onclick": "location.href='/captcha'"})}</p>
225
- '''
226
-
227
- info = f"""
228
- <h1>Quasarr</h1>
229
- <p>
230
- <code id="current-url" style="background-color: #f0f0f0; padding: 5px; border-radius: 3px;">
231
- {shared_state.values["internal_address"]}
232
- </code>
233
- </p>
234
- <p>Use this exact URL as 'Newznab Indexer' and 'SABnzbd Download Client' in Sonarr/Radarr.<br>
235
- Leave settings at default and use this API key: 'quasarr'</p>
236
- {captcha_hint}
237
- """
238
- return render_centered_html(info)
239
-
240
- @app.get('/download/')
241
- def fake_download_container():
242
- payload = request.query.payload
243
- decoded_payload = urlsafe_b64decode(payload).decode("utf-8").split("|")
244
- title = decoded_payload[0]
245
- url = decoded_payload[1]
246
- size_mb = decoded_payload[2]
247
- password = decoded_payload[3]
248
- return f'<nzb><file title="{title}" url="{url}" size_mb="{size_mb}" password="{password}"/></nzb>'
249
-
250
- @app.post('/api')
251
- def download():
252
- downloads = request.files.getall('name')
253
- nzo_ids = []
254
- for upload in downloads:
255
- file_content = upload.file.read()
256
- root = ET.fromstring(file_content)
257
- title = root.find(".//file").attrib["title"]
258
- url = root.find(".//file").attrib["url"]
259
- size_mb = root.find(".//file").attrib["size_mb"]
260
- password = root.find(".//file").attrib.get("password")
261
- print(f"Attempting download for {title}")
262
-
263
- request_from = request.headers.get('User-Agent')
264
-
265
- nzo_id = download_package(shared_state, request_from, title, url, size_mb, password)
266
- if nzo_id:
267
- print(f"{title} added successfully!")
268
- nzo_ids.append(nzo_id)
269
- else:
270
- print(f"{title} could not be added!")
271
-
272
- return {
273
- "status": True,
274
- "nzo_ids": nzo_ids
275
- }
276
-
277
- @app.get('/api')
278
- def fake_api():
279
- api_type = 'sabnzbd' if request.query.mode and request.query.apikey else 'newznab' if request.query.t else None
280
-
281
- if api_type == 'sabnzbd':
282
- try:
283
- mode = request.query.mode
284
- if mode == "version":
285
- return {
286
- "version": "4.3.2"
287
- }
288
- elif mode == "get_config":
289
- return {
290
- "config": {
291
- "misc": {
292
- "quasarr": True,
293
- "complete_dir": "/tmp/"
294
- },
295
- "categories": [
296
- {
297
- "name": "*",
298
- "order": 0,
299
- "dir": "",
300
- },
301
- {
302
- "name": "movies",
303
- "order": 1,
304
- "dir": "",
305
- },
306
- {
307
- "name": "tv",
308
- "order": 2,
309
- "dir": "",
310
- }
311
- ]
312
- }
313
- }
314
- elif mode == "fullstatus":
315
- return {
316
- "status": {
317
- "quasarr": True
318
- }
319
- }
320
- elif mode == "queue" or mode == "history":
321
- if request.query.name and request.query.name == "delete":
322
- package_id = request.query.value
323
- deleted = delete_package(shared_state, package_id)
324
- return {
325
- "status": deleted,
326
- "nzo_ids": [package_id]
327
- }
328
-
329
- packages = get_packages(shared_state)
330
- if mode == "queue":
331
- return {
332
- "queue": {
333
- "paused": False,
334
- "slots": packages["queue"]
335
- }
336
- }
337
- elif mode == "history":
338
- return {
339
- "history": {
340
- "paused": False,
341
- "slots": packages["history"]
342
- }
343
- }
344
- except Exception as e:
345
- print(f"Error loading packages: {e}")
346
- print(traceback.format_exc())
347
- return {
348
- "status": False
349
- }
350
-
351
- elif api_type == 'newznab':
352
- try:
353
- mode = request.query.t
354
- if mode == 'movie':
355
- if request.query.imdbid:
356
- imdb_id = f"tt{request.query.imdbid}"
357
- else:
358
- imdb_id = None
359
-
360
- request_from = request.headers.get('User-Agent')
361
-
362
- releases = get_search_results(shared_state, request_from, imdb_id=imdb_id)
363
-
364
- items = ""
365
-
366
- if not releases:
367
- items += f'''
368
- <item>
369
- <title>No releases found</title>
370
- <link></link>
371
- <pubDate>{datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')}</pubDate>
372
- <enclosure url="_" length="0" type="application/x-nzb"/>
373
- <guid></guid>
374
- <comments></comments>
375
- <description></description>
376
- </item>'''
377
-
378
- for release in releases:
379
- release = release["details"]
380
-
381
- items += f'''
382
- <item>
383
- <title>{release["title"]}</title>
384
- <guid isPermaLink="True">{release["link"]}</guid>
385
- <link>{release["link"]}</link>
386
- <comments>{release["source"]}</comments>
387
- <pubDate>{release["date"]}</pubDate>
388
- <enclosure url="{release["link"]}" length="{release["size"]}" type="application/x-nzb" />
389
- </item>'''
390
-
391
- return f'''<?xml version="1.0" encoding="UTF-8"?>
392
- <rss version="2.0">
393
- <channel>
394
- {items}
395
- </channel>
396
- </rss>'''
397
- elif mode == 'caps':
398
- return '''<?xml version="1.0" encoding="UTF-8"?>
399
- <caps>
400
- <categories>
401
- <category id="2000" name="Movies">
402
- <subcat id="2010" name="Foreign"/>
403
- <subcat id="2020" name="Other"/>
404
- <subcat id="2030" name="SD"/>
405
- <subcat id="2040" name="HD"/>
406
- <subcat id="2050" name="BluRay"/>
407
- <subcat id="2060" name="3D"/>
408
- </category>
409
- <category id="5000" name="TV">
410
- <subcat id="5020" name="Foreign"/>
411
- <subcat id="5030" name="SD"/>
412
- <subcat id="5040" name="HD"/>
413
- <subcat id="5050" name="Other"/>
414
- <subcat id="5060" name="Sport"/>
415
- </category>
416
- </categories>
417
- </caps>'''
418
- except Exception as e:
419
- print(f"Error loading search results: {e}")
420
- print(traceback.format_exc())
421
- return {"error": True}
422
-
423
- Server(app, listen='0.0.0.0', port=shared_state.values["port"]).serve_forever()