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
@@ -0,0 +1,387 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Quasarr
3
+ # Project by https://github.com/rix1337
4
+
5
+ import traceback
6
+ import xml.sax.saxutils as sax_utils
7
+ from base64 import urlsafe_b64decode
8
+ from datetime import datetime
9
+ from functools import wraps
10
+ from urllib.parse import urlparse, parse_qs
11
+ from xml.etree import ElementTree
12
+
13
+ from bottle import abort, request
14
+
15
+ from quasarr.downloads import download
16
+ from quasarr.downloads.packages import get_packages, delete_package
17
+ from quasarr.providers import shared_state
18
+ from quasarr.providers.log import info, debug
19
+ from quasarr.providers.version import get_version
20
+ from quasarr.search import get_search_results
21
+ from quasarr.storage.config import Config
22
+
23
+
24
+ def require_api_key(func):
25
+ @wraps(func)
26
+ def decorated(*args, **kwargs):
27
+ api_key = Config('API').get('key')
28
+ if not request.query.apikey:
29
+ return abort(401, "Missing API key")
30
+ if request.query.apikey != api_key:
31
+ return abort(403, "Invalid API key")
32
+ return func(*args, **kwargs)
33
+
34
+ return decorated
35
+
36
+
37
+ def setup_arr_routes(app):
38
+ @app.get('/download/')
39
+ def fake_nzb_file():
40
+ payload = request.query.payload
41
+ decoded_payload = urlsafe_b64decode(payload).decode("utf-8").split("|")
42
+ title = decoded_payload[0]
43
+ url = decoded_payload[1]
44
+ mirror = decoded_payload[2]
45
+ size_mb = decoded_payload[3]
46
+ password = decoded_payload[4]
47
+ imdb_id = decoded_payload[5]
48
+ return f'<nzb><file title="{title}" url="{url}" mirror="{mirror}" size_mb="{size_mb}" password="{password}" imdb_id="{imdb_id}"/></nzb>'
49
+
50
+ @app.post('/api')
51
+ @require_api_key
52
+ def download_fake_nzb_file():
53
+ downloads = request.files.getall('name')
54
+ nzo_ids = [] # naming structure for package IDs expected in newznab
55
+
56
+ for upload in downloads:
57
+ file_content = upload.file.read()
58
+ root = ElementTree.fromstring(file_content)
59
+
60
+ title = sax_utils.unescape(root.find(".//file").attrib["title"])
61
+
62
+ url = root.find(".//file").attrib["url"]
63
+ mirror = None if (mirror := root.find(".//file").attrib.get("mirror")) == "None" else mirror
64
+
65
+ size_mb = root.find(".//file").attrib["size_mb"]
66
+ password = root.find(".//file").attrib.get("password")
67
+ imdb_id = root.find(".//file").attrib.get("imdb_id")
68
+
69
+ info(f'Attempting download for "{title}"')
70
+ request_from = request.headers.get('User-Agent')
71
+ downloaded = download(shared_state, request_from, title, url, mirror, size_mb, password, imdb_id)
72
+ try:
73
+ success = downloaded["success"]
74
+ package_id = downloaded["package_id"]
75
+ title = downloaded["title"]
76
+
77
+ if success:
78
+ info(f'"{title}" added successfully!')
79
+ else:
80
+ info(f'"{title}" added unsuccessfully! See log for details.')
81
+ nzo_ids.append(package_id)
82
+ except KeyError:
83
+ info(f'Failed to download "{title}" - no package_id returned')
84
+
85
+ return {
86
+ "status": True,
87
+ "nzo_ids": nzo_ids
88
+ }
89
+
90
+ @app.get('/api')
91
+ @app.get('/api/<mirror>')
92
+ @require_api_key
93
+ def quasarr_api(mirror=None):
94
+ api_type = 'arr_download_client' if request.query.mode else 'arr_indexer' if request.query.t else None
95
+
96
+ if api_type == 'arr_download_client':
97
+ # This builds a mock SABnzbd API response based on the My JDownloader integration
98
+ try:
99
+ mode = request.query.mode
100
+ if mode == "auth":
101
+ return {
102
+ "auth": "apikey"
103
+ }
104
+ elif mode == "version":
105
+ return {
106
+ "version": f"Quasarr {get_version()}"
107
+ }
108
+ elif mode == "get_cats":
109
+ return {
110
+ "categories": [
111
+ "*",
112
+ "movies",
113
+ "tv",
114
+ "docs"
115
+ ]
116
+ }
117
+ elif mode == "get_config":
118
+ return {
119
+ "config": {
120
+ "misc": {
121
+ "quasarr": True,
122
+ "complete_dir": "/tmp/"
123
+ },
124
+ "categories": [
125
+ {
126
+ "name": "*",
127
+ "order": 0,
128
+ "dir": "",
129
+ },
130
+ {
131
+ "name": "movies",
132
+ "order": 1,
133
+ "dir": "",
134
+ },
135
+ {
136
+ "name": "tv",
137
+ "order": 2,
138
+ "dir": "",
139
+ },
140
+ {
141
+ "name": "docs",
142
+ "order": 3,
143
+ "dir": "",
144
+ },
145
+ ]
146
+ }
147
+ }
148
+ elif mode == "fullstatus":
149
+ return {
150
+ "status": {
151
+ "quasarr": True
152
+ }
153
+ }
154
+ elif mode == "addurl":
155
+ raw_name = getattr(request.query, "name", None)
156
+ if not raw_name:
157
+ abort(400, "missing or empty 'name' parameter")
158
+
159
+ payload = False
160
+ try:
161
+ parsed = urlparse(raw_name)
162
+ qs = parse_qs(parsed.query)
163
+ payload = qs.get("payload", [None])[0]
164
+ except Exception as e:
165
+ abort(400, f"invalid URL in 'name': {e}")
166
+ if not payload:
167
+ abort(400, "missing 'payload' parameter in URL")
168
+
169
+ title = url = mirror = size_mb = password = imdb_id = None
170
+ try:
171
+ decoded = urlsafe_b64decode(payload.encode()).decode()
172
+ parts = decoded.split("|")
173
+ if len(parts) != 6:
174
+ raise ValueError(f"expected 6 fields, got {len(parts)}")
175
+ title, url, mirror, size_mb, password, imdb_id = parts
176
+ except Exception as e:
177
+ abort(400, f"invalid payload format: {e}")
178
+
179
+ mirror = None if mirror == "None" else mirror
180
+
181
+ nzo_ids = []
182
+ info(f'Attempting download for "{title}"')
183
+ request_from = "lazylibrarian"
184
+
185
+ downloaded = download(
186
+ shared_state,
187
+ request_from,
188
+ title,
189
+ url,
190
+ mirror,
191
+ size_mb,
192
+ password or None,
193
+ imdb_id or None,
194
+ )
195
+
196
+ try:
197
+ success = downloaded["success"]
198
+ package_id = downloaded["package_id"]
199
+ title = downloaded.get("title", title)
200
+
201
+ if success:
202
+ info(f'"{title}" added successfully!')
203
+ else:
204
+ info(f'"{title}" added unsuccessfully! See log for details.')
205
+ nzo_ids.append(package_id)
206
+ except KeyError:
207
+ info(f'Failed to download "{title}" - no package_id returned')
208
+
209
+ return {
210
+ "status": True,
211
+ "nzo_ids": nzo_ids
212
+ }
213
+
214
+ elif mode == "queue" or mode == "history":
215
+ if request.query.name and request.query.name == "delete":
216
+ package_id = request.query.value
217
+ deleted = delete_package(shared_state, package_id)
218
+ return {
219
+ "status": deleted,
220
+ "nzo_ids": [package_id]
221
+ }
222
+
223
+ packages = get_packages(shared_state)
224
+ if mode == "queue":
225
+ return {
226
+ "queue": {
227
+ "paused": False,
228
+ "slots": packages.get("queue", [])
229
+ }
230
+ }
231
+ elif mode == "history":
232
+ return {
233
+ "history": {
234
+ "paused": False,
235
+ "slots": packages.get("history", [])
236
+ }
237
+ }
238
+ except Exception as e:
239
+ info(f"Error loading packages: {e}")
240
+ info(traceback.format_exc())
241
+ info(f"[ERROR] Unknown download client request: {dict(request.query)}")
242
+ return {
243
+ "status": False
244
+ }
245
+
246
+ elif api_type == 'arr_indexer':
247
+ # this builds a mock Newznab API response based on Quasarr search
248
+ try:
249
+ if mirror:
250
+ debug(f'Search will only return releases that match this mirror: "{mirror}"')
251
+
252
+ mode = request.query.t
253
+ request_from = request.headers.get('User-Agent')
254
+
255
+ if mode == 'caps':
256
+ info(f"Providing indexer capability information to {request_from}")
257
+ return '''<?xml version="1.0" encoding="UTF-8"?>
258
+ <caps>
259
+ <server
260
+ version="1.33.7"
261
+ title="Quasarr"
262
+ url="https://quasarr.indexer/"
263
+ email="support@quasarr.indexer"
264
+ />
265
+ <limits max="9999" default="9999" />
266
+ <registration available="no" open="no" />
267
+ <searching>
268
+ <search available="yes" supportedParams="q" />
269
+ <tv-search available="yes" supportedParams="imdbid,season,ep" />
270
+ <movie-search available="yes" supportedParams="imdbid" />
271
+ </searching>
272
+ <categories>
273
+ <category id="5000" name="TV" />
274
+ <category id="2000" name="Movies" />
275
+ <category id="7000" name="Books">
276
+ </category>
277
+ </categories>
278
+ </caps>'''
279
+ elif mode in ['movie', 'tvsearch', 'book', 'search']:
280
+ releases = []
281
+
282
+ try:
283
+ offset = int(getattr(request.query, 'offset', 0))
284
+ except (AttributeError, ValueError):
285
+ offset = 0
286
+
287
+ if offset > 0:
288
+ debug(f"Ignoring offset parameter: {offset} - it leads to redundant requests")
289
+
290
+ else:
291
+ if mode == 'movie':
292
+ # supported params: imdbid
293
+ imdb_id = getattr(request.query, 'imdbid', '')
294
+
295
+ releases = get_search_results(shared_state, request_from,
296
+ imdb_id=imdb_id,
297
+ mirror=mirror
298
+ )
299
+
300
+ elif mode == 'tvsearch':
301
+ # supported params: imdbid, season, ep
302
+ imdb_id = getattr(request.query, 'imdbid', '')
303
+ season = getattr(request.query, 'season', None)
304
+ episode = getattr(request.query, 'ep', None)
305
+ releases = get_search_results(shared_state, request_from,
306
+ imdb_id=imdb_id,
307
+ mirror=mirror,
308
+ season=season,
309
+ episode=episode
310
+ )
311
+ elif mode == 'book':
312
+ author = getattr(request.query, 'author', '')
313
+ title = getattr(request.query, 'title', '')
314
+ search_phrase = " ".join(filter(None, [author, title]))
315
+ releases = get_search_results(shared_state, request_from,
316
+ search_phrase=search_phrase,
317
+ mirror=mirror
318
+ )
319
+
320
+ elif mode == 'search':
321
+ if "lazylibrarian" in request_from.lower():
322
+ search_phrase = getattr(request.query, 'q', '')
323
+ releases = get_search_results(shared_state, request_from,
324
+ search_phrase=search_phrase,
325
+ mirror=mirror
326
+ )
327
+ else:
328
+ info(
329
+ f'Ignoring search request from {request_from} - only imdbid searches are supported')
330
+ releases = [{}] # sonarr expects this but we will not support non-imdbid searches
331
+
332
+ items = ""
333
+ for release in releases:
334
+ release = release.get("details", {})
335
+
336
+ # Ensure clean XML output
337
+ title = sax_utils.escape(release.get("title", ""))
338
+ source = sax_utils.escape(release.get("source", ""))
339
+
340
+ if not "lazylibrarian" in request_from.lower():
341
+ title = f'[{release.get("hostname", "").upper()}] {title}'
342
+
343
+ # Get publication date - sources should provide valid dates
344
+ pub_date = release.get("date", "").strip()
345
+
346
+ items += f'''
347
+ <item>
348
+ <title>{title}</title>
349
+ <guid isPermaLink="True">{release.get("link", "")}</guid>
350
+ <link>{release.get("link", "")}</link>
351
+ <comments>{source}</comments>
352
+ <pubDate>{pub_date}</pubDate>
353
+ <enclosure url="{release.get("link", "")}" length="{release.get("size", 0)}" type="application/x-nzb" />
354
+ </item>'''
355
+
356
+ if not items:
357
+ items = f'''
358
+ <item>
359
+ <title>No results found</title>
360
+ <guid isPermaLink="False">0</guid>
361
+ <link>https://github.com/rix1337/Quasarr</link>
362
+ <comments>No results matched your search criteria.</comments>
363
+ <pubDate>{datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000")}</pubDate>
364
+ <enclosure url="https://github.com/rix1337/Quasarr" length="0" type="application/x-nzb" />
365
+ </item>'''
366
+
367
+ return f'''<?xml version="1.0" encoding="UTF-8"?>
368
+ <rss>
369
+ <channel>
370
+ {items}
371
+ </channel>
372
+ </rss>'''
373
+ except Exception as e:
374
+ info(f"Error loading search results: {e}")
375
+ info(traceback.format_exc())
376
+ info(f"[ERROR] Unknown indexer request: {dict(request.query)}")
377
+ return '''<?xml version="1.0" encoding="UTF-8"?>
378
+ <rss>
379
+ <channel>
380
+ <title>Quasarr Indexer</title>
381
+ <description>Quasarr Indexer API</description>
382
+ <link>https://quasarr.indexer/</link>
383
+ </channel>
384
+ </rss>'''
385
+
386
+ info(f"[ERROR] Unknown general request: {dict(request.query)}")
387
+ return {"error": True}