quasarr 2.1.5__py3-none-any.whl → 2.2.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 (57) hide show
  1. quasarr/api/__init__.py +94 -23
  2. quasarr/api/captcha/__init__.py +0 -12
  3. quasarr/api/config/__init__.py +22 -11
  4. quasarr/api/packages/__init__.py +26 -34
  5. quasarr/api/statistics/__init__.py +15 -15
  6. quasarr/downloads/__init__.py +9 -1
  7. quasarr/downloads/packages/__init__.py +2 -2
  8. quasarr/downloads/sources/al.py +6 -0
  9. quasarr/downloads/sources/by.py +29 -20
  10. quasarr/downloads/sources/dd.py +9 -1
  11. quasarr/downloads/sources/dl.py +3 -0
  12. quasarr/downloads/sources/dt.py +16 -7
  13. quasarr/downloads/sources/dw.py +22 -17
  14. quasarr/downloads/sources/he.py +11 -6
  15. quasarr/downloads/sources/mb.py +9 -3
  16. quasarr/downloads/sources/nk.py +9 -3
  17. quasarr/downloads/sources/nx.py +21 -17
  18. quasarr/downloads/sources/sf.py +21 -13
  19. quasarr/downloads/sources/sl.py +10 -2
  20. quasarr/downloads/sources/wd.py +18 -9
  21. quasarr/downloads/sources/wx.py +7 -11
  22. quasarr/providers/auth.py +1 -1
  23. quasarr/providers/cloudflare.py +1 -1
  24. quasarr/providers/hostname_issues.py +63 -0
  25. quasarr/providers/html_images.py +1 -18
  26. quasarr/providers/html_templates.py +104 -12
  27. quasarr/providers/obfuscated.py +11 -11
  28. quasarr/providers/sessions/al.py +27 -11
  29. quasarr/providers/sessions/dd.py +12 -4
  30. quasarr/providers/sessions/dl.py +19 -11
  31. quasarr/providers/sessions/nx.py +12 -4
  32. quasarr/providers/version.py +1 -1
  33. quasarr/search/sources/al.py +12 -1
  34. quasarr/search/sources/by.py +15 -4
  35. quasarr/search/sources/dd.py +22 -3
  36. quasarr/search/sources/dj.py +12 -1
  37. quasarr/search/sources/dl.py +12 -6
  38. quasarr/search/sources/dt.py +17 -4
  39. quasarr/search/sources/dw.py +15 -4
  40. quasarr/search/sources/fx.py +19 -6
  41. quasarr/search/sources/he.py +15 -2
  42. quasarr/search/sources/mb.py +15 -4
  43. quasarr/search/sources/nk.py +15 -2
  44. quasarr/search/sources/nx.py +15 -4
  45. quasarr/search/sources/sf.py +25 -8
  46. quasarr/search/sources/sj.py +14 -1
  47. quasarr/search/sources/sl.py +17 -2
  48. quasarr/search/sources/wd.py +15 -4
  49. quasarr/search/sources/wx.py +16 -18
  50. quasarr/storage/setup.py +150 -35
  51. {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/METADATA +6 -3
  52. quasarr-2.2.0.dist-info/RECORD +82 -0
  53. {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/WHEEL +1 -1
  54. quasarr-2.1.5.dist-info/RECORD +0 -81
  55. {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/entry_points.txt +0 -0
  56. {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/licenses/LICENSE +0 -0
  57. {quasarr-2.1.5.dist-info → quasarr-2.2.0.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@ from urllib.parse import quote_plus
14
14
  import requests
15
15
  from bs4 import BeautifulSoup
16
16
 
17
+ from quasarr.providers.hostname_issues import mark_hostname_issue, clear_hostname_issue
17
18
  from quasarr.providers.imdb_metadata import get_localized_title
18
19
  from quasarr.providers.log import info, debug
19
20
 
@@ -60,8 +61,9 @@ def sl_feed(shared_state, start_time, request_from, mirror=None):
60
61
  headers = {'User-Agent': shared_state.values['user_agent']}
61
62
 
62
63
  try:
63
- xml_text = requests.get(url, headers=headers, timeout=10).text
64
- root = ET.fromstring(xml_text)
64
+ r = requests.get(url, headers=headers, timeout=30)
65
+ r.raise_for_status()
66
+ root = ET.fromstring(r.text)
65
67
 
66
68
  for item in root.find('channel').findall('item'):
67
69
  try:
@@ -110,13 +112,18 @@ def sl_feed(shared_state, start_time, request_from, mirror=None):
110
112
 
111
113
  except Exception as e:
112
114
  info(f"Error parsing {hostname.upper()} feed item: {e}")
115
+ mark_hostname_issue(hostname, "feed", str(e) if "e" in dir() else "Error occurred")
113
116
  continue
114
117
 
115
118
  except Exception as e:
116
119
  info(f"Error loading {hostname.upper()} feed: {e}")
120
+ mark_hostname_issue(hostname, "feed", str(e) if "e" in dir() else "Error occurred")
117
121
 
118
122
  elapsed = time.time() - start_time
119
123
  debug(f"Time taken: {elapsed:.2f}s ({hostname})")
124
+
125
+ if releases:
126
+ clear_hostname_issue(hostname)
120
127
  return releases
121
128
 
122
129
 
@@ -162,6 +169,7 @@ def sl_search(shared_state, start_time, request_from, search_string, mirror=None
162
169
  return r.text
163
170
  except Exception as e:
164
171
  info(f"Error fetching {hostname} url {url}: {e}")
172
+ mark_hostname_issue(hostname, "search", str(e) if "e" in dir() else "Error occurred")
165
173
  return ''
166
174
 
167
175
  html_texts = []
@@ -172,6 +180,7 @@ def sl_search(shared_state, start_time, request_from, search_string, mirror=None
172
180
  html_texts.append(future.result())
173
181
  except Exception as e:
174
182
  info(f"Error fetching {hostname} search page: {e}")
183
+ mark_hostname_issue(hostname, "search", str(e) if "e" in dir() else "Error occurred")
175
184
 
176
185
  # Parse each result and collect unique releases (dedupe by source link)
177
186
  seen_sources = set()
@@ -233,14 +242,20 @@ def sl_search(shared_state, start_time, request_from, search_string, mirror=None
233
242
  })
234
243
  except Exception as e:
235
244
  info(f"Error parsing {hostname.upper()} search item: {e}")
245
+ mark_hostname_issue(hostname, "search", str(e) if "e" in dir() else "Error occurred")
236
246
  continue
237
247
  except Exception as e:
238
248
  info(f"Error parsing {hostname.upper()} search HTML: {e}")
249
+ mark_hostname_issue(hostname, "search", str(e) if "e" in dir() else "Error occurred")
239
250
  continue
240
251
 
241
252
  except Exception as e:
242
253
  info(f"Error loading {hostname.upper()} search page: {e}")
254
+ mark_hostname_issue(hostname, "search", str(e) if "e" in dir() else "Error occurred")
243
255
 
244
256
  elapsed = time.time() - start_time
245
257
  debug(f"Search time: {elapsed:.2f}s ({hostname})")
258
+
259
+ if releases:
260
+ clear_hostname_issue(hostname)
246
261
  return releases
@@ -12,6 +12,7 @@ from urllib.parse import quote, quote_plus
12
12
  import requests
13
13
  from bs4 import BeautifulSoup
14
14
 
15
+ from quasarr.providers.hostname_issues import mark_hostname_issue, clear_hostname_issue
15
16
  from quasarr.providers.imdb_metadata import get_localized_title
16
17
  from quasarr.providers.log import info, debug
17
18
 
@@ -165,13 +166,18 @@ def wd_feed(shared_state, start_time, request_from, mirror=None):
165
166
  url = f"https://{wd}/{feed_type}"
166
167
  headers = {'User-Agent': shared_state.values["user_agent"]}
167
168
  try:
168
- response = requests.get(url, headers=headers, timeout=10).content
169
- soup = BeautifulSoup(response, "html.parser")
169
+ r = requests.get(url, headers=headers, timeout=10)
170
+ r.raise_for_status()
171
+ soup = BeautifulSoup(r.content, "html.parser")
170
172
  releases = _parse_rows(soup, shared_state, wd, password, mirror)
171
173
  except Exception as e:
172
174
  info(f"Error loading {hostname.upper()} feed: {e}")
175
+ mark_hostname_issue(hostname, "feed", str(e) if "e" in dir() else "Error occurred")
173
176
  releases = []
174
177
  debug(f"Time taken: {time.time() - start_time:.2f}s ({hostname})")
178
+
179
+ if releases:
180
+ clear_hostname_issue(hostname)
175
181
  return releases
176
182
 
177
183
 
@@ -193,8 +199,9 @@ def wd_search(shared_state, start_time, request_from, search_string, mirror=None
193
199
  headers = {'User-Agent': shared_state.values["user_agent"]}
194
200
 
195
201
  try:
196
- response = requests.get(url, headers=headers, timeout=10).content
197
- soup = BeautifulSoup(response, "html.parser")
202
+ r = requests.get(url, headers=headers, timeout=10)
203
+ r.raise_for_status()
204
+ soup = BeautifulSoup(r.content, "html.parser")
198
205
  releases = _parse_rows(
199
206
  soup, shared_state, wd, password, mirror,
200
207
  request_from=request_from,
@@ -203,6 +210,10 @@ def wd_search(shared_state, start_time, request_from, search_string, mirror=None
203
210
  )
204
211
  except Exception as e:
205
212
  info(f"Error loading {hostname.upper()} search: {e}")
213
+ mark_hostname_issue(hostname, "search", str(e) if "e" in dir() else "Error occurred")
206
214
  releases = []
207
215
  debug(f"Time taken: {time.time() - start_time:.2f}s ({hostname})")
216
+
217
+ if releases:
218
+ clear_hostname_issue(hostname)
208
219
  return releases
@@ -13,6 +13,7 @@ import requests
13
13
  from bs4 import BeautifulSoup
14
14
  from bs4 import XMLParsedAsHTMLWarning
15
15
 
16
+ from quasarr.providers.hostname_issues import mark_hostname_issue, clear_hostname_issue
16
17
  from quasarr.providers.imdb_metadata import get_localized_title
17
18
  from quasarr.providers.log import info, debug
18
19
 
@@ -39,13 +40,10 @@ def wx_feed(shared_state, start_time, request_from, mirror=None):
39
40
  }
40
41
 
41
42
  try:
42
- response = requests.get(rss_url, headers=headers, timeout=10)
43
+ r = requests.get(rss_url, headers=headers, timeout=10)
44
+ r.raise_for_status()
43
45
 
44
- if response.status_code != 200:
45
- info(f"{hostname.upper()}: RSS feed returned status {response.status_code}")
46
- return releases
47
-
48
- soup = BeautifulSoup(response.content, 'html.parser')
46
+ soup = BeautifulSoup(r.content, 'html.parser')
49
47
  items = soup.find_all('entry')
50
48
 
51
49
  if not items:
@@ -120,11 +118,14 @@ def wx_feed(shared_state, start_time, request_from, mirror=None):
120
118
 
121
119
  except Exception as e:
122
120
  info(f"Error loading {hostname.upper()} feed: {e}")
121
+ mark_hostname_issue(hostname, "feed", str(e) if "e" in dir() else "Error occurred")
123
122
  return releases
124
123
 
125
124
  elapsed_time = time.time() - start_time
126
125
  debug(f"Time taken: {elapsed_time:.2f}s ({hostname})")
127
126
 
127
+ if releases:
128
+ clear_hostname_issue(hostname)
128
129
  return releases
129
130
 
130
131
 
@@ -183,13 +184,10 @@ def wx_search(shared_state, start_time, request_from, search_string, mirror=None
183
184
  debug(f"{hostname.upper()}: Searching: '{search_string}'")
184
185
 
185
186
  try:
186
- response = requests.get(api_url, headers=headers, params=params, timeout=10)
187
-
188
- if response.status_code != 200:
189
- debug(f"{hostname.upper()}: Search API returned status {response.status_code}")
190
- return releases
187
+ r = requests.get(api_url, headers=headers, params=params, timeout=10)
188
+ r.raise_for_status()
191
189
 
192
- data = response.json()
190
+ data = r.json()
193
191
 
194
192
  if 'items' in data and 'data' in data['items']:
195
193
  items = data['items']['data']
@@ -215,13 +213,10 @@ def wx_search(shared_state, start_time, request_from, search_string, mirror=None
215
213
  debug(f"{hostname.upper()}: Fetching details for UID: {uid}")
216
214
 
217
215
  detail_url = f'https://api.{host}/start/d/{uid}'
218
- detail_response = requests.get(detail_url, headers=headers, timeout=10)
219
-
220
- if detail_response.status_code != 200:
221
- debug(f"{hostname.upper()}: Detail API returned {detail_response.status_code} for {uid}")
222
- continue
216
+ detail_r = requests.get(detail_url, headers=headers, timeout=10)
217
+ detail_r.raise_for_status()
223
218
 
224
- detail_data = detail_response.json()
219
+ detail_data = detail_r.json()
225
220
 
226
221
  if 'item' in detail_data:
227
222
  detail_item = detail_data['item']
@@ -344,6 +339,7 @@ def wx_search(shared_state, start_time, request_from, search_string, mirror=None
344
339
 
345
340
  except Exception as e:
346
341
  info(f"Error in {hostname.upper()} search: {e}")
342
+ mark_hostname_issue(hostname, "search", str(e) if "e" in dir() else "Error occurred")
347
343
 
348
344
  debug(f"{hostname.upper()}: {traceback.format_exc()}")
349
345
  return releases
@@ -351,4 +347,6 @@ def wx_search(shared_state, start_time, request_from, search_string, mirror=None
351
347
  elapsed_time = time.time() - start_time
352
348
  debug(f"Time taken: {elapsed_time:.2f}s ({hostname})")
353
349
 
350
+ if releases:
351
+ clear_hostname_issue(hostname)
354
352
  return releases
quasarr/storage/setup.py CHANGED
@@ -16,6 +16,7 @@ import quasarr.providers.sessions.dd
16
16
  import quasarr.providers.sessions.dl
17
17
  import quasarr.providers.sessions.nx
18
18
  from quasarr.providers.auth import add_auth_routes, add_auth_hook
19
+ from quasarr.providers.hostname_issues import get_all_hostname_issues
19
20
  from quasarr.providers.html_templates import render_button, render_form, render_success, render_fail, \
20
21
  render_centered_html
21
22
  from quasarr.providers.log import info
@@ -74,7 +75,7 @@ def render_reconnect_success(message, countdown_seconds=3):
74
75
  '''
75
76
 
76
77
  content = f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
77
- <h2>✓ Success</h2>
78
+ <h2>✅ Success</h2>
78
79
  <p>{message}</p>
79
80
  {button_html}
80
81
  {script}
@@ -156,9 +157,20 @@ def path_config(shared_state):
156
157
  return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
157
158
 
158
159
 
160
+ def _escape_js_for_html_attr(s):
161
+ """Escape a string for use inside a JS string literal within an HTML attribute."""
162
+ if s is None:
163
+ return ""
164
+ return str(s).replace("\\", "\\\\").replace("'", "\\'").replace('"', '&quot;').replace("\n", "\\n").replace("\r", "")
165
+
166
+
159
167
  def hostname_form_html(shared_state, message, show_restart_button=False, show_skip_management=False):
160
168
  hostname_fields = '''
161
- <label for="{id}" style="display:inline-flex; align-items:center; gap:4px;">{label}{img_html}</label>
169
+ <label for="{id}" onclick="showStatusDetail(\'{id}\', \'{label}\', \'{status}\', \'{error_details_for_modal}\', \'{timestamp}\', \'{operation}\', \'{url}\')"
170
+ style="cursor:pointer; display:inline-flex; align-items:center; gap:4px;" title="{status_title}">
171
+ <span class="status-indicator" id="status-{id}" data-status="{status}">{status_emoji}</span>
172
+ {label}
173
+ </label>
162
174
  <input type="text" id="{id}" name="{id}" placeholder="example.com" autocorrect="off" autocomplete="off" value="{value}"><br>
163
175
  '''
164
176
 
@@ -172,28 +184,58 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
172
184
  field_html = []
173
185
  hostnames = Config('Hostnames') # Load once outside the loop
174
186
  skip_login_db = DataBase("skip_login")
187
+ hostname_issues = get_all_hostname_issues()
175
188
  login_required_sites = ['al', 'dd', 'dl', 'nx']
176
189
 
177
190
  for label in shared_state.values["sites"]:
178
191
  field_id = label.lower()
179
- img_html = ''
180
- try:
181
- img_data = getattr(images, field_id)
182
- if img_data:
183
- img_html = f' <img src="{img_data}" width="16" height="16" style="filter: blur(2px);" alt="{label} icon">'
184
- except AttributeError:
185
- pass
186
192
 
187
193
  # Get the current value (if any and non-empty)
188
194
  current_value = hostnames.get(field_id)
189
195
  if not current_value:
190
196
  current_value = '' # Ensure it's empty if None or ""
191
197
 
198
+ # Determine traffic light status
199
+ is_login_skipped = field_id in login_required_sites and skip_login_db.retrieve(field_id)
200
+ issue = hostname_issues.get(field_id)
201
+ timestamp = ""
202
+ operation = ""
203
+ error_details_for_modal = "" # New variable to hold the full error message for the modal
204
+
205
+ if not current_value:
206
+ status = "unset"
207
+ status_emoji = "⚫️"
208
+ status_title = "Hostname not configured"
209
+ error_details_for_modal = "This hostname is not configured."
210
+ elif is_login_skipped:
211
+ status = "skipped"
212
+ status_emoji = "🟡"
213
+ status_title = "Login was skipped"
214
+ error_details_for_modal = "Login was skipped for this site."
215
+ elif issue:
216
+ status = "error"
217
+ status_emoji = "🔴"
218
+ operation = issue.get("operation", "unknown")
219
+ error_details_for_modal = issue.get("error", "Unknown error") # Get the full error message
220
+ timestamp = issue.get("timestamp", "")
221
+ status_title = f"Error in {operation}"
222
+ else:
223
+ status = "ok"
224
+ status_emoji = "🟢"
225
+ status_title = "Working normally"
226
+ error_details_for_modal = "Configured and working normally."
227
+
192
228
  field_html.append(hostname_fields.format(
193
229
  id=field_id,
194
- label=label,
195
- img_html=img_html,
196
- value=current_value
230
+ label=_escape_js_for_html_attr(label),
231
+ value=current_value,
232
+ status=status,
233
+ status_emoji=status_emoji,
234
+ status_title=status_title,
235
+ error_details_for_modal=_escape_js_for_html_attr(error_details_for_modal),
236
+ timestamp=timestamp,
237
+ operation=_escape_js_for_html_attr(operation),
238
+ url=_escape_js_for_html_attr(current_value)
197
239
  ))
198
240
 
199
241
  # Add skip indicator for login-required sites if skip management is enabled
@@ -254,6 +296,12 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
254
296
  .import-status.success {{ color: #198754; }}
255
297
  .import-status.error {{ color: #dc3545; }}
256
298
  .import-status.loading {{ color: var(--secondary, #6c757d); }}
299
+ .status-indicator {{
300
+ transition: transform 0.1s ease;
301
+ }}
302
+ .status-indicator:hover {{
303
+ transform: scale(1.2);
304
+ }}
257
305
  .btn-subtle {{
258
306
  background: transparent;
259
307
  color: var(--fg-color, #212529);
@@ -274,7 +322,7 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
274
322
  <div class="url-import-section">
275
323
  <h3>📥 Import from URL</h3>
276
324
  <div class="url-import-row">
277
- <input type="text" id="hostnamesUrl" placeholder="https://quasarr-host.name/ini?token=123..." value="{stored_url}" autocorrect="off" autocomplete="off" onfocus="onHostnameFieldFocus()">
325
+ <input type="url" id="hostnamesUrl" placeholder="https://quasarr-host.name/ini?token=123..." value="{stored_url}" onfocus="onHostnameFieldFocus()">
278
326
  <button type="button" class="btn-secondary" id="importBtn" onclick="importHostnames()">Import</button>
279
327
  </div>
280
328
  <div id="importStatus" class="import-status"></div>
@@ -313,7 +361,6 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
313
361
  }}
314
362
 
315
363
  errorDiv.textContent = 'Please fill in at least one hostname!';
316
- inputs[0].focus();
317
364
  return false;
318
365
  }}
319
366
 
@@ -391,33 +438,38 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
391
438
  .then(response => response.json())
392
439
  .then(data => {{
393
440
  if (data.success) {{
394
- // Remove the skip indicator using the button's parent
395
441
  var indicator = btnElement.closest('.skip-indicator');
396
442
  if (indicator) indicator.remove();
397
- alert('Login requirement restored for ' + shorthand.toUpperCase() + '. Restart Quasarr to be prompted for credentials.');
443
+ showStatusDetail(shorthand, shorthand.toUpperCase(), 'info', 'Login requirement restored. Restart Quasarr to be prompted for credentials.', '', '', '');
398
444
  }} else {{
399
- alert('Failed to clear skip preference');
445
+ showStatusDetail(shorthand, shorthand.toUpperCase(), 'error', 'Failed to clear skip preference', '', '', '');
400
446
  }}
401
447
  }})
402
448
  .catch(error => {{
403
- alert('Error: ' + error.message);
449
+ showStatusDetail(shorthand, shorthand.toUpperCase(), 'error', 'Error: ' + error.message, '', '', '');
404
450
  }});
405
451
  }}
406
452
 
407
453
  function confirmRestart() {{
408
- if (confirm('Restart Quasarr now? Any unsaved changes will be lost.')) {{
409
- fetch('/api/restart', {{ method: 'POST' }})
410
- .then(response => response.json())
411
- .then(data => {{
412
- if (data.success) {{
413
- showRestartOverlay();
414
- }}
415
- }})
416
- .catch(error => {{
417
- // Expected - connection will be lost during restart
454
+ showModal('Restart Quasarr?', 'Are you sure you want to restart Quasarr now? Any unsaved changes will be lost.',
455
+ `<button class="btn-secondary" onclick="closeModal()">Cancel</button>
456
+ <button class="btn-primary" onclick="performRestart()">Restart</button>`
457
+ );
458
+ }}
459
+
460
+ function performRestart() {{
461
+ closeModal();
462
+ fetch('/api/restart', {{ method: 'POST' }})
463
+ .then(response => response.json())
464
+ .then(data => {{
465
+ if (data.success) {{
418
466
  showRestartOverlay();
419
- }});
420
- }}
467
+ }}
468
+ }})
469
+ .catch(error => {{
470
+ // Expected - connection will be lost during restart
471
+ showRestartOverlay();
472
+ }});
421
473
  }}
422
474
 
423
475
  function showRestartOverlay() {{
@@ -481,6 +533,69 @@ def hostname_form_html(shared_state, message, show_restart_button=False, show_sk
481
533
  attempt();
482
534
  }}
483
535
  </script>
536
+ <script>
537
+ function showStatusDetail(id, label, status, error_details, timestamp, operation, url) {{
538
+ var statusTextMap = {{
539
+ ok: 'Operational',
540
+ error: 'Error',
541
+ unset: 'Not configured',
542
+ skipped: 'Login skipped',
543
+ info: 'Information'
544
+ }};
545
+
546
+ var emojiMap = {{
547
+ ok: '🟢',
548
+ error: '🔴',
549
+ unset: '⚫️',
550
+ skipped: '🟡',
551
+ info: 'ℹ️'
552
+ }};
553
+
554
+ var content_html = '';
555
+ if (status === 'error') {{
556
+ content_html += '<p>' + (error_details || 'No details available.') + '</p>';
557
+ }} else {{
558
+ content_html += '<p>' + (error_details || 'No additional details available.') + '</p>';
559
+ }}
560
+
561
+ var timestamp_html = '';
562
+ if (timestamp) {{
563
+ var d = new Date(timestamp);
564
+ var day = ("0" + d.getDate()).slice(-2);
565
+ var month = ("0" + (d.getMonth() + 1)).slice(-2);
566
+ var year = d.getFullYear();
567
+ var hours = ("0" + d.getHours()).slice(-2);
568
+ var minutes = ("0" + d.getMinutes()).slice(-2);
569
+ var seconds = ("0" + d.getSeconds()).slice(-2);
570
+ var formattedTimestamp = day + "." + month + "." + year + " " + hours + ":" + minutes + ":" + seconds;
571
+
572
+ if (operation) {{
573
+ timestamp_html = '<p><small>Occurred in ' + operation + ' at ' + formattedTimestamp + '</small></p>';
574
+ }} else {{
575
+ timestamp_html = '<p><small>Occurred at: ' + formattedTimestamp + '</small></p>';
576
+ }}
577
+ }}
578
+
579
+ var content = content_html + timestamp_html;
580
+ var title = '<span>' + (emojiMap[status] || 'ℹ️') + '</span> ' + label + ' - ' + (statusTextMap[status] || status);
581
+
582
+ var buttons = '';
583
+ if (url) {{
584
+ var href = url;
585
+ if (!href.startsWith('http://') && !href.startsWith('https://')) {{
586
+ href = 'https://' + href;
587
+ }}
588
+ buttons = `
589
+ <button class="btn-primary" style="margin-right: auto;" onclick="window.open('${{href}}', '_blank')">Check ${{id.toUpperCase()}}</button>
590
+ <button class="btn-secondary" onclick="closeModal()">Close</button>
591
+ `;
592
+ }} else {{
593
+ buttons = '<button class="btn-secondary" onclick="closeModal()">Close</button>';
594
+ }}
595
+
596
+ showModal(title, content, buttons);
597
+ }}
598
+ </script>
484
599
  """
485
600
  return template.format(
486
601
  message=message,
@@ -719,14 +834,14 @@ def hostname_credentials_config(shared_state, shorthand, domain):
719
834
  if (response.ok) {{
720
835
  window.location.href = '/skip-success';
721
836
  }} else {{
722
- alert('Failed to skip login');
837
+ showModal('Error', 'Failed to skip login');
723
838
  formSubmitted = false;
724
839
  if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
725
840
  if (submitBtn) {{ submitBtn.disabled = false; }}
726
841
  }}
727
842
  }})
728
843
  .catch(error => {{
729
- alert('Error: ' + error.message);
844
+ showModal('Error', 'Error: ' + error.message);
730
845
  formSubmitted = false;
731
846
  if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
732
847
  if (submitBtn) {{ submitBtn.disabled = false; }}
@@ -870,14 +985,14 @@ def flaresolverr_config(shared_state):
870
985
  if (response.ok) {{
871
986
  window.location.href = '/skip-success';
872
987
  }} else {{
873
- alert('Failed to skip FlareSolverr setup');
988
+ showModal('Error', 'Failed to skip FlareSolverr setup');
874
989
  formSubmitted = false;
875
990
  if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
876
991
  if (submitBtn) {{ submitBtn.disabled = false; }}
877
992
  }}
878
993
  }})
879
994
  .catch(error => {{
880
- alert('Error: ' + error.message);
995
+ showModal('Error', 'Error: ' + error.message);
881
996
  formSubmitted = false;
882
997
  if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
883
998
  if (submitBtn) {{ submitBtn.disabled = false; }}
@@ -1010,7 +1125,7 @@ def jdownloader_config(shared_state):
1010
1125
  document.getElementById("verifyButton").style.display = "none";
1011
1126
  document.getElementById('deviceForm').style.display = 'block';
1012
1127
  } else {
1013
- alert('Fehler! Bitte die Zugangsdaten überprüfen.');
1128
+ showModal('Error', 'Fehler! Bitte die Zugangsdaten überprüfen.');
1014
1129
  verifyInProgress = false;
1015
1130
  if (btn) { btn.disabled = false; btn.textContent = 'Verify Credentials'; }
1016
1131
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quasarr
3
- Version: 2.1.5
3
+ Version: 2.2.0
4
4
  Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
5
5
  Home-page: https://github.com/rix1337/Quasarr
6
6
  Author: rix1337
@@ -25,7 +25,7 @@ Dynamic: license-file
25
25
  Dynamic: requires-dist
26
26
  Dynamic: summary
27
27
 
28
- #
28
+ #
29
29
 
30
30
  <img src="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png" data-canonical-src="https://raw.githubusercontent.com/rix1337/Quasarr/main/Quasarr.png" width="64" height="64" />
31
31
 
@@ -125,12 +125,15 @@ Add Quasarr as both a **Newznab Indexer** and **SABnzbd Download Client** using
125
125
  <details>
126
126
  <summary>Restrict results to a specific mirror</summary>
127
127
 
128
- Append the mirror name to your Newznab URL:
128
+ 1. In the Newznab Settings for Quasarr, enable advanced settings.
129
+ 2. Append the desired mirror name to the `API Path` field.
129
130
 
130
131
  ```
131
132
  /api/dropbox/
132
133
  ```
133
134
 
135
+ Using the `URL` field will not work!
136
+
134
137
  Only releases with `dropbox` in a link will be returned. If the mirror isn't available, the release will fail.
135
138
 
136
139
  </details>
@@ -0,0 +1,82 @@
1
+ quasarr/__init__.py,sha256=cEtxN2AuwKvrxpIvAR7UL997VtYQ4iN3Eo3ZnP-WjZQ,14682
2
+ quasarr/api/__init__.py,sha256=KLnFSe5l3MrVgrbu6-7GlE2PqouVyizqiRZfQkBtge0,19587
3
+ quasarr/api/arr/__init__.py,sha256=eEop8A5t936uT5azn4qz0bq1DMX84_Ja16wyleGFhyM,18495
4
+ quasarr/api/captcha/__init__.py,sha256=Mqg2HhWMaUc07cVaEYHAbf-YvnxkiYVbkWT-g92J-2k,72960
5
+ quasarr/api/config/__init__.py,sha256=kIGCHtKTUovOHe9xMEdz-6_psCmx6aFoyrTP-jJah0s,14187
6
+ quasarr/api/packages/__init__.py,sha256=ox0vzuXByag49RUEwYPWtMacsXl_iksvubHgDmG5RWQ,25192
7
+ quasarr/api/sponsors_helper/__init__.py,sha256=vZIFGkc5HTRozjvi47tqxz6XpwDe8sDXVyeydc9k0Y0,6708
8
+ quasarr/api/statistics/__init__.py,sha256=0Os2rbqQ8ZN3R0XAavGVHlacKsAjp7GYjEIJCwvnsl8,7063
9
+ quasarr/downloads/__init__.py,sha256=ikoHK5C8veDiU4M3eoDaUjFl0pYPSa91_7h65qEFiUM,16435
10
+ quasarr/downloads/linkcrypters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ quasarr/downloads/linkcrypters/al.py,sha256=mfUG5VclC_-FcGoZL9zHYD7dz7X_YpaNmoKkgiyl9-0,8812
12
+ quasarr/downloads/linkcrypters/filecrypt.py,sha256=nUZCTmvKylaNk1KAXcYUV1FgQCVAKNE3roXCNaqHLYA,17057
13
+ quasarr/downloads/linkcrypters/hide.py,sha256=H4hJWhENkszV1u_ULC3aOW2fu9infC-Nv-7wx2DYqrA,6266
14
+ quasarr/downloads/packages/__init__.py,sha256=VUbDnJqhqNDMLwCHYyWQKFKhU32xGMPXdrO0N5fxH8Q,32747
15
+ quasarr/downloads/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ quasarr/downloads/sources/al.py,sha256=cV0rOpda4YHmv5X07855wzoaczd3yFGdPyFdm07srFM,27842
17
+ quasarr/downloads/sources/by.py,sha256=ThyEVblYxaxKS_iROpxLhpqp2gOpcHjI1TCKX7CtrNw,4310
18
+ quasarr/downloads/sources/dd.py,sha256=6eOr8FVlbLElBXkoPmKxZ4AQq_Ruq2CimAHHrihHb-A,3372
19
+ quasarr/downloads/sources/dj.py,sha256=wY00hVRNhucZBG1hfExKqayhP1ISD8FFQm7wHYxutOk,404
20
+ quasarr/downloads/sources/dl.py,sha256=zLKVDQS4t_dWbE_I8VlYFOrN28RkBqSHMhJeLcZgqOY,15052
21
+ quasarr/downloads/sources/dt.py,sha256=ycGQtFMOX_8uoeYNjTTCc7FFRCjjwGX6l9FZaCXpfgc,3064
22
+ quasarr/downloads/sources/dw.py,sha256=J0_WhcFJKKNi3cfjwsz7qRM3K8rvNyXC7TjWVRj9Qlc,2743
23
+ quasarr/downloads/sources/he.py,sha256=fmv4f6wrVfJpZtKlQlRq32XCy-I2ROnolBD1CWPyOvY,3986
24
+ quasarr/downloads/sources/mb.py,sha256=ELoHcCmS3iXb0IZXlX444jqIbgrVxQrWB2MwILpjm8c,1826
25
+ quasarr/downloads/sources/nk.py,sha256=XdHzg_u5BfKEU8Mhoim_tiu5oJtECvjNKh2JBal9emA,2052
26
+ quasarr/downloads/sources/nx.py,sha256=IQizLMSa867fZWaOSgVBtGjUMNi06EWIUBMjyAcL6xE,3839
27
+ quasarr/downloads/sources/sf.py,sha256=-IOB3zc94Rd6TzJE59HMmsySNd7feYdqWqNqPhzxjZY,6556
28
+ quasarr/downloads/sources/sj.py,sha256=Bkv0c14AXct50n_viaTNK3bYG-Bpvx8x2D0UN_6gm78,404
29
+ quasarr/downloads/sources/sl.py,sha256=zhU3C172IwyfrVmFSwZ34PywERHJFk0ownT2gbpUAVg,3521
30
+ quasarr/downloads/sources/wd.py,sha256=pZrLnRvVFzsZTWTIcHLCnUlSz5Y6HmEJpqcZjDigx2E,4820
31
+ quasarr/downloads/sources/wx.py,sha256=jk_RSKKPa8iDPXfXKERG_4NwRMkFV6_rJhJ-zZlNQuo,6855
32
+ quasarr/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ quasarr/providers/auth.py,sha256=ELJeKtJL0zy1UuBORzQibN6frAnh3lMv_HiofSW0hXA,10533
34
+ quasarr/providers/cloudflare.py,sha256=oUDR7OQ8E-8vCtagZLnIS2ZZV3ERffhxmW0njKKbtf0,7867
35
+ quasarr/providers/hostname_issues.py,sha256=9PJFIosLB-bMTmgWlR5-sYAmcyps7TDoSYjoL9cw9TE,1460
36
+ quasarr/providers/html_images.py,sha256=rrovPNl-FTTKKA-4HCPEhsYpq5b20VDrsB7t4RrQf3w,15531
37
+ quasarr/providers/html_templates.py,sha256=IGWwt78bP2oJx4VzOP6w9zp7KVXgDY6Qz5ySL9cLGWI,15815
38
+ quasarr/providers/imdb_metadata.py,sha256=10L4kZkt6Fg0HGdNcc6KCtIQHRYEqdarLyaMVN6mT8w,4843
39
+ quasarr/providers/jd_cache.py,sha256=mSvMrs3UwTn3sd9yGSJKGT-qwYeyYKC_l8whpXTVn7s,13530
40
+ quasarr/providers/log.py,sha256=_g5RwtfuksARXnvryhsngzoJyFcNzj6suqd3ndqZM0Y,313
41
+ quasarr/providers/myjd_api.py,sha256=Z3PEiO3c3UfDSr4Up5rgwTAnjloWHb-H1RkJ6BLKZv8,34140
42
+ quasarr/providers/notifications.py,sha256=bohT-6yudmFnmZMc3BwCGX0n1HdzSVgQG_LDZm_38dI,4630
43
+ quasarr/providers/obfuscated.py,sha256=EYm_7SfdJd9ae_m4HZgY9ruDXC5J9hb4KEV_WAnk-ms,2275588
44
+ quasarr/providers/shared_state.py,sha256=5a_ZbGqTvt4-OqBt2a1WtR9I5J_Ky7IlkEY8EGtKVu8,30646
45
+ quasarr/providers/statistics.py,sha256=cEQixYnDMDqtm5wWe40E_2ucyo4mD0n3SrfelhQi1L8,6452
46
+ quasarr/providers/utils.py,sha256=mcUPbcXMsLmrYv0CTZO5a9aOt2-JLyL3SZxu6N8OyjU,12075
47
+ quasarr/providers/version.py,sha256=iakqDG1xdl-OhipfMJ9jdOG8du1BnS6rllqOId1-LAo,4003
48
+ quasarr/providers/web_server.py,sha256=AYd0KRxdDWMBr87BP8wlSMuL4zZo0I_rY-vHBai6Pfg,1688
49
+ quasarr/providers/sessions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
+ quasarr/providers/sessions/al.py,sha256=AQ59vVU7uQSuwZLNppNsZAFvpow3zcxQ29dirPbyYc4,13432
51
+ quasarr/providers/sessions/dd.py,sha256=ty9dnDFVJs-tFNcTS5QT9_wP82cKQGnCvb6v5In3Mog,3324
52
+ quasarr/providers/sessions/dl.py,sha256=yTJlD84ItotViA1d-m0RwrbEJlL-VK-0nGw_4kfNLe0,5923
53
+ quasarr/providers/sessions/nx.py,sha256=ZuWuqfb_rPJVom0c1dsXefXPXdzAIYqnQZapOPaUYUI,3421
54
+ quasarr/search/__init__.py,sha256=V59LIiC75mQvasDdTjiWZRbPD1jXO1lhXlKeNVX0iOc,5726
55
+ quasarr/search/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
+ quasarr/search/sources/al.py,sha256=-2-yRGubyE7bw4-ntGtZ04_LkbnEXhTidfKzKfmeVws,17745
57
+ quasarr/search/sources/by.py,sha256=vNKMEmFpWxpZS9shh5M8gzrOlyyoOc8CveFv6piJ9FM,8344
58
+ quasarr/search/sources/dd.py,sha256=JBQFdCHHv8bOoHAqpzdRSvgVPpRi4udNlFCo9eOoo-M,5593
59
+ quasarr/search/sources/dj.py,sha256=G8O7-Nqx9kuaRtm-kZw1A1Fy0BqorCeNs20qfKF-b_I,7546
60
+ quasarr/search/sources/dl.py,sha256=L4GK58Mp46dAZzmwtMB4ia1w0SSpp3z3eFvrmT-5278,13136
61
+ quasarr/search/sources/dt.py,sha256=hvOqPKQRw5joSaTb9mpdPZXL4xpU167SFmLg8yhsPwM,10227
62
+ quasarr/search/sources/dw.py,sha256=hna1ueKjdi9uqRQJ7UPenT0ym7igQgWGrv_--yGChVs,8215
63
+ quasarr/search/sources/fx.py,sha256=xZUrv7dJSSmeLR2xnRQsRZAk9Q0-fDfQLNjz4wdBTqo,9452
64
+ quasarr/search/sources/he.py,sha256=SoH6X-PsnaOUiQL3yaUbWkI-DDjnyQCMSAwAmv-vpAc,7063
65
+ quasarr/search/sources/mb.py,sha256=Hq1zupo27FzYSQUio03HPG0wP4jYwOXl6cqgdOpjlzQ,8178
66
+ quasarr/search/sources/nk.py,sha256=trb5rTQL_j9br6yBsdSFUp-V4L8_lFYEYpQ4qcB-JlE,6989
67
+ quasarr/search/sources/nx.py,sha256=UXUSYEL4zwYVwCri359I26GYN8CDuCKokpOOR21YEns,7602
68
+ quasarr/search/sources/sf.py,sha256=9k9K8_tYVarpW8n20HA2qAplBL14mIQCsorJO-ZxN6g,15811
69
+ quasarr/search/sources/sj.py,sha256=LW2dVDfZ90mDdrQ6ZYtXb0eOjV3cCh6kEW7lTra1c5M,7608
70
+ quasarr/search/sources/sl.py,sha256=kzojkXw34AFlBfQIOKm8S-iuu9VXLNSfd3VgrkHItbY,10408
71
+ quasarr/search/sources/wd.py,sha256=IvB0Lm8Vb0XI-OwuqCccakhMSQBoTKhEDnvrKDTmP14,7969
72
+ quasarr/search/sources/wx.py,sha256=E7vSLV11540pson1HU-WVb7v7oX67kW8Y3-wiS-mv7w,13844
73
+ quasarr/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
+ quasarr/storage/config.py,sha256=SSTgIce2FVYoVTK_6OCU3msknhxuLA3EC4Kcrrf_dxQ,6378
75
+ quasarr/storage/setup.py,sha256=Cbo0phZbC6JP2wx_qER3vpaLSTDLbKEfdXj6KoAMkWw,47403
76
+ quasarr/storage/sqlite_database.py,sha256=yMqFQfKf0k7YS-6Z3_7pj4z1GwWSXJ8uvF4IydXsuTE,3554
77
+ quasarr-2.2.0.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
78
+ quasarr-2.2.0.dist-info/METADATA,sha256=dkJt9lLP1HUd-JSYhswlcYSsHYPz9wlU44gBXFlu7NA,15024
79
+ quasarr-2.2.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
80
+ quasarr-2.2.0.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
81
+ quasarr-2.2.0.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
82
+ quasarr-2.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5