quasarr 2.4.8__py3-none-any.whl → 2.4.10__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 (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.8.dist-info → quasarr-2.4.10.dist-info}/METADATA +4 -3
  72. quasarr-2.4.10.dist-info/RECORD +81 -0
  73. quasarr-2.4.8.dist-info/RECORD +0 -81
  74. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/WHEEL +0 -0
  75. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/entry_points.txt +0 -0
  76. {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/licenses/LICENSE +0 -0
@@ -12,33 +12,49 @@ from urllib.parse import urlparse
12
12
  import requests
13
13
  from bottle import request, response
14
14
 
15
- from quasarr.providers.html_templates import render_form, render_button, render_fail
15
+ from quasarr.providers.html_templates import render_button, render_fail, render_form
16
16
  from quasarr.providers.log import info
17
17
  from quasarr.providers.shared_state import extract_valid_hostname
18
- from quasarr.providers.utils import extract_kv_pairs, extract_allowed_keys, check_flaresolverr
18
+ from quasarr.providers.utils import (
19
+ check_flaresolverr,
20
+ extract_allowed_keys,
21
+ extract_kv_pairs,
22
+ )
19
23
  from quasarr.storage.config import Config
20
- from quasarr.storage.setup import hostname_form_html, save_hostnames, render_reconnect_success
24
+ from quasarr.storage.setup import (
25
+ hostname_form_html,
26
+ render_reconnect_success,
27
+ save_hostnames,
28
+ )
21
29
  from quasarr.storage.sqlite_database import DataBase
22
30
 
23
31
 
24
32
  def setup_config(app, shared_state):
25
33
  @app.get("/api/hostname-issues")
26
34
  def get_hostname_issues_api():
27
- response.content_type = 'application/json'
35
+ response.content_type = "application/json"
28
36
  from quasarr.providers.hostname_issues import get_all_hostname_issues
37
+
29
38
  return {"issues": get_all_hostname_issues()}
30
39
 
31
- @app.get('/hostnames')
40
+ @app.get("/hostnames")
32
41
  def hostnames_ui():
33
42
  message = """<p>
34
43
  At least one hostname must be kept.
35
44
  </p>"""
36
- back_button = f'''<p>
45
+ back_button = f"""<p>
37
46
  {render_button("Back", "secondary", {"onclick": "location.href='/'"})}
38
- </p>'''
39
- return render_form("Hostnames",
40
- hostname_form_html(shared_state, message, show_restart_button=True,
41
- show_skip_management=True) + back_button)
47
+ </p>"""
48
+ return render_form(
49
+ "Hostnames",
50
+ hostname_form_html(
51
+ shared_state,
52
+ message,
53
+ show_restart_button=True,
54
+ show_skip_management=True,
55
+ )
56
+ + back_button,
57
+ )
42
58
 
43
59
  @app.post("/api/hostnames")
44
60
  def hostnames_api():
@@ -47,10 +63,10 @@ def setup_config(app, shared_state):
47
63
  @app.post("/api/hostnames/import-url")
48
64
  def import_hostnames_from_url():
49
65
  """Fetch URL and parse hostnames, return JSON for JS to populate fields."""
50
- response.content_type = 'application/json'
66
+ response.content_type = "application/json"
51
67
  try:
52
68
  data = request.json
53
- url = data.get('url', '').strip()
69
+ url = data.get("url", "").strip()
54
70
 
55
71
  if not url:
56
72
  return {"success": False, "error": "No URL provided"}
@@ -67,33 +83,44 @@ def setup_config(app, shared_state):
67
83
  content = resp.text
68
84
  except requests.RequestException as e:
69
85
  info(f"Failed to fetch hostnames URL: {e}")
70
- return {"success": False, "error": "Failed to fetch URL. Check the console log for details."}
86
+ return {
87
+ "success": False,
88
+ "error": "Failed to fetch URL. Check the console log for details.",
89
+ }
71
90
 
72
91
  # Parse hostnames
73
- allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, 'Hostnames')
92
+ allowed_keys = extract_allowed_keys(Config._DEFAULT_CONFIG, "Hostnames")
74
93
  results = extract_kv_pairs(content, allowed_keys)
75
94
 
76
95
  if not results:
77
- return {"success": False, "error": "No hostnames found in the provided URL"}
96
+ return {
97
+ "success": False,
98
+ "error": "No hostnames found in the provided URL",
99
+ }
78
100
 
79
101
  # Validate each hostname
80
102
  valid_hostnames = {}
81
103
  invalid_hostnames = {}
82
104
  for shorthand, hostname in results.items():
83
105
  domain_check = extract_valid_hostname(hostname, shorthand)
84
- domain = domain_check.get('domain')
106
+ domain = domain_check.get("domain")
85
107
  if domain:
86
108
  valid_hostnames[shorthand] = domain
87
109
  else:
88
- invalid_hostnames[shorthand] = domain_check.get('message', 'Invalid')
110
+ invalid_hostnames[shorthand] = domain_check.get(
111
+ "message", "Invalid"
112
+ )
89
113
 
90
114
  if not valid_hostnames:
91
- return {"success": False, "error": "No valid hostnames found in the provided URL"}
115
+ return {
116
+ "success": False,
117
+ "error": "No valid hostnames found in the provided URL",
118
+ }
92
119
 
93
120
  return {
94
121
  "success": True,
95
122
  "hostnames": valid_hostnames,
96
- "errors": invalid_hostnames
123
+ "errors": invalid_hostnames,
97
124
  }
98
125
 
99
126
  except Exception as e:
@@ -102,9 +129,9 @@ def setup_config(app, shared_state):
102
129
  @app.get("/api/skip-login")
103
130
  def get_skip_login():
104
131
  """Return list of hostnames with skipped login."""
105
- response.content_type = 'application/json'
132
+ response.content_type = "application/json"
106
133
  skip_db = DataBase("skip_login")
107
- login_required_sites = ['al', 'dd', 'dl', 'nx']
134
+ login_required_sites = ["al", "dd", "dl", "nx"]
108
135
  skipped = []
109
136
  for site in login_required_sites:
110
137
  if skip_db.retrieve(site):
@@ -114,9 +141,9 @@ def setup_config(app, shared_state):
114
141
  @app.delete("/api/skip-login/<shorthand>")
115
142
  def clear_skip_login(shorthand):
116
143
  """Clear skip login preference for a hostname."""
117
- response.content_type = 'application/json'
144
+ response.content_type = "application/json"
118
145
  shorthand = shorthand.lower()
119
- login_required_sites = ['al', 'dd', 'dl', 'nx']
146
+ login_required_sites = ["al", "dd", "dl", "nx"]
120
147
  if shorthand not in login_required_sites:
121
148
  return {"success": False, "error": f"Invalid shorthand: {shorthand}"}
122
149
 
@@ -125,23 +152,23 @@ def setup_config(app, shared_state):
125
152
  info(f'Skip login preference cleared for "{shorthand.upper()}"')
126
153
  return {"success": True}
127
154
 
128
- @app.get('/flaresolverr')
155
+ @app.get("/flaresolverr")
129
156
  def flaresolverr_ui():
130
157
  """Web UI page for configuring FlareSolverr."""
131
158
  skip_db = DataBase("skip_flaresolverr")
132
159
  is_skipped = skip_db.retrieve("skipped")
133
- current_url = Config('FlareSolverr').get('url') or ""
160
+ current_url = Config("FlareSolverr").get("url") or ""
134
161
 
135
162
  skip_indicator = ""
136
163
  if is_skipped:
137
- skip_indicator = '''
164
+ skip_indicator = """
138
165
  <div class="skip-indicator" style="margin-bottom:1rem; padding:0.75rem; background:var(--code-bg, #f8f9fa); border-radius:0.25rem; font-size:0.875rem;">
139
166
  <span style="color:#dc3545;">⚠️ FlareSolverr setup was skipped</span>
140
167
  <p style="margin:0.5rem 0 0 0; font-size:0.75rem; color:var(--secondary, #6c757d);">
141
168
  Some sites (like AL) won't work until FlareSolverr is configured.
142
169
  </p>
143
170
  </div>
144
- '''
171
+ """
145
172
 
146
173
  form_content = f'''
147
174
  {skip_indicator}
@@ -151,7 +178,7 @@ def setup_config(app, shared_state):
151
178
  <input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1" value="{current_url}"><br>
152
179
  '''
153
180
 
154
- form_html = f'''
181
+ form_html = f"""
155
182
  <form action="/api/flaresolverr" method="post" onsubmit="return handleSubmit(this)">
156
183
  {form_content}
157
184
  {render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
@@ -245,13 +272,13 @@ def setup_config(app, shared_state):
245
272
  attempt();
246
273
  }}
247
274
  </script>
248
- '''
275
+ """
249
276
  return render_form("FlareSolverr", form_html)
250
277
 
251
- @app.post('/api/flaresolverr')
278
+ @app.post("/api/flaresolverr")
252
279
  def set_flaresolverr_url():
253
280
  """Save FlareSolverr URL from web UI."""
254
- url = request.forms.get('url', '').strip()
281
+ url = request.forms.get("url", "").strip()
255
282
  config = Config("FlareSolverr")
256
283
 
257
284
  if not url:
@@ -262,14 +289,16 @@ def setup_config(app, shared_state):
262
289
 
263
290
  # Validate URL format
264
291
  if not re.search(r"/v\d+$", url):
265
- return render_fail("FlareSolverr URL must end with /v1 (or similar version path).")
292
+ return render_fail(
293
+ "FlareSolverr URL must end with /v1 (or similar version path)."
294
+ )
266
295
 
267
296
  try:
268
297
  headers = {"Content-Type": "application/json"}
269
298
  data = {
270
299
  "cmd": "request.get",
271
300
  "url": "http://www.google.com/",
272
- "maxTimeout": 30000
301
+ "maxTimeout": 30000,
273
302
  }
274
303
  resp = requests.post(url, headers=headers, json=data, timeout=30)
275
304
  if resp.status_code == 200:
@@ -285,46 +314,47 @@ def setup_config(app, shared_state):
285
314
  shared_state.update("user_agent", solution_ua)
286
315
  info(f'FlareSolverr URL configured: "{url}"')
287
316
  return render_reconnect_success(
288
- "FlareSolverr URL saved successfully! A restart is recommended.")
317
+ "FlareSolverr URL saved successfully! A restart is recommended."
318
+ )
289
319
  else:
290
- return render_fail(f"FlareSolverr returned unexpected status: {json_data.get('status')}")
320
+ return render_fail(
321
+ f"FlareSolverr returned unexpected status: {json_data.get('status')}"
322
+ )
291
323
  except requests.RequestException:
292
324
  return render_fail(f"Could not reach FlareSolverr!")
293
325
 
294
- return render_fail("Could not reach FlareSolverr at that URL (expected HTTP 200).")
326
+ return render_fail(
327
+ "Could not reach FlareSolverr at that URL (expected HTTP 200)."
328
+ )
295
329
 
296
330
  @app.get("/api/flaresolverr/status")
297
331
  def get_flaresolverr_status():
298
332
  """Return FlareSolverr configuration status."""
299
- response.content_type = 'application/json'
333
+ response.content_type = "application/json"
300
334
  skip_db = DataBase("skip_flaresolverr")
301
335
  is_skipped = bool(skip_db.retrieve("skipped"))
302
- current_url = Config('FlareSolverr').get('url') or ""
336
+ current_url = Config("FlareSolverr").get("url") or ""
303
337
 
304
338
  # Test connection if URL is set
305
339
  is_working = False
306
340
  if current_url and not is_skipped:
307
341
  is_working = check_flaresolverr(shared_state, current_url)
308
342
 
309
- return {
310
- "skipped": is_skipped,
311
- "url": current_url,
312
- "working": is_working
313
- }
343
+ return {"skipped": is_skipped, "url": current_url, "working": is_working}
314
344
 
315
345
  @app.delete("/api/skip-flaresolverr")
316
346
  def clear_skip_flaresolverr():
317
347
  """Clear skip FlareSolverr preference."""
318
- response.content_type = 'application/json'
348
+ response.content_type = "application/json"
319
349
  skip_db = DataBase("skip_flaresolverr")
320
350
  skip_db.delete("skipped")
321
- info('Skip FlareSolverr preference cleared')
351
+ info("Skip FlareSolverr preference cleared")
322
352
  return {"success": True}
323
353
 
324
354
  @app.post("/api/restart")
325
355
  def restart_quasarr():
326
356
  """Restart Quasarr. In Docker with the restart loop, exit(0) triggers restart."""
327
- response.content_type = 'application/json'
357
+ response.content_type = "application/json"
328
358
  info("Restart requested via web UI")
329
359
 
330
360
  def delayed_exit():