quasarr 2.4.8__py3-none-any.whl → 2.4.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of quasarr might be problematic. Click here for more details.
- 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.8.dist-info → quasarr-2.4.10.dist-info}/METADATA +4 -3
- quasarr-2.4.10.dist-info/RECORD +81 -0
- quasarr-2.4.8.dist-info/RECORD +0 -81
- {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/WHEEL +0 -0
- {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,14 +7,19 @@ import os
|
|
|
7
7
|
import re
|
|
8
8
|
import time
|
|
9
9
|
import traceback
|
|
10
|
-
from datetime import datetime, timedelta, date
|
|
11
|
-
from urllib import parse
|
|
12
|
-
|
|
13
10
|
import unicodedata
|
|
11
|
+
from datetime import date, datetime, timedelta
|
|
12
|
+
from urllib import parse
|
|
14
13
|
|
|
15
14
|
import quasarr
|
|
16
|
-
from quasarr.providers.log import
|
|
17
|
-
from quasarr.providers.myjd_api import
|
|
15
|
+
from quasarr.providers.log import debug, info
|
|
16
|
+
from quasarr.providers.myjd_api import (
|
|
17
|
+
Jddevice,
|
|
18
|
+
Myjdapi,
|
|
19
|
+
MYJDException,
|
|
20
|
+
RequestTimeoutException,
|
|
21
|
+
TokenExpiredException,
|
|
22
|
+
)
|
|
18
23
|
from quasarr.storage.config import Config
|
|
19
24
|
from quasarr.storage.sqlite_database import DataBase
|
|
20
25
|
|
|
@@ -22,9 +27,13 @@ values = {}
|
|
|
22
27
|
lock = None
|
|
23
28
|
|
|
24
29
|
# regex to detect season/episode tags for series filtering during search
|
|
25
|
-
SEASON_EP_REGEX = re.compile(
|
|
30
|
+
SEASON_EP_REGEX = re.compile(
|
|
31
|
+
r"(?i)(?:S\d{1,3}(?:E\d{1,3}(?:-\d{1,3})?)?|S\d{1,3}-\d{1,3})"
|
|
32
|
+
)
|
|
26
33
|
# regex to filter out season/episode tags for movies
|
|
27
|
-
MOVIE_REGEX = re.compile(
|
|
34
|
+
MOVIE_REGEX = re.compile(
|
|
35
|
+
r"^(?!.*(?:S\d{1,3}(?:E\d{1,3}(?:-\d{1,3})?)?|S\d{1,3}-\d{1,3})).*$", re.IGNORECASE
|
|
36
|
+
)
|
|
28
37
|
# List of known file hosters that should not be used as search/feed sites
|
|
29
38
|
SHARE_HOSTERS = {
|
|
30
39
|
"rapidgator",
|
|
@@ -74,18 +83,18 @@ def set_files(config_path):
|
|
|
74
83
|
|
|
75
84
|
def generate_api_key():
|
|
76
85
|
api_key = os.urandom(32).hex()
|
|
77
|
-
Config(
|
|
86
|
+
Config("API").save("key", api_key)
|
|
78
87
|
info(f'API Key replaced with: "{api_key}!"')
|
|
79
88
|
return api_key
|
|
80
89
|
|
|
81
90
|
|
|
82
91
|
def extract_valid_hostname(url, shorthand):
|
|
83
92
|
try:
|
|
84
|
-
if
|
|
85
|
-
url =
|
|
93
|
+
if "://" not in url:
|
|
94
|
+
url = "http://" + url
|
|
86
95
|
result = parse.urlparse(url)
|
|
87
96
|
domain = result.netloc
|
|
88
|
-
parts = domain.split(
|
|
97
|
+
parts = domain.split(".")
|
|
89
98
|
|
|
90
99
|
if domain.startswith(".") or domain.endswith(".") or "." not in domain[1:-1]:
|
|
91
100
|
message = f'Error: "{domain}" must contain a "." somewhere in the middle – you need to provide a full domain name!'
|
|
@@ -122,13 +131,17 @@ def connect_to_jd(jd, user, password, device_name):
|
|
|
122
131
|
info("Error connecting to JDownloader: " + str(e).strip())
|
|
123
132
|
return False
|
|
124
133
|
if not device or not isinstance(device, (type, Jddevice)):
|
|
125
|
-
info(
|
|
134
|
+
info(
|
|
135
|
+
f'Device "{device_name}" not found. Available devices may differ or be offline.'
|
|
136
|
+
)
|
|
126
137
|
return False
|
|
127
138
|
else:
|
|
128
139
|
device.downloadcontroller.get_current_state() # request forces direct_connection info update
|
|
129
140
|
connection_info = device.check_direct_connection()
|
|
130
141
|
if connection_info["status"]:
|
|
131
|
-
info(
|
|
142
|
+
info(
|
|
143
|
+
f'Direct connection to JDownloader established: "{connection_info["ip"]}"'
|
|
144
|
+
)
|
|
132
145
|
else:
|
|
133
146
|
info("Could not establish direct connection to JDownloader.")
|
|
134
147
|
update("device", device)
|
|
@@ -137,42 +150,50 @@ def connect_to_jd(jd, user, password, device_name):
|
|
|
137
150
|
|
|
138
151
|
def set_device(user, password, device):
|
|
139
152
|
jd = Myjdapi()
|
|
140
|
-
jd.set_app_key(
|
|
153
|
+
jd.set_app_key("Quasarr")
|
|
141
154
|
return connect_to_jd(jd, user, password, device)
|
|
142
155
|
|
|
143
156
|
|
|
144
157
|
def set_device_from_config():
|
|
145
|
-
config = Config(
|
|
146
|
-
user = str(config.get(
|
|
147
|
-
password = str(config.get(
|
|
148
|
-
device = str(config.get(
|
|
158
|
+
config = Config("JDownloader")
|
|
159
|
+
user = str(config.get("user"))
|
|
160
|
+
password = str(config.get("password"))
|
|
161
|
+
device = str(config.get("device"))
|
|
149
162
|
|
|
150
163
|
update("device", device)
|
|
151
164
|
|
|
152
165
|
if user and password and device:
|
|
153
166
|
jd = Myjdapi()
|
|
154
|
-
jd.set_app_key(
|
|
167
|
+
jd.set_app_key("Quasarr")
|
|
155
168
|
return connect_to_jd(jd, user, password, device)
|
|
156
169
|
return False
|
|
157
170
|
|
|
158
171
|
|
|
159
172
|
def check_device(device):
|
|
160
173
|
try:
|
|
161
|
-
valid =
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
valid = (
|
|
175
|
+
isinstance(device, (type, Jddevice))
|
|
176
|
+
and device.downloadcontroller.get_current_state()
|
|
177
|
+
)
|
|
178
|
+
except (
|
|
179
|
+
AttributeError,
|
|
180
|
+
KeyError,
|
|
181
|
+
TokenExpiredException,
|
|
182
|
+
RequestTimeoutException,
|
|
183
|
+
MYJDException,
|
|
184
|
+
):
|
|
164
185
|
valid = False
|
|
165
186
|
return valid
|
|
166
187
|
|
|
167
188
|
|
|
168
189
|
def connect_device():
|
|
169
|
-
config = Config(
|
|
170
|
-
user = str(config.get(
|
|
171
|
-
password = str(config.get(
|
|
172
|
-
device = str(config.get(
|
|
190
|
+
config = Config("JDownloader")
|
|
191
|
+
user = str(config.get("user"))
|
|
192
|
+
password = str(config.get("password"))
|
|
193
|
+
device = str(config.get("device"))
|
|
173
194
|
|
|
174
195
|
jd = Myjdapi()
|
|
175
|
-
jd.set_app_key(
|
|
196
|
+
jd.set_app_key("Quasarr")
|
|
176
197
|
|
|
177
198
|
if user and password and device:
|
|
178
199
|
try:
|
|
@@ -197,7 +218,13 @@ def get_device():
|
|
|
197
218
|
try:
|
|
198
219
|
if check_device(values["device"]):
|
|
199
220
|
break
|
|
200
|
-
except (
|
|
221
|
+
except (
|
|
222
|
+
AttributeError,
|
|
223
|
+
KeyError,
|
|
224
|
+
TokenExpiredException,
|
|
225
|
+
RequestTimeoutException,
|
|
226
|
+
MYJDException,
|
|
227
|
+
):
|
|
201
228
|
pass
|
|
202
229
|
attempts += 1
|
|
203
230
|
|
|
@@ -208,19 +235,27 @@ def get_device():
|
|
|
208
235
|
# First 10 failures: 3 seconds
|
|
209
236
|
sleep_time = 3
|
|
210
237
|
if attempts == 10:
|
|
211
|
-
info(
|
|
238
|
+
info(
|
|
239
|
+
f"WARNING: {attempts} consecutive JDownloader connection errors. Switching to 1-minute intervals."
|
|
240
|
+
)
|
|
212
241
|
elif attempts <= 15:
|
|
213
242
|
# Next 5 failures (11-15): 1 minute
|
|
214
243
|
sleep_time = 60
|
|
215
244
|
if attempts % 10 == 0:
|
|
216
|
-
info(
|
|
245
|
+
info(
|
|
246
|
+
f"WARNING: {attempts} consecutive JDownloader connection errors. Please check your credentials!"
|
|
247
|
+
)
|
|
217
248
|
if attempts == 15:
|
|
218
|
-
info(
|
|
249
|
+
info(
|
|
250
|
+
f"WARNING: Still failing after {attempts} attempts. Switching to 5-minute intervals."
|
|
251
|
+
)
|
|
219
252
|
else:
|
|
220
253
|
# After 15 failures: 5 minutes
|
|
221
254
|
sleep_time = 300
|
|
222
255
|
if attempts % 10 == 0:
|
|
223
|
-
info(
|
|
256
|
+
info(
|
|
257
|
+
f"WARNING: {attempts} consecutive JDownloader connection errors. Please check your credentials!"
|
|
258
|
+
)
|
|
224
259
|
|
|
225
260
|
if connect_device():
|
|
226
261
|
break
|
|
@@ -232,7 +267,7 @@ def get_device():
|
|
|
232
267
|
|
|
233
268
|
def get_devices(user, password):
|
|
234
269
|
jd = Myjdapi()
|
|
235
|
-
jd.set_app_key(
|
|
270
|
+
jd.set_app_key("Quasarr")
|
|
236
271
|
try:
|
|
237
272
|
jd.connect(user, password)
|
|
238
273
|
jd.update_devices()
|
|
@@ -342,46 +377,96 @@ def set_device_settings():
|
|
|
342
377
|
"storage": "cfg/org.jdownloader.extensions.extraction.ExtractionExtension",
|
|
343
378
|
"setting": "BlacklistPatterns",
|
|
344
379
|
"expected_values": [
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
]
|
|
380
|
+
".*sample/.*",
|
|
381
|
+
".*Sample/.*",
|
|
382
|
+
".*\\.jpe?g",
|
|
383
|
+
".*\\.idx",
|
|
384
|
+
".*\\.sub",
|
|
385
|
+
".*\\.srt",
|
|
386
|
+
".*\\.nfo",
|
|
387
|
+
".*\\.bat",
|
|
388
|
+
".*\\.txt",
|
|
389
|
+
".*\\.exe",
|
|
390
|
+
".*\\.sfv",
|
|
391
|
+
],
|
|
357
392
|
},
|
|
358
393
|
{
|
|
359
394
|
"namespace": "org.jdownloader.controlling.filter.LinkFilterSettings",
|
|
360
395
|
"storage": "null",
|
|
361
396
|
"setting": "FilterList",
|
|
362
397
|
"expected_values": [
|
|
363
|
-
{
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
398
|
+
{
|
|
399
|
+
"conditionFilter": {
|
|
400
|
+
"conditions": [],
|
|
401
|
+
"enabled": False,
|
|
402
|
+
"matchType": "IS_TRUE",
|
|
403
|
+
},
|
|
404
|
+
"created": 0,
|
|
405
|
+
"enabled": True,
|
|
406
|
+
"filenameFilter": {
|
|
407
|
+
"enabled": True,
|
|
408
|
+
"matchType": "CONTAINS",
|
|
409
|
+
"regex": ".*\\.(sfv|jpe?g|idx|srt|nfo|bat|txt|exe)",
|
|
410
|
+
"useRegex": True,
|
|
411
|
+
},
|
|
412
|
+
"filesizeFilter": {
|
|
413
|
+
"enabled": False,
|
|
414
|
+
"from": 0,
|
|
415
|
+
"matchType": "BETWEEN",
|
|
416
|
+
"to": 0,
|
|
417
|
+
},
|
|
418
|
+
"filetypeFilter": {
|
|
419
|
+
"archivesEnabled": False,
|
|
420
|
+
"audioFilesEnabled": False,
|
|
421
|
+
"customs": None,
|
|
422
|
+
"docFilesEnabled": False,
|
|
423
|
+
"enabled": False,
|
|
424
|
+
"exeFilesEnabled": False,
|
|
425
|
+
"hashEnabled": False,
|
|
426
|
+
"imagesEnabled": False,
|
|
427
|
+
"matchType": "IS",
|
|
428
|
+
"subFilesEnabled": False,
|
|
429
|
+
"useRegex": False,
|
|
430
|
+
"videoFilesEnabled": False,
|
|
431
|
+
},
|
|
432
|
+
"hosterURLFilter": {
|
|
433
|
+
"enabled": False,
|
|
434
|
+
"matchType": "CONTAINS",
|
|
435
|
+
"regex": "",
|
|
436
|
+
"useRegex": False,
|
|
437
|
+
},
|
|
438
|
+
"matchAlwaysFilter": {"enabled": False},
|
|
439
|
+
"name": "Quasarr_Block_Files",
|
|
440
|
+
"onlineStatusFilter": {
|
|
441
|
+
"enabled": False,
|
|
442
|
+
"matchType": "IS",
|
|
443
|
+
"onlineStatus": "OFFLINE",
|
|
444
|
+
},
|
|
445
|
+
"originFilter": {
|
|
446
|
+
"enabled": False,
|
|
447
|
+
"matchType": "IS",
|
|
448
|
+
"origins": [],
|
|
449
|
+
},
|
|
450
|
+
"packagenameFilter": {
|
|
451
|
+
"enabled": False,
|
|
452
|
+
"matchType": "CONTAINS",
|
|
453
|
+
"regex": "",
|
|
454
|
+
"useRegex": False,
|
|
455
|
+
},
|
|
456
|
+
"pluginStatusFilter": {
|
|
457
|
+
"enabled": False,
|
|
458
|
+
"matchType": "IS",
|
|
459
|
+
"pluginStatus": "PREMIUM",
|
|
460
|
+
},
|
|
461
|
+
"sourceURLFilter": {
|
|
462
|
+
"enabled": False,
|
|
463
|
+
"matchType": "CONTAINS",
|
|
464
|
+
"regex": "",
|
|
465
|
+
"useRegex": False,
|
|
466
|
+
},
|
|
467
|
+
"testUrl": "",
|
|
468
|
+
}
|
|
469
|
+
],
|
|
385
470
|
},
|
|
386
471
|
]
|
|
387
472
|
|
|
@@ -418,7 +503,9 @@ def update_jdownloader():
|
|
|
418
503
|
is_collecting = device.linkgrabber.is_collecting()
|
|
419
504
|
update_available = device.update.update_available()
|
|
420
505
|
|
|
421
|
-
if (current_state.lower() == "idle") and (
|
|
506
|
+
if (current_state.lower() == "idle") and (
|
|
507
|
+
not is_collecting and update_available
|
|
508
|
+
):
|
|
422
509
|
info("JDownloader update ready. Starting update...")
|
|
423
510
|
device.update.restart_and_update()
|
|
424
511
|
except quasarr.providers.myjd_api.TokenExpiredException:
|
|
@@ -454,21 +541,23 @@ def get_db(table):
|
|
|
454
541
|
|
|
455
542
|
|
|
456
543
|
def convert_to_mb(item):
|
|
457
|
-
size = float(item[
|
|
458
|
-
unit = item[
|
|
544
|
+
size = float(item["size"])
|
|
545
|
+
unit = item["sizeunit"].upper()
|
|
459
546
|
|
|
460
|
-
if unit ==
|
|
547
|
+
if unit == "B":
|
|
461
548
|
size_b = size
|
|
462
|
-
elif unit ==
|
|
549
|
+
elif unit == "KB":
|
|
463
550
|
size_b = size * 1024
|
|
464
|
-
elif unit ==
|
|
551
|
+
elif unit == "MB":
|
|
465
552
|
size_b = size * 1024 * 1024
|
|
466
|
-
elif unit ==
|
|
553
|
+
elif unit == "GB":
|
|
467
554
|
size_b = size * 1024 * 1024 * 1024
|
|
468
|
-
elif unit ==
|
|
555
|
+
elif unit == "TB":
|
|
469
556
|
size_b = size * 1024 * 1024 * 1024 * 1024
|
|
470
557
|
else:
|
|
471
|
-
raise ValueError(
|
|
558
|
+
raise ValueError(
|
|
559
|
+
f"Unsupported size unit {item['name']} {item['size']} {item['sizeunit']}"
|
|
560
|
+
)
|
|
472
561
|
|
|
473
562
|
size_mb = size_b / (1024 * 1024)
|
|
474
563
|
return int(size_mb)
|
|
@@ -476,10 +565,13 @@ def convert_to_mb(item):
|
|
|
476
565
|
|
|
477
566
|
def sanitize_title(title: str) -> str:
|
|
478
567
|
umlaut_map = {
|
|
479
|
-
"Ä": "Ae",
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
"
|
|
568
|
+
"Ä": "Ae",
|
|
569
|
+
"ä": "ae",
|
|
570
|
+
"Ö": "Oe",
|
|
571
|
+
"ö": "oe",
|
|
572
|
+
"Ü": "Ue",
|
|
573
|
+
"ü": "ue",
|
|
574
|
+
"ß": "ss",
|
|
483
575
|
}
|
|
484
576
|
for umlaut, replacement in umlaut_map.items():
|
|
485
577
|
title = title.replace(umlaut, replacement)
|
|
@@ -503,32 +595,32 @@ def sanitize_string(s):
|
|
|
503
595
|
s = s.lower()
|
|
504
596
|
|
|
505
597
|
# Remove dots / pluses
|
|
506
|
-
s = s.replace(
|
|
507
|
-
s = s.replace(
|
|
508
|
-
s = s.replace(
|
|
509
|
-
s = s.replace(
|
|
598
|
+
s = s.replace(".", " ")
|
|
599
|
+
s = s.replace("+", " ")
|
|
600
|
+
s = s.replace("_", " ")
|
|
601
|
+
s = s.replace("-", " ")
|
|
510
602
|
|
|
511
603
|
# Umlauts
|
|
512
|
-
s = re.sub(r
|
|
513
|
-
s = re.sub(r
|
|
514
|
-
s = re.sub(r
|
|
515
|
-
s = re.sub(r
|
|
604
|
+
s = re.sub(r"ä", "ae", s)
|
|
605
|
+
s = re.sub(r"ö", "oe", s)
|
|
606
|
+
s = re.sub(r"ü", "ue", s)
|
|
607
|
+
s = re.sub(r"ß", "ss", s)
|
|
516
608
|
|
|
517
609
|
# Remove special characters
|
|
518
|
-
s = re.sub(r
|
|
610
|
+
s = re.sub(r"[^a-zA-Z0-9\s]", "", s)
|
|
519
611
|
|
|
520
612
|
# Remove season and episode patterns
|
|
521
|
-
s = re.sub(r
|
|
613
|
+
s = re.sub(r"\bs\d{1,3}(e\d{1,3})?\b", "", s)
|
|
522
614
|
|
|
523
615
|
# Remove German and English articles
|
|
524
|
-
articles = r
|
|
525
|
-
s = re.sub(articles,
|
|
616
|
+
articles = r"\b(?:der|die|das|ein|eine|einer|eines|einem|einen|the|a|an|and)\b"
|
|
617
|
+
s = re.sub(articles, "", s, re.IGNORECASE)
|
|
526
618
|
|
|
527
619
|
# Replace obsolete titles
|
|
528
|
-
s = s.replace(
|
|
620
|
+
s = s.replace("navy cis", "ncis")
|
|
529
621
|
|
|
530
622
|
# Remove extra whitespace
|
|
531
|
-
s =
|
|
623
|
+
s = " ".join(s.split())
|
|
532
624
|
|
|
533
625
|
return s
|
|
534
626
|
|
|
@@ -538,11 +630,15 @@ def search_string_in_sanitized_title(search_string, title):
|
|
|
538
630
|
sanitized_title = sanitize_string(title)
|
|
539
631
|
|
|
540
632
|
# Use word boundaries to ensure full word/phrase match
|
|
541
|
-
if re.search(rf
|
|
542
|
-
debug(
|
|
633
|
+
if re.search(rf"\b{re.escape(sanitized_search_string)}\b", sanitized_title):
|
|
634
|
+
debug(
|
|
635
|
+
f"Matched search string: {sanitized_search_string} with title: {sanitized_title}"
|
|
636
|
+
)
|
|
543
637
|
return True
|
|
544
638
|
else:
|
|
545
|
-
debug(
|
|
639
|
+
debug(
|
|
640
|
+
f"Skipping {title} as it doesn't match search string: {sanitized_search_string}"
|
|
641
|
+
)
|
|
546
642
|
return False
|
|
547
643
|
|
|
548
644
|
|
|
@@ -602,11 +698,13 @@ def match_in_title(title: str, season: int = None, episode: int = None) -> bool:
|
|
|
602
698
|
return False
|
|
603
699
|
|
|
604
700
|
|
|
605
|
-
def is_valid_release(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
701
|
+
def is_valid_release(
|
|
702
|
+
title: str,
|
|
703
|
+
request_from: str,
|
|
704
|
+
search_string: str,
|
|
705
|
+
season: int = None,
|
|
706
|
+
episode: int = None,
|
|
707
|
+
) -> bool:
|
|
610
708
|
"""
|
|
611
709
|
Return True if the given release title is valid for the given search parameters.
|
|
612
710
|
- title: the release title to test
|
|
@@ -618,20 +716,24 @@ def is_valid_release(title: str,
|
|
|
618
716
|
try:
|
|
619
717
|
# Determine whether this is a movie or TV search
|
|
620
718
|
rf = request_from.lower()
|
|
621
|
-
is_movie_search =
|
|
622
|
-
is_tv_search =
|
|
623
|
-
is_docs_search =
|
|
719
|
+
is_movie_search = "radarr" in rf
|
|
720
|
+
is_tv_search = "sonarr" in rf
|
|
721
|
+
is_docs_search = "lazylibrarian" in rf
|
|
624
722
|
|
|
625
723
|
# if search string is NOT an imdb id check search_string_in_sanitized_title - if not match, its not valid
|
|
626
724
|
if not is_docs_search and not is_imdb_id(search_string):
|
|
627
725
|
if not search_string_in_sanitized_title(search_string, title):
|
|
628
|
-
debug(
|
|
726
|
+
debug(
|
|
727
|
+
f"Skipping {title!r} as it doesn't match sanitized search string: {search_string!r}"
|
|
728
|
+
)
|
|
629
729
|
return False
|
|
630
730
|
|
|
631
731
|
# if it's a movie search, don't allow any TV show titles (check for NO season or episode tags in the title)
|
|
632
732
|
if is_movie_search:
|
|
633
733
|
if not MOVIE_REGEX.match(title):
|
|
634
|
-
debug(
|
|
734
|
+
debug(
|
|
735
|
+
f"Skipping {title!r} as title doesn't match movie regex: {MOVIE_REGEX.pattern}"
|
|
736
|
+
)
|
|
635
737
|
return False
|
|
636
738
|
return True
|
|
637
739
|
|
|
@@ -639,12 +741,16 @@ def is_valid_release(title: str,
|
|
|
639
741
|
if is_tv_search:
|
|
640
742
|
# must have some S/E tag present
|
|
641
743
|
if not SEASON_EP_REGEX.search(title):
|
|
642
|
-
debug(
|
|
744
|
+
debug(
|
|
745
|
+
f"Skipping {title!r} as title doesn't match TV show regex: {SEASON_EP_REGEX.pattern}"
|
|
746
|
+
)
|
|
643
747
|
return False
|
|
644
748
|
# if caller specified a season or episode, double‑check the match
|
|
645
749
|
if season is not None or episode is not None:
|
|
646
750
|
if not match_in_title(title, season, episode):
|
|
647
|
-
debug(
|
|
751
|
+
debug(
|
|
752
|
+
f"Skipping {title!r} as it doesn't match season {season} and episode {episode}"
|
|
753
|
+
)
|
|
648
754
|
return False
|
|
649
755
|
return True
|
|
650
756
|
|
|
@@ -652,7 +758,9 @@ def is_valid_release(title: str,
|
|
|
652
758
|
if is_docs_search:
|
|
653
759
|
# must NOT have any S/E tag present
|
|
654
760
|
if SEASON_EP_REGEX.search(title):
|
|
655
|
-
debug(
|
|
761
|
+
debug(
|
|
762
|
+
f"Skipping {title!r} as title matches TV show regex: {SEASON_EP_REGEX.pattern}"
|
|
763
|
+
)
|
|
656
764
|
return False
|
|
657
765
|
return True
|
|
658
766
|
|
|
@@ -663,10 +771,12 @@ def is_valid_release(title: str,
|
|
|
663
771
|
except Exception as e:
|
|
664
772
|
# log exception message and short stack trace
|
|
665
773
|
tb = traceback.format_exc()
|
|
666
|
-
debug(
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
774
|
+
debug(
|
|
775
|
+
f"Exception in is_valid_release: {e!r}\n{tb}"
|
|
776
|
+
f"is_valid_release called with "
|
|
777
|
+
f"title={title!r}, request_from={request_from!r}, "
|
|
778
|
+
f"search_string={search_string!r}, season={season!r}, episode={episode!r}"
|
|
779
|
+
)
|
|
670
780
|
return False
|
|
671
781
|
|
|
672
782
|
|
|
@@ -724,7 +834,7 @@ def normalize_magazine_title(title: str) -> str:
|
|
|
724
834
|
r"\b(?:vom\s*)?(\d{1,2})\.?\s+([A-Za-zÄÖÜäöüß]+)\s+(\d{4})\b",
|
|
725
835
|
repl_dmony,
|
|
726
836
|
title,
|
|
727
|
-
flags=re.IGNORECASE
|
|
837
|
+
flags=re.IGNORECASE,
|
|
728
838
|
)
|
|
729
839
|
|
|
730
840
|
# 3) MonthName YYYY -> "YYYY MM"
|
|
@@ -740,7 +850,9 @@ def normalize_magazine_title(title: str) -> str:
|
|
|
740
850
|
pass
|
|
741
851
|
return match.group(0)
|
|
742
852
|
|
|
743
|
-
title = re.sub(
|
|
853
|
+
title = re.sub(
|
|
854
|
+
r"\b([A-Za-zÄÖÜäöüß]+)\s+(\d{4})\b", repl_mony, title, flags=re.IGNORECASE
|
|
855
|
+
)
|
|
744
856
|
|
|
745
857
|
# 4) YYYYMMDD -> "YYYY MM DD"
|
|
746
858
|
def repl_ymd(match):
|
|
@@ -753,7 +865,9 @@ def normalize_magazine_title(title: str) -> str:
|
|
|
753
865
|
except ValueError:
|
|
754
866
|
return match.group(0)
|
|
755
867
|
|
|
756
|
-
title = re.sub(
|
|
868
|
+
title = re.sub(
|
|
869
|
+
r"\b(20\d{2})(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\b", repl_ymd, title
|
|
870
|
+
)
|
|
757
871
|
|
|
758
872
|
# 5) YYYYMM -> "YYYY MM"
|
|
759
873
|
def repl_ym(match):
|
|
@@ -798,7 +912,7 @@ def normalize_magazine_title(title: str) -> str:
|
|
|
798
912
|
r"\b(?:No|Nr|Sonderheft)\s*(\d{1,2})\.(\d{4})\b",
|
|
799
913
|
repl_nmy,
|
|
800
914
|
title,
|
|
801
|
-
flags=re.IGNORECASE
|
|
915
|
+
flags=re.IGNORECASE,
|
|
802
916
|
)
|
|
803
917
|
|
|
804
918
|
return title
|
|
@@ -808,11 +922,44 @@ def normalize_magazine_title(title: str) -> str:
|
|
|
808
922
|
def _month_num(name: str) -> int:
|
|
809
923
|
name = name.lower()
|
|
810
924
|
mmap = {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
925
|
+
"januar": 1,
|
|
926
|
+
"jan": 1,
|
|
927
|
+
"februar": 2,
|
|
928
|
+
"feb": 2,
|
|
929
|
+
"märz": 3,
|
|
930
|
+
"maerz": 3,
|
|
931
|
+
"mär": 3,
|
|
932
|
+
"mrz": 3,
|
|
933
|
+
"mae": 3,
|
|
934
|
+
"april": 4,
|
|
935
|
+
"apr": 4,
|
|
936
|
+
"mai": 5,
|
|
937
|
+
"juni": 6,
|
|
938
|
+
"jun": 6,
|
|
939
|
+
"juli": 7,
|
|
940
|
+
"jul": 7,
|
|
941
|
+
"august": 8,
|
|
942
|
+
"aug": 8,
|
|
943
|
+
"september": 9,
|
|
944
|
+
"sep": 9,
|
|
945
|
+
"oktober": 10,
|
|
946
|
+
"okt": 10,
|
|
947
|
+
"november": 11,
|
|
948
|
+
"nov": 11,
|
|
949
|
+
"dezember": 12,
|
|
950
|
+
"dez": 12,
|
|
951
|
+
"january": 1,
|
|
952
|
+
"february": 2,
|
|
953
|
+
"march": 3,
|
|
954
|
+
"april": 4,
|
|
955
|
+
"may": 5,
|
|
956
|
+
"june": 6,
|
|
957
|
+
"july": 7,
|
|
958
|
+
"august": 8,
|
|
959
|
+
"september": 9,
|
|
960
|
+
"october": 10,
|
|
961
|
+
"november": 11,
|
|
962
|
+
"december": 12,
|
|
816
963
|
}
|
|
817
964
|
return mmap.get(name)
|
|
818
965
|
|
|
@@ -820,7 +967,11 @@ def _month_num(name: str) -> int:
|
|
|
820
967
|
def get_recently_searched(shared_state, context, timeout_seconds):
|
|
821
968
|
recently_searched = shared_state.values.get(context, {})
|
|
822
969
|
threshold = datetime.now() - timedelta(seconds=timeout_seconds)
|
|
823
|
-
keys_to_remove = [
|
|
970
|
+
keys_to_remove = [
|
|
971
|
+
key
|
|
972
|
+
for key, value in recently_searched.items()
|
|
973
|
+
if value["timestamp"] <= threshold
|
|
974
|
+
]
|
|
824
975
|
for key in keys_to_remove:
|
|
825
976
|
debug(f"Removing '{key}' from recently searched memory ({context})...")
|
|
826
977
|
del recently_searched[key]
|
|
@@ -831,19 +982,21 @@ def download_package(links, title, password, package_id):
|
|
|
831
982
|
links = [sanitize_url(link) for link in links]
|
|
832
983
|
|
|
833
984
|
device = get_device()
|
|
834
|
-
downloaded = device.linkgrabber.add_links(
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
985
|
+
downloaded = device.linkgrabber.add_links(
|
|
986
|
+
params=[
|
|
987
|
+
{
|
|
988
|
+
"autostart": False,
|
|
989
|
+
"links": json.dumps(links),
|
|
990
|
+
"packageName": title,
|
|
991
|
+
"extractPassword": password,
|
|
992
|
+
"priority": "DEFAULT",
|
|
993
|
+
"downloadPassword": password,
|
|
994
|
+
"destinationFolder": "Quasarr/<jd:packagename>",
|
|
995
|
+
"comment": package_id,
|
|
996
|
+
"overwritePackagizerRules": True,
|
|
997
|
+
}
|
|
998
|
+
]
|
|
999
|
+
)
|
|
847
1000
|
return downloaded
|
|
848
1001
|
|
|
849
1002
|
|
|
@@ -852,12 +1005,10 @@ def sanitize_url(url: str) -> str:
|
|
|
852
1005
|
url = unicodedata.normalize("NFKC", url)
|
|
853
1006
|
|
|
854
1007
|
# 1) real control characters (U+0000–U+001F, U+007F–U+009F)
|
|
855
|
-
_REAL_CTRL_RE = re.compile(r
|
|
1008
|
+
_REAL_CTRL_RE = re.compile(r"[\u0000-\u001f\u007f-\u009f]")
|
|
856
1009
|
|
|
857
1010
|
# 2) *literal* escaped unicode junk: \u0010, \x10, repeated variants
|
|
858
|
-
_ESCAPED_CTRL_RE = re.compile(
|
|
859
|
-
r'(?:\\u00[0-1][0-9a-fA-F]|\\x[0-1][0-9a-fA-F])'
|
|
860
|
-
)
|
|
1011
|
+
_ESCAPED_CTRL_RE = re.compile(r"(?:\\u00[0-1][0-9a-fA-F]|\\x[0-1][0-9a-fA-F])")
|
|
861
1012
|
|
|
862
1013
|
# remove literal escaped control sequences like "\u0010"
|
|
863
1014
|
url = _ESCAPED_CTRL_RE.sub("", url)
|