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
|
@@ -5,24 +5,26 @@
|
|
|
5
5
|
import html
|
|
6
6
|
import re
|
|
7
7
|
from datetime import datetime, timedelta
|
|
8
|
-
from json import
|
|
8
|
+
from json import dumps, loads
|
|
9
9
|
from urllib.parse import quote
|
|
10
10
|
|
|
11
11
|
import requests
|
|
12
12
|
from bs4 import BeautifulSoup
|
|
13
13
|
|
|
14
|
-
from quasarr.providers.log import
|
|
14
|
+
from quasarr.providers.log import debug, info
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _get_db(table_name):
|
|
18
18
|
"""Lazy import to avoid circular dependency."""
|
|
19
19
|
from quasarr.storage.sqlite_database import DataBase
|
|
20
|
+
|
|
20
21
|
return DataBase(table_name)
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
def _get_config(section):
|
|
24
25
|
"""Lazy import to avoid circular dependency."""
|
|
25
26
|
from quasarr.storage.config import Config
|
|
27
|
+
|
|
26
28
|
return Config(section)
|
|
27
29
|
|
|
28
30
|
|
|
@@ -32,9 +34,11 @@ class TitleCleaner:
|
|
|
32
34
|
if not title:
|
|
33
35
|
return ""
|
|
34
36
|
sanitized_title = html.unescape(title)
|
|
35
|
-
sanitized_title = re.sub(
|
|
37
|
+
sanitized_title = re.sub(
|
|
38
|
+
r"[^a-zA-Z0-9äöüÄÖÜß&-']", " ", sanitized_title
|
|
39
|
+
).strip()
|
|
36
40
|
sanitized_title = sanitized_title.replace(" - ", "-")
|
|
37
|
-
sanitized_title = re.sub(r
|
|
41
|
+
sanitized_title = re.sub(r"\s{2,}", " ", sanitized_title)
|
|
38
42
|
return sanitized_title
|
|
39
43
|
|
|
40
44
|
@staticmethod
|
|
@@ -49,10 +53,18 @@ class TitleCleaner:
|
|
|
49
53
|
extracted_title = title
|
|
50
54
|
|
|
51
55
|
tags_to_remove = [
|
|
52
|
-
r
|
|
53
|
-
r
|
|
54
|
-
r
|
|
55
|
-
r
|
|
56
|
+
r"[\.\s]UNRATED.*",
|
|
57
|
+
r"[\.\s]Unrated.*",
|
|
58
|
+
r"[\.\s]Uncut.*",
|
|
59
|
+
r"[\.\s]UNCUT.*",
|
|
60
|
+
r"[\.\s]Directors[\.\s]Cut.*",
|
|
61
|
+
r"[\.\s]Final[\.\s]Cut.*",
|
|
62
|
+
r"[\.\s]DC.*",
|
|
63
|
+
r"[\.\s]REMASTERED.*",
|
|
64
|
+
r"[\.\s]EXTENDED.*",
|
|
65
|
+
r"[\.\s]Extended.*",
|
|
66
|
+
r"[\.\s]Theatrical.*",
|
|
67
|
+
r"[\.\s]THEATRICAL.*",
|
|
56
68
|
]
|
|
57
69
|
|
|
58
70
|
clean_title = extracted_title
|
|
@@ -60,7 +72,7 @@ class TitleCleaner:
|
|
|
60
72
|
clean_title = re.sub(tag, "", clean_title, flags=re.IGNORECASE)
|
|
61
73
|
|
|
62
74
|
clean_title = clean_title.replace(".", " ").strip()
|
|
63
|
-
clean_title = re.sub(r
|
|
75
|
+
clean_title = re.sub(r"\s+", " ", clean_title)
|
|
64
76
|
clean_title = clean_title.replace(" ", "+")
|
|
65
77
|
|
|
66
78
|
return clean_title
|
|
@@ -71,6 +83,7 @@ class TitleCleaner:
|
|
|
71
83
|
|
|
72
84
|
class IMDbAPI:
|
|
73
85
|
"""Tier 1: api.imdbapi.dev - Primary, fast, comprehensive."""
|
|
86
|
+
|
|
74
87
|
BASE_URL = "https://api.imdbapi.dev"
|
|
75
88
|
|
|
76
89
|
@staticmethod
|
|
@@ -86,7 +99,9 @@ class IMDbAPI:
|
|
|
86
99
|
@staticmethod
|
|
87
100
|
def get_akas(imdb_id):
|
|
88
101
|
try:
|
|
89
|
-
response = requests.get(
|
|
102
|
+
response = requests.get(
|
|
103
|
+
f"{IMDbAPI.BASE_URL}/titles/{imdb_id}/akas", timeout=30
|
|
104
|
+
)
|
|
90
105
|
response.raise_for_status()
|
|
91
106
|
return response.json().get("akas", [])
|
|
92
107
|
except Exception as e:
|
|
@@ -96,7 +111,10 @@ class IMDbAPI:
|
|
|
96
111
|
@staticmethod
|
|
97
112
|
def search_titles(query):
|
|
98
113
|
try:
|
|
99
|
-
response = requests.get(
|
|
114
|
+
response = requests.get(
|
|
115
|
+
f"{IMDbAPI.BASE_URL}/search/titles?query={quote(query)}&limit=5",
|
|
116
|
+
timeout=30,
|
|
117
|
+
)
|
|
100
118
|
response.raise_for_status()
|
|
101
119
|
return response.json().get("titles", [])
|
|
102
120
|
except Exception as e:
|
|
@@ -106,6 +124,7 @@ class IMDbAPI:
|
|
|
106
124
|
|
|
107
125
|
class IMDbCDN:
|
|
108
126
|
"""Tier 2: v2.sg.media-imdb.com - Fast fallback for English data."""
|
|
127
|
+
|
|
109
128
|
CDN_URL = "https://v2.sg.media-imdb.com/suggestion"
|
|
110
129
|
|
|
111
130
|
@staticmethod
|
|
@@ -115,9 +134,9 @@ class IMDbCDN:
|
|
|
115
134
|
return None
|
|
116
135
|
|
|
117
136
|
headers = {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
"Accept-Language": f"{language},en;q=0.9",
|
|
138
|
+
"User-Agent": user_agent,
|
|
139
|
+
"Accept": "application/json",
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
first_char = imdb_id[0].lower()
|
|
@@ -141,7 +160,7 @@ class IMDbCDN:
|
|
|
141
160
|
|
|
142
161
|
@staticmethod
|
|
143
162
|
def get_poster(imdb_id, user_agent):
|
|
144
|
-
data = IMDbCDN._get_cdn_data(imdb_id,
|
|
163
|
+
data = IMDbCDN._get_cdn_data(imdb_id, "en", user_agent)
|
|
145
164
|
if data:
|
|
146
165
|
image_node = data.get("i")
|
|
147
166
|
if image_node and "imageUrl" in image_node:
|
|
@@ -151,7 +170,7 @@ class IMDbCDN:
|
|
|
151
170
|
@staticmethod
|
|
152
171
|
def get_title(imdb_id, user_agent):
|
|
153
172
|
"""Returns the English title from CDN."""
|
|
154
|
-
data = IMDbCDN._get_cdn_data(imdb_id,
|
|
173
|
+
data = IMDbCDN._get_cdn_data(imdb_id, "en", user_agent)
|
|
155
174
|
if data and "l" in data:
|
|
156
175
|
return data["l"]
|
|
157
176
|
return None
|
|
@@ -160,11 +179,12 @@ class IMDbCDN:
|
|
|
160
179
|
def search_titles(query, ttype, language, user_agent):
|
|
161
180
|
try:
|
|
162
181
|
clean_query = quote(query.lower().replace(" ", "_"))
|
|
163
|
-
if not clean_query:
|
|
182
|
+
if not clean_query:
|
|
183
|
+
return []
|
|
164
184
|
|
|
165
185
|
headers = {
|
|
166
|
-
|
|
167
|
-
|
|
186
|
+
"Accept-Language": f"{language},en;q=0.9",
|
|
187
|
+
"User-Agent": user_agent,
|
|
168
188
|
}
|
|
169
189
|
|
|
170
190
|
first_char = clean_query[0]
|
|
@@ -177,15 +197,18 @@ class IMDbCDN:
|
|
|
177
197
|
results = []
|
|
178
198
|
if "d" in data:
|
|
179
199
|
for item in data["d"]:
|
|
180
|
-
results.append(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
200
|
+
results.append(
|
|
201
|
+
{
|
|
202
|
+
"id": item.get("id"),
|
|
203
|
+
"titleNameText": item.get("l"),
|
|
204
|
+
"titleReleaseText": item.get("y"),
|
|
205
|
+
}
|
|
206
|
+
)
|
|
185
207
|
return results
|
|
186
208
|
|
|
187
209
|
except Exception as e:
|
|
188
210
|
from quasarr.providers.log import debug
|
|
211
|
+
|
|
189
212
|
debug(f"IMDb CDN search failed: {e}")
|
|
190
213
|
|
|
191
214
|
return []
|
|
@@ -193,11 +216,12 @@ class IMDbCDN:
|
|
|
193
216
|
|
|
194
217
|
class IMDbFlareSolverr:
|
|
195
218
|
"""Tier 3: FlareSolverr - Robust fallback using browser automation."""
|
|
219
|
+
|
|
196
220
|
WEB_URL = "https://www.imdb.com"
|
|
197
221
|
|
|
198
222
|
@staticmethod
|
|
199
223
|
def _request(url):
|
|
200
|
-
flaresolverr_url = _get_config(
|
|
224
|
+
flaresolverr_url = _get_config("FlareSolverr").get("url")
|
|
201
225
|
flaresolverr_skipped = _get_db("skip_flaresolverr").retrieve("skipped")
|
|
202
226
|
|
|
203
227
|
if not flaresolverr_url or flaresolverr_skipped:
|
|
@@ -210,8 +234,12 @@ class IMDbFlareSolverr:
|
|
|
210
234
|
"maxTimeout": 60000,
|
|
211
235
|
}
|
|
212
236
|
|
|
213
|
-
response = requests.post(
|
|
214
|
-
|
|
237
|
+
response = requests.post(
|
|
238
|
+
flaresolverr_url,
|
|
239
|
+
json=post_data,
|
|
240
|
+
headers={"Content-Type": "application/json"},
|
|
241
|
+
timeout=60,
|
|
242
|
+
)
|
|
215
243
|
if response.status_code == 200:
|
|
216
244
|
json_response = response.json()
|
|
217
245
|
if json_response.get("status") == "ok":
|
|
@@ -223,11 +251,13 @@ class IMDbFlareSolverr:
|
|
|
223
251
|
|
|
224
252
|
@staticmethod
|
|
225
253
|
def get_poster(imdb_id):
|
|
226
|
-
html_content = IMDbFlareSolverr._request(
|
|
254
|
+
html_content = IMDbFlareSolverr._request(
|
|
255
|
+
f"{IMDbFlareSolverr.WEB_URL}/title/{imdb_id}/"
|
|
256
|
+
)
|
|
227
257
|
if html_content:
|
|
228
258
|
try:
|
|
229
259
|
soup = BeautifulSoup(html_content, "html.parser")
|
|
230
|
-
poster_div = soup.find(
|
|
260
|
+
poster_div = soup.find("div", class_="ipc-poster")
|
|
231
261
|
if poster_div and poster_div.div and poster_div.div.img:
|
|
232
262
|
poster_set = poster_div.div.img.get("srcset")
|
|
233
263
|
if poster_set:
|
|
@@ -250,14 +280,14 @@ class IMDbFlareSolverr:
|
|
|
250
280
|
|
|
251
281
|
# Map language codes to country names commonly used in IMDb AKAs
|
|
252
282
|
country_map = {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
283
|
+
"de": ["Germany", "Austria", "Switzerland", "West Germany"],
|
|
284
|
+
"fr": ["France", "Canada", "Belgium"],
|
|
285
|
+
"es": ["Spain", "Mexico", "Argentina"],
|
|
286
|
+
"it": ["Italy"],
|
|
287
|
+
"pt": ["Portugal", "Brazil"],
|
|
288
|
+
"ru": ["Russia", "Soviet Union"],
|
|
289
|
+
"ja": ["Japan"],
|
|
290
|
+
"hi": ["India"],
|
|
261
291
|
}
|
|
262
292
|
|
|
263
293
|
target_countries = country_map.get(language, [])
|
|
@@ -267,17 +297,24 @@ class IMDbFlareSolverr:
|
|
|
267
297
|
items = soup.find_all("li", class_="ipc-metadata-list__item")
|
|
268
298
|
|
|
269
299
|
for item in items:
|
|
270
|
-
label_span = item.find(
|
|
300
|
+
label_span = item.find(
|
|
301
|
+
"span", class_="ipc-metadata-list-item__label"
|
|
302
|
+
)
|
|
271
303
|
if not label_span:
|
|
272
304
|
# Sometimes it's an anchor if it's a link
|
|
273
|
-
label_span = item.find(
|
|
305
|
+
label_span = item.find(
|
|
306
|
+
"a", class_="ipc-metadata-list-item__label"
|
|
307
|
+
)
|
|
274
308
|
|
|
275
309
|
if label_span:
|
|
276
310
|
country = label_span.get_text(strip=True)
|
|
277
311
|
# Check if this country matches our target language
|
|
278
312
|
if any(c in country for c in target_countries):
|
|
279
313
|
# Found a matching country, get the title
|
|
280
|
-
title_span = item.find(
|
|
314
|
+
title_span = item.find(
|
|
315
|
+
"span",
|
|
316
|
+
class_="ipc-metadata-list-item__list-content-item",
|
|
317
|
+
)
|
|
281
318
|
if title_span:
|
|
282
319
|
return title_span.get_text(strip=True)
|
|
283
320
|
|
|
@@ -297,21 +334,27 @@ class IMDbFlareSolverr:
|
|
|
297
334
|
props = soup.find("script", text=re.compile("props"))
|
|
298
335
|
if props:
|
|
299
336
|
details = loads(props.string)
|
|
300
|
-
results = details[
|
|
337
|
+
results = details["props"]["pageProps"]["titleResults"]["results"]
|
|
301
338
|
mapped_results = []
|
|
302
339
|
for result in results:
|
|
303
340
|
try:
|
|
304
|
-
mapped_results.append(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
341
|
+
mapped_results.append(
|
|
342
|
+
{
|
|
343
|
+
"id": result["listItem"]["titleId"],
|
|
344
|
+
"titleNameText": result["listItem"]["titleText"],
|
|
345
|
+
"titleReleaseText": result["listItem"].get(
|
|
346
|
+
"releaseYear"
|
|
347
|
+
),
|
|
348
|
+
}
|
|
349
|
+
)
|
|
309
350
|
except KeyError:
|
|
310
|
-
mapped_results.append(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
351
|
+
mapped_results.append(
|
|
352
|
+
{
|
|
353
|
+
"id": result.get("id"),
|
|
354
|
+
"titleNameText": result.get("titleNameText"),
|
|
355
|
+
"titleReleaseText": result.get("titleReleaseText"),
|
|
356
|
+
}
|
|
357
|
+
)
|
|
315
358
|
return mapped_results
|
|
316
359
|
|
|
317
360
|
results = []
|
|
@@ -322,11 +365,13 @@ class IMDbFlareSolverr:
|
|
|
322
365
|
href = a_tag.get("href", "")
|
|
323
366
|
id_match = re.search(r"(tt\d+)", href)
|
|
324
367
|
if id_match:
|
|
325
|
-
results.append(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
368
|
+
results.append(
|
|
369
|
+
{
|
|
370
|
+
"id": id_match.group(1),
|
|
371
|
+
"titleNameText": a_tag.get_text(strip=True),
|
|
372
|
+
"titleReleaseText": "",
|
|
373
|
+
}
|
|
374
|
+
)
|
|
330
375
|
return results
|
|
331
376
|
|
|
332
377
|
except Exception as e:
|
|
@@ -338,6 +383,7 @@ class IMDbFlareSolverr:
|
|
|
338
383
|
# Main Functions (Chain of Responsibility)
|
|
339
384
|
# =============================================================================
|
|
340
385
|
|
|
386
|
+
|
|
341
387
|
def _update_cache(imdb_id, key, value, language=None):
|
|
342
388
|
db = _get_db("imdb_metadata")
|
|
343
389
|
try:
|
|
@@ -350,11 +396,13 @@ def _update_cache(imdb_id, key, value, language=None):
|
|
|
350
396
|
"year": None,
|
|
351
397
|
"poster_link": None,
|
|
352
398
|
"localized": {},
|
|
353
|
-
"ttl": 0
|
|
399
|
+
"ttl": 0,
|
|
354
400
|
}
|
|
355
401
|
|
|
356
402
|
if key == "localized" and language:
|
|
357
|
-
if "localized" not in metadata or not isinstance(
|
|
403
|
+
if "localized" not in metadata or not isinstance(
|
|
404
|
+
metadata["localized"], dict
|
|
405
|
+
):
|
|
358
406
|
metadata["localized"] = {}
|
|
359
407
|
metadata["localized"][language] = value
|
|
360
408
|
else:
|
|
@@ -391,18 +439,19 @@ def get_poster_link(shared_state, imdb_id):
|
|
|
391
439
|
return None
|
|
392
440
|
|
|
393
441
|
|
|
394
|
-
def get_localized_title(shared_state, imdb_id, language=
|
|
442
|
+
def get_localized_title(shared_state, imdb_id, language="de"):
|
|
395
443
|
# 0. Check Cache (via get_imdb_metadata)
|
|
396
444
|
imdb_metadata = get_imdb_metadata(imdb_id)
|
|
397
445
|
if imdb_metadata:
|
|
398
446
|
localized = imdb_metadata.get("localized", {}).get(language)
|
|
399
|
-
if localized:
|
|
400
|
-
|
|
447
|
+
if localized:
|
|
448
|
+
return localized
|
|
449
|
+
if language == "en" and imdb_metadata.get("title"):
|
|
401
450
|
return imdb_metadata.get("title")
|
|
402
451
|
|
|
403
452
|
user_agent = shared_state.values["user_agent"]
|
|
404
453
|
|
|
405
|
-
if language ==
|
|
454
|
+
if language == "en":
|
|
406
455
|
title = IMDbCDN.get_title(imdb_id, user_agent)
|
|
407
456
|
if title:
|
|
408
457
|
sanitized_title = TitleCleaner.sanitize(title)
|
|
@@ -447,14 +496,16 @@ def get_imdb_metadata(imdb_id):
|
|
|
447
496
|
"year": None,
|
|
448
497
|
"poster_link": None,
|
|
449
498
|
"localized": {},
|
|
450
|
-
"ttl": 0
|
|
499
|
+
"ttl": 0,
|
|
451
500
|
}
|
|
452
501
|
|
|
453
502
|
# 1. Try API
|
|
454
503
|
response_json = IMDbAPI.get_title(imdb_id)
|
|
455
504
|
|
|
456
505
|
if response_json:
|
|
457
|
-
imdb_metadata["title"] = TitleCleaner.sanitize(
|
|
506
|
+
imdb_metadata["title"] = TitleCleaner.sanitize(
|
|
507
|
+
response_json.get("primaryTitle", "")
|
|
508
|
+
)
|
|
458
509
|
imdb_metadata["year"] = response_json.get("startYear")
|
|
459
510
|
|
|
460
511
|
days = 7 if imdb_metadata.get("title") and imdb_metadata.get("year") else 1
|
|
@@ -468,9 +519,12 @@ def get_imdb_metadata(imdb_id):
|
|
|
468
519
|
akas = IMDbAPI.get_akas(imdb_id)
|
|
469
520
|
if akas:
|
|
470
521
|
for aka in akas:
|
|
471
|
-
if aka.get("language"):
|
|
522
|
+
if aka.get("language"):
|
|
523
|
+
continue
|
|
472
524
|
if aka.get("country", {}).get("code", "").lower() == "de":
|
|
473
|
-
imdb_metadata["localized"]["de"] = TitleCleaner.sanitize(
|
|
525
|
+
imdb_metadata["localized"]["de"] = TitleCleaner.sanitize(
|
|
526
|
+
aka.get("text")
|
|
527
|
+
)
|
|
474
528
|
break
|
|
475
529
|
|
|
476
530
|
db.update_store(imdb_id, dumps(imdb_metadata))
|
|
@@ -483,7 +537,7 @@ def get_imdb_metadata(imdb_id):
|
|
|
483
537
|
# 2. Fallback: Try CDN for basic info (English title, Year, Poster)
|
|
484
538
|
# We can't get localized titles from CDN, but we can get the rest.
|
|
485
539
|
# We need a user agent, but this function doesn't receive shared_state.
|
|
486
|
-
# We'll skip CDN fallback here to avoid circular deps or complexity,
|
|
540
|
+
# We'll skip CDN fallback here to avoid circular deps or complexity,
|
|
487
541
|
# as get_poster_link and get_localized_title handle their own fallbacks.
|
|
488
542
|
# But to populate the DB, we could try. For now, return empty/partial if API fails.
|
|
489
543
|
|
|
@@ -508,8 +562,9 @@ def get_imdb_id_from_title(shared_state, title, language="de"):
|
|
|
508
562
|
cached_data = db.retrieve(title)
|
|
509
563
|
if cached_data:
|
|
510
564
|
data = loads(cached_data)
|
|
511
|
-
if data.get("timestamp") and datetime.fromtimestamp(
|
|
512
|
-
|
|
565
|
+
if data.get("timestamp") and datetime.fromtimestamp(
|
|
566
|
+
data["timestamp"]
|
|
567
|
+
) > datetime.now() - timedelta(hours=48):
|
|
513
568
|
return data.get("imdb_id")
|
|
514
569
|
except Exception:
|
|
515
570
|
pass
|
|
@@ -519,26 +574,31 @@ def get_imdb_id_from_title(shared_state, title, language="de"):
|
|
|
519
574
|
# 1. Try API
|
|
520
575
|
search_results = IMDbAPI.search_titles(title)
|
|
521
576
|
if search_results:
|
|
522
|
-
imdb_id = _match_result(
|
|
577
|
+
imdb_id = _match_result(
|
|
578
|
+
shared_state, title, search_results, ttype_api, is_api=True
|
|
579
|
+
)
|
|
523
580
|
|
|
524
581
|
# 2. Try CDN (Fallback)
|
|
525
582
|
if not imdb_id:
|
|
526
583
|
search_results = IMDbCDN.search_titles(title, ttype_web, language, user_agent)
|
|
527
584
|
if search_results:
|
|
528
|
-
imdb_id = _match_result(
|
|
585
|
+
imdb_id = _match_result(
|
|
586
|
+
shared_state, title, search_results, ttype_api, is_api=False
|
|
587
|
+
)
|
|
529
588
|
|
|
530
589
|
# 3. Try FlareSolverr (Last Resort)
|
|
531
590
|
if not imdb_id:
|
|
532
591
|
search_results = IMDbFlareSolverr.search_titles(title, ttype_web)
|
|
533
592
|
if search_results:
|
|
534
|
-
imdb_id = _match_result(
|
|
593
|
+
imdb_id = _match_result(
|
|
594
|
+
shared_state, title, search_results, ttype_api, is_api=False
|
|
595
|
+
)
|
|
535
596
|
|
|
536
597
|
# Update Cache
|
|
537
598
|
try:
|
|
538
|
-
db.update_store(
|
|
539
|
-
"imdb_id": imdb_id,
|
|
540
|
-
|
|
541
|
-
}))
|
|
599
|
+
db.update_store(
|
|
600
|
+
title, dumps({"imdb_id": imdb_id, "timestamp": datetime.now().timestamp()})
|
|
601
|
+
)
|
|
542
602
|
except Exception:
|
|
543
603
|
pass
|
|
544
604
|
|
|
@@ -550,19 +610,28 @@ def get_imdb_id_from_title(shared_state, title, language="de"):
|
|
|
550
610
|
|
|
551
611
|
def _match_result(shared_state, title, results, ttype_api, is_api=False):
|
|
552
612
|
for result in results:
|
|
553
|
-
found_title =
|
|
613
|
+
found_title = (
|
|
614
|
+
result.get("primaryTitle") if is_api else result.get("titleNameText")
|
|
615
|
+
)
|
|
554
616
|
found_id = result.get("id")
|
|
555
617
|
|
|
556
618
|
if is_api:
|
|
557
619
|
found_type = result.get("type")
|
|
558
|
-
if ttype_api == "TV_SERIES" and found_type not in [
|
|
559
|
-
|
|
620
|
+
if ttype_api == "TV_SERIES" and found_type not in [
|
|
621
|
+
"tvSeries",
|
|
622
|
+
"tvMiniSeries",
|
|
623
|
+
]:
|
|
624
|
+
continue
|
|
625
|
+
if ttype_api == "MOVIE" and found_type not in ["movie", "tvMovie"]:
|
|
626
|
+
continue
|
|
560
627
|
|
|
561
628
|
if shared_state.search_string_in_sanitized_title(title, found_title):
|
|
562
629
|
return found_id
|
|
563
630
|
|
|
564
631
|
for result in results:
|
|
565
|
-
found_title =
|
|
632
|
+
found_title = (
|
|
633
|
+
result.get("primaryTitle") if is_api else result.get("titleNameText")
|
|
634
|
+
)
|
|
566
635
|
found_id = result.get("id")
|
|
567
636
|
if shared_state.search_string_in_sanitized_title(title, found_title):
|
|
568
637
|
return found_id
|