quasarr 2.4.7__py3-none-any.whl → 2.4.9__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.
Files changed (76) hide show
  1. quasarr/__init__.py +134 -70
  2. quasarr/api/__init__.py +40 -31
  3. quasarr/api/arr/__init__.py +116 -108
  4. quasarr/api/captcha/__init__.py +262 -137
  5. quasarr/api/config/__init__.py +76 -46
  6. quasarr/api/packages/__init__.py +138 -102
  7. quasarr/api/sponsors_helper/__init__.py +29 -16
  8. quasarr/api/statistics/__init__.py +19 -19
  9. quasarr/downloads/__init__.py +165 -72
  10. quasarr/downloads/linkcrypters/al.py +35 -18
  11. quasarr/downloads/linkcrypters/filecrypt.py +107 -52
  12. quasarr/downloads/linkcrypters/hide.py +5 -6
  13. quasarr/downloads/packages/__init__.py +342 -177
  14. quasarr/downloads/sources/al.py +191 -100
  15. quasarr/downloads/sources/by.py +31 -13
  16. quasarr/downloads/sources/dd.py +27 -14
  17. quasarr/downloads/sources/dj.py +1 -3
  18. quasarr/downloads/sources/dl.py +126 -71
  19. quasarr/downloads/sources/dt.py +11 -5
  20. quasarr/downloads/sources/dw.py +28 -14
  21. quasarr/downloads/sources/he.py +32 -24
  22. quasarr/downloads/sources/mb.py +19 -9
  23. quasarr/downloads/sources/nk.py +14 -10
  24. quasarr/downloads/sources/nx.py +8 -18
  25. quasarr/downloads/sources/sf.py +45 -20
  26. quasarr/downloads/sources/sj.py +1 -3
  27. quasarr/downloads/sources/sl.py +9 -5
  28. quasarr/downloads/sources/wd.py +32 -12
  29. quasarr/downloads/sources/wx.py +35 -21
  30. quasarr/providers/auth.py +42 -37
  31. quasarr/providers/cloudflare.py +28 -30
  32. quasarr/providers/hostname_issues.py +2 -1
  33. quasarr/providers/html_images.py +2 -2
  34. quasarr/providers/html_templates.py +22 -14
  35. quasarr/providers/imdb_metadata.py +149 -80
  36. quasarr/providers/jd_cache.py +131 -39
  37. quasarr/providers/log.py +1 -1
  38. quasarr/providers/myjd_api.py +260 -196
  39. quasarr/providers/notifications.py +53 -41
  40. quasarr/providers/obfuscated.py +9 -4
  41. quasarr/providers/sessions/al.py +71 -55
  42. quasarr/providers/sessions/dd.py +21 -14
  43. quasarr/providers/sessions/dl.py +30 -19
  44. quasarr/providers/sessions/nx.py +23 -14
  45. quasarr/providers/shared_state.py +292 -141
  46. quasarr/providers/statistics.py +75 -43
  47. quasarr/providers/utils.py +33 -27
  48. quasarr/providers/version.py +45 -14
  49. quasarr/providers/web_server.py +10 -5
  50. quasarr/search/__init__.py +30 -18
  51. quasarr/search/sources/al.py +124 -73
  52. quasarr/search/sources/by.py +110 -59
  53. quasarr/search/sources/dd.py +57 -35
  54. quasarr/search/sources/dj.py +69 -48
  55. quasarr/search/sources/dl.py +159 -100
  56. quasarr/search/sources/dt.py +110 -74
  57. quasarr/search/sources/dw.py +121 -61
  58. quasarr/search/sources/fx.py +108 -62
  59. quasarr/search/sources/he.py +78 -49
  60. quasarr/search/sources/mb.py +96 -48
  61. quasarr/search/sources/nk.py +80 -50
  62. quasarr/search/sources/nx.py +91 -62
  63. quasarr/search/sources/sf.py +171 -106
  64. quasarr/search/sources/sj.py +69 -48
  65. quasarr/search/sources/sl.py +115 -71
  66. quasarr/search/sources/wd.py +67 -44
  67. quasarr/search/sources/wx.py +188 -123
  68. quasarr/storage/config.py +65 -52
  69. quasarr/storage/setup.py +238 -140
  70. quasarr/storage/sqlite_database.py +10 -4
  71. {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/METADATA +2 -2
  72. quasarr-2.4.9.dist-info/RECORD +81 -0
  73. quasarr-2.4.7.dist-info/RECORD +0 -81
  74. {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/WHEEL +0 -0
  75. {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/entry_points.txt +0 -0
  76. {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/licenses/LICENSE +0 -0
@@ -6,16 +6,16 @@ import traceback
6
6
  import xml.sax.saxutils as sax_utils
7
7
  from base64 import urlsafe_b64decode
8
8
  from datetime import datetime
9
- from urllib.parse import urlparse, parse_qs
9
+ from urllib.parse import parse_qs, urlparse
10
10
  from xml.etree import ElementTree
11
11
 
12
12
  from bottle import abort, request
13
13
 
14
14
  from quasarr.downloads import download
15
- from quasarr.downloads.packages import get_packages, delete_package
15
+ from quasarr.downloads.packages import delete_package, get_packages
16
16
  from quasarr.providers import shared_state
17
17
  from quasarr.providers.auth import require_api_key
18
- from quasarr.providers.log import info, debug
18
+ from quasarr.providers.log import debug, info
19
19
  from quasarr.providers.version import get_version
20
20
  from quasarr.search import get_search_results
21
21
 
@@ -51,12 +51,12 @@ def parse_payload(payload_str):
51
51
  "size_mb": size_mb,
52
52
  "password": password if password else None,
53
53
  "imdb_id": imdb_id if imdb_id else None,
54
- "source_key": source_key if source_key else None
54
+ "source_key": source_key if source_key else None,
55
55
  }
56
56
 
57
57
 
58
58
  def setup_arr_routes(app):
59
- @app.get('/download/')
59
+ @app.get("/download/")
60
60
  def fake_nzb_file():
61
61
  payload = request.query.payload
62
62
  decoded_payload = urlsafe_b64decode(payload).decode("utf-8").split("|")
@@ -72,10 +72,10 @@ def setup_arr_routes(app):
72
72
 
73
73
  return f'<nzb><file title="{title}" url="{url}" mirror="{mirror}" size_mb="{size_mb}" password="{password}" imdb_id="{imdb_id}" source_key="{source_key}"/></nzb>'
74
74
 
75
- @app.post('/api')
75
+ @app.post("/api")
76
76
  @require_api_key
77
77
  def download_fake_nzb_file():
78
- downloads = request.files.getall('name')
78
+ downloads = request.files.getall("name")
79
79
  nzo_ids = [] # naming structure for package IDs expected in newznab
80
80
 
81
81
  for upload in downloads:
@@ -85,7 +85,11 @@ def setup_arr_routes(app):
85
85
  title = sax_utils.unescape(root.find(".//file").attrib["title"])
86
86
 
87
87
  url = root.find(".//file").attrib["url"]
88
- mirror = None if (mirror := root.find(".//file").attrib.get("mirror")) == "None" else mirror
88
+ mirror = (
89
+ None
90
+ if (mirror := root.find(".//file").attrib.get("mirror")) == "None"
91
+ else mirror
92
+ )
89
93
 
90
94
  size_mb = root.find(".//file").attrib["size_mb"]
91
95
  password = root.find(".//file").attrib.get("password")
@@ -93,9 +97,18 @@ def setup_arr_routes(app):
93
97
  source_key = root.find(".//file").attrib.get("source_key") or None
94
98
 
95
99
  info(f'Attempting download for "{title}"')
96
- request_from = request.headers.get('User-Agent')
97
- downloaded = download(shared_state, request_from, title, url, mirror, size_mb, password, imdb_id,
98
- source_key)
100
+ request_from = request.headers.get("User-Agent")
101
+ downloaded = download(
102
+ shared_state,
103
+ request_from,
104
+ title,
105
+ url,
106
+ mirror,
107
+ size_mb,
108
+ password,
109
+ imdb_id,
110
+ source_key,
111
+ )
99
112
  try:
100
113
  success = downloaded["success"]
101
114
  package_id = downloaded["package_id"]
@@ -109,45 +122,34 @@ def setup_arr_routes(app):
109
122
  except KeyError:
110
123
  info(f'Failed to download "{title}" - no package_id returned')
111
124
 
112
- return {
113
- "status": True,
114
- "nzo_ids": nzo_ids
115
- }
125
+ return {"status": True, "nzo_ids": nzo_ids}
116
126
 
117
- @app.get('/api')
118
- @app.get('/api/<mirror>')
127
+ @app.get("/api")
128
+ @app.get("/api/<mirror>")
119
129
  @require_api_key
120
130
  def quasarr_api(mirror=None):
121
- api_type = 'arr_download_client' if request.query.mode else 'arr_indexer' if request.query.t else None
122
-
123
- if api_type == 'arr_download_client':
131
+ api_type = (
132
+ "arr_download_client"
133
+ if request.query.mode
134
+ else "arr_indexer"
135
+ if request.query.t
136
+ else None
137
+ )
138
+
139
+ if api_type == "arr_download_client":
124
140
  # This builds a mock SABnzbd API response based on the My JDownloader integration
125
141
  try:
126
142
  mode = request.query.mode
127
143
  if mode == "auth":
128
- return {
129
- "auth": "apikey"
130
- }
144
+ return {"auth": "apikey"}
131
145
  elif mode == "version":
132
- return {
133
- "version": f"Quasarr {get_version()}"
134
- }
146
+ return {"version": f"Quasarr {get_version()}"}
135
147
  elif mode == "get_cats":
136
- return {
137
- "categories": [
138
- "*",
139
- "movies",
140
- "tv",
141
- "docs"
142
- ]
143
- }
148
+ return {"categories": ["*", "movies", "tv", "docs"]}
144
149
  elif mode == "get_config":
145
150
  return {
146
151
  "config": {
147
- "misc": {
148
- "quasarr": True,
149
- "complete_dir": "/tmp/"
150
- },
152
+ "misc": {"quasarr": True, "complete_dir": "/tmp/"},
151
153
  "categories": [
152
154
  {
153
155
  "name": "*",
@@ -169,15 +171,11 @@ def setup_arr_routes(app):
169
171
  "order": 3,
170
172
  "dir": "",
171
173
  },
172
- ]
174
+ ],
173
175
  }
174
176
  }
175
177
  elif mode == "fullstatus":
176
- return {
177
- "status": {
178
- "quasarr": True
179
- }
180
- }
178
+ return {"status": {"quasarr": True}}
181
179
  elif mode == "addurl":
182
180
  raw_name = getattr(request.query, "name", None)
183
181
  if not raw_name:
@@ -222,60 +220,58 @@ def setup_arr_routes(app):
222
220
  if success:
223
221
  info(f'"{title}" added successfully!')
224
222
  else:
225
- info(f'"{title}" added unsuccessfully! See log for details.')
223
+ info(
224
+ f'"{title}" added unsuccessfully! See log for details.'
225
+ )
226
226
  nzo_ids.append(package_id)
227
227
  except KeyError:
228
- info(f'Failed to download "{parsed_payload["title"]}" - no package_id returned')
228
+ info(
229
+ f'Failed to download "{parsed_payload["title"]}" - no package_id returned'
230
+ )
229
231
 
230
- return {
231
- "status": True,
232
- "nzo_ids": nzo_ids
233
- }
232
+ return {"status": True, "nzo_ids": nzo_ids}
234
233
 
235
234
  elif mode == "queue" or mode == "history":
236
235
  if request.query.name and request.query.name == "delete":
237
236
  package_id = request.query.value
238
237
  deleted = delete_package(shared_state, package_id)
239
- return {
240
- "status": deleted,
241
- "nzo_ids": [package_id]
242
- }
238
+ return {"status": deleted, "nzo_ids": [package_id]}
243
239
 
244
240
  packages = get_packages(shared_state)
245
241
  if mode == "queue":
246
242
  return {
247
243
  "queue": {
248
244
  "paused": False,
249
- "slots": packages.get("queue", [])
245
+ "slots": packages.get("queue", []),
250
246
  }
251
247
  }
252
248
  elif mode == "history":
253
249
  return {
254
250
  "history": {
255
251
  "paused": False,
256
- "slots": packages.get("history", [])
252
+ "slots": packages.get("history", []),
257
253
  }
258
254
  }
259
255
  except Exception as e:
260
256
  info(f"Error loading packages: {e}")
261
257
  info(traceback.format_exc())
262
258
  info(f"[ERROR] Unknown download client request: {dict(request.query)}")
263
- return {
264
- "status": False
265
- }
259
+ return {"status": False}
266
260
 
267
- elif api_type == 'arr_indexer':
261
+ elif api_type == "arr_indexer":
268
262
  # this builds a mock Newznab API response based on Quasarr search
269
263
  try:
270
264
  if mirror:
271
- debug(f'Search will only return releases that match this mirror: "{mirror}"')
265
+ debug(
266
+ f'Search will only return releases that match this mirror: "{mirror}"'
267
+ )
272
268
 
273
269
  mode = request.query.t
274
- request_from = request.headers.get('User-Agent')
270
+ request_from = request.headers.get("User-Agent")
275
271
 
276
- if mode == 'caps':
272
+ if mode == "caps":
277
273
  info(f"Providing indexer capability information to {request_from}")
278
- return '''<?xml version="1.0" encoding="UTF-8"?>
274
+ return """<?xml version="1.0" encoding="UTF-8"?>
279
275
  <caps>
280
276
  <server
281
277
  version="1.33.7"
@@ -296,58 +292,69 @@ def setup_arr_routes(app):
296
292
  <category id="7000" name="Books">
297
293
  </category>
298
294
  </categories>
299
- </caps>'''
300
- elif mode in ['movie', 'tvsearch', 'book', 'search']:
295
+ </caps>"""
296
+ elif mode in ["movie", "tvsearch", "book", "search"]:
301
297
  releases = []
302
298
 
303
299
  try:
304
- offset = int(getattr(request.query, 'offset', 0))
300
+ offset = int(getattr(request.query, "offset", 0))
305
301
  except (AttributeError, ValueError):
306
302
  offset = 0
307
303
 
308
304
  if offset > 0:
309
- debug(f"Ignoring offset parameter: {offset} - it leads to redundant requests")
305
+ debug(
306
+ f"Ignoring offset parameter: {offset} - it leads to redundant requests"
307
+ )
310
308
 
311
309
  else:
312
- if mode == 'movie':
310
+ if mode == "movie":
313
311
  # supported params: imdbid
314
- imdb_id = getattr(request.query, 'imdbid', '')
312
+ imdb_id = getattr(request.query, "imdbid", "")
315
313
 
316
- releases = get_search_results(shared_state, request_from,
317
- imdb_id=imdb_id,
318
- mirror=mirror
319
- )
314
+ releases = get_search_results(
315
+ shared_state,
316
+ request_from,
317
+ imdb_id=imdb_id,
318
+ mirror=mirror,
319
+ )
320
320
 
321
- elif mode == 'tvsearch':
321
+ elif mode == "tvsearch":
322
322
  # supported params: imdbid, season, ep
323
- imdb_id = getattr(request.query, 'imdbid', '')
324
- season = getattr(request.query, 'season', None)
325
- episode = getattr(request.query, 'ep', None)
326
- releases = get_search_results(shared_state, request_from,
327
- imdb_id=imdb_id,
328
- mirror=mirror,
329
- season=season,
330
- episode=episode
331
- )
332
- elif mode == 'book':
333
- author = getattr(request.query, 'author', '')
334
- title = getattr(request.query, 'title', '')
323
+ imdb_id = getattr(request.query, "imdbid", "")
324
+ season = getattr(request.query, "season", None)
325
+ episode = getattr(request.query, "ep", None)
326
+ releases = get_search_results(
327
+ shared_state,
328
+ request_from,
329
+ imdb_id=imdb_id,
330
+ mirror=mirror,
331
+ season=season,
332
+ episode=episode,
333
+ )
334
+ elif mode == "book":
335
+ author = getattr(request.query, "author", "")
336
+ title = getattr(request.query, "title", "")
335
337
  search_phrase = " ".join(filter(None, [author, title]))
336
- releases = get_search_results(shared_state, request_from,
337
- search_phrase=search_phrase,
338
- mirror=mirror
339
- )
340
-
341
- elif mode == 'search':
338
+ releases = get_search_results(
339
+ shared_state,
340
+ request_from,
341
+ search_phrase=search_phrase,
342
+ mirror=mirror,
343
+ )
344
+
345
+ elif mode == "search":
342
346
  if "lazylibrarian" in request_from.lower():
343
- search_phrase = getattr(request.query, 'q', '')
344
- releases = get_search_results(shared_state, request_from,
345
- search_phrase=search_phrase,
346
- mirror=mirror
347
- )
347
+ search_phrase = getattr(request.query, "q", "")
348
+ releases = get_search_results(
349
+ shared_state,
350
+ request_from,
351
+ search_phrase=search_phrase,
352
+ mirror=mirror,
353
+ )
348
354
  else:
349
355
  info(
350
- f'Ignoring search request from {request_from} - only imdbid searches are supported')
356
+ f"Ignoring search request from {request_from} - only imdbid searches are supported"
357
+ )
351
358
  releases = [] # sonarr expects this but we will not support non-imdbid searches
352
359
 
353
360
  items = ""
@@ -359,7 +366,7 @@ def setup_arr_routes(app):
359
366
  source = sax_utils.escape(release.get("source", ""))
360
367
 
361
368
  if not "lazylibrarian" in request_from.lower():
362
- title = f'[{release.get("hostname", "").upper()}] {title}'
369
+ title = f"[{release.get('hostname', '').upper()}] {title}"
363
370
 
364
371
  # Get publication date - sources should provide valid dates
365
372
  pub_date = release.get("date", "").strip()
@@ -374,10 +381,11 @@ def setup_arr_routes(app):
374
381
  <enclosure url="{release.get("link", "")}" length="{release.get("size", 0)}" type="application/x-nzb" />
375
382
  </item>'''
376
383
 
377
- requires_placeholder_item = not getattr(request.query, 'imdbid', '') and not getattr(request.query,
378
- 'q', '')
384
+ requires_placeholder_item = not getattr(
385
+ request.query, "imdbid", ""
386
+ ) and not getattr(request.query, "q", "")
379
387
  if requires_placeholder_item and not items:
380
- items = f'''
388
+ items = f"""
381
389
  <item>
382
390
  <title>No results found</title>
383
391
  <guid isPermaLink="False">0</guid>
@@ -385,26 +393,26 @@ def setup_arr_routes(app):
385
393
  <comments>No results matched your search criteria.</comments>
386
394
  <pubDate>{datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000")}</pubDate>
387
395
  <enclosure url="https://github.com/rix1337/Quasarr" length="0" type="application/x-nzb" />
388
- </item>'''
396
+ </item>"""
389
397
 
390
- return f'''<?xml version="1.0" encoding="UTF-8"?>
398
+ return f"""<?xml version="1.0" encoding="UTF-8"?>
391
399
  <rss>
392
400
  <channel>
393
401
  {items}
394
402
  </channel>
395
- </rss>'''
403
+ </rss>"""
396
404
  except Exception as e:
397
405
  info(f"Error loading search results: {e}")
398
406
  info(traceback.format_exc())
399
407
  info(f"[ERROR] Unknown indexer request: {dict(request.query)}")
400
- return '''<?xml version="1.0" encoding="UTF-8"?>
408
+ return """<?xml version="1.0" encoding="UTF-8"?>
401
409
  <rss>
402
410
  <channel>
403
411
  <title>Quasarr Indexer</title>
404
412
  <description>Quasarr Indexer API</description>
405
413
  <link>https://quasarr.indexer/</link>
406
414
  </channel>
407
- </rss>'''
415
+ </rss>"""
408
416
 
409
417
  info(f"[ERROR] Unknown general request: {dict(request.query)}")
410
418
  return {"error": True}