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.
- quasarr/__init__.py +134 -70
- quasarr/api/__init__.py +40 -31
- quasarr/api/arr/__init__.py +116 -108
- quasarr/api/captcha/__init__.py +262 -137
- quasarr/api/config/__init__.py +76 -46
- quasarr/api/packages/__init__.py +138 -102
- quasarr/api/sponsors_helper/__init__.py +29 -16
- quasarr/api/statistics/__init__.py +19 -19
- quasarr/downloads/__init__.py +165 -72
- quasarr/downloads/linkcrypters/al.py +35 -18
- quasarr/downloads/linkcrypters/filecrypt.py +107 -52
- quasarr/downloads/linkcrypters/hide.py +5 -6
- quasarr/downloads/packages/__init__.py +342 -177
- quasarr/downloads/sources/al.py +191 -100
- quasarr/downloads/sources/by.py +31 -13
- quasarr/downloads/sources/dd.py +27 -14
- quasarr/downloads/sources/dj.py +1 -3
- quasarr/downloads/sources/dl.py +126 -71
- quasarr/downloads/sources/dt.py +11 -5
- quasarr/downloads/sources/dw.py +28 -14
- quasarr/downloads/sources/he.py +32 -24
- quasarr/downloads/sources/mb.py +19 -9
- quasarr/downloads/sources/nk.py +14 -10
- quasarr/downloads/sources/nx.py +8 -18
- quasarr/downloads/sources/sf.py +45 -20
- quasarr/downloads/sources/sj.py +1 -3
- quasarr/downloads/sources/sl.py +9 -5
- quasarr/downloads/sources/wd.py +32 -12
- quasarr/downloads/sources/wx.py +35 -21
- quasarr/providers/auth.py +42 -37
- quasarr/providers/cloudflare.py +28 -30
- quasarr/providers/hostname_issues.py +2 -1
- quasarr/providers/html_images.py +2 -2
- quasarr/providers/html_templates.py +22 -14
- quasarr/providers/imdb_metadata.py +149 -80
- quasarr/providers/jd_cache.py +131 -39
- quasarr/providers/log.py +1 -1
- quasarr/providers/myjd_api.py +260 -196
- quasarr/providers/notifications.py +53 -41
- quasarr/providers/obfuscated.py +9 -4
- quasarr/providers/sessions/al.py +71 -55
- quasarr/providers/sessions/dd.py +21 -14
- quasarr/providers/sessions/dl.py +30 -19
- quasarr/providers/sessions/nx.py +23 -14
- quasarr/providers/shared_state.py +292 -141
- quasarr/providers/statistics.py +75 -43
- quasarr/providers/utils.py +33 -27
- quasarr/providers/version.py +45 -14
- quasarr/providers/web_server.py +10 -5
- quasarr/search/__init__.py +30 -18
- quasarr/search/sources/al.py +124 -73
- quasarr/search/sources/by.py +110 -59
- quasarr/search/sources/dd.py +57 -35
- quasarr/search/sources/dj.py +69 -48
- quasarr/search/sources/dl.py +159 -100
- quasarr/search/sources/dt.py +110 -74
- quasarr/search/sources/dw.py +121 -61
- quasarr/search/sources/fx.py +108 -62
- quasarr/search/sources/he.py +78 -49
- quasarr/search/sources/mb.py +96 -48
- quasarr/search/sources/nk.py +80 -50
- quasarr/search/sources/nx.py +91 -62
- quasarr/search/sources/sf.py +171 -106
- quasarr/search/sources/sj.py +69 -48
- quasarr/search/sources/sl.py +115 -71
- quasarr/search/sources/wd.py +67 -44
- quasarr/search/sources/wx.py +188 -123
- quasarr/storage/config.py +65 -52
- quasarr/storage/setup.py +238 -140
- quasarr/storage/sqlite_database.py +10 -4
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/METADATA +2 -2
- quasarr-2.4.9.dist-info/RECORD +81 -0
- quasarr-2.4.7.dist-info/RECORD +0 -81
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/WHEEL +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/licenses/LICENSE +0 -0
quasarr/providers/statistics.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
from json import loads
|
|
6
|
-
from typing import
|
|
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
|
|
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(
|
|
156
|
-
|
|
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(
|
|
159
|
-
|
|
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 =
|
|
164
|
-
|
|
165
|
-
|
|
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 =
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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())
|
quasarr/providers/utils.py
CHANGED
|
@@ -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(
|
|
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(
|
|
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((
|
|
86
|
+
s.connect(("10.255.255.255", 0))
|
|
85
87
|
ip = s.getsockname()[0]
|
|
86
88
|
except:
|
|
87
|
-
ip =
|
|
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(
|
|
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"](
|
|
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"](
|
|
176
|
+
hostname = shared_state.values["config"]("Hostnames").get(shorthand)
|
|
176
177
|
if not hostname:
|
|
177
178
|
return False
|
|
178
179
|
|
|
179
|
-
login_required_sites = [
|
|
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(
|
|
190
|
-
password = config.get(
|
|
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(
|
|
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
|
|
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
|
|
228
|
+
if "hide." in url_lower:
|
|
225
229
|
return "hide"
|
|
226
|
-
elif
|
|
230
|
+
elif "tolink." in url_lower:
|
|
227
231
|
return "tolink"
|
|
228
|
-
elif
|
|
232
|
+
elif "filecrypt." in url_lower:
|
|
229
233
|
return "filecrypt"
|
|
230
|
-
elif
|
|
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 ==
|
|
244
|
-
img = img.convert(
|
|
245
|
-
img = img.convert(
|
|
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 = [
|
|
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()
|
quasarr/providers/version.py
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import re
|
|
6
6
|
import sys
|
|
7
7
|
|
|
8
|
-
__version__ = "2.4.
|
|
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
|
|
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=("
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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'"
|
|
107
|
-
|
|
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'"
|
|
113
|
-
|
|
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(
|
|
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__ ==
|
|
123
|
-
if len(sys.argv) > 1 and sys.argv[1] ==
|
|
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())
|
quasarr/providers/web_server.py
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
import time
|
|
6
|
-
from socketserver import
|
|
7
|
-
from wsgiref.simple_server import
|
|
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=
|
|
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(
|
|
34
|
-
|
|
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
|
quasarr/search/__init__.py
CHANGED
|
@@ -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
|
|
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
|
|
13
|
-
from quasarr.search.sources.dj import
|
|
14
|
-
from quasarr.search.sources.dl import
|
|
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
|
|
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(
|
|
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(
|
|
33
|
-
imdb_id = f
|
|
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
|
-
{
|
|
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
|
|
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
|
-
{
|
|
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(
|
|
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(
|
|
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
|