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
@@ -3,7 +3,7 @@
3
3
  # Project by https://github.com/rix1337
4
4
 
5
5
  from json import loads
6
- from typing import Dict, Any
6
+ from typing import Any, Dict
7
7
 
8
8
 
9
9
  class StatsHelper:
@@ -29,7 +29,7 @@ class StatsHelper:
29
29
  "captcha_decryptions_manual": 0,
30
30
  "failed_downloads": 0,
31
31
  "failed_decryptions_automatic": 0,
32
- "failed_decryptions_manual": 0
32
+ "failed_decryptions_manual": 0,
33
33
  }
34
34
 
35
35
  db = self._get_db()
@@ -58,7 +58,7 @@ class StatsHelper:
58
58
  Args:
59
59
  links: Can be:
60
60
  - list/array: counts the length
61
- - int: uses the value directly
61
+ - int: uses the value directly
62
62
  - None/False/empty: treats as failed download
63
63
  """
64
64
  # Handle different input types
@@ -127,7 +127,11 @@ class StatsHelper:
127
127
  if data.get("poster_link"):
128
128
  with_poster += 1
129
129
 
130
- if data.get("localized") and isinstance(data["localized"], dict) and len(data["localized"]) > 0:
130
+ if (
131
+ data.get("localized")
132
+ and isinstance(data["localized"], dict)
133
+ and len(data["localized"]) > 0
134
+ ):
131
135
  with_localized += 1
132
136
 
133
137
  except (ValueError, TypeError):
@@ -137,14 +141,14 @@ class StatsHelper:
137
141
  "imdb_total_cached": total_cached,
138
142
  "imdb_with_title": with_title,
139
143
  "imdb_with_poster": with_poster,
140
- "imdb_with_localized": with_localized
144
+ "imdb_with_localized": with_localized,
141
145
  }
142
146
  except Exception:
143
147
  return {
144
148
  "imdb_total_cached": 0,
145
149
  "imdb_with_title": 0,
146
150
  "imdb_with_poster": 0,
147
- "imdb_with_localized": 0
151
+ "imdb_with_localized": 0,
148
152
  }
149
153
 
150
154
  def get_stats(self) -> Dict[str, Any]:
@@ -152,50 +156,78 @@ class StatsHelper:
152
156
  stats = {
153
157
  "packages_downloaded": self._get_stat("packages_downloaded", 0),
154
158
  "links_processed": self._get_stat("links_processed", 0),
155
- "captcha_decryptions_automatic": self._get_stat("captcha_decryptions_automatic", 0),
156
- "captcha_decryptions_manual": self._get_stat("captcha_decryptions_manual", 0),
159
+ "captcha_decryptions_automatic": self._get_stat(
160
+ "captcha_decryptions_automatic", 0
161
+ ),
162
+ "captcha_decryptions_manual": self._get_stat(
163
+ "captcha_decryptions_manual", 0
164
+ ),
157
165
  "failed_downloads": self._get_stat("failed_downloads", 0),
158
- "failed_decryptions_automatic": self._get_stat("failed_decryptions_automatic", 0),
159
- "failed_decryptions_manual": self._get_stat("failed_decryptions_manual", 0)
166
+ "failed_decryptions_automatic": self._get_stat(
167
+ "failed_decryptions_automatic", 0
168
+ ),
169
+ "failed_decryptions_manual": self._get_stat("failed_decryptions_manual", 0),
160
170
  }
161
171
 
162
172
  # Calculate totals and rates
163
- total_captcha_decryptions = stats["captcha_decryptions_automatic"] + stats["captcha_decryptions_manual"]
164
- total_failed_decryptions = stats["failed_decryptions_automatic"] + stats["failed_decryptions_manual"]
165
- total_download_attempts = stats["packages_downloaded"] + stats["failed_downloads"]
173
+ total_captcha_decryptions = (
174
+ stats["captcha_decryptions_automatic"] + stats["captcha_decryptions_manual"]
175
+ )
176
+ total_failed_decryptions = (
177
+ stats["failed_decryptions_automatic"] + stats["failed_decryptions_manual"]
178
+ )
179
+ total_download_attempts = (
180
+ stats["packages_downloaded"] + stats["failed_downloads"]
181
+ )
166
182
  total_decryption_attempts = total_captcha_decryptions + total_failed_decryptions
167
- total_automatic_attempts = stats["captcha_decryptions_automatic"] + stats["failed_decryptions_automatic"]
168
- total_manual_attempts = stats["captcha_decryptions_manual"] + stats["failed_decryptions_manual"]
183
+ total_automatic_attempts = (
184
+ stats["captcha_decryptions_automatic"]
185
+ + stats["failed_decryptions_automatic"]
186
+ )
187
+ total_manual_attempts = (
188
+ stats["captcha_decryptions_manual"] + stats["failed_decryptions_manual"]
189
+ )
169
190
 
170
191
  # Add calculated fields
171
- stats.update({
172
- "total_captcha_decryptions": total_captcha_decryptions,
173
- "total_failed_decryptions": total_failed_decryptions,
174
- "total_download_attempts": total_download_attempts,
175
- "total_decryption_attempts": total_decryption_attempts,
176
- "total_automatic_attempts": total_automatic_attempts,
177
- "total_manual_attempts": total_manual_attempts,
178
- "download_success_rate": (
179
- (stats["packages_downloaded"] / total_download_attempts * 100)
180
- if total_download_attempts > 0 else 0
181
- ),
182
- "decryption_success_rate": (
183
- (total_captcha_decryptions / total_decryption_attempts * 100)
184
- if total_decryption_attempts > 0 else 0
185
- ),
186
- "automatic_decryption_success_rate": (
187
- (stats["captcha_decryptions_automatic"] / total_automatic_attempts * 100)
188
- if total_automatic_attempts > 0 else 0
189
- ),
190
- "manual_decryption_success_rate": (
191
- (stats["captcha_decryptions_manual"] / total_manual_attempts * 100)
192
- if total_manual_attempts > 0 else 0
193
- ),
194
- "average_links_per_package": (
195
- stats["links_processed"] / stats["packages_downloaded"]
196
- if stats["packages_downloaded"] > 0 else 0
197
- )
198
- })
192
+ stats.update(
193
+ {
194
+ "total_captcha_decryptions": total_captcha_decryptions,
195
+ "total_failed_decryptions": total_failed_decryptions,
196
+ "total_download_attempts": total_download_attempts,
197
+ "total_decryption_attempts": total_decryption_attempts,
198
+ "total_automatic_attempts": total_automatic_attempts,
199
+ "total_manual_attempts": total_manual_attempts,
200
+ "download_success_rate": (
201
+ (stats["packages_downloaded"] / total_download_attempts * 100)
202
+ if total_download_attempts > 0
203
+ else 0
204
+ ),
205
+ "decryption_success_rate": (
206
+ (total_captcha_decryptions / total_decryption_attempts * 100)
207
+ if total_decryption_attempts > 0
208
+ else 0
209
+ ),
210
+ "automatic_decryption_success_rate": (
211
+ (
212
+ stats["captcha_decryptions_automatic"]
213
+ / total_automatic_attempts
214
+ * 100
215
+ )
216
+ if total_automatic_attempts > 0
217
+ else 0
218
+ ),
219
+ "manual_decryption_success_rate": (
220
+ (stats["captcha_decryptions_manual"] / total_manual_attempts * 100)
221
+ if total_manual_attempts > 0
222
+ else 0
223
+ ),
224
+ "average_links_per_package": (
225
+ stats["links_processed"] / stats["packages_downloaded"]
226
+ if stats["packages_downloaded"] > 0
227
+ else 0
228
+ ),
229
+ }
230
+ )
199
231
 
200
232
  # Add IMDb cache stats
201
233
  stats.update(self.get_imdb_cache_stats())
@@ -62,7 +62,7 @@ def extract_kv_pairs(input_text, allowed_keys):
62
62
  """
63
63
  kv_pattern = re.compile(rf"^({'|'.join(map(re.escape, allowed_keys))})\s*=\s*(.*)$")
64
64
  kv_pairs = {}
65
- debug = os.getenv('DEBUG')
65
+ debug = os.getenv("DEBUG")
66
66
 
67
67
  for line in input_text.splitlines():
68
68
  match = kv_pattern.match(line.strip())
@@ -73,7 +73,9 @@ def extract_kv_pairs(input_text, allowed_keys):
73
73
  pass
74
74
  else:
75
75
  if debug:
76
- print(f"Skipping line because it does not contain any supported hostname: {line}")
76
+ print(
77
+ f"Skipping line because it does not contain any supported hostname: {line}"
78
+ )
77
79
 
78
80
  return kv_pairs
79
81
 
@@ -81,10 +83,10 @@ def extract_kv_pairs(input_text, allowed_keys):
81
83
  def check_ip():
82
84
  s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
83
85
  try:
84
- s.connect(('10.255.255.255', 0))
86
+ s.connect(("10.255.255.255", 0))
85
87
  ip = s.getsockname()[0]
86
88
  except:
87
- ip = '127.0.0.1'
89
+ ip = "127.0.0.1"
88
90
  finally:
89
91
  s.close()
90
92
  return ip
@@ -98,14 +100,12 @@ def check_flaresolverr(shared_state, flaresolverr_url):
98
100
 
99
101
  # Try sending a simple test request
100
102
  headers = {"Content-Type": "application/json"}
101
- data = {
102
- "cmd": "request.get",
103
- "url": "http://www.google.com/",
104
- "maxTimeout": 10000
105
- }
103
+ data = {"cmd": "request.get", "url": "http://www.google.com/", "maxTimeout": 10000}
106
104
 
107
105
  try:
108
- response = requests.post(flaresolverr_url, headers=headers, json=data, timeout=10)
106
+ response = requests.post(
107
+ flaresolverr_url, headers=headers, json=data, timeout=10
108
+ )
109
109
  response.raise_for_status()
110
110
  json_data = response.json()
111
111
 
@@ -132,7 +132,8 @@ def validate_address(address, name):
132
132
  colon_count = address.count(":")
133
133
  if colon_count < 1 or colon_count > 2:
134
134
  sys.exit(
135
- f"Error: {name} '{address}' is invalid. It must contain 1 or 2 colons, but it has {colon_count}.")
135
+ f"Error: {name} '{address}' is invalid. It must contain 1 or 2 colons, but it has {colon_count}."
136
+ )
136
137
 
137
138
 
138
139
  def is_flaresolverr_available(shared_state):
@@ -147,7 +148,7 @@ def is_flaresolverr_available(shared_state):
147
148
  return False
148
149
 
149
150
  # Check if FlareSolverr URL is configured
150
- flaresolverr_url = shared_state.values["config"]('FlareSolverr').get('url')
151
+ flaresolverr_url = shared_state.values["config"]("FlareSolverr").get("url")
151
152
  if not flaresolverr_url:
152
153
  return False
153
154
 
@@ -172,11 +173,11 @@ def is_site_usable(shared_state, shorthand):
172
173
  shorthand = shorthand.lower()
173
174
 
174
175
  # Check if hostname is set
175
- hostname = shared_state.values["config"]('Hostnames').get(shorthand)
176
+ hostname = shared_state.values["config"]("Hostnames").get(shorthand)
176
177
  if not hostname:
177
178
  return False
178
179
 
179
- login_required_sites = ['al', 'dd', 'dl', 'nx']
180
+ login_required_sites = ["al", "dd", "dl", "nx"]
180
181
  if shorthand not in login_required_sites:
181
182
  return True # No login needed, hostname is enough
182
183
 
@@ -186,8 +187,8 @@ def is_site_usable(shared_state, shorthand):
186
187
 
187
188
  # Check for credentials
188
189
  config = shared_state.values["config"](shorthand.upper())
189
- user = config.get('user')
190
- password = config.get('password')
190
+ user = config.get("user")
191
+ password = config.get("password")
191
192
 
192
193
  return bool(user and password)
193
194
 
@@ -196,6 +197,7 @@ def is_site_usable(shared_state, shorthand):
196
197
  # LINK STATUS CHECKING
197
198
  # =============================================================================
198
199
 
200
+
199
201
  def generate_status_url(href, crypter_type):
200
202
  """
201
203
  Generate a status URL for crypters that support it.
@@ -203,14 +205,16 @@ def generate_status_url(href, crypter_type):
203
205
  """
204
206
  if crypter_type == "hide":
205
207
  # hide.cx links: https://hide.cx/folder/{UUID} or /container/{UUID} → https://hide.cx/state/{UUID}
206
- match = re.search(r'hide\.cx/(?:folder/|container/)?([a-f0-9-]{36})', href, re.IGNORECASE)
208
+ match = re.search(
209
+ r"hide\.cx/(?:folder/|container/)?([a-f0-9-]{36})", href, re.IGNORECASE
210
+ )
207
211
  if match:
208
212
  uuid = match.group(1)
209
213
  return f"https://hide.cx/state/{uuid}"
210
214
 
211
215
  elif crypter_type == "tolink":
212
216
  # tolink links: https://tolink.to/f/{ID} → https://tolink.to/f/{ID}/s/status.png
213
- match = re.search(r'tolink\.to/f/([a-zA-Z0-9]+)', href, re.IGNORECASE)
217
+ match = re.search(r"tolink\.to/f/([a-zA-Z0-9]+)", href, re.IGNORECASE)
214
218
  if match:
215
219
  link_id = match.group(1)
216
220
  return f"https://tolink.to/f/{link_id}/s/status.png"
@@ -221,13 +225,13 @@ def generate_status_url(href, crypter_type):
221
225
  def detect_crypter_type(url):
222
226
  """Detect crypter type from URL for status checking."""
223
227
  url_lower = url.lower()
224
- if 'hide.' in url_lower:
228
+ if "hide." in url_lower:
225
229
  return "hide"
226
- elif 'tolink.' in url_lower:
230
+ elif "tolink." in url_lower:
227
231
  return "tolink"
228
- elif 'filecrypt.' in url_lower:
232
+ elif "filecrypt." in url_lower:
229
233
  return "filecrypt"
230
- elif 'keeplinks.' in url_lower:
234
+ elif "keeplinks." in url_lower:
231
235
  return "keeplinks"
232
236
  return None
233
237
 
@@ -240,9 +244,9 @@ def image_has_green(image_data):
240
244
  try:
241
245
  img = Image.open(BytesIO(image_data))
242
246
  # Convert palette images with transparency to RGBA first to avoid warning
243
- if img.mode == 'P' and 'transparency' in img.info:
244
- img = img.convert('RGBA')
245
- img = img.convert('RGB')
247
+ if img.mode == "P" and "transparency" in img.info:
248
+ img = img.convert("RGBA")
249
+ img = img.convert("RGB")
246
250
 
247
251
  pixels = list(img.getdata())
248
252
 
@@ -297,9 +301,11 @@ def check_links_online_status(links_with_status, shared_state=None):
297
301
 
298
302
  batch_size = 10
299
303
  for i in range(0, len(status_urls), batch_size):
300
- batch = status_urls[i:i + batch_size]
304
+ batch = status_urls[i : i + batch_size]
301
305
  with ThreadPoolExecutor(max_workers=batch_size) as executor:
302
- futures = [executor.submit(fetch_status_image, url, shared_state) for url in batch]
306
+ futures = [
307
+ executor.submit(fetch_status_image, url, shared_state) for url in batch
308
+ ]
303
309
  for future in as_completed(futures):
304
310
  try:
305
311
  status_url, image_data = future.result()
@@ -5,7 +5,7 @@
5
5
  import re
6
6
  import sys
7
7
 
8
- __version__ = "2.4.7"
8
+ __version__ = "2.4.9"
9
9
 
10
10
 
11
11
  def get_version():
@@ -76,7 +76,7 @@ def newer_version_available():
76
76
 
77
77
  def create_version_file():
78
78
  version = get_version()
79
- version_clean = re.sub(r'[^\d.]', '', version)
79
+ version_clean = re.sub(r"[^\d.]", "", version)
80
80
  if "a" in version:
81
81
  suffix = version.split("a")[1]
82
82
  else:
@@ -85,10 +85,24 @@ def create_version_file():
85
85
  version_info = [
86
86
  "VSVersionInfo(",
87
87
  " ffi=FixedFileInfo(",
88
- " filevers=(" + str(int(version_split[0])) + ", " + str(int(version_split[1])) + ", " + str(
89
- int(version_split[2])) + ", " + str(int(suffix)) + "),",
90
- " prodvers=(" + str(int(version_split[0])) + ", " + str(int(version_split[1])) + ", " + str(
91
- int(version_split[2])) + ", " + str(int(suffix)) + "),",
88
+ " filevers=("
89
+ + str(int(version_split[0]))
90
+ + ", "
91
+ + str(int(version_split[1]))
92
+ + ", "
93
+ + str(int(version_split[2]))
94
+ + ", "
95
+ + str(int(suffix))
96
+ + "),",
97
+ " prodvers=("
98
+ + str(int(version_split[0]))
99
+ + ", "
100
+ + str(int(version_split[1]))
101
+ + ", "
102
+ + str(int(version_split[2]))
103
+ + ", "
104
+ + str(int(suffix))
105
+ + "),",
92
106
  " mask=0x3f,",
93
107
  " flags=0x0,",
94
108
  " OS=0x4,",
@@ -103,24 +117,41 @@ def create_version_file():
103
117
  " u'040704b0',",
104
118
  " [StringStruct(u'CompanyName', u'RiX'),",
105
119
  " StringStruct(u'FileDescription', u'Quasarr'),",
106
- " StringStruct(u'FileVersion', u'" + str(int(version_split[0])) + "." + str(
107
- int(version_split[1])) + "." + str(int(version_split[2])) + "." + str(int(suffix)) + "'),",
120
+ " StringStruct(u'FileVersion', u'"
121
+ + str(int(version_split[0]))
122
+ + "."
123
+ + str(int(version_split[1]))
124
+ + "."
125
+ + str(int(version_split[2]))
126
+ + "."
127
+ + str(int(suffix))
128
+ + "'),",
108
129
  " StringStruct(u'InternalName', u'Quasarr'),",
109
130
  " StringStruct(u'LegalCopyright', u'Copyright © RiX'),",
110
131
  " StringStruct(u'OriginalFilename', u'Quasarr.exe'),",
111
132
  " StringStruct(u'ProductName', u'Quasarr'),",
112
- " StringStruct(u'ProductVersion', u'" + str(int(version_split[0])) + "." + str(
113
- int(version_split[1])) + "." + str(int(version_split[2])) + "." + str(int(suffix)) + "')])",
133
+ " StringStruct(u'ProductVersion', u'"
134
+ + str(int(version_split[0]))
135
+ + "."
136
+ + str(int(version_split[1]))
137
+ + "."
138
+ + str(int(version_split[2]))
139
+ + "."
140
+ + str(int(suffix))
141
+ + "')])",
114
142
  " ]),",
115
143
  " VarFileInfo([VarStruct(u'Translation', [1031, 1200])])",
116
144
  " ]",
117
- ")"
145
+ ")",
118
146
  ]
119
- print("\n".join(version_info), file=open('file_version_info.txt', 'w', encoding='utf-8'))
147
+ print(
148
+ "\n".join(version_info),
149
+ file=open("file_version_info.txt", "w", encoding="utf-8"),
150
+ )
120
151
 
121
152
 
122
- if __name__ == '__main__':
123
- if len(sys.argv) > 1 and sys.argv[1] == '--create-version-file':
153
+ if __name__ == "__main__":
154
+ if len(sys.argv) > 1 and sys.argv[1] == "--create-version-file":
124
155
  create_version_file()
125
156
  else:
126
157
  print(get_version())
@@ -3,8 +3,8 @@
3
3
  # Project by https://github.com/rix1337
4
4
 
5
5
  import time
6
- from socketserver import ThreadingMixIn, TCPServer
7
- from wsgiref.simple_server import WSGIServer, WSGIRequestHandler, make_server
6
+ from socketserver import TCPServer, ThreadingMixIn
7
+ from wsgiref.simple_server import WSGIRequestHandler, WSGIServer, make_server
8
8
 
9
9
  temp_server_success = False
10
10
 
@@ -26,12 +26,17 @@ class NoLoggingWSGIRequestHandler(WSGIRequestHandler):
26
26
 
27
27
 
28
28
  class Server:
29
- def __init__(self, wsgi_app, listen='127.0.0.1', port=8080):
29
+ def __init__(self, wsgi_app, listen="127.0.0.1", port=8080):
30
30
  self.wsgi_app = wsgi_app
31
31
  self.listen = listen
32
32
  self.port = port
33
- self.server = make_server(self.listen, self.port, self.wsgi_app,
34
- ThreadingWSGIServer, handler_class=NoLoggingWSGIRequestHandler)
33
+ self.server = make_server(
34
+ self.listen,
35
+ self.port,
36
+ self.wsgi_app,
37
+ ThreadingWSGIServer,
38
+ handler_class=NoLoggingWSGIRequestHandler,
39
+ )
35
40
 
36
41
  def serve_temporarily(self):
37
42
  global temp_server_success
@@ -6,12 +6,12 @@ import time
6
6
  from concurrent.futures import ThreadPoolExecutor, as_completed
7
7
 
8
8
  from quasarr.providers.imdb_metadata import get_imdb_metadata
9
- from quasarr.providers.log import info, debug
9
+ from quasarr.providers.log import debug, info
10
10
  from quasarr.search.sources.al import al_feed, al_search
11
11
  from quasarr.search.sources.by import by_feed, by_search
12
- from quasarr.search.sources.dd import dd_search, dd_feed
13
- from quasarr.search.sources.dj import dj_search, dj_feed
14
- from quasarr.search.sources.dl import dl_search, dl_feed
12
+ from quasarr.search.sources.dd import dd_feed, dd_search
13
+ from quasarr.search.sources.dj import dj_feed, dj_search
14
+ from quasarr.search.sources.dl import dl_feed, dl_search
15
15
  from quasarr.search.sources.dt import dt_feed, dt_search
16
16
  from quasarr.search.sources.dw import dw_feed, dw_search
17
17
  from quasarr.search.sources.fx import fx_feed, fx_search
@@ -20,17 +20,25 @@ from quasarr.search.sources.mb import mb_feed, mb_search
20
20
  from quasarr.search.sources.nk import nk_feed, nk_search
21
21
  from quasarr.search.sources.nx import nx_feed, nx_search
22
22
  from quasarr.search.sources.sf import sf_feed, sf_search
23
- from quasarr.search.sources.sj import sj_search, sj_feed
23
+ from quasarr.search.sources.sj import sj_feed, sj_search
24
24
  from quasarr.search.sources.sl import sl_feed, sl_search
25
25
  from quasarr.search.sources.wd import wd_feed, wd_search
26
26
  from quasarr.search.sources.wx import wx_feed, wx_search
27
27
 
28
28
 
29
- def get_search_results(shared_state, request_from, imdb_id="", search_phrase="", mirror=None, season="", episode=""):
29
+ def get_search_results(
30
+ shared_state,
31
+ request_from,
32
+ imdb_id="",
33
+ search_phrase="",
34
+ mirror=None,
35
+ season="",
36
+ episode="",
37
+ ):
30
38
  results = []
31
39
 
32
- if imdb_id and not imdb_id.startswith('tt'):
33
- imdb_id = f'tt{imdb_id}'
40
+ if imdb_id and not imdb_id.startswith("tt"):
41
+ imdb_id = f"tt{imdb_id}"
34
42
 
35
43
  # Pre-populate IMDb metadata cache to avoid API hammering by search threads
36
44
  if imdb_id:
@@ -115,16 +123,18 @@ def get_search_results(shared_state, request_from, imdb_id="", search_phrase="",
115
123
  if imdb_id: # only Radarr/Sonarr are using imdb_id
116
124
  args, kwargs = (
117
125
  (shared_state, start_time, request_from, imdb_id),
118
- {'mirror': mirror, 'season': season, 'episode': episode}
126
+ {"mirror": mirror, "season": season, "episode": episode},
119
127
  )
120
128
  for flag, func in imdb_map:
121
129
  if flag:
122
130
  functions.append(lambda f=func, a=args, kw=kwargs: f(*a, **kw))
123
131
 
124
- elif search_phrase and docs_search: # only LazyLibrarian is allowed to use search_phrase
132
+ elif (
133
+ search_phrase and docs_search
134
+ ): # only LazyLibrarian is allowed to use search_phrase
125
135
  args, kwargs = (
126
136
  (shared_state, start_time, request_from, search_phrase),
127
- {'mirror': mirror, 'season': season, 'episode': episode}
137
+ {"mirror": mirror, "season": season, "episode": episode},
128
138
  )
129
139
  for flag, func in phrase_map:
130
140
  if flag:
@@ -132,13 +142,11 @@ def get_search_results(shared_state, request_from, imdb_id="", search_phrase="",
132
142
 
133
143
  elif search_phrase:
134
144
  debug(
135
- f"Search phrase '{search_phrase}' is not supported for {request_from}. Only LazyLibrarian can use search phrases.")
145
+ f"Search phrase '{search_phrase}' is not supported for {request_from}. Only LazyLibrarian can use search phrases."
146
+ )
136
147
 
137
148
  else:
138
- args, kwargs = (
139
- (shared_state, start_time, request_from),
140
- {'mirror': mirror}
141
- )
149
+ args, kwargs = ((shared_state, start_time, request_from), {"mirror": mirror})
142
150
  for flag, func in feed_map:
143
151
  if flag:
144
152
  functions.append(lambda f=func, a=args, kw=kwargs: f(*a, **kw))
@@ -150,7 +158,9 @@ def get_search_results(shared_state, request_from, imdb_id="", search_phrase="",
150
158
  else:
151
159
  stype = "feed search"
152
160
 
153
- info(f'Starting {len(functions)} search functions for {stype}... This may take some time.')
161
+ info(
162
+ f"Starting {len(functions)} search functions for {stype}... This may take some time."
163
+ )
154
164
 
155
165
  with ThreadPoolExecutor() as executor:
156
166
  futures = [executor.submit(func) for func in functions]
@@ -162,6 +172,8 @@ def get_search_results(shared_state, request_from, imdb_id="", search_phrase="",
162
172
  info(f"An error occurred: {e}")
163
173
 
164
174
  elapsed_time = time.time() - start_time
165
- info(f"Providing {len(results)} releases to {request_from} for {stype}. Time taken: {elapsed_time:.2f} seconds")
175
+ info(
176
+ f"Providing {len(results)} releases to {request_from} for {stype}. Time taken: {elapsed_time:.2f} seconds"
177
+ )
166
178
 
167
179
  return results