quasarr 2.3.0__py3-none-any.whl → 2.3.2__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 +24 -6
- quasarr/api/config/__init__.py +2 -2
- quasarr/providers/imdb_metadata.py +420 -198
- quasarr/providers/version.py +1 -1
- quasarr/search/sources/he.py +6 -5
- quasarr/search/sources/nk.py +6 -5
- {quasarr-2.3.0.dist-info → quasarr-2.3.2.dist-info}/METADATA +1 -1
- {quasarr-2.3.0.dist-info → quasarr-2.3.2.dist-info}/RECORD +12 -12
- {quasarr-2.3.0.dist-info → quasarr-2.3.2.dist-info}/WHEEL +0 -0
- {quasarr-2.3.0.dist-info → quasarr-2.3.2.dist-info}/entry_points.txt +0 -0
- {quasarr-2.3.0.dist-info → quasarr-2.3.2.dist-info}/licenses/LICENSE +0 -0
- {quasarr-2.3.0.dist-info → quasarr-2.3.2.dist-info}/top_level.txt +0 -0
quasarr/__init__.py
CHANGED
|
@@ -169,6 +169,14 @@ def run():
|
|
|
169
169
|
else:
|
|
170
170
|
hostname_credentials_config(shared_state, site.upper(), hostname)
|
|
171
171
|
|
|
172
|
+
# Check FlareSolverr configuration
|
|
173
|
+
skip_flaresolverr_db = DataBase("skip_flaresolverr")
|
|
174
|
+
flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
|
|
175
|
+
flaresolverr_url = Config('FlareSolverr').get('url')
|
|
176
|
+
|
|
177
|
+
if not flaresolverr_url and not flaresolverr_skipped:
|
|
178
|
+
flaresolverr_config(shared_state)
|
|
179
|
+
|
|
172
180
|
config = Config('JDownloader')
|
|
173
181
|
user = config.get('user')
|
|
174
182
|
password = config.get('password')
|
|
@@ -249,23 +257,33 @@ def flaresolverr_checker(shared_state_dict, shared_state_lock):
|
|
|
249
257
|
flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
|
|
250
258
|
|
|
251
259
|
flaresolverr_url = Config('FlareSolverr').get('url')
|
|
260
|
+
|
|
261
|
+
# If FlareSolverr is not configured and not skipped, it means it's the first run
|
|
262
|
+
# and the user needs to be prompted via the WebUI.
|
|
263
|
+
# This background process should NOT block or prompt the user.
|
|
264
|
+
# It should only check and log the status.
|
|
252
265
|
if not flaresolverr_url and not flaresolverr_skipped:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
flaresolverr_url = Config('FlareSolverr').get('url')
|
|
266
|
+
info('FlareSolverr URL not configured. Please configure it via the WebUI.')
|
|
267
|
+
info('Some sites (AL) will not work without FlareSolverr.')
|
|
268
|
+
return # Exit the checker, it will be re-checked if user configures it later
|
|
257
269
|
|
|
258
270
|
if flaresolverr_skipped:
|
|
259
271
|
info('FlareSolverr setup skipped by user preference')
|
|
260
272
|
info('Some sites (AL) will not work without FlareSolverr. Configure it later in the web UI.')
|
|
261
273
|
elif flaresolverr_url:
|
|
262
|
-
|
|
274
|
+
info(f'Checking FlareSolverr at URL: "{flaresolverr_url}"')
|
|
263
275
|
flaresolverr_check = check_flaresolverr(shared_state, flaresolverr_url)
|
|
264
276
|
if flaresolverr_check:
|
|
265
|
-
|
|
277
|
+
info(f'FlareSolverr connection successful. Using User-Agent: "{shared_state.values["user_agent"]}"')
|
|
278
|
+
else:
|
|
279
|
+
info('FlareSolverr check failed - using fallback user agent')
|
|
280
|
+
# Fallback user agent is already set in main process, but we log it
|
|
281
|
+
info(f'User Agent (fallback): "{FALLBACK_USER_AGENT}"')
|
|
266
282
|
|
|
267
283
|
except KeyboardInterrupt:
|
|
268
284
|
pass
|
|
285
|
+
except Exception as e:
|
|
286
|
+
info(f"An unexpected error occurred in FlareSolverr checker: {e}")
|
|
269
287
|
|
|
270
288
|
|
|
271
289
|
def update_checker(shared_state_dict, shared_state_lock):
|
quasarr/api/config/__init__.py
CHANGED
|
@@ -288,8 +288,8 @@ def setup_config(app, shared_state):
|
|
|
288
288
|
"FlareSolverr URL saved successfully! A restart is recommended.")
|
|
289
289
|
else:
|
|
290
290
|
return render_fail(f"FlareSolverr returned unexpected status: {json_data.get('status')}")
|
|
291
|
-
except requests.RequestException
|
|
292
|
-
return render_fail(f"Could not reach FlareSolverr
|
|
291
|
+
except requests.RequestException:
|
|
292
|
+
return render_fail(f"Could not reach FlareSolverr!")
|
|
293
293
|
|
|
294
294
|
return render_fail("Could not reach FlareSolverr at that URL (expected HTTP 200).")
|
|
295
295
|
|
|
@@ -20,8 +20,57 @@ def _get_db(table_name):
|
|
|
20
20
|
return DataBase(table_name)
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _get_config(section):
|
|
24
|
+
"""Lazy import to avoid circular dependency."""
|
|
25
|
+
from quasarr.storage.config import Config
|
|
26
|
+
return Config(section)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TitleCleaner:
|
|
30
|
+
@staticmethod
|
|
31
|
+
def sanitize(title):
|
|
32
|
+
if not title:
|
|
33
|
+
return ""
|
|
34
|
+
sanitized_title = html.unescape(title)
|
|
35
|
+
sanitized_title = re.sub(r"[^a-zA-Z0-9äöüÄÖÜß&-']", ' ', sanitized_title).strip()
|
|
36
|
+
sanitized_title = sanitized_title.replace(" - ", "-")
|
|
37
|
+
sanitized_title = re.sub(r'\s{2,}', ' ', sanitized_title)
|
|
38
|
+
return sanitized_title
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def clean(title):
|
|
42
|
+
try:
|
|
43
|
+
# Regex to find the title part before common release tags
|
|
44
|
+
pattern = r"(.*?)(?:[\.\s](?!19|20)\d{2}|[\.\s]German|[\.\s]GERMAN|[\.\s]\d{3,4}p|[\.\s]S(?:\d{1,3}))"
|
|
45
|
+
match = re.search(pattern, title)
|
|
46
|
+
if match:
|
|
47
|
+
extracted_title = match.group(1)
|
|
48
|
+
else:
|
|
49
|
+
extracted_title = title
|
|
50
|
+
|
|
51
|
+
tags_to_remove = [
|
|
52
|
+
r'[\.\s]UNRATED.*', r'[\.\s]Unrated.*', r'[\.\s]Uncut.*', r'[\.\s]UNCUT.*',
|
|
53
|
+
r'[\.\s]Directors[\.\s]Cut.*', r'[\.\s]Final[\.\s]Cut.*', r'[\.\s]DC.*',
|
|
54
|
+
r'[\.\s]REMASTERED.*', r'[\.\s]EXTENDED.*', r'[\.\s]Extended.*',
|
|
55
|
+
r'[\.\s]Theatrical.*', r'[\.\s]THEATRICAL.*'
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
clean_title = extracted_title
|
|
59
|
+
for tag in tags_to_remove:
|
|
60
|
+
clean_title = re.sub(tag, "", clean_title, flags=re.IGNORECASE)
|
|
61
|
+
|
|
62
|
+
clean_title = clean_title.replace(".", " ").strip()
|
|
63
|
+
clean_title = re.sub(r'\s+', ' ', clean_title)
|
|
64
|
+
clean_title = clean_title.replace(" ", "+")
|
|
65
|
+
|
|
66
|
+
return clean_title
|
|
67
|
+
except Exception as e:
|
|
68
|
+
debug(f"Error cleaning title '{title}': {e}")
|
|
69
|
+
return title
|
|
70
|
+
|
|
71
|
+
|
|
23
72
|
class IMDbAPI:
|
|
24
|
-
"""
|
|
73
|
+
"""Tier 1: api.imdbapi.dev - Primary, fast, comprehensive."""
|
|
25
74
|
BASE_URL = "https://api.imdbapi.dev"
|
|
26
75
|
|
|
27
76
|
@staticmethod
|
|
@@ -31,7 +80,7 @@ class IMDbAPI:
|
|
|
31
80
|
response.raise_for_status()
|
|
32
81
|
return response.json()
|
|
33
82
|
except Exception as e:
|
|
34
|
-
info(f"
|
|
83
|
+
info(f"IMDbAPI get_title failed for {imdb_id}: {e}")
|
|
35
84
|
return None
|
|
36
85
|
|
|
37
86
|
@staticmethod
|
|
@@ -41,7 +90,7 @@ class IMDbAPI:
|
|
|
41
90
|
response.raise_for_status()
|
|
42
91
|
return response.json().get("akas", [])
|
|
43
92
|
except Exception as e:
|
|
44
|
-
info(f"
|
|
93
|
+
info(f"IMDbAPI get_akas failed for {imdb_id}: {e}")
|
|
45
94
|
return []
|
|
46
95
|
|
|
47
96
|
@staticmethod
|
|
@@ -51,157 +100,348 @@ class IMDbAPI:
|
|
|
51
100
|
response.raise_for_status()
|
|
52
101
|
return response.json().get("titles", [])
|
|
53
102
|
except Exception as e:
|
|
54
|
-
debug(f"
|
|
103
|
+
debug(f"IMDbAPI search_titles failed: {e}")
|
|
55
104
|
return []
|
|
56
105
|
|
|
57
106
|
|
|
58
|
-
class
|
|
59
|
-
"""
|
|
60
|
-
|
|
107
|
+
class IMDbCDN:
|
|
108
|
+
"""Tier 2: v2.sg.media-imdb.com - Fast fallback for English data."""
|
|
109
|
+
CDN_URL = "https://v2.sg.media-imdb.com/suggestion"
|
|
61
110
|
|
|
62
111
|
@staticmethod
|
|
63
|
-
def
|
|
64
|
-
headers = {'User-Agent': user_agent}
|
|
112
|
+
def _get_cdn_data(imdb_id, language, user_agent):
|
|
65
113
|
try:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
poster_set = soup.find('div', class_='ipc-poster').div.img["srcset"]
|
|
69
|
-
poster_links = [x for x in poster_set.split(" ") if len(x) > 10]
|
|
70
|
-
return poster_links[-1]
|
|
71
|
-
except Exception as e:
|
|
72
|
-
debug(f"Could not get poster title for {imdb_id} from IMDb: {e}")
|
|
73
|
-
return None
|
|
114
|
+
if not imdb_id or len(imdb_id) < 2:
|
|
115
|
+
return None
|
|
74
116
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
117
|
+
headers = {
|
|
118
|
+
'Accept-Language': f'{language},en;q=0.9',
|
|
119
|
+
'User-Agent': user_agent,
|
|
120
|
+
'Accept': 'application/json'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
first_char = imdb_id[0].lower()
|
|
124
|
+
url = f"{IMDbCDN.CDN_URL}/{first_char}/{imdb_id}.json"
|
|
125
|
+
|
|
126
|
+
response = requests.get(url, headers=headers, timeout=5)
|
|
83
127
|
response.raise_for_status()
|
|
84
128
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
129
|
+
data = response.json()
|
|
130
|
+
|
|
131
|
+
if "d" in data and len(data["d"]) > 0:
|
|
132
|
+
for entry in data["d"]:
|
|
133
|
+
if entry.get("id") == imdb_id:
|
|
134
|
+
return entry
|
|
135
|
+
return data["d"][0]
|
|
88
136
|
|
|
89
|
-
if match:
|
|
90
|
-
return match.group(1)
|
|
91
137
|
except Exception as e:
|
|
92
|
-
|
|
138
|
+
debug(f"IMDbCDN request failed for {imdb_id}: {e}")
|
|
139
|
+
|
|
140
|
+
return None
|
|
93
141
|
|
|
142
|
+
@staticmethod
|
|
143
|
+
def get_poster(imdb_id, user_agent):
|
|
144
|
+
data = IMDbCDN._get_cdn_data(imdb_id, 'en', user_agent)
|
|
145
|
+
if data:
|
|
146
|
+
image_node = data.get("i")
|
|
147
|
+
if image_node and "imageUrl" in image_node:
|
|
148
|
+
return image_node["imageUrl"]
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def get_title(imdb_id, user_agent):
|
|
153
|
+
"""Returns the English title from CDN."""
|
|
154
|
+
data = IMDbCDN._get_cdn_data(imdb_id, 'en', user_agent)
|
|
155
|
+
if data and "l" in data:
|
|
156
|
+
return data["l"]
|
|
94
157
|
return None
|
|
95
158
|
|
|
96
159
|
@staticmethod
|
|
97
160
|
def search_titles(query, ttype, language, user_agent):
|
|
98
|
-
headers = {
|
|
99
|
-
'Accept-Language': language,
|
|
100
|
-
'User-Agent': user_agent
|
|
101
|
-
}
|
|
102
161
|
try:
|
|
103
|
-
|
|
104
|
-
|
|
162
|
+
clean_query = quote(query.lower().replace(" ", "_"))
|
|
163
|
+
if not clean_query: return []
|
|
164
|
+
|
|
165
|
+
headers = {
|
|
166
|
+
'Accept-Language': f'{language},en;q=0.9',
|
|
167
|
+
'User-Agent': user_agent
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
first_char = clean_query[0]
|
|
171
|
+
url = f"{IMDbCDN.CDN_URL}/{first_char}/{clean_query}.json"
|
|
172
|
+
|
|
173
|
+
response = requests.get(url, headers=headers, timeout=5)
|
|
174
|
+
|
|
175
|
+
if response.status_code == 200:
|
|
176
|
+
data = response.json()
|
|
177
|
+
results = []
|
|
178
|
+
if "d" in data:
|
|
179
|
+
for item in data["d"]:
|
|
180
|
+
results.append({
|
|
181
|
+
'id': item.get('id'),
|
|
182
|
+
'titleNameText': item.get('l'),
|
|
183
|
+
'titleReleaseText': item.get('y')
|
|
184
|
+
})
|
|
185
|
+
return results
|
|
105
186
|
|
|
106
|
-
if results.status_code == 200:
|
|
107
|
-
soup = BeautifulSoup(results.text, "html.parser")
|
|
108
|
-
props = soup.find("script", text=re.compile("props"))
|
|
109
|
-
if props:
|
|
110
|
-
details = loads(props.string)
|
|
111
|
-
return details['props']['pageProps']['titleResults']['results']
|
|
112
|
-
else:
|
|
113
|
-
debug(f"Request on IMDb failed: {results.status_code}")
|
|
114
187
|
except Exception as e:
|
|
115
|
-
|
|
188
|
+
from quasarr.providers.log import debug
|
|
189
|
+
debug(f"IMDb CDN search failed: {e}")
|
|
116
190
|
|
|
117
191
|
return []
|
|
118
192
|
|
|
119
193
|
|
|
120
|
-
class
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if not title:
|
|
124
|
-
return ""
|
|
125
|
-
sanitized_title = html.unescape(title)
|
|
126
|
-
sanitized_title = re.sub(r"[^a-zA-Z0-9äöüÄÖÜß&-']", ' ', sanitized_title).strip()
|
|
127
|
-
sanitized_title = sanitized_title.replace(" - ", "-")
|
|
128
|
-
sanitized_title = re.sub(r'\s{2,}', ' ', sanitized_title)
|
|
129
|
-
return sanitized_title
|
|
194
|
+
class IMDbFlareSolverr:
|
|
195
|
+
"""Tier 3: FlareSolverr - Robust fallback using browser automation."""
|
|
196
|
+
WEB_URL = "https://www.imdb.com"
|
|
130
197
|
|
|
131
198
|
@staticmethod
|
|
132
|
-
def
|
|
199
|
+
def _request(url):
|
|
200
|
+
flaresolverr_url = _get_config('FlareSolverr').get('url')
|
|
201
|
+
flaresolverr_skipped = _get_db("skip_flaresolverr").retrieve("skipped")
|
|
202
|
+
|
|
203
|
+
if not flaresolverr_url or flaresolverr_skipped:
|
|
204
|
+
return None
|
|
205
|
+
|
|
133
206
|
try:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
207
|
+
post_data = {
|
|
208
|
+
"cmd": "request.get",
|
|
209
|
+
"url": url,
|
|
210
|
+
"maxTimeout": 60000,
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
response = requests.post(flaresolverr_url, json=post_data, headers={"Content-Type": "application/json"},
|
|
214
|
+
timeout=60)
|
|
215
|
+
if response.status_code == 200:
|
|
216
|
+
json_response = response.json()
|
|
217
|
+
if json_response.get("status") == "ok":
|
|
218
|
+
return json_response.get("solution", {}).get("response", "")
|
|
219
|
+
except Exception as e:
|
|
220
|
+
debug(f"FlareSolverr request failed for {url}: {e}")
|
|
146
221
|
|
|
147
|
-
|
|
148
|
-
tags_to_remove = [
|
|
149
|
-
r'[\.\s]UNRATED.*', r'[\.\s]Unrated.*', r'[\.\s]Uncut.*', r'[\.\s]UNCUT.*',
|
|
150
|
-
r'[\.\s]Directors[\.\s]Cut.*', r'[\.\s]Final[\.\s]Cut.*', r'[\.\s]DC.*',
|
|
151
|
-
r'[\.\s]REMASTERED.*', r'[\.\s]EXTENDED.*', r'[\.\s]Extended.*',
|
|
152
|
-
r'[\.\s]Theatrical.*', r'[\.\s]THEATRICAL.*'
|
|
153
|
-
]
|
|
222
|
+
return None
|
|
154
223
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
224
|
+
@staticmethod
|
|
225
|
+
def get_poster(imdb_id):
|
|
226
|
+
html_content = IMDbFlareSolverr._request(f"{IMDbFlareSolverr.WEB_URL}/title/{imdb_id}/")
|
|
227
|
+
if html_content:
|
|
228
|
+
try:
|
|
229
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
230
|
+
poster_div = soup.find('div', class_='ipc-poster')
|
|
231
|
+
if poster_div and poster_div.div and poster_div.div.img:
|
|
232
|
+
poster_set = poster_div.div.img.get("srcset")
|
|
233
|
+
if poster_set:
|
|
234
|
+
poster_links = [x for x in poster_set.split(" ") if len(x) > 10]
|
|
235
|
+
return poster_links[-1]
|
|
236
|
+
except Exception as e:
|
|
237
|
+
debug(f"FlareSolverr poster parsing failed: {e}")
|
|
238
|
+
return None
|
|
158
239
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
240
|
+
@staticmethod
|
|
241
|
+
def get_localized_title(imdb_id, language):
|
|
242
|
+
# FlareSolverr doesn't reliably support headers for localization.
|
|
243
|
+
# Instead, we scrape the release info page which lists AKAs.
|
|
244
|
+
url = f"{IMDbFlareSolverr.WEB_URL}/title/{imdb_id}/releaseinfo"
|
|
245
|
+
html_content = IMDbFlareSolverr._request(url)
|
|
246
|
+
|
|
247
|
+
if html_content:
|
|
248
|
+
try:
|
|
249
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
250
|
+
|
|
251
|
+
# Map language codes to country names commonly used in IMDb AKAs
|
|
252
|
+
country_map = {
|
|
253
|
+
'de': ['Germany', 'Austria', 'Switzerland', 'West Germany'],
|
|
254
|
+
'fr': ['France', 'Canada', 'Belgium'],
|
|
255
|
+
'es': ['Spain', 'Mexico', 'Argentina'],
|
|
256
|
+
'it': ['Italy'],
|
|
257
|
+
'pt': ['Portugal', 'Brazil'],
|
|
258
|
+
'ru': ['Russia', 'Soviet Union'],
|
|
259
|
+
'ja': ['Japan'],
|
|
260
|
+
'hi': ['India']
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
target_countries = country_map.get(language, [])
|
|
264
|
+
|
|
265
|
+
# Find the AKAs list
|
|
266
|
+
# The structure is a list of items with country names and titles
|
|
267
|
+
items = soup.find_all("li", class_="ipc-metadata-list__item")
|
|
268
|
+
|
|
269
|
+
for item in items:
|
|
270
|
+
label_span = item.find("span", class_="ipc-metadata-list-item__label")
|
|
271
|
+
if not label_span:
|
|
272
|
+
# Sometimes it's an anchor if it's a link
|
|
273
|
+
label_span = item.find("a", class_="ipc-metadata-list-item__label")
|
|
274
|
+
|
|
275
|
+
if label_span:
|
|
276
|
+
country = label_span.get_text(strip=True)
|
|
277
|
+
# Check if this country matches our target language
|
|
278
|
+
if any(c in country for c in target_countries):
|
|
279
|
+
# Found a matching country, get the title
|
|
280
|
+
title_span = item.find("span", class_="ipc-metadata-list-item__list-content-item")
|
|
281
|
+
if title_span:
|
|
282
|
+
return title_span.get_text(strip=True)
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
debug(f"FlareSolverr localized title parsing failed: {e}")
|
|
162
286
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def search_titles(query, ttype):
|
|
291
|
+
url = f"{IMDbFlareSolverr.WEB_URL}/find/?q={quote(query)}&s=tt&ttype={ttype}&ref_=fn_{ttype}"
|
|
292
|
+
html_content = IMDbFlareSolverr._request(url)
|
|
293
|
+
|
|
294
|
+
if html_content:
|
|
295
|
+
try:
|
|
296
|
+
soup = BeautifulSoup(html_content, "html.parser")
|
|
297
|
+
props = soup.find("script", text=re.compile("props"))
|
|
298
|
+
if props:
|
|
299
|
+
details = loads(props.string)
|
|
300
|
+
results = details['props']['pageProps']['titleResults']['results']
|
|
301
|
+
mapped_results = []
|
|
302
|
+
for result in results:
|
|
303
|
+
try:
|
|
304
|
+
mapped_results.append({
|
|
305
|
+
'id': result["listItem"]["titleId"],
|
|
306
|
+
'titleNameText': result["listItem"]["titleText"],
|
|
307
|
+
'titleReleaseText': result["listItem"].get("releaseYear")
|
|
308
|
+
})
|
|
309
|
+
except KeyError:
|
|
310
|
+
mapped_results.append({
|
|
311
|
+
'id': result.get('id'),
|
|
312
|
+
'titleNameText': result.get("titleNameText"),
|
|
313
|
+
'titleReleaseText': result.get("titleReleaseText")
|
|
314
|
+
})
|
|
315
|
+
return mapped_results
|
|
316
|
+
|
|
317
|
+
results = []
|
|
318
|
+
items = soup.find_all("li", class_="ipc-metadata-list-summary-item")
|
|
319
|
+
for item in items:
|
|
320
|
+
a_tag = item.find("a", class_="ipc-metadata-list-summary-item__t")
|
|
321
|
+
if a_tag:
|
|
322
|
+
href = a_tag.get("href", "")
|
|
323
|
+
id_match = re.search(r"(tt\d+)", href)
|
|
324
|
+
if id_match:
|
|
325
|
+
results.append({
|
|
326
|
+
'id': id_match.group(1),
|
|
327
|
+
'titleNameText': a_tag.get_text(strip=True),
|
|
328
|
+
'titleReleaseText': ""
|
|
329
|
+
})
|
|
330
|
+
return results
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
333
|
+
debug(f"FlareSolverr search parsing failed: {e}")
|
|
334
|
+
return []
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# =============================================================================
|
|
338
|
+
# Main Functions (Chain of Responsibility)
|
|
339
|
+
# =============================================================================
|
|
340
|
+
|
|
341
|
+
def _update_cache(imdb_id, key, value, language=None):
|
|
342
|
+
db = _get_db("imdb_metadata")
|
|
343
|
+
try:
|
|
344
|
+
cached_data = db.retrieve(imdb_id)
|
|
345
|
+
if cached_data:
|
|
346
|
+
metadata = loads(cached_data)
|
|
347
|
+
else:
|
|
348
|
+
metadata = {
|
|
349
|
+
"title": None,
|
|
350
|
+
"year": None,
|
|
351
|
+
"poster_link": None,
|
|
352
|
+
"localized": {},
|
|
353
|
+
"ttl": 0
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if key == "localized" and language:
|
|
357
|
+
if "localized" not in metadata or not isinstance(metadata["localized"], dict):
|
|
358
|
+
metadata["localized"] = {}
|
|
359
|
+
metadata["localized"][language] = value
|
|
360
|
+
else:
|
|
361
|
+
metadata[key] = value
|
|
362
|
+
|
|
363
|
+
now = datetime.now().timestamp()
|
|
364
|
+
days = 7 if metadata.get("title") and metadata.get("year") else 1
|
|
365
|
+
metadata["ttl"] = now + timedelta(days=days).total_seconds()
|
|
366
|
+
|
|
367
|
+
db.update_store(imdb_id, dumps(metadata))
|
|
368
|
+
except Exception as e:
|
|
369
|
+
debug(f"Error updating IMDb metadata cache for {imdb_id}: {e}")
|
|
167
370
|
|
|
168
371
|
|
|
169
372
|
def get_poster_link(shared_state, imdb_id):
|
|
373
|
+
# 0. Check Cache (via get_imdb_metadata)
|
|
170
374
|
imdb_metadata = get_imdb_metadata(imdb_id)
|
|
171
|
-
if imdb_metadata:
|
|
172
|
-
|
|
173
|
-
if poster_link:
|
|
174
|
-
return poster_link
|
|
375
|
+
if imdb_metadata and imdb_metadata.get("poster_link"):
|
|
376
|
+
return imdb_metadata.get("poster_link")
|
|
175
377
|
|
|
176
|
-
|
|
177
|
-
if imdb_id:
|
|
178
|
-
poster_link = IMDbWeb.get_poster(imdb_id, shared_state.values["user_agent"])
|
|
378
|
+
user_agent = shared_state.values["user_agent"]
|
|
179
379
|
|
|
180
|
-
|
|
181
|
-
|
|
380
|
+
poster = IMDbCDN.get_poster(imdb_id, user_agent)
|
|
381
|
+
if poster:
|
|
382
|
+
_update_cache(imdb_id, "poster_link", poster)
|
|
383
|
+
return poster
|
|
182
384
|
|
|
183
|
-
|
|
385
|
+
poster = IMDbFlareSolverr.get_poster(imdb_id)
|
|
386
|
+
if poster:
|
|
387
|
+
_update_cache(imdb_id, "poster_link", poster)
|
|
388
|
+
return poster
|
|
389
|
+
|
|
390
|
+
debug(f"Could not get poster title for {imdb_id}")
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def get_localized_title(shared_state, imdb_id, language='de'):
|
|
395
|
+
# 0. Check Cache (via get_imdb_metadata)
|
|
396
|
+
imdb_metadata = get_imdb_metadata(imdb_id)
|
|
397
|
+
if imdb_metadata:
|
|
398
|
+
localized = imdb_metadata.get("localized", {}).get(language)
|
|
399
|
+
if localized: return localized
|
|
400
|
+
if language == 'en' and imdb_metadata.get("title"):
|
|
401
|
+
return imdb_metadata.get("title")
|
|
402
|
+
|
|
403
|
+
user_agent = shared_state.values["user_agent"]
|
|
404
|
+
|
|
405
|
+
if language == 'en':
|
|
406
|
+
title = IMDbCDN.get_title(imdb_id, user_agent)
|
|
407
|
+
if title:
|
|
408
|
+
sanitized_title = TitleCleaner.sanitize(title)
|
|
409
|
+
_update_cache(imdb_id, "title", sanitized_title)
|
|
410
|
+
return sanitized_title
|
|
411
|
+
|
|
412
|
+
title = IMDbFlareSolverr.get_localized_title(imdb_id, language)
|
|
413
|
+
if title:
|
|
414
|
+
sanitized_title = TitleCleaner.sanitize(title)
|
|
415
|
+
_update_cache(imdb_id, "localized", sanitized_title, language)
|
|
416
|
+
return sanitized_title
|
|
417
|
+
|
|
418
|
+
# Final fallback: Try CDN for English title if localization failed
|
|
419
|
+
title = IMDbCDN.get_title(imdb_id, user_agent)
|
|
420
|
+
if title:
|
|
421
|
+
sanitized_title = TitleCleaner.sanitize(title)
|
|
422
|
+
_update_cache(imdb_id, "title", sanitized_title)
|
|
423
|
+
return sanitized_title
|
|
424
|
+
|
|
425
|
+
debug(f"Could not get localized title for {imdb_id} in {language}")
|
|
426
|
+
return None
|
|
184
427
|
|
|
185
428
|
|
|
186
429
|
def get_imdb_metadata(imdb_id):
|
|
187
430
|
db = _get_db("imdb_metadata")
|
|
188
431
|
now = datetime.now().timestamp()
|
|
189
|
-
|
|
190
|
-
# Try to load from DB
|
|
191
432
|
cached_metadata = None
|
|
433
|
+
|
|
434
|
+
# 0. Check Cache
|
|
192
435
|
try:
|
|
193
436
|
cached_data = db.retrieve(imdb_id)
|
|
194
437
|
if cached_data:
|
|
195
438
|
cached_metadata = loads(cached_data)
|
|
196
|
-
# If valid, update TTL and return
|
|
197
439
|
if cached_metadata.get("ttl") and cached_metadata["ttl"] > now:
|
|
198
|
-
cached_metadata["ttl"] = now + timedelta(days=30).total_seconds()
|
|
199
|
-
db.update_store(imdb_id, dumps(cached_metadata))
|
|
200
440
|
return cached_metadata
|
|
201
441
|
except Exception as e:
|
|
202
442
|
debug(f"Error retrieving IMDb metadata from DB for {imdb_id}: {e}")
|
|
443
|
+
cached_metadata = None
|
|
203
444
|
|
|
204
|
-
# Initialize new metadata structure
|
|
205
445
|
imdb_metadata = {
|
|
206
446
|
"title": None,
|
|
207
447
|
"year": None,
|
|
@@ -210,66 +450,44 @@ def get_imdb_metadata(imdb_id):
|
|
|
210
450
|
"ttl": 0
|
|
211
451
|
}
|
|
212
452
|
|
|
213
|
-
#
|
|
453
|
+
# 1. Try API
|
|
214
454
|
response_json = IMDbAPI.get_title(imdb_id)
|
|
215
455
|
|
|
216
|
-
if
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
# Process API response
|
|
224
|
-
imdb_metadata["title"] = TitleCleaner.sanitize(response_json.get("primaryTitle", ""))
|
|
225
|
-
imdb_metadata["year"] = response_json.get("startYear")
|
|
226
|
-
imdb_metadata["ttl"] = now + timedelta(days=30).total_seconds()
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
imdb_metadata["poster_link"] = response_json.get("primaryImage").get("url")
|
|
230
|
-
except Exception as e:
|
|
231
|
-
debug(f"Could not find poster link for {imdb_id} from imdbapi.dev: {e}")
|
|
232
|
-
# Shorten TTL if data is incomplete
|
|
233
|
-
imdb_metadata["ttl"] = now + timedelta(days=1).total_seconds()
|
|
234
|
-
|
|
235
|
-
akas = IMDbAPI.get_akas(imdb_id)
|
|
236
|
-
if akas:
|
|
237
|
-
for aka in akas:
|
|
238
|
-
if aka.get("language"):
|
|
239
|
-
continue # skip entries with specific language tags
|
|
240
|
-
if aka.get("country", {}).get("code", "").lower() == "de":
|
|
241
|
-
imdb_metadata["localized"]["de"] = TitleCleaner.sanitize(aka.get("text"))
|
|
242
|
-
break
|
|
243
|
-
else:
|
|
244
|
-
# Shorten TTL if AKAs failed
|
|
245
|
-
imdb_metadata["ttl"] = now + timedelta(days=1).total_seconds()
|
|
456
|
+
if response_json:
|
|
457
|
+
imdb_metadata["title"] = TitleCleaner.sanitize(response_json.get("primaryTitle", ""))
|
|
458
|
+
imdb_metadata["year"] = response_json.get("startYear")
|
|
459
|
+
|
|
460
|
+
days = 7 if imdb_metadata.get("title") and imdb_metadata.get("year") else 1
|
|
461
|
+
imdb_metadata["ttl"] = now + timedelta(days=days).total_seconds()
|
|
246
462
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
463
|
+
try:
|
|
464
|
+
imdb_metadata["poster_link"] = response_json.get("primaryImage").get("url")
|
|
465
|
+
except:
|
|
466
|
+
pass
|
|
467
|
+
|
|
468
|
+
akas = IMDbAPI.get_akas(imdb_id)
|
|
469
|
+
if akas:
|
|
470
|
+
for aka in akas:
|
|
471
|
+
if aka.get("language"): continue
|
|
472
|
+
if aka.get("country", {}).get("code", "").lower() == "de":
|
|
473
|
+
imdb_metadata["localized"]["de"] = TitleCleaner.sanitize(aka.get("text"))
|
|
474
|
+
break
|
|
256
475
|
|
|
476
|
+
db.update_store(imdb_id, dumps(imdb_metadata))
|
|
477
|
+
return imdb_metadata
|
|
257
478
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
localized_title = imdb_metadata.get("localized").get(language)
|
|
262
|
-
if localized_title:
|
|
263
|
-
return localized_title
|
|
264
|
-
return imdb_metadata.get("title")
|
|
479
|
+
# API Failed. If we have stale cache, return it.
|
|
480
|
+
if cached_metadata:
|
|
481
|
+
return cached_metadata
|
|
265
482
|
|
|
266
|
-
|
|
483
|
+
# 2. Fallback: Try CDN for basic info (English title, Year, Poster)
|
|
484
|
+
# We can't get localized titles from CDN, but we can get the rest.
|
|
485
|
+
# 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,
|
|
487
|
+
# as get_poster_link and get_localized_title handle their own fallbacks.
|
|
488
|
+
# But to populate the DB, we could try. For now, return empty/partial if API fails.
|
|
267
489
|
|
|
268
|
-
|
|
269
|
-
debug(f"Could not get localized title for {imdb_id} in {language} from IMDb")
|
|
270
|
-
else:
|
|
271
|
-
localized_title = TitleCleaner.sanitize(localized_title)
|
|
272
|
-
return localized_title
|
|
490
|
+
return imdb_metadata
|
|
273
491
|
|
|
274
492
|
|
|
275
493
|
def get_imdb_id_from_title(shared_state, title, language="de"):
|
|
@@ -284,72 +502,76 @@ def get_imdb_id_from_title(shared_state, title, language="de"):
|
|
|
284
502
|
|
|
285
503
|
title = TitleCleaner.clean(title)
|
|
286
504
|
|
|
287
|
-
# Check Search Cache
|
|
505
|
+
# 0. Check Search Cache
|
|
288
506
|
db = _get_db("imdb_searches")
|
|
289
507
|
try:
|
|
290
508
|
cached_data = db.retrieve(title)
|
|
291
509
|
if cached_data:
|
|
292
510
|
data = loads(cached_data)
|
|
293
|
-
# Check TTL (48 hours)
|
|
294
511
|
if data.get("timestamp") and datetime.fromtimestamp(data["timestamp"]) > datetime.now() - timedelta(
|
|
295
512
|
hours=48):
|
|
296
513
|
return data.get("imdb_id")
|
|
297
|
-
except Exception
|
|
298
|
-
|
|
514
|
+
except Exception:
|
|
515
|
+
pass
|
|
299
516
|
|
|
300
|
-
|
|
517
|
+
user_agent = shared_state.values["user_agent"]
|
|
518
|
+
|
|
519
|
+
# 1. Try API
|
|
301
520
|
search_results = IMDbAPI.search_titles(title)
|
|
302
521
|
if search_results:
|
|
303
|
-
|
|
304
|
-
found_title = result.get("primaryTitle")
|
|
305
|
-
found_id = result.get("id")
|
|
306
|
-
found_type = result.get("type")
|
|
522
|
+
imdb_id = _match_result(shared_state, title, search_results, ttype_api, is_api=True)
|
|
307
523
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if shared_state.search_string_in_sanitized_title(title, found_title):
|
|
315
|
-
imdb_id = found_id
|
|
316
|
-
break
|
|
317
|
-
|
|
318
|
-
# If no exact match found with type filtering, try relaxed matching
|
|
319
|
-
if not imdb_id:
|
|
320
|
-
for result in search_results:
|
|
321
|
-
found_title = result.get("primaryTitle")
|
|
322
|
-
found_id = result.get("id")
|
|
323
|
-
if shared_state.search_string_in_sanitized_title(title, found_title):
|
|
324
|
-
imdb_id = found_id
|
|
325
|
-
break
|
|
524
|
+
# 2. Try CDN (Fallback)
|
|
525
|
+
if not imdb_id:
|
|
526
|
+
search_results = IMDbCDN.search_titles(title, ttype_web, language, user_agent)
|
|
527
|
+
if search_results:
|
|
528
|
+
imdb_id = _match_result(shared_state, title, search_results, ttype_api, is_api=False)
|
|
326
529
|
|
|
327
|
-
#
|
|
530
|
+
# 3. Try FlareSolverr (Last Resort)
|
|
328
531
|
if not imdb_id:
|
|
329
|
-
search_results =
|
|
532
|
+
search_results = IMDbFlareSolverr.search_titles(title, ttype_web)
|
|
330
533
|
if search_results:
|
|
331
|
-
|
|
332
|
-
try:
|
|
333
|
-
found_title = result["listItem"]["titleText"]
|
|
334
|
-
found_id = result["listItem"]["titleId"]
|
|
335
|
-
except KeyError:
|
|
336
|
-
found_title = result["titleNameText"]
|
|
337
|
-
found_id = result['id']
|
|
338
|
-
|
|
339
|
-
if shared_state.search_string_in_sanitized_title(title, found_title):
|
|
340
|
-
imdb_id = found_id
|
|
341
|
-
break
|
|
534
|
+
imdb_id = _match_result(shared_state, title, search_results, ttype_api, is_api=False)
|
|
342
535
|
|
|
343
|
-
# Update
|
|
536
|
+
# Update Cache
|
|
344
537
|
try:
|
|
345
538
|
db.update_store(title, dumps({
|
|
346
539
|
"imdb_id": imdb_id,
|
|
347
540
|
"timestamp": datetime.now().timestamp()
|
|
348
541
|
}))
|
|
349
|
-
except Exception
|
|
350
|
-
|
|
542
|
+
except Exception:
|
|
543
|
+
pass
|
|
351
544
|
|
|
352
545
|
if not imdb_id:
|
|
353
546
|
debug(f"No IMDb-ID found for {title}")
|
|
354
547
|
|
|
355
548
|
return imdb_id
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def _match_result(shared_state, title, results, ttype_api, is_api=False):
|
|
552
|
+
for result in results:
|
|
553
|
+
found_title = result.get("primaryTitle") if is_api else result.get("titleNameText")
|
|
554
|
+
found_id = result.get("id")
|
|
555
|
+
|
|
556
|
+
if is_api:
|
|
557
|
+
found_type = result.get("type")
|
|
558
|
+
if ttype_api == "TV_SERIES" and found_type not in ["tvSeries", "tvMiniSeries"]: continue
|
|
559
|
+
if ttype_api == "MOVIE" and found_type not in ["movie", "tvMovie"]: continue
|
|
560
|
+
|
|
561
|
+
if shared_state.search_string_in_sanitized_title(title, found_title):
|
|
562
|
+
return found_id
|
|
563
|
+
|
|
564
|
+
for result in results:
|
|
565
|
+
found_title = result.get("primaryTitle") if is_api else result.get("titleNameText")
|
|
566
|
+
found_id = result.get("id")
|
|
567
|
+
if shared_state.search_string_in_sanitized_title(title, found_title):
|
|
568
|
+
return found_id
|
|
569
|
+
|
|
570
|
+
return None
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def get_year(imdb_id):
|
|
574
|
+
imdb_metadata = get_imdb_metadata(imdb_id)
|
|
575
|
+
if imdb_metadata:
|
|
576
|
+
return imdb_metadata.get("year")
|
|
577
|
+
return None
|
quasarr/providers/version.py
CHANGED
quasarr/search/sources/he.py
CHANGED
|
@@ -84,9 +84,10 @@ def he_search(shared_state, start_time, request_from, search_string="", mirror=N
|
|
|
84
84
|
if not local_title:
|
|
85
85
|
info(f"{hostname}: no title for IMDb {imdb_id}")
|
|
86
86
|
return releases
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
if not season:
|
|
88
|
+
year = get_year(imdb_id)
|
|
89
|
+
if year:
|
|
90
|
+
local_title += f" {year}"
|
|
90
91
|
source_search = local_title
|
|
91
92
|
else:
|
|
92
93
|
return releases
|
|
@@ -104,8 +105,8 @@ def he_search(shared_state, start_time, request_from, search_string="", mirror=N
|
|
|
104
105
|
if season:
|
|
105
106
|
source_search += f" S{int(season):02d}"
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
if episode:
|
|
109
|
+
source_search += f"E{int(episode):02d}"
|
|
109
110
|
|
|
110
111
|
url = f'https://{host}/tag/{tag}/'
|
|
111
112
|
|
quasarr/search/sources/nk.py
CHANGED
|
@@ -75,9 +75,10 @@ def nk_search(shared_state, start_time, request_from, search_string="", mirror=N
|
|
|
75
75
|
if not local_title:
|
|
76
76
|
info(f"{hostname}: no title for IMDb {imdb_id}")
|
|
77
77
|
return releases
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
if not season:
|
|
79
|
+
year = get_year(imdb_id)
|
|
80
|
+
if year:
|
|
81
|
+
local_title += f" {year}"
|
|
81
82
|
source_search = local_title
|
|
82
83
|
else:
|
|
83
84
|
return releases
|
|
@@ -95,8 +96,8 @@ def nk_search(shared_state, start_time, request_from, search_string="", mirror=N
|
|
|
95
96
|
if season:
|
|
96
97
|
source_search += f" S{int(season):02d}"
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
if episode:
|
|
100
|
+
source_search += f"E{int(episode):02d}"
|
|
100
101
|
|
|
101
102
|
url = f'https://{host}/search'
|
|
102
103
|
headers = {"User-Agent": shared_state.values["user_agent"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
quasarr/__init__.py,sha256=
|
|
1
|
+
quasarr/__init__.py,sha256=QRu_dlfLdToYkeO96bHaA8Kp0GmSL0IZc9ceRSxKWS0,15766
|
|
2
2
|
quasarr/api/__init__.py,sha256=KLnFSe5l3MrVgrbu6-7GlE2PqouVyizqiRZfQkBtge0,19587
|
|
3
3
|
quasarr/api/arr/__init__.py,sha256=eEop8A5t936uT5azn4qz0bq1DMX84_Ja16wyleGFhyM,18495
|
|
4
4
|
quasarr/api/captcha/__init__.py,sha256=Mqg2HhWMaUc07cVaEYHAbf-YvnxkiYVbkWT-g92J-2k,72960
|
|
5
|
-
quasarr/api/config/__init__.py,sha256=
|
|
5
|
+
quasarr/api/config/__init__.py,sha256=q-7vK5YULrSDgTicho--bNK8aAhcbzCdhhNwEwUEwWg,14173
|
|
6
6
|
quasarr/api/packages/__init__.py,sha256=ox0vzuXByag49RUEwYPWtMacsXl_iksvubHgDmG5RWQ,25192
|
|
7
7
|
quasarr/api/sponsors_helper/__init__.py,sha256=vZIFGkc5HTRozjvi47tqxz6XpwDe8sDXVyeydc9k0Y0,6708
|
|
8
8
|
quasarr/api/statistics/__init__.py,sha256=0Os2rbqQ8ZN3R0XAavGVHlacKsAjp7GYjEIJCwvnsl8,7063
|
|
@@ -35,7 +35,7 @@ quasarr/providers/cloudflare.py,sha256=oUDR7OQ8E-8vCtagZLnIS2ZZV3ERffhxmW0njKKbt
|
|
|
35
35
|
quasarr/providers/hostname_issues.py,sha256=9PJFIosLB-bMTmgWlR5-sYAmcyps7TDoSYjoL9cw9TE,1460
|
|
36
36
|
quasarr/providers/html_images.py,sha256=rrovPNl-FTTKKA-4HCPEhsYpq5b20VDrsB7t4RrQf3w,15531
|
|
37
37
|
quasarr/providers/html_templates.py,sha256=IGWwt78bP2oJx4VzOP6w9zp7KVXgDY6Qz5ySL9cLGWI,15815
|
|
38
|
-
quasarr/providers/imdb_metadata.py,sha256=
|
|
38
|
+
quasarr/providers/imdb_metadata.py,sha256=a_kn9lw5cj5ZbxtrRBQKyF78ctMgHJJTW0DF2DONWOY,20771
|
|
39
39
|
quasarr/providers/jd_cache.py,sha256=mSvMrs3UwTn3sd9yGSJKGT-qwYeyYKC_l8whpXTVn7s,13530
|
|
40
40
|
quasarr/providers/log.py,sha256=_g5RwtfuksARXnvryhsngzoJyFcNzj6suqd3ndqZM0Y,313
|
|
41
41
|
quasarr/providers/myjd_api.py,sha256=Z3PEiO3c3UfDSr4Up5rgwTAnjloWHb-H1RkJ6BLKZv8,34140
|
|
@@ -44,7 +44,7 @@ quasarr/providers/obfuscated.py,sha256=EYm_7SfdJd9ae_m4HZgY9ruDXC5J9hb4KEV_WAnk-
|
|
|
44
44
|
quasarr/providers/shared_state.py,sha256=5a_ZbGqTvt4-OqBt2a1WtR9I5J_Ky7IlkEY8EGtKVu8,30646
|
|
45
45
|
quasarr/providers/statistics.py,sha256=cEQixYnDMDqtm5wWe40E_2ucyo4mD0n3SrfelhQi1L8,6452
|
|
46
46
|
quasarr/providers/utils.py,sha256=mcUPbcXMsLmrYv0CTZO5a9aOt2-JLyL3SZxu6N8OyjU,12075
|
|
47
|
-
quasarr/providers/version.py,sha256=
|
|
47
|
+
quasarr/providers/version.py,sha256=SOiiYu52mdnbNCFK5iCGtzvQ0XMh6AbCbN3NFCotiF8,4003
|
|
48
48
|
quasarr/providers/web_server.py,sha256=AYd0KRxdDWMBr87BP8wlSMuL4zZo0I_rY-vHBai6Pfg,1688
|
|
49
49
|
quasarr/providers/sessions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
50
|
quasarr/providers/sessions/al.py,sha256=AQ59vVU7uQSuwZLNppNsZAFvpow3zcxQ29dirPbyYc4,13432
|
|
@@ -61,9 +61,9 @@ quasarr/search/sources/dl.py,sha256=L4GK58Mp46dAZzmwtMB4ia1w0SSpp3z3eFvrmT-5278,
|
|
|
61
61
|
quasarr/search/sources/dt.py,sha256=hvOqPKQRw5joSaTb9mpdPZXL4xpU167SFmLg8yhsPwM,10227
|
|
62
62
|
quasarr/search/sources/dw.py,sha256=hna1ueKjdi9uqRQJ7UPenT0ym7igQgWGrv_--yGChVs,8215
|
|
63
63
|
quasarr/search/sources/fx.py,sha256=xZUrv7dJSSmeLR2xnRQsRZAk9Q0-fDfQLNjz4wdBTqo,9452
|
|
64
|
-
quasarr/search/sources/he.py,sha256=
|
|
64
|
+
quasarr/search/sources/he.py,sha256=LZM5JquDdocTpqRUS7ObYEwEGo5pyJWOvZ91GCp7YJ8,7378
|
|
65
65
|
quasarr/search/sources/mb.py,sha256=Hq1zupo27FzYSQUio03HPG0wP4jYwOXl6cqgdOpjlzQ,8178
|
|
66
|
-
quasarr/search/sources/nk.py,sha256=
|
|
66
|
+
quasarr/search/sources/nk.py,sha256=Y-FgWmKyiPqcTdDsAGviClL_wyip7zPDNwrSPCcx4Ew,7146
|
|
67
67
|
quasarr/search/sources/nx.py,sha256=UXUSYEL4zwYVwCri359I26GYN8CDuCKokpOOR21YEns,7602
|
|
68
68
|
quasarr/search/sources/sf.py,sha256=9k9K8_tYVarpW8n20HA2qAplBL14mIQCsorJO-ZxN6g,15811
|
|
69
69
|
quasarr/search/sources/sj.py,sha256=LW2dVDfZ90mDdrQ6ZYtXb0eOjV3cCh6kEW7lTra1c5M,7608
|
|
@@ -74,9 +74,9 @@ quasarr/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
74
74
|
quasarr/storage/config.py,sha256=SSTgIce2FVYoVTK_6OCU3msknhxuLA3EC4Kcrrf_dxQ,6378
|
|
75
75
|
quasarr/storage/setup.py,sha256=Cbo0phZbC6JP2wx_qER3vpaLSTDLbKEfdXj6KoAMkWw,47403
|
|
76
76
|
quasarr/storage/sqlite_database.py,sha256=yMqFQfKf0k7YS-6Z3_7pj4z1GwWSXJ8uvF4IydXsuTE,3554
|
|
77
|
-
quasarr-2.3.
|
|
78
|
-
quasarr-2.3.
|
|
79
|
-
quasarr-2.3.
|
|
80
|
-
quasarr-2.3.
|
|
81
|
-
quasarr-2.3.
|
|
82
|
-
quasarr-2.3.
|
|
77
|
+
quasarr-2.3.2.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
|
|
78
|
+
quasarr-2.3.2.dist-info/METADATA,sha256=iF4XkBvoRySRgnKxa57ezN0mvvpKs0F8V6Qr3hEjgFA,15024
|
|
79
|
+
quasarr-2.3.2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
80
|
+
quasarr-2.3.2.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
|
|
81
|
+
quasarr-2.3.2.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
|
|
82
|
+
quasarr-2.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|