quasarr 1.4.1__py3-none-any.whl → 1.20.4__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 (67) hide show
  1. quasarr/__init__.py +157 -67
  2. quasarr/api/__init__.py +126 -43
  3. quasarr/api/arr/__init__.py +197 -78
  4. quasarr/api/captcha/__init__.py +885 -39
  5. quasarr/api/config/__init__.py +23 -0
  6. quasarr/api/sponsors_helper/__init__.py +84 -22
  7. quasarr/api/statistics/__init__.py +196 -0
  8. quasarr/downloads/__init__.py +236 -487
  9. quasarr/downloads/linkcrypters/al.py +237 -0
  10. quasarr/downloads/linkcrypters/filecrypt.py +178 -31
  11. quasarr/downloads/linkcrypters/hide.py +123 -0
  12. quasarr/downloads/packages/__init__.py +461 -0
  13. quasarr/downloads/sources/al.py +697 -0
  14. quasarr/downloads/sources/by.py +106 -0
  15. quasarr/downloads/sources/dd.py +6 -78
  16. quasarr/downloads/sources/dj.py +7 -0
  17. quasarr/downloads/sources/dt.py +1 -1
  18. quasarr/downloads/sources/dw.py +2 -2
  19. quasarr/downloads/sources/he.py +112 -0
  20. quasarr/downloads/sources/mb.py +47 -0
  21. quasarr/downloads/sources/nk.py +51 -0
  22. quasarr/downloads/sources/nx.py +36 -81
  23. quasarr/downloads/sources/sf.py +27 -4
  24. quasarr/downloads/sources/sj.py +7 -0
  25. quasarr/downloads/sources/sl.py +90 -0
  26. quasarr/downloads/sources/wd.py +110 -0
  27. quasarr/providers/cloudflare.py +204 -0
  28. quasarr/providers/html_images.py +20 -0
  29. quasarr/providers/html_templates.py +48 -39
  30. quasarr/providers/imdb_metadata.py +15 -2
  31. quasarr/providers/myjd_api.py +34 -5
  32. quasarr/providers/notifications.py +30 -5
  33. quasarr/providers/obfuscated.py +35 -0
  34. quasarr/providers/sessions/__init__.py +0 -0
  35. quasarr/providers/sessions/al.py +286 -0
  36. quasarr/providers/sessions/dd.py +78 -0
  37. quasarr/providers/sessions/nx.py +76 -0
  38. quasarr/providers/shared_state.py +347 -20
  39. quasarr/providers/statistics.py +154 -0
  40. quasarr/providers/version.py +1 -1
  41. quasarr/search/__init__.py +112 -36
  42. quasarr/search/sources/al.py +448 -0
  43. quasarr/search/sources/by.py +203 -0
  44. quasarr/search/sources/dd.py +17 -6
  45. quasarr/search/sources/dj.py +213 -0
  46. quasarr/search/sources/dt.py +37 -7
  47. quasarr/search/sources/dw.py +27 -47
  48. quasarr/search/sources/fx.py +27 -29
  49. quasarr/search/sources/he.py +196 -0
  50. quasarr/search/sources/mb.py +195 -0
  51. quasarr/search/sources/nk.py +188 -0
  52. quasarr/search/sources/nx.py +22 -6
  53. quasarr/search/sources/sf.py +143 -151
  54. quasarr/search/sources/sj.py +213 -0
  55. quasarr/search/sources/sl.py +246 -0
  56. quasarr/search/sources/wd.py +208 -0
  57. quasarr/storage/config.py +20 -4
  58. quasarr/storage/setup.py +216 -51
  59. quasarr-1.20.4.dist-info/METADATA +304 -0
  60. quasarr-1.20.4.dist-info/RECORD +72 -0
  61. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
  62. quasarr/providers/tvmaze_metadata.py +0 -23
  63. quasarr-1.4.1.dist-info/METADATA +0 -174
  64. quasarr-1.4.1.dist-info/RECORD +0 -43
  65. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
  66. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
  67. {quasarr-1.4.1.dist-info → quasarr-1.20.4.dist-info}/top_level.txt +0 -0
@@ -7,14 +7,16 @@ import xml.sax.saxutils as sax_utils
7
7
  from base64 import urlsafe_b64decode
8
8
  from datetime import datetime
9
9
  from functools import wraps
10
+ from urllib.parse import urlparse, parse_qs
10
11
  from xml.etree import ElementTree
11
12
 
12
13
  from bottle import abort, request
13
14
 
14
- from quasarr.downloads import download, delete_package, get_packages
15
+ from quasarr.downloads import download
16
+ from quasarr.downloads.packages import get_packages, delete_package
15
17
  from quasarr.providers import shared_state
16
18
  from quasarr.providers.log import info, debug
17
- from quasarr.providers.tvmaze_metadata import get_title_from_tvrage_id
19
+ from quasarr.providers.version import get_version
18
20
  from quasarr.search import get_search_results
19
21
  from quasarr.storage.config import Config
20
22
 
@@ -64,15 +66,21 @@ def setup_arr_routes(app):
64
66
  password = root.find(".//file").attrib.get("password")
65
67
  imdb_id = root.find(".//file").attrib.get("imdb_id")
66
68
 
67
- info(f"Attempting download for {title}")
69
+ info(f'Attempting download for "{title}"')
68
70
  request_from = request.headers.get('User-Agent')
69
- package_id = download(shared_state, request_from, title, url, mirror, size_mb, password, imdb_id)
70
-
71
- if package_id:
72
- info(f"{title} added successfully!")
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.')
73
81
  nzo_ids.append(package_id)
74
- else:
75
- info(f"{title} could not be added!")
82
+ except KeyError:
83
+ info(f'Failed to download "{title}" - no package_id returned')
76
84
 
77
85
  return {
78
86
  "status": True,
@@ -86,12 +94,25 @@ def setup_arr_routes(app):
86
94
  api_type = 'arr_download_client' if request.query.mode else 'arr_indexer' if request.query.t else None
87
95
 
88
96
  if api_type == 'arr_download_client':
89
- # This yields with a mock SABnzbd API response based on the My JDownloader integration
97
+ # This builds a mock SABnzbd API response based on the My JDownloader integration
90
98
  try:
91
99
  mode = request.query.mode
92
- if mode == "version":
100
+ if mode == "auth":
101
+ return {
102
+ "auth": "apikey"
103
+ }
104
+ elif mode == "version":
93
105
  return {
94
- "version": "4.3.2"
106
+ "version": f"Quasarr {get_version()}"
107
+ }
108
+ elif mode == "get_cats":
109
+ return {
110
+ "categories": [
111
+ "*",
112
+ "movies",
113
+ "tv",
114
+ "docs"
115
+ ]
95
116
  }
96
117
  elif mode == "get_config":
97
118
  return {
@@ -115,7 +136,12 @@ def setup_arr_routes(app):
115
136
  "name": "tv",
116
137
  "order": 2,
117
138
  "dir": "",
118
- }
139
+ },
140
+ {
141
+ "name": "docs",
142
+ "order": 3,
143
+ "dir": "",
144
+ },
119
145
  ]
120
146
  }
121
147
  }
@@ -125,6 +151,66 @@ def setup_arr_routes(app):
125
151
  "quasarr": True
126
152
  }
127
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
+
128
214
  elif mode == "queue" or mode == "history":
129
215
  if request.query.name and request.query.name == "delete":
130
216
  package_id = request.query.value
@@ -139,109 +225,133 @@ def setup_arr_routes(app):
139
225
  return {
140
226
  "queue": {
141
227
  "paused": False,
142
- "slots": packages["queue"]
228
+ "slots": packages.get("queue", [])
143
229
  }
144
230
  }
145
231
  elif mode == "history":
146
232
  return {
147
233
  "history": {
148
234
  "paused": False,
149
- "slots": packages["history"]
235
+ "slots": packages.get("history", [])
150
236
  }
151
237
  }
152
238
  except Exception as e:
153
239
  info(f"Error loading packages: {e}")
154
240
  info(traceback.format_exc())
241
+ info(f"[ERROR] Unknown download client request: {dict(request.query)}")
155
242
  return {
156
243
  "status": False
157
244
  }
158
245
 
159
246
  elif api_type == 'arr_indexer':
160
- # this yields with a mock Newznab API response based on Quasarr search
247
+ # this builds a mock Newznab API response based on Quasarr search
161
248
  try:
162
249
  if mirror:
163
250
  debug(f'Search will only return releases that match this mirror: "{mirror}"')
164
251
 
165
252
  mode = request.query.t
253
+ request_from = request.headers.get('User-Agent')
254
+
166
255
  if mode == 'caps':
256
+ info(f"Providing indexer capability information to {request_from}")
167
257
  return '''<?xml version="1.0" encoding="UTF-8"?>
168
- <caps>
169
- <categories>
170
- <category id="2000" name="Movies">
171
- </category>
172
- <category id="5000" name="TV">
173
- </category>
174
- </categories>
175
- </caps>'''
176
- elif mode in ['movie', 'tvsearch', 'search']:
177
- request_from = request.headers.get('User-Agent')
178
-
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']:
179
280
  releases = []
180
281
 
181
- if mode == 'movie':
182
- # only imdb is implemented
183
- search_param = f"tt{getattr(request.query, 'imdbid', '')}" \
184
- if getattr(request.query, 'imdbid', '') else ""
185
-
186
- releases = get_search_results(shared_state, request_from,
187
- search_string=search_param,
188
- mirror=mirror
189
- )
190
-
191
- elif mode == 'search':
192
- debug(f'Search in Anime-Order is not supported. Ignoring request: {dict(request.query)}')
193
-
194
- elif mode == 'tvsearch':
195
- season = getattr(request.query, 'season', "")
196
- episode = getattr(request.query, 'ep', "")
197
- # only plain search string and tvrage id is implemented
198
- search_param = getattr(request.query, 'q', "")
199
- if not search_param:
200
- tvrage_id = getattr(request.query, 'rid', "")
201
- if tvrage_id:
202
- search_param = get_title_from_tvrage_id(tvrage_id)
203
-
204
- offset = getattr(request.query, 'offset', "") # ignoring offset higher than 0 on purpose
205
- if int(offset) == 0:
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
+
206
295
  releases = get_search_results(shared_state, request_from,
207
- search_string=search_param,
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,
208
307
  mirror=mirror,
209
308
  season=season,
210
309
  episode=episode
211
310
  )
212
- else:
213
- debug(f'Offset higher than 0 is not supported. Ignoring request: {dict(request.query)}')
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
+ )
214
319
 
215
- items = ""
216
- if not releases:
217
- items += f'''
218
- <item>
219
- <title>No releases found</title>
220
- <link></link>
221
- <pubDate>{datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')}</pubDate>
222
- <enclosure url="_" length="0" type="application/x-nzb"/>
223
- <guid></guid>
224
- <comments></comments>
225
- <description></description>
226
- </item>'''
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
227
331
 
332
+ items = ""
228
333
  for release in releases:
229
- release = release["details"]
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", ""))
230
339
 
231
- prefix = f'[{release["hostname"].upper()}]'
340
+ if not "lazylibrarian" in request_from.lower():
341
+ title = f'[{release.get("hostname", "").upper()}] {title}'
232
342
 
233
343
  items += f'''
234
344
  <item>
235
- <title>{prefix} {sax_utils.escape(release["title"])}</title>
236
- <guid isPermaLink="True">{release["link"]}</guid>
237
- <link>{release["link"]}</link>
238
- <comments>{release["source"]}</comments>
239
- <pubDate>{release["date"]}</pubDate>
240
- <enclosure url="{release["link"]}" length="{release["size"]}" type="application/x-nzb" />
345
+ <title>{title}</title>
346
+ <guid isPermaLink="True">{release.get("link", "")}</guid>
347
+ <link>{release.get("link", "")}</link>
348
+ <comments>{source}</comments>
349
+ <pubDate>{release.get("date", datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000"))}</pubDate>
350
+ <enclosure url="{release.get("link", "")}" length="{release.get("size", 0)}" type="application/x-nzb" />
241
351
  </item>'''
242
352
 
243
353
  return f'''<?xml version="1.0" encoding="UTF-8"?>
244
- <rss version="2.0">
354
+ <rss>
245
355
  <channel>
246
356
  {items}
247
357
  </channel>
@@ -249,6 +359,15 @@ def setup_arr_routes(app):
249
359
  except Exception as e:
250
360
  info(f"Error loading search results: {e}")
251
361
  info(traceback.format_exc())
252
-
253
- info(f"Unknown request: {dict(request.query)}")
362
+ info(f"[ERROR] Unknown indexer request: {dict(request.query)}")
363
+ return '''<?xml version="1.0" encoding="UTF-8"?>
364
+ <rss>
365
+ <channel>
366
+ <title>Quasarr Indexer</title>
367
+ <description>Quasarr Indexer API</description>
368
+ <link>https://quasarr.indexer/</link>
369
+ </channel>
370
+ </rss>'''
371
+
372
+ info(f"[ERROR] Unknown general request: {dict(request.query)}")
254
373
  return {"error": True}