quasarr 1.21.0__py3-none-any.whl → 1.21.1__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/providers/version.py +1 -1
- quasarr/search/sources/dl.py +89 -61
- quasarr/storage/setup.py +1 -1
- {quasarr-1.21.0.dist-info → quasarr-1.21.1.dist-info}/METADATA +1 -1
- {quasarr-1.21.0.dist-info → quasarr-1.21.1.dist-info}/RECORD +9 -9
- {quasarr-1.21.0.dist-info → quasarr-1.21.1.dist-info}/WHEEL +0 -0
- {quasarr-1.21.0.dist-info → quasarr-1.21.1.dist-info}/entry_points.txt +0 -0
- {quasarr-1.21.0.dist-info → quasarr-1.21.1.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.21.0.dist-info → quasarr-1.21.1.dist-info}/top_level.txt +0 -0
quasarr/providers/version.py
CHANGED
quasarr/search/sources/dl.py
CHANGED
|
@@ -4,21 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
6
|
import time
|
|
7
|
-
import warnings
|
|
8
7
|
from base64 import urlsafe_b64encode
|
|
9
8
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
10
9
|
from datetime import datetime
|
|
11
10
|
from html import unescape
|
|
12
11
|
|
|
13
12
|
from bs4 import BeautifulSoup
|
|
14
|
-
from bs4 import XMLParsedAsHTMLWarning
|
|
15
13
|
|
|
16
14
|
from quasarr.providers.imdb_metadata import get_localized_title
|
|
17
15
|
from quasarr.providers.log import info, debug
|
|
18
16
|
from quasarr.providers.sessions.dl import retrieve_and_validate_session, invalidate_session, fetch_via_requests_session
|
|
19
17
|
|
|
20
|
-
warnings.filterwarnings("ignore", category=XMLParsedAsHTMLWarning) # we dont want to use lxml
|
|
21
|
-
|
|
22
18
|
hostname = "dl"
|
|
23
19
|
supported_mirrors = []
|
|
24
20
|
|
|
@@ -37,11 +33,18 @@ def normalize_title_for_sonarr(title):
|
|
|
37
33
|
|
|
38
34
|
def dl_feed(shared_state, start_time, request_from, mirror=None):
|
|
39
35
|
"""
|
|
40
|
-
Parse the
|
|
36
|
+
Parse the correct forum and return releases.
|
|
41
37
|
"""
|
|
42
38
|
releases = []
|
|
43
39
|
host = shared_state.values["config"]("Hostnames").get(hostname)
|
|
44
40
|
|
|
41
|
+
if "lazylibrarian" in request_from.lower():
|
|
42
|
+
forum = "magazine-zeitschriften.72"
|
|
43
|
+
elif "radarr" in request_from.lower():
|
|
44
|
+
forum = "hd.8"
|
|
45
|
+
else:
|
|
46
|
+
forum = "hd.14"
|
|
47
|
+
|
|
45
48
|
if not host:
|
|
46
49
|
debug(f"{hostname}: hostname not configured")
|
|
47
50
|
return releases
|
|
@@ -52,49 +55,61 @@ def dl_feed(shared_state, start_time, request_from, mirror=None):
|
|
|
52
55
|
info(f"Could not retrieve valid session for {host}")
|
|
53
56
|
return releases
|
|
54
57
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
response = sess.get(rss_url, timeout=30)
|
|
58
|
+
forum_url = f'https://www.{host}/forums/{forum}/?order=post_date&direction=desc'
|
|
59
|
+
response = sess.get(forum_url, timeout=30)
|
|
58
60
|
|
|
59
61
|
if response.status_code != 200:
|
|
60
|
-
info(f"{hostname}:
|
|
62
|
+
info(f"{hostname}: Forum request failed with {response.status_code}")
|
|
61
63
|
return releases
|
|
62
64
|
|
|
63
65
|
soup = BeautifulSoup(response.content, 'html.parser')
|
|
64
|
-
|
|
66
|
+
|
|
67
|
+
# Find all thread items in the forum
|
|
68
|
+
items = soup.select('div.structItem.structItem--thread')
|
|
65
69
|
|
|
66
70
|
if not items:
|
|
67
|
-
debug(f"{hostname}: No entries found in
|
|
71
|
+
debug(f"{hostname}: No entries found in Forum")
|
|
68
72
|
return releases
|
|
69
73
|
|
|
70
74
|
for item in items:
|
|
71
75
|
try:
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
# Extract title from the thread
|
|
77
|
+
title_elem = item.select_one('div.structItem-title a')
|
|
78
|
+
if not title_elem:
|
|
74
79
|
continue
|
|
75
80
|
|
|
76
|
-
title =
|
|
81
|
+
title = title_elem.get_text(strip=True)
|
|
77
82
|
if not title:
|
|
78
83
|
continue
|
|
79
84
|
|
|
80
85
|
title = unescape(title)
|
|
81
|
-
title = title.replace(']]>', '').replace('<![CDATA[', '')
|
|
82
86
|
title = normalize_title_for_sonarr(title)
|
|
83
87
|
|
|
84
|
-
|
|
85
|
-
thread_url =
|
|
86
|
-
match = re.search(r'https://[^\s]+/threads/[^\s]+', item_text)
|
|
87
|
-
if match:
|
|
88
|
-
thread_url = match.group(0)
|
|
88
|
+
# Extract thread URL
|
|
89
|
+
thread_url = title_elem.get('href')
|
|
89
90
|
if not thread_url:
|
|
90
91
|
continue
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
if
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
93
|
+
# Make sure URL is absolute
|
|
94
|
+
if thread_url.startswith('/'):
|
|
95
|
+
thread_url = f"https://www.{host}{thread_url}"
|
|
96
|
+
|
|
97
|
+
# Extract date and convert to RFC 2822 format
|
|
98
|
+
date_str = None
|
|
99
|
+
date_elem = item.select_one('time.u-dt')
|
|
100
|
+
if date_elem:
|
|
101
|
+
iso_date = date_elem.get('datetime', '')
|
|
102
|
+
if iso_date:
|
|
103
|
+
try:
|
|
104
|
+
# Parse ISO format and convert to RFC 2822
|
|
105
|
+
dt = datetime.fromisoformat(iso_date.replace('Z', '+00:00'))
|
|
106
|
+
date_str = dt.strftime("%a, %d %b %Y %H:%M:%S %z")
|
|
107
|
+
except Exception:
|
|
108
|
+
date_str = None
|
|
109
|
+
|
|
110
|
+
# Fallback: use current time if no date found
|
|
111
|
+
if not date_str:
|
|
112
|
+
date_str = datetime.now().strftime("%a, %d %b %Y %H:%M:%S %z")
|
|
98
113
|
|
|
99
114
|
mb = 0
|
|
100
115
|
imdb_id = None
|
|
@@ -120,11 +135,11 @@ def dl_feed(shared_state, start_time, request_from, mirror=None):
|
|
|
120
135
|
})
|
|
121
136
|
|
|
122
137
|
except Exception as e:
|
|
123
|
-
debug(f"{hostname}: error parsing
|
|
138
|
+
debug(f"{hostname}: error parsing Forum item: {e}")
|
|
124
139
|
continue
|
|
125
140
|
|
|
126
141
|
except Exception as e:
|
|
127
|
-
info(f"{hostname}:
|
|
142
|
+
info(f"{hostname}: Forum feed error: {e}")
|
|
128
143
|
invalidate_session(shared_state)
|
|
129
144
|
|
|
130
145
|
elapsed = time.time() - start_time
|
|
@@ -132,6 +147,23 @@ def dl_feed(shared_state, start_time, request_from, mirror=None):
|
|
|
132
147
|
return releases
|
|
133
148
|
|
|
134
149
|
|
|
150
|
+
def _replace_umlauts(text):
|
|
151
|
+
replacements = {
|
|
152
|
+
'ä': 'ae',
|
|
153
|
+
'ö': 'oe',
|
|
154
|
+
'ü': 'ue',
|
|
155
|
+
'Ä': 'Ae',
|
|
156
|
+
'Ö': 'Oe',
|
|
157
|
+
'Ü': 'Ue',
|
|
158
|
+
'ß': 'ss'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for umlaut, replacement in replacements.items():
|
|
162
|
+
text = text.replace(umlaut, replacement)
|
|
163
|
+
|
|
164
|
+
return text
|
|
165
|
+
|
|
166
|
+
|
|
135
167
|
def _search_single_page(shared_state, host, search_string, search_id, page_num, imdb_id, mirror, request_from, season,
|
|
136
168
|
episode):
|
|
137
169
|
"""
|
|
@@ -139,6 +171,8 @@ def _search_single_page(shared_state, host, search_string, search_id, page_num,
|
|
|
139
171
|
"""
|
|
140
172
|
page_releases = []
|
|
141
173
|
|
|
174
|
+
search_string = _replace_umlauts(search_string)
|
|
175
|
+
|
|
142
176
|
try:
|
|
143
177
|
if page_num == 1:
|
|
144
178
|
search_params = {
|
|
@@ -247,8 +281,8 @@ def _search_single_page(shared_state, host, search_string, search_id, page_num,
|
|
|
247
281
|
def dl_search(shared_state, start_time, request_from, search_string,
|
|
248
282
|
mirror=None, season=None, episode=None):
|
|
249
283
|
"""
|
|
250
|
-
Search with
|
|
251
|
-
|
|
284
|
+
Search with sequential pagination (max 5 pages) to find best quality releases.
|
|
285
|
+
Stops searching if a page returns 0 results.
|
|
252
286
|
"""
|
|
253
287
|
releases = []
|
|
254
288
|
host = shared_state.values["config"]("Hostnames").get(hostname)
|
|
@@ -264,8 +298,8 @@ def dl_search(shared_state, start_time, request_from, search_string,
|
|
|
264
298
|
search_string = unescape(search_string)
|
|
265
299
|
max_pages = 5
|
|
266
300
|
|
|
267
|
-
|
|
268
|
-
f"{hostname}: Starting
|
|
301
|
+
debug(
|
|
302
|
+
f"{hostname}: Starting sequential paginated search for '{search_string}' (Season: {season}, Episode: {episode}) - up to {max_pages} pages")
|
|
269
303
|
|
|
270
304
|
try:
|
|
271
305
|
sess = retrieve_and_validate_session(shared_state)
|
|
@@ -273,42 +307,36 @@ def dl_search(shared_state, start_time, request_from, search_string,
|
|
|
273
307
|
info(f"Could not retrieve valid session for {host}")
|
|
274
308
|
return releases
|
|
275
309
|
|
|
276
|
-
|
|
277
|
-
page_1_releases, search_id = _search_single_page(
|
|
278
|
-
shared_state, host, search_string, None, 1,
|
|
279
|
-
imdb_id, mirror, request_from, season, episode
|
|
280
|
-
)
|
|
281
|
-
releases.extend(page_1_releases)
|
|
310
|
+
search_id = None
|
|
282
311
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
312
|
+
# Sequential search through pages
|
|
313
|
+
for page_num in range(1, max_pages + 1):
|
|
314
|
+
page_releases, extracted_search_id = _search_single_page(
|
|
315
|
+
shared_state, host, search_string, search_id, page_num,
|
|
316
|
+
imdb_id, mirror, request_from, season, episode
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Update search_id from first page
|
|
320
|
+
if page_num == 1:
|
|
321
|
+
search_id = extracted_search_id
|
|
322
|
+
if not search_id:
|
|
323
|
+
info(f"{hostname}: Could not extract search ID, stopping pagination")
|
|
324
|
+
break
|
|
325
|
+
|
|
326
|
+
# Add releases from this page
|
|
327
|
+
releases.extend(page_releases)
|
|
328
|
+
debug(f"{hostname}: [Page {page_num}] completed with {len(page_releases)} valid releases")
|
|
286
329
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
future = executor.submit(
|
|
292
|
-
_search_single_page,
|
|
293
|
-
shared_state, host, search_string, search_id, page_num,
|
|
294
|
-
imdb_id, mirror, request_from, season, episode
|
|
295
|
-
)
|
|
296
|
-
futures[future] = page_num
|
|
297
|
-
|
|
298
|
-
for future in as_completed(futures):
|
|
299
|
-
page_num = futures[future]
|
|
300
|
-
try:
|
|
301
|
-
page_releases, _ = future.result()
|
|
302
|
-
releases.extend(page_releases)
|
|
303
|
-
debug(f"{hostname}: [Page {page_num}] completed with {len(page_releases)} valid releases")
|
|
304
|
-
except Exception as e:
|
|
305
|
-
info(f"{hostname}: [Page {page_num}] failed: {e}")
|
|
330
|
+
# Stop if this page returned 0 results
|
|
331
|
+
if len(page_releases) == 0:
|
|
332
|
+
debug(f"{hostname}: [Page {page_num}] returned 0 results, stopping pagination")
|
|
333
|
+
break
|
|
306
334
|
|
|
307
335
|
except Exception as e:
|
|
308
336
|
info(f"{hostname}: search error: {e}")
|
|
309
337
|
invalidate_session(shared_state)
|
|
310
338
|
|
|
311
|
-
|
|
339
|
+
debug(f"{hostname}: FINAL - Found {len(releases)} valid releases - providing to {request_from}")
|
|
312
340
|
|
|
313
341
|
elapsed = time.time() - start_time
|
|
314
342
|
debug(f"Time taken: {elapsed:.2f}s ({hostname})")
|
quasarr/storage/setup.py
CHANGED
|
@@ -197,7 +197,7 @@ def save_hostnames(shared_state, timeout=5, first_run=True):
|
|
|
197
197
|
if not first_run:
|
|
198
198
|
# Append restart notice for specific sites that actually changed
|
|
199
199
|
for site in changed_sites:
|
|
200
|
-
if site.lower() in {'al', 'dd', 'nx'}:
|
|
200
|
+
if site.lower() in {'al', 'dd', 'dl', 'nx'}:
|
|
201
201
|
optional_text += f"{site.upper()}: You must restart Quasarr and follow additional steps to start using this site.<br>"
|
|
202
202
|
|
|
203
203
|
return render_success(success_msg, timeout, optional_text=optional_text)
|
|
@@ -39,7 +39,7 @@ quasarr/providers/notifications.py,sha256=bohT-6yudmFnmZMc3BwCGX0n1HdzSVgQG_LDZm
|
|
|
39
39
|
quasarr/providers/obfuscated.py,sha256=YydQJHrZ485pHaXK0DHHRW3eLZygGr6c0xnUKD6mcCE,236502
|
|
40
40
|
quasarr/providers/shared_state.py,sha256=1NUKtm9YXWPvN64By2O2OYH5ke5TmBkJSbSxiNczgtU,29849
|
|
41
41
|
quasarr/providers/statistics.py,sha256=cEQixYnDMDqtm5wWe40E_2ucyo4mD0n3SrfelhQi1L8,6452
|
|
42
|
-
quasarr/providers/version.py,sha256=
|
|
42
|
+
quasarr/providers/version.py,sha256=gwbSbe-TLBE-0LqZeM1tE3yUIUCdoBvCmQNeSPuUT-E,4004
|
|
43
43
|
quasarr/providers/web_server.py,sha256=XPj98T-axxgotovuB-rVw1IPCkJiNdXBlEeFvM_zSlM,1432
|
|
44
44
|
quasarr/providers/sessions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
45
|
quasarr/providers/sessions/al.py,sha256=mlP6SWfCY2HyOSV40uyotQ5T4eSBNYG9A5GWOEAdz-c,9589
|
|
@@ -52,7 +52,7 @@ quasarr/search/sources/al.py,sha256=yr6wx-VcSOFYK_o3N1bepC4t6Gvt9eDvcG9fQBFg0bg,
|
|
|
52
52
|
quasarr/search/sources/by.py,sha256=OPoAqS5kSSNrRVsPlALhX59h3lEZWGA7LlFfL4vH2-o,7914
|
|
53
53
|
quasarr/search/sources/dd.py,sha256=pVpdHLZlw2CYklBf_YLkeDWbCNsDLR2iecccR2c2RyI,4889
|
|
54
54
|
quasarr/search/sources/dj.py,sha256=2HIdg5ddXP4DtjHlyXmuQ8QVhOPt3Hh2kL4uxhFJK-8,7074
|
|
55
|
-
quasarr/search/sources/dl.py,sha256
|
|
55
|
+
quasarr/search/sources/dl.py,sha256=JKNz1fKPnCSBZ5AoZL7BP71aoq08i_1feIIlg_RWf98,12023
|
|
56
56
|
quasarr/search/sources/dt.py,sha256=m1kQ7mC43QlWZyVIkw-OXJGjWiT9IbQuFtHWiR8CjhA,9580
|
|
57
57
|
quasarr/search/sources/dw.py,sha256=-daUTBTA5izeatrE7TITVlnzNCQ5HfovYMMZ8UTM-2o,7636
|
|
58
58
|
quasarr/search/sources/fx.py,sha256=JAyD727yDAFIP14bzfi2SkX9paysXGkQdIybShYtdko,8596
|
|
@@ -67,11 +67,11 @@ quasarr/search/sources/wd.py,sha256=O02j3irSlVw2qES82g_qHuavAk-njjSRH1dHSCnOUas,
|
|
|
67
67
|
quasarr/search/sources/wx.py,sha256=B6Ra0Wie9Ii96Kiz4Zdpb_qUmBwWkyA0rIoA7XdNNYo,13118
|
|
68
68
|
quasarr/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
69
69
|
quasarr/storage/config.py,sha256=hOI7vvIo1YaML3dtAkTmp0HSedWF6brVhRk3d8pJtXI,6300
|
|
70
|
-
quasarr/storage/setup.py,sha256=
|
|
70
|
+
quasarr/storage/setup.py,sha256=Gf7e125KlHsyu-hhq3uFfH7N6i7-8DDONGcYJX0haLs,18261
|
|
71
71
|
quasarr/storage/sqlite_database.py,sha256=yMqFQfKf0k7YS-6Z3_7pj4z1GwWSXJ8uvF4IydXsuTE,3554
|
|
72
|
-
quasarr-1.21.
|
|
73
|
-
quasarr-1.21.
|
|
74
|
-
quasarr-1.21.
|
|
75
|
-
quasarr-1.21.
|
|
76
|
-
quasarr-1.21.
|
|
77
|
-
quasarr-1.21.
|
|
72
|
+
quasarr-1.21.1.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
|
|
73
|
+
quasarr-1.21.1.dist-info/METADATA,sha256=4HZd5NBTzVWVfQyrSGnuqh7ITVU60oKZyl_8ZoLpHcE,12743
|
|
74
|
+
quasarr-1.21.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
75
|
+
quasarr-1.21.1.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
|
|
76
|
+
quasarr-1.21.1.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
|
|
77
|
+
quasarr-1.21.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|