quasarr 2.6.1__py3-none-any.whl → 2.7.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.
- quasarr/__init__.py +71 -61
- quasarr/api/__init__.py +1 -2
- quasarr/api/arr/__init__.py +159 -56
- quasarr/api/captcha/__init__.py +203 -154
- quasarr/downloads/__init__.py +12 -8
- quasarr/downloads/linkcrypters/al.py +3 -3
- quasarr/downloads/linkcrypters/filecrypt.py +1 -2
- quasarr/downloads/packages/__init__.py +62 -88
- quasarr/downloads/sources/al.py +3 -3
- quasarr/downloads/sources/by.py +3 -3
- quasarr/downloads/sources/he.py +8 -9
- quasarr/downloads/sources/nk.py +3 -3
- quasarr/downloads/sources/sl.py +6 -1
- quasarr/downloads/sources/wd.py +93 -37
- quasarr/downloads/sources/wx.py +11 -17
- quasarr/providers/auth.py +9 -13
- quasarr/providers/cloudflare.py +4 -3
- quasarr/providers/imdb_metadata.py +0 -2
- quasarr/providers/jd_cache.py +64 -90
- quasarr/providers/log.py +226 -8
- quasarr/providers/myjd_api.py +116 -94
- quasarr/providers/sessions/al.py +20 -22
- quasarr/providers/sessions/dd.py +1 -1
- quasarr/providers/sessions/dl.py +8 -10
- quasarr/providers/sessions/nx.py +1 -1
- quasarr/providers/shared_state.py +26 -15
- quasarr/providers/utils.py +15 -6
- quasarr/providers/version.py +1 -1
- quasarr/search/__init__.py +91 -78
- quasarr/search/sources/al.py +19 -23
- quasarr/search/sources/by.py +6 -6
- quasarr/search/sources/dd.py +8 -10
- quasarr/search/sources/dj.py +15 -18
- quasarr/search/sources/dl.py +25 -37
- quasarr/search/sources/dt.py +13 -15
- quasarr/search/sources/dw.py +24 -16
- quasarr/search/sources/fx.py +25 -11
- quasarr/search/sources/he.py +16 -14
- quasarr/search/sources/hs.py +7 -7
- quasarr/search/sources/mb.py +7 -7
- quasarr/search/sources/nk.py +24 -25
- quasarr/search/sources/nx.py +22 -15
- quasarr/search/sources/sf.py +18 -9
- quasarr/search/sources/sj.py +7 -7
- quasarr/search/sources/sl.py +26 -14
- quasarr/search/sources/wd.py +61 -31
- quasarr/search/sources/wx.py +33 -47
- quasarr/storage/config.py +1 -3
- {quasarr-2.6.1.dist-info → quasarr-2.7.0.dist-info}/METADATA +4 -1
- quasarr-2.7.0.dist-info/RECORD +84 -0
- quasarr-2.6.1.dist-info/RECORD +0 -84
- {quasarr-2.6.1.dist-info → quasarr-2.7.0.dist-info}/WHEEL +0 -0
- {quasarr-2.6.1.dist-info → quasarr-2.7.0.dist-info}/entry_points.txt +0 -0
- {quasarr-2.6.1.dist-info → quasarr-2.7.0.dist-info}/licenses/LICENSE +0 -0
quasarr/__init__.py
CHANGED
|
@@ -15,7 +15,14 @@ import requests
|
|
|
15
15
|
import quasarr.providers.web_server
|
|
16
16
|
from quasarr.api import get_api
|
|
17
17
|
from quasarr.providers import shared_state, version
|
|
18
|
-
from quasarr.providers.log import
|
|
18
|
+
from quasarr.providers.log import (
|
|
19
|
+
crit,
|
|
20
|
+
debug,
|
|
21
|
+
error,
|
|
22
|
+
get_log_level,
|
|
23
|
+
info,
|
|
24
|
+
log_level_names,
|
|
25
|
+
)
|
|
19
26
|
from quasarr.providers.notifications import send_discord_message
|
|
20
27
|
from quasarr.providers.utils import (
|
|
21
28
|
FALLBACK_USER_AGENT,
|
|
@@ -67,26 +74,21 @@ def run():
|
|
|
67
74
|
└────────────────────────────────────┘""")
|
|
68
75
|
|
|
69
76
|
print("\n===== Recommended Services =====")
|
|
77
|
+
print('👉 Fast premium downloads: "https://linksnappy.com/?ref=397097" 👈')
|
|
70
78
|
print(
|
|
71
|
-
'
|
|
72
|
-
)
|
|
73
|
-
print(
|
|
74
|
-
'Sponsors get automated CAPTCHA solutions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper"'
|
|
79
|
+
'👉 Automated CAPTCHA solutions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper" 👈'
|
|
75
80
|
)
|
|
76
81
|
|
|
77
|
-
print("\n===== Startup Info =====")
|
|
78
82
|
port = int("8080")
|
|
79
83
|
config_path = ""
|
|
80
84
|
if os.environ.get("DOCKER"):
|
|
81
85
|
config_path = "/config"
|
|
82
86
|
if not arguments.internal_address:
|
|
83
|
-
|
|
87
|
+
error(
|
|
84
88
|
"You must set the INTERNAL_ADDRESS variable to a locally reachable URL, e.g. http://192.168.1.1:8080"
|
|
89
|
+
+ " The local URL will be used by Radarr/Sonarr to connect to Quasarr"
|
|
90
|
+
+ " Stopping Quasarr..."
|
|
85
91
|
)
|
|
86
|
-
print(
|
|
87
|
-
"The local URL will be used by Radarr/Sonarr to connect to Quasarr"
|
|
88
|
-
)
|
|
89
|
-
print("Stopping Quasarr...")
|
|
90
92
|
sys.exit(1)
|
|
91
93
|
else:
|
|
92
94
|
if arguments.port:
|
|
@@ -118,7 +120,7 @@ def run():
|
|
|
118
120
|
temp_file = tempfile.TemporaryFile(dir=config_path)
|
|
119
121
|
temp_file.close()
|
|
120
122
|
except Exception as e:
|
|
121
|
-
|
|
123
|
+
error(f'Could not access "{config_path}": {e}"Stopping Quasarr...')
|
|
122
124
|
sys.exit(1)
|
|
123
125
|
|
|
124
126
|
shared_state.set_files(config_path)
|
|
@@ -130,16 +132,13 @@ def run():
|
|
|
130
132
|
shared_state.update("user_agent", FALLBACK_USER_AGENT)
|
|
131
133
|
shared_state.update("helper_active", False)
|
|
132
134
|
|
|
133
|
-
print(f'Config path: "{config_path}"')
|
|
134
|
-
|
|
135
|
-
print("\n===== Hostnames =====")
|
|
136
135
|
try:
|
|
137
136
|
if arguments.hostnames:
|
|
138
137
|
hostnames_link = arguments.hostnames
|
|
139
138
|
if is_valid_url(hostnames_link):
|
|
140
139
|
# Store the hostnames URL for later use in web UI
|
|
141
140
|
Config("Settings").save("hostnames_url", hostnames_link)
|
|
142
|
-
|
|
141
|
+
info(f"Extracting hostnames from {hostnames_link}...")
|
|
143
142
|
allowed_keys = supported_hostnames
|
|
144
143
|
max_keys = len(allowed_keys)
|
|
145
144
|
shorthand_list = (
|
|
@@ -147,7 +146,7 @@ def run():
|
|
|
147
146
|
+ " and "
|
|
148
147
|
+ f'"{allowed_keys[-1]}"'
|
|
149
148
|
)
|
|
150
|
-
|
|
149
|
+
info(
|
|
151
150
|
f"There are up to {max_keys} hostnames currently supported: {shorthand_list}"
|
|
152
151
|
)
|
|
153
152
|
data = requests.get(hostnames_link).text
|
|
@@ -165,36 +164,32 @@ def run():
|
|
|
165
164
|
if valid_domain:
|
|
166
165
|
hostnames.save(shorthand, hostname)
|
|
167
166
|
extracted_hostnames += 1
|
|
168
|
-
|
|
167
|
+
info(
|
|
169
168
|
f'Hostname for "{shorthand}" successfully set to "{hostname}"'
|
|
170
169
|
)
|
|
171
170
|
else:
|
|
172
|
-
|
|
171
|
+
info(
|
|
173
172
|
f'Skipping invalid hostname for "{shorthand}" ("{hostname}")'
|
|
174
173
|
)
|
|
175
174
|
if extracted_hostnames == max_keys:
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
info(f"All {max_keys} hostnames successfully extracted!")
|
|
176
|
+
info(
|
|
178
177
|
"You can now remove the hostnames link from the command line / environment variable."
|
|
179
178
|
)
|
|
180
179
|
else:
|
|
181
|
-
|
|
180
|
+
info(
|
|
182
181
|
f'No Hostnames found at "{hostnames_link}". '
|
|
183
182
|
"Ensure to pass a plain hostnames list, not html or json!"
|
|
184
183
|
)
|
|
185
184
|
else:
|
|
186
|
-
|
|
185
|
+
error(f'Invalid hostnames URL: "{hostnames_link}"')
|
|
187
186
|
except Exception as e:
|
|
188
|
-
|
|
187
|
+
error(f'Error parsing hostnames link: "{e}"')
|
|
189
188
|
|
|
190
189
|
hostnames = get_clean_hostnames(shared_state)
|
|
191
190
|
if not hostnames:
|
|
192
191
|
hostnames_config(shared_state)
|
|
193
192
|
hostnames = get_clean_hostnames(shared_state)
|
|
194
|
-
print(
|
|
195
|
-
f"You have [{len(hostnames)} of {len(Config._DEFAULT_CONFIG['Hostnames'])}] supported hostnames set up"
|
|
196
|
-
)
|
|
197
|
-
print("For efficiency it is recommended to set up as few hostnames as needed.")
|
|
198
193
|
|
|
199
194
|
# Check credentials for login-required hostnames
|
|
200
195
|
skip_login_db = DataBase("skip_login")
|
|
@@ -237,43 +232,54 @@ def run():
|
|
|
237
232
|
if not user or not password or not device:
|
|
238
233
|
jdownloader_config(shared_state)
|
|
239
234
|
|
|
240
|
-
print("\n===== Notifications =====")
|
|
241
235
|
discord_url = ""
|
|
242
236
|
if arguments.discord:
|
|
243
237
|
discord_webhook_pattern = r"^https://discord\.com/api/webhooks/\d+/[\w-]+$"
|
|
244
238
|
if re.match(discord_webhook_pattern, arguments.discord):
|
|
245
239
|
shared_state.update("webhook", arguments.discord)
|
|
246
|
-
print("Using Discord Webhook URL for notifications.")
|
|
247
240
|
discord_url = arguments.discord
|
|
248
241
|
else:
|
|
249
|
-
|
|
250
|
-
else:
|
|
251
|
-
print("No Discord Webhook URL provided")
|
|
242
|
+
error(f"Invalid Discord Webhook URL provided: {arguments.discord}")
|
|
252
243
|
shared_state.update("discord", discord_url)
|
|
253
244
|
|
|
254
|
-
print("\n===== API Information =====")
|
|
255
245
|
api_key = Config("API").get("key")
|
|
256
246
|
if not api_key:
|
|
257
247
|
api_key = shared_state.generate_api_key()
|
|
248
|
+
info("API-Key generated: <g>" + api_key + "</g>")
|
|
258
249
|
|
|
259
|
-
print(
|
|
260
|
-
'Setup instructions: "https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions"'
|
|
261
|
-
)
|
|
262
|
-
print(f'URL: "{shared_state.values["internal_address"]}"')
|
|
263
|
-
print(f'API Key: "{api_key}" (without quotes)')
|
|
250
|
+
print(f"\n===== Quasarr {log_level_names[get_log_level()]} Log =====")
|
|
264
251
|
|
|
265
|
-
|
|
266
|
-
|
|
252
|
+
# Start Logging
|
|
253
|
+
info(f"Web UI: <blue>{shared_state.values['external_address']}</blue>")
|
|
254
|
+
debug(f'Config path: "{config_path}"')
|
|
267
255
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
256
|
+
# Hostnames log
|
|
257
|
+
hostnames_log = []
|
|
258
|
+
set_hostnames_count = 0
|
|
259
|
+
for key in supported_hostnames:
|
|
260
|
+
if key in hostnames:
|
|
261
|
+
hostnames_log.append(
|
|
262
|
+
f"<bg green><black>{key.upper()}</black></bg green>"
|
|
263
|
+
)
|
|
264
|
+
set_hostnames_count += 1
|
|
265
|
+
else:
|
|
266
|
+
hostnames_log.append(
|
|
267
|
+
f"<bg black><white>{key.upper()}</white></bg black>"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
total_hostnames_count = len(supported_hostnames)
|
|
271
|
+
if set_hostnames_count == total_hostnames_count:
|
|
272
|
+
count_str = f"<g>{set_hostnames_count}</g>/<g>{total_hostnames_count}</g>"
|
|
273
|
+
else:
|
|
274
|
+
count_str = f"<y>{set_hostnames_count}</y>/<g>{total_hostnames_count}</g>"
|
|
275
|
+
|
|
276
|
+
info(f"Hostnames: [{' '.join(hostnames_log)}] {count_str} set")
|
|
271
277
|
|
|
272
278
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
273
279
|
if protected:
|
|
274
280
|
package_count = len(protected)
|
|
275
281
|
info(
|
|
276
|
-
f"CAPTCHA-Solution required for {package_count} package{'s' if package_count > 1 else ''} at: "
|
|
282
|
+
f"CAPTCHA-Solution required for <y>{package_count}</y> package{'s' if package_count > 1 else ''} at: "
|
|
277
283
|
f'"{shared_state.values["external_address"]}/captcha"!'
|
|
278
284
|
)
|
|
279
285
|
|
|
@@ -329,21 +335,26 @@ def flaresolverr_checker(shared_state_dict, shared_state_lock):
|
|
|
329
335
|
"Some sites (AL) will not work without FlareSolverr. Configure it later in the web UI."
|
|
330
336
|
)
|
|
331
337
|
elif flaresolverr_url:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
338
|
+
debug(f"Checking FlareSolverr at URL: <blue>{flaresolverr_url}</blue>")
|
|
339
|
+
flaresolverr_version_checked = check_flaresolverr(
|
|
340
|
+
shared_state, flaresolverr_url
|
|
341
|
+
)
|
|
342
|
+
if flaresolverr_version_checked:
|
|
335
343
|
info(
|
|
336
|
-
f
|
|
344
|
+
f"FlareSolverr connection successful: <g>v.{flaresolverr_version_checked}</g>"
|
|
345
|
+
)
|
|
346
|
+
debug(
|
|
347
|
+
f"Using Flaresolverr's User-Agent: <g>{shared_state.values['user_agent']}</g>"
|
|
337
348
|
)
|
|
338
349
|
else:
|
|
339
|
-
|
|
350
|
+
error("FlareSolverr check failed - using fallback user agent")
|
|
340
351
|
# Fallback user agent is already set in main process, but we log it
|
|
341
352
|
info(f'User Agent (fallback): "{FALLBACK_USER_AGENT}"')
|
|
342
353
|
|
|
343
354
|
except KeyboardInterrupt:
|
|
344
355
|
pass
|
|
345
356
|
except Exception as e:
|
|
346
|
-
|
|
357
|
+
error(f"An unexpected error occurred in FlareSolverr checker: {e}")
|
|
347
358
|
|
|
348
359
|
|
|
349
360
|
def update_checker(shared_state_dict, shared_state_lock):
|
|
@@ -359,8 +370,9 @@ def update_checker(shared_state_dict, shared_state_lock):
|
|
|
359
370
|
try:
|
|
360
371
|
update_available = version.newer_version_available()
|
|
361
372
|
except Exception as e:
|
|
362
|
-
|
|
363
|
-
|
|
373
|
+
error(
|
|
374
|
+
f"Error getting latest version: {e}!\nPlease manually check: <blue>{link}</blue> for more information!"
|
|
375
|
+
)
|
|
364
376
|
update_available = None
|
|
365
377
|
|
|
366
378
|
if (
|
|
@@ -392,27 +404,25 @@ def jdownloader_connection(shared_state_dict, shared_state_lock):
|
|
|
392
404
|
device = shared_state.get_device()
|
|
393
405
|
|
|
394
406
|
try:
|
|
395
|
-
info(
|
|
396
|
-
f'Connection to JDownloader successful. Device name: "{device.name}"'
|
|
397
|
-
)
|
|
407
|
+
info(f"Connection to JDownloader successful: <g>{device.name}</g>")
|
|
398
408
|
except Exception as e:
|
|
399
|
-
|
|
409
|
+
crit(f"Error connecting to JDownloader: {e}! Stopping Quasarr...")
|
|
400
410
|
sys.exit(1)
|
|
401
411
|
|
|
402
412
|
try:
|
|
403
413
|
shared_state.set_device_settings()
|
|
404
414
|
except Exception as e:
|
|
405
|
-
|
|
415
|
+
error(f"Error checking settings: {e}")
|
|
406
416
|
|
|
407
417
|
try:
|
|
408
418
|
shared_state.update_jdownloader()
|
|
409
419
|
except Exception as e:
|
|
410
|
-
|
|
420
|
+
error(f"Error updating JDownloader: {e}")
|
|
411
421
|
|
|
412
422
|
try:
|
|
413
423
|
shared_state.start_downloads()
|
|
414
424
|
except Exception as e:
|
|
415
|
-
|
|
425
|
+
error(f"Error starting downloads: {e}")
|
|
416
426
|
|
|
417
427
|
while True:
|
|
418
428
|
time.sleep(300)
|
|
@@ -420,7 +430,7 @@ def jdownloader_connection(shared_state_dict, shared_state_lock):
|
|
|
420
430
|
shared_state.values.get("device")
|
|
421
431
|
)
|
|
422
432
|
if not device_state:
|
|
423
|
-
|
|
433
|
+
error("Lost connection to JDownloader. Reconnecting...")
|
|
424
434
|
shared_state.update("device", False)
|
|
425
435
|
break
|
|
426
436
|
|
quasarr/api/__init__.py
CHANGED
|
@@ -34,8 +34,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
34
34
|
add_auth_routes(app)
|
|
35
35
|
add_auth_hook(
|
|
36
36
|
app,
|
|
37
|
-
|
|
38
|
-
whitelist_suffixes=[".user.js"],
|
|
37
|
+
whitelist=["/api", "/api/", "/sponsors_helper/", "/download/", ".user.js"],
|
|
39
38
|
)
|
|
40
39
|
|
|
41
40
|
setup_arr_routes(app)
|
quasarr/api/arr/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# Quasarr
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
|
+
import time
|
|
5
6
|
import traceback
|
|
6
7
|
import xml.sax.saxutils as sax_utils
|
|
7
8
|
from base64 import urlsafe_b64decode
|
|
@@ -59,7 +60,8 @@ def setup_arr_routes(app):
|
|
|
59
60
|
def check_user_agent():
|
|
60
61
|
user_agent = request.headers.get("User-Agent") or ""
|
|
61
62
|
if not any(
|
|
62
|
-
tool in user_agent.lower()
|
|
63
|
+
tool in user_agent.lower()
|
|
64
|
+
for tool in ["radarr", "sonarr", "lazylibrarian", "python-requests"]
|
|
63
65
|
):
|
|
64
66
|
msg = f"Unsupported User-Agent: {user_agent}. Quasarr as a compatibility layer must be called by Radarr, Sonarr or LazyLibrarian directly."
|
|
65
67
|
info(msg)
|
|
@@ -107,7 +109,7 @@ def setup_arr_routes(app):
|
|
|
107
109
|
imdb_id = root.find(".//file").attrib.get("imdb_id")
|
|
108
110
|
source_key = root.find(".//file").attrib.get("source_key") or None
|
|
109
111
|
|
|
110
|
-
info(f
|
|
112
|
+
info(f"Attempting download for <y>{title}</y>")
|
|
111
113
|
downloaded = download(
|
|
112
114
|
shared_state,
|
|
113
115
|
request_from,
|
|
@@ -125,12 +127,12 @@ def setup_arr_routes(app):
|
|
|
125
127
|
title = downloaded["title"]
|
|
126
128
|
|
|
127
129
|
if success:
|
|
128
|
-
info(f
|
|
130
|
+
info(f"<y>{title}</y> added successfully!")
|
|
129
131
|
else:
|
|
130
|
-
info(f
|
|
132
|
+
info(f"<y>{title}</y> added unsuccessfully! See log for details.")
|
|
131
133
|
nzo_ids.append(package_id)
|
|
132
134
|
except KeyError:
|
|
133
|
-
info(f
|
|
135
|
+
info(f"Failed to download <y>{title}</y> - no package_id returned")
|
|
134
136
|
|
|
135
137
|
return {"status": True, "nzo_ids": nzo_ids}
|
|
136
138
|
|
|
@@ -209,7 +211,7 @@ def setup_arr_routes(app):
|
|
|
209
211
|
abort(400, f"invalid payload format: {e}")
|
|
210
212
|
|
|
211
213
|
nzo_ids = []
|
|
212
|
-
info(f
|
|
214
|
+
info(f"Attempting download for <y>{parsed_payload['title']}</y>")
|
|
213
215
|
|
|
214
216
|
downloaded = download(
|
|
215
217
|
shared_state,
|
|
@@ -229,11 +231,9 @@ def setup_arr_routes(app):
|
|
|
229
231
|
title = downloaded.get("title", parsed_payload["title"])
|
|
230
232
|
|
|
231
233
|
if success:
|
|
232
|
-
info(f'"{title}
|
|
234
|
+
info(f'"{title} added successfully!')
|
|
233
235
|
else:
|
|
234
|
-
info(
|
|
235
|
-
f'"{title}" added unsuccessfully! See log for details.'
|
|
236
|
-
)
|
|
236
|
+
info(f'"{title} added unsuccessfully! See log for details.')
|
|
237
237
|
nzo_ids.append(package_id)
|
|
238
238
|
except KeyError:
|
|
239
239
|
info(
|
|
@@ -304,69 +304,92 @@ def setup_arr_routes(app):
|
|
|
304
304
|
</caps>"""
|
|
305
305
|
elif mode in ["movie", "tvsearch", "book", "search"]:
|
|
306
306
|
releases = []
|
|
307
|
+
cache_key = None
|
|
307
308
|
|
|
308
309
|
try:
|
|
309
310
|
offset = int(getattr(request.query, "offset", 0))
|
|
310
|
-
except (AttributeError, ValueError):
|
|
311
|
+
except (AttributeError, ValueError) as e:
|
|
312
|
+
debug(f"Error parsing offset parameter: {e}")
|
|
311
313
|
offset = 0
|
|
312
314
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
315
|
+
try:
|
|
316
|
+
limit = int(getattr(request.query, "limit", 100))
|
|
317
|
+
except (AttributeError, ValueError) as e:
|
|
318
|
+
debug(f"Error parsing limit parameter: {e}")
|
|
319
|
+
limit = 100
|
|
320
|
+
|
|
321
|
+
if mode == "movie":
|
|
322
|
+
# supported params: imdbid
|
|
323
|
+
imdb_id = getattr(request.query, "imdbid", "")
|
|
324
|
+
|
|
325
|
+
if imdb_id != "":
|
|
326
|
+
cache_key = f"{request_from}::{imdb_id}::${mirror}"
|
|
327
|
+
|
|
328
|
+
if result := results_cache.get(cache_key, offset, limit):
|
|
329
|
+
debug(
|
|
330
|
+
f"Returning offset {offset}, limit {limit} for {cache_key}"
|
|
331
|
+
)
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
releases = get_search_results(
|
|
335
|
+
shared_state,
|
|
336
|
+
request_from,
|
|
337
|
+
imdb_id=imdb_id,
|
|
338
|
+
mirror=mirror,
|
|
316
339
|
)
|
|
317
340
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
341
|
+
elif mode == "tvsearch":
|
|
342
|
+
# supported params: imdbid, season, ep
|
|
343
|
+
imdb_id = getattr(request.query, "imdbid", "")
|
|
344
|
+
season = getattr(request.query, "season", None)
|
|
345
|
+
episode = getattr(request.query, "ep", None)
|
|
322
346
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
request_from,
|
|
326
|
-
imdb_id=imdb_id,
|
|
327
|
-
mirror=mirror,
|
|
328
|
-
)
|
|
347
|
+
if imdb_id != "":
|
|
348
|
+
cache_key = f"{request_from}::{imdb_id}::${mirror}::{season}::{episode}"
|
|
329
349
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
350
|
+
if result := results_cache.get(cache_key, offset, limit):
|
|
351
|
+
debug(
|
|
352
|
+
f"Returning offset {offset}, limit {limit} for {cache_key}"
|
|
353
|
+
)
|
|
354
|
+
return result
|
|
355
|
+
|
|
356
|
+
releases = get_search_results(
|
|
357
|
+
shared_state,
|
|
358
|
+
request_from,
|
|
359
|
+
imdb_id=imdb_id,
|
|
360
|
+
mirror=mirror,
|
|
361
|
+
season=season,
|
|
362
|
+
episode=episode,
|
|
363
|
+
)
|
|
364
|
+
elif mode == "book":
|
|
365
|
+
author = getattr(request.query, "author", "")
|
|
366
|
+
title = getattr(request.query, "title", "")
|
|
367
|
+
search_phrase = " ".join(filter(None, [author, title]))
|
|
368
|
+
releases = get_search_results(
|
|
369
|
+
shared_state,
|
|
370
|
+
request_from,
|
|
371
|
+
search_phrase=search_phrase,
|
|
372
|
+
mirror=mirror,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
elif mode == "search":
|
|
376
|
+
if "lazylibrarian" in request_from.lower():
|
|
377
|
+
search_phrase = getattr(request.query, "q", "")
|
|
347
378
|
releases = get_search_results(
|
|
348
379
|
shared_state,
|
|
349
380
|
request_from,
|
|
350
381
|
search_phrase=search_phrase,
|
|
351
382
|
mirror=mirror,
|
|
352
383
|
)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
shared_state,
|
|
359
|
-
request_from,
|
|
360
|
-
search_phrase=search_phrase,
|
|
361
|
-
mirror=mirror,
|
|
362
|
-
)
|
|
363
|
-
else:
|
|
364
|
-
# sonarr expects this but we will not support non-imdbid searches
|
|
365
|
-
debug(
|
|
366
|
-
f"Ignoring search request from {request_from} - only imdbid searches are supported"
|
|
367
|
-
)
|
|
384
|
+
else:
|
|
385
|
+
# sonarr expects this but we will not support non-imdbid searches
|
|
386
|
+
debug(
|
|
387
|
+
f"Ignoring search request from {request_from} - only imdbid searches are supported"
|
|
388
|
+
)
|
|
368
389
|
|
|
369
390
|
items = ""
|
|
391
|
+
items_amount = 0
|
|
392
|
+
items_processed = 0
|
|
370
393
|
for release in releases:
|
|
371
394
|
release = release.get("details", {})
|
|
372
395
|
|
|
@@ -389,6 +412,43 @@ def setup_arr_routes(app):
|
|
|
389
412
|
<pubDate>{pub_date}</pubDate>
|
|
390
413
|
<enclosure url="{release.get("link", "")}" length="{release.get("size", 0)}" type="application/x-nzb" />
|
|
391
414
|
</item>'''
|
|
415
|
+
items_amount += 1
|
|
416
|
+
|
|
417
|
+
if cache_key and items_amount == limit:
|
|
418
|
+
items_processed += items_amount
|
|
419
|
+
debug(
|
|
420
|
+
f"Processed {items_processed}/{len(releases)} releases"
|
|
421
|
+
)
|
|
422
|
+
results_cache.set(
|
|
423
|
+
cache_key,
|
|
424
|
+
f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
425
|
+
<rss>
|
|
426
|
+
<channel>
|
|
427
|
+
{items}
|
|
428
|
+
</channel>
|
|
429
|
+
</rss>""",
|
|
430
|
+
items_processed - items_amount,
|
|
431
|
+
limit,
|
|
432
|
+
)
|
|
433
|
+
items = ""
|
|
434
|
+
items_amount = 0
|
|
435
|
+
|
|
436
|
+
if cache_key and items_amount > 0:
|
|
437
|
+
items_processed += items_amount
|
|
438
|
+
debug(f"Processed {items_processed}/{len(releases)} releases")
|
|
439
|
+
results_cache.set(
|
|
440
|
+
cache_key,
|
|
441
|
+
f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
442
|
+
<rss>
|
|
443
|
+
<channel>
|
|
444
|
+
{items}
|
|
445
|
+
</channel>
|
|
446
|
+
</rss>""",
|
|
447
|
+
items_processed - items_amount,
|
|
448
|
+
limit,
|
|
449
|
+
)
|
|
450
|
+
items = ""
|
|
451
|
+
items_amount = 0
|
|
392
452
|
|
|
393
453
|
requires_placeholder_item = not getattr(
|
|
394
454
|
request.query, "imdbid", ""
|
|
@@ -404,6 +464,13 @@ def setup_arr_routes(app):
|
|
|
404
464
|
<enclosure url="https://github.com/rix1337/Quasarr" length="0" type="application/x-nzb" />
|
|
405
465
|
</item>"""
|
|
406
466
|
|
|
467
|
+
if cache_key:
|
|
468
|
+
if result := results_cache.get(cache_key, offset, limit):
|
|
469
|
+
debug(
|
|
470
|
+
f"Returning offset {offset}, limit {limit} for {cache_key}"
|
|
471
|
+
)
|
|
472
|
+
return result
|
|
473
|
+
|
|
407
474
|
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
408
475
|
<rss>
|
|
409
476
|
<channel>
|
|
@@ -425,3 +492,39 @@ def setup_arr_routes(app):
|
|
|
425
492
|
|
|
426
493
|
info(f"[ERROR] Unknown general request: {dict(request.query)}")
|
|
427
494
|
return {"error": True}
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class ResultsCache:
|
|
498
|
+
def __init__(self):
|
|
499
|
+
self.last_cleaned = time.time()
|
|
500
|
+
self.cache = {}
|
|
501
|
+
|
|
502
|
+
def clean(self, now):
|
|
503
|
+
if now - self.last_cleaned < 60:
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
keys_to_delete = [
|
|
507
|
+
key for key, (_, expiry) in self.cache.items() if now >= expiry
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
for key in keys_to_delete:
|
|
511
|
+
del self.cache[key]
|
|
512
|
+
|
|
513
|
+
self.last_cleaned = now
|
|
514
|
+
|
|
515
|
+
def get(self, key, offset, limit):
|
|
516
|
+
key = key + f"::{offset}::{limit}"
|
|
517
|
+
value, expiry = self.cache.get(key, (None, 0))
|
|
518
|
+
if time.time() < expiry:
|
|
519
|
+
return value
|
|
520
|
+
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
def set(self, key, value, offset, limit, ttl=300):
|
|
524
|
+
now = time.time()
|
|
525
|
+
key = key + f"::{offset}::{limit}"
|
|
526
|
+
self.cache[key] = (value, now + ttl)
|
|
527
|
+
self.clean(now)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
results_cache = ResultsCache()
|