quasarr 2.4.11__py3-none-any.whl → 2.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of quasarr might be problematic. Click here for more details.
- quasarr/__init__.py +30 -35
- quasarr/api/__init__.py +23 -15
- quasarr/api/arr/__init__.py +15 -6
- quasarr/api/captcha/__init__.py +2 -9
- quasarr/api/config/__init__.py +31 -168
- quasarr/api/jdownloader/__init__.py +232 -0
- quasarr/api/packages/__init__.py +2 -12
- quasarr/downloads/__init__.py +2 -0
- quasarr/downloads/sources/hs.py +131 -0
- quasarr/providers/html_templates.py +14 -3
- quasarr/providers/sessions/al.py +4 -0
- quasarr/providers/shared_state.py +17 -17
- quasarr/providers/version.py +1 -1
- quasarr/search/__init__.py +90 -15
- quasarr/search/sources/al.py +17 -13
- quasarr/search/sources/by.py +4 -1
- quasarr/search/sources/dd.py +16 -4
- quasarr/search/sources/dl.py +13 -1
- quasarr/search/sources/hs.py +515 -0
- quasarr/search/sources/mb.py +1 -7
- quasarr/search/sources/nx.py +4 -1
- quasarr/search/sources/wd.py +4 -1
- quasarr/search/sources/wx.py +10 -8
- quasarr/storage/config.py +1 -0
- quasarr/storage/setup.py +564 -266
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/METADATA +1 -1
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/RECORD +30 -27
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/WHEEL +0 -0
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.11.dist-info → quasarr-2.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
from quasarr.providers.html_templates import render_button
|
|
6
|
+
from quasarr.storage.config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_jdownloader_status(shared_state):
|
|
10
|
+
"""Get JDownloader connection status and device name."""
|
|
11
|
+
try:
|
|
12
|
+
device = shared_state.values.get("device")
|
|
13
|
+
jd_connected = device is not None and device is not False
|
|
14
|
+
except:
|
|
15
|
+
jd_connected = False
|
|
16
|
+
|
|
17
|
+
jd_config = Config("JDownloader")
|
|
18
|
+
jd_device = jd_config.get("device") or ""
|
|
19
|
+
|
|
20
|
+
dev_name = jd_device if jd_device else "JDownloader"
|
|
21
|
+
dev_name_safe = dev_name.replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
22
|
+
|
|
23
|
+
if jd_connected:
|
|
24
|
+
status_text = f"✅ {dev_name_safe} connected"
|
|
25
|
+
status_class = "success"
|
|
26
|
+
elif jd_device:
|
|
27
|
+
status_text = f"❌ {dev_name_safe} disconnected"
|
|
28
|
+
status_class = "error"
|
|
29
|
+
else:
|
|
30
|
+
status_text = "❌ JDownloader disconnected"
|
|
31
|
+
status_class = "error"
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"connected": jd_connected,
|
|
35
|
+
"device_name": jd_device,
|
|
36
|
+
"status_text": status_text,
|
|
37
|
+
"status_class": status_class
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_jdownloader_modal_script():
|
|
42
|
+
"""Return the JavaScript for the JDownloader configuration modal."""
|
|
43
|
+
jd_config = Config("JDownloader")
|
|
44
|
+
jd_user = jd_config.get("user") or ""
|
|
45
|
+
jd_pass = jd_config.get("password") or ""
|
|
46
|
+
jd_device = jd_config.get("device") or ""
|
|
47
|
+
|
|
48
|
+
jd_user_js = jd_user.replace("\\", "\\\\").replace("'", "\\'")
|
|
49
|
+
jd_pass_js = jd_pass.replace("\\", "\\\\").replace("'", "\\'")
|
|
50
|
+
jd_device_js = jd_device.replace("\\", "\\\\").replace("'", "\\'")
|
|
51
|
+
|
|
52
|
+
return f"""
|
|
53
|
+
<script>
|
|
54
|
+
function openJDownloaderModal() {{
|
|
55
|
+
var currentUser = '{jd_user_js}';
|
|
56
|
+
var currentPass = '{jd_pass_js}';
|
|
57
|
+
var currentDevice = '{jd_device_js}';
|
|
58
|
+
|
|
59
|
+
var content = `
|
|
60
|
+
<div id="jd-step-1">
|
|
61
|
+
<input type="hidden" id="jd-current-device" value="${{currentDevice}}">
|
|
62
|
+
<p><strong>JDownloader must be running and connected to My JDownloader!</strong></p>
|
|
63
|
+
<div style="margin-bottom: 1rem;">
|
|
64
|
+
<label style="display:block; font-size: 0.875rem;">E-Mail</label>
|
|
65
|
+
<input type="text" id="jd-user" value="${{currentUser}}" placeholder="user@example.org" style="width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem;">
|
|
66
|
+
</div>
|
|
67
|
+
<div style="margin-bottom: 1rem;">
|
|
68
|
+
<label style="display:block; font-size: 0.875rem;">Password</label>
|
|
69
|
+
<input type="password" id="jd-pass" value="${{currentPass}}" placeholder="Password" style="width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem;">
|
|
70
|
+
</div>
|
|
71
|
+
<div id="jd-status" style="margin-bottom: 0.5rem; font-size: 0.875rem; min-height: 1.25em;"></div>
|
|
72
|
+
<button class="btn-primary" onclick="verifyJDCredentials()">Verify Credentials</button>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div id="jd-step-2" style="display:none;">
|
|
76
|
+
<p>Select your JDownloader instance:</p>
|
|
77
|
+
<div style="margin-bottom: 1rem;">
|
|
78
|
+
<select id="jd-device" style="width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem;"></select>
|
|
79
|
+
</div>
|
|
80
|
+
<div id="jd-save-status" style="margin-bottom: 0.5rem; font-size: 0.875rem; min-height: 1.25em;"></div>
|
|
81
|
+
<button class="btn-primary" onclick="saveJDSettings()">Save</button>
|
|
82
|
+
</div>
|
|
83
|
+
`;
|
|
84
|
+
|
|
85
|
+
showModal('Configure JDownloader', content, '<button class="btn-secondary" onclick="closeModal()">Close</button>');
|
|
86
|
+
}}
|
|
87
|
+
|
|
88
|
+
function verifyJDCredentials() {{
|
|
89
|
+
var user = document.getElementById('jd-user').value;
|
|
90
|
+
var pass = document.getElementById('jd-pass').value;
|
|
91
|
+
var statusDiv = document.getElementById('jd-status');
|
|
92
|
+
|
|
93
|
+
statusDiv.innerHTML = 'Verifying...';
|
|
94
|
+
statusDiv.style.color = 'var(--secondary, #6c757d)';
|
|
95
|
+
|
|
96
|
+
fetch('/api/jdownloader/verify', {{
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {{ 'Content-Type': 'application/json' }},
|
|
99
|
+
body: JSON.stringify({{ user: user, pass: pass }})
|
|
100
|
+
}})
|
|
101
|
+
.then(response => response.json())
|
|
102
|
+
.then(data => {{
|
|
103
|
+
if (data.success) {{
|
|
104
|
+
var select = document.getElementById('jd-device');
|
|
105
|
+
select.innerHTML = '';
|
|
106
|
+
var currentDevice = document.getElementById('jd-current-device').value;
|
|
107
|
+
data.devices.forEach(device => {{
|
|
108
|
+
var opt = document.createElement('option');
|
|
109
|
+
opt.value = device;
|
|
110
|
+
opt.innerHTML = device;
|
|
111
|
+
if (device === currentDevice) {{
|
|
112
|
+
opt.selected = true;
|
|
113
|
+
}}
|
|
114
|
+
select.appendChild(opt);
|
|
115
|
+
}});
|
|
116
|
+
|
|
117
|
+
document.getElementById('jd-step-1').style.display = 'none';
|
|
118
|
+
document.getElementById('jd-step-2').style.display = 'block';
|
|
119
|
+
}} else {{
|
|
120
|
+
statusDiv.innerHTML = '❌ ' + (data.message || 'Verification failed');
|
|
121
|
+
statusDiv.style.color = '#dc3545';
|
|
122
|
+
}}
|
|
123
|
+
}})
|
|
124
|
+
.catch(error => {{
|
|
125
|
+
statusDiv.innerHTML = '❌ Error: ' + error.message;
|
|
126
|
+
statusDiv.style.color = '#dc3545';
|
|
127
|
+
}});
|
|
128
|
+
}}
|
|
129
|
+
|
|
130
|
+
function saveJDSettings() {{
|
|
131
|
+
var user = document.getElementById('jd-user').value;
|
|
132
|
+
var pass = document.getElementById('jd-pass').value;
|
|
133
|
+
var device = document.getElementById('jd-device').value;
|
|
134
|
+
var statusDiv = document.getElementById('jd-save-status');
|
|
135
|
+
|
|
136
|
+
statusDiv.innerHTML = 'Saving...';
|
|
137
|
+
statusDiv.style.color = 'var(--secondary, #6c757d)';
|
|
138
|
+
|
|
139
|
+
fetch('/api/jdownloader/save', {{
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers: {{ 'Content-Type': 'application/json' }},
|
|
142
|
+
body: JSON.stringify({{ user: user, pass: pass, device: device }})
|
|
143
|
+
}})
|
|
144
|
+
.then(response => response.json())
|
|
145
|
+
.then(data => {{
|
|
146
|
+
if (data.success) {{
|
|
147
|
+
statusDiv.innerHTML = '✅ ' + data.message;
|
|
148
|
+
statusDiv.style.color = '#198754';
|
|
149
|
+
setTimeout(function() {{
|
|
150
|
+
window.location.reload();
|
|
151
|
+
}}, 1000);
|
|
152
|
+
}} else {{
|
|
153
|
+
statusDiv.innerHTML = '❌ ' + data.message;
|
|
154
|
+
statusDiv.style.color = '#dc3545';
|
|
155
|
+
}}
|
|
156
|
+
}})
|
|
157
|
+
.catch(error => {{
|
|
158
|
+
statusDiv.innerHTML = '❌ Error: ' + error.message;
|
|
159
|
+
statusDiv.style.color = '#dc3545';
|
|
160
|
+
}});
|
|
161
|
+
}}
|
|
162
|
+
</script>
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_jdownloader_status_pill(shared_state):
|
|
167
|
+
"""Return the HTML for the JDownloader status pill."""
|
|
168
|
+
status = get_jdownloader_status(shared_state)
|
|
169
|
+
|
|
170
|
+
return f"""
|
|
171
|
+
<span class="status-pill {status['status_class']}"
|
|
172
|
+
onclick="openJDownloaderModal()"
|
|
173
|
+
style="cursor: pointer;"
|
|
174
|
+
title="Click to configure JDownloader">
|
|
175
|
+
{status['status_text']}
|
|
176
|
+
</span>
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_jdownloader_disconnected_page(shared_state, back_url="/"):
|
|
181
|
+
"""Return a full error page when JDownloader is disconnected."""
|
|
182
|
+
import quasarr.providers.html_images as images
|
|
183
|
+
from quasarr.providers.html_templates import render_centered_html
|
|
184
|
+
|
|
185
|
+
status_pill = get_jdownloader_status_pill(shared_state)
|
|
186
|
+
modal_script = get_jdownloader_modal_script()
|
|
187
|
+
|
|
188
|
+
back_btn = render_button("Back", "secondary", {"onclick": f"location.href='{back_url}'"})
|
|
189
|
+
|
|
190
|
+
content = f'''
|
|
191
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
192
|
+
<div class="status-bar">
|
|
193
|
+
{status_pill}
|
|
194
|
+
</div>
|
|
195
|
+
<p>{back_btn}</p>
|
|
196
|
+
<style>
|
|
197
|
+
.status-pill {{
|
|
198
|
+
font-size: 0.9em;
|
|
199
|
+
padding: 8px 16px;
|
|
200
|
+
border-radius: 0.5rem;
|
|
201
|
+
font-weight: 500;
|
|
202
|
+
transition: transform 0.1s ease;
|
|
203
|
+
}}
|
|
204
|
+
.status-pill:hover {{
|
|
205
|
+
transform: scale(1.05);
|
|
206
|
+
}}
|
|
207
|
+
.status-pill.success {{
|
|
208
|
+
background: var(--status-success-bg, #e8f5e9);
|
|
209
|
+
color: var(--status-success-color, #2e7d32);
|
|
210
|
+
border: 1px solid var(--status-success-border, #a5d6a7);
|
|
211
|
+
}}
|
|
212
|
+
.status-pill.error {{
|
|
213
|
+
background: var(--status-error-bg, #ffebee);
|
|
214
|
+
color: var(--status-error-color, #c62828);
|
|
215
|
+
border: 1px solid var(--status-error-border, #ef9a9a);
|
|
216
|
+
}}
|
|
217
|
+
/* Dark mode */
|
|
218
|
+
@media (prefers-color-scheme: dark) {{
|
|
219
|
+
:root {{
|
|
220
|
+
--status-success-bg: #1c4532;
|
|
221
|
+
--status-success-color: #68d391;
|
|
222
|
+
--status-success-border: #276749;
|
|
223
|
+
--status-error-bg: #3d2d2d;
|
|
224
|
+
--status-error-color: #fc8181;
|
|
225
|
+
--status-error-border: #c53030;
|
|
226
|
+
}}
|
|
227
|
+
}}
|
|
228
|
+
</style>
|
|
229
|
+
{modal_script}
|
|
230
|
+
'''
|
|
231
|
+
|
|
232
|
+
return render_centered_html(content)
|
quasarr/api/packages/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
import quasarr.providers.html_images as images
|
|
6
|
+
from quasarr.api.jdownloader import get_jdownloader_disconnected_page
|
|
6
7
|
from quasarr.downloads.packages import delete_package, get_packages
|
|
7
8
|
from quasarr.providers import shared_state
|
|
8
9
|
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
@@ -349,18 +350,7 @@ def setup_packages_routes(app):
|
|
|
349
350
|
device = None
|
|
350
351
|
|
|
351
352
|
if not device:
|
|
352
|
-
|
|
353
|
-
"Back", "secondary", {"onclick": "location.href='/'"}
|
|
354
|
-
)
|
|
355
|
-
return render_centered_html(f'''
|
|
356
|
-
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
357
|
-
<div class="status-bar">
|
|
358
|
-
<span class="status-pill error">
|
|
359
|
-
❌ JDownloader disconnected
|
|
360
|
-
</span>
|
|
361
|
-
</div>
|
|
362
|
-
<p>{back_btn}</p>
|
|
363
|
-
''')
|
|
353
|
+
return get_jdownloader_disconnected_page(shared_state)
|
|
364
354
|
|
|
365
355
|
# Check for delete status from redirect
|
|
366
356
|
deleted = request.query.get("deleted")
|
quasarr/downloads/__init__.py
CHANGED
|
@@ -16,6 +16,7 @@ from quasarr.downloads.sources.dl import get_dl_download_links
|
|
|
16
16
|
from quasarr.downloads.sources.dt import get_dt_download_links
|
|
17
17
|
from quasarr.downloads.sources.dw import get_dw_download_links
|
|
18
18
|
from quasarr.downloads.sources.he import get_he_download_links
|
|
19
|
+
from quasarr.downloads.sources.hs import get_hs_download_links
|
|
19
20
|
from quasarr.downloads.sources.mb import get_mb_download_links
|
|
20
21
|
from quasarr.downloads.sources.nk import get_nk_download_links
|
|
21
22
|
from quasarr.downloads.sources.nx import get_nx_download_links
|
|
@@ -57,6 +58,7 @@ SOURCE_GETTERS = {
|
|
|
57
58
|
"dt": get_dt_download_links,
|
|
58
59
|
"dw": get_dw_download_links,
|
|
59
60
|
"he": get_he_download_links,
|
|
61
|
+
"hs": get_hs_download_links,
|
|
60
62
|
"mb": get_mb_download_links,
|
|
61
63
|
"nk": get_nk_download_links,
|
|
62
64
|
"nx": get_nx_download_links,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
|
|
10
|
+
from quasarr.providers.hostname_issues import mark_hostname_issue
|
|
11
|
+
from quasarr.providers.log import debug, info
|
|
12
|
+
|
|
13
|
+
hostname = "hs"
|
|
14
|
+
|
|
15
|
+
FILECRYPT_REGEX = re.compile(
|
|
16
|
+
r"https?://(?:www\.)?filecrypt\.(?:cc|co|to)/[Cc]ontainer/[A-Za-z0-9]+\.html", re.I
|
|
17
|
+
)
|
|
18
|
+
AFFILIATE_REGEX = re.compile(r"af\.php\?v=([a-zA-Z0-9]+)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def normalize_mirror_name(name):
|
|
22
|
+
"""Normalize mirror names - ddlto/ddl.to -> ddownload"""
|
|
23
|
+
if not name:
|
|
24
|
+
return None
|
|
25
|
+
name_lower = name.lower().strip()
|
|
26
|
+
if "ddlto" in name_lower or "ddl.to" in name_lower or "ddownload" in name_lower:
|
|
27
|
+
return "ddownload"
|
|
28
|
+
if "rapidgator" in name_lower:
|
|
29
|
+
return "rapidgator"
|
|
30
|
+
if "katfile" in name_lower:
|
|
31
|
+
return "katfile"
|
|
32
|
+
return name_lower
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_hs_download_links(shared_state, url, mirror, title, password):
|
|
36
|
+
"""
|
|
37
|
+
KEEP THE SIGNATURE EVEN IF SOME PARAMETERS ARE UNUSED!
|
|
38
|
+
|
|
39
|
+
HS handler - extracts filecrypt download links from release pages.
|
|
40
|
+
The site structure pairs affiliate links (indicating mirror) with filecrypt links.
|
|
41
|
+
"""
|
|
42
|
+
headers = {"User-Agent": shared_state.values["user_agent"]}
|
|
43
|
+
|
|
44
|
+
mirror_lower = mirror.lower() if mirror else None
|
|
45
|
+
links = []
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
r = requests.get(url, headers=headers, timeout=30)
|
|
49
|
+
r.raise_for_status()
|
|
50
|
+
soup = BeautifulSoup(r.text, "html.parser")
|
|
51
|
+
|
|
52
|
+
# Find all links in the page
|
|
53
|
+
all_links = soup.find_all("a", href=True)
|
|
54
|
+
|
|
55
|
+
# Strategy: Build mirror detection from multiple sources
|
|
56
|
+
# 1. Text labels on filecrypt links (most reliable)
|
|
57
|
+
# 2. Affiliate links preceding filecrypt links (fallback)
|
|
58
|
+
|
|
59
|
+
# First pass: detect mirrors from link text labels
|
|
60
|
+
text_labeled_mirrors = {} # url -> mirror_name
|
|
61
|
+
for link in all_links:
|
|
62
|
+
href = link.get("href", "")
|
|
63
|
+
if FILECRYPT_REGEX.match(href):
|
|
64
|
+
link_text = link.get_text(strip=True).lower()
|
|
65
|
+
detected_mirror = None
|
|
66
|
+
if "ddownload" in link_text or "ddl" in link_text:
|
|
67
|
+
detected_mirror = "ddownload"
|
|
68
|
+
elif "rapidgator" in link_text:
|
|
69
|
+
detected_mirror = "rapidgator"
|
|
70
|
+
elif "katfile" in link_text:
|
|
71
|
+
detected_mirror = "katfile"
|
|
72
|
+
if detected_mirror:
|
|
73
|
+
text_labeled_mirrors[href] = detected_mirror
|
|
74
|
+
|
|
75
|
+
# Second pass: track affiliate links for fallback
|
|
76
|
+
affiliate_mirrors = {} # url -> mirror_name (from preceding affiliate)
|
|
77
|
+
current_mirror = None
|
|
78
|
+
for link in all_links:
|
|
79
|
+
href = link.get("href", "")
|
|
80
|
+
|
|
81
|
+
# Check if this is an affiliate link (indicates mirror name)
|
|
82
|
+
aff_match = AFFILIATE_REGEX.search(href)
|
|
83
|
+
if aff_match:
|
|
84
|
+
current_mirror = normalize_mirror_name(aff_match.group(1))
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# Check if this is a filecrypt link
|
|
88
|
+
if FILECRYPT_REGEX.match(href):
|
|
89
|
+
if current_mirror and href not in affiliate_mirrors:
|
|
90
|
+
affiliate_mirrors[href] = current_mirror
|
|
91
|
+
current_mirror = None # Reset for next pair
|
|
92
|
+
|
|
93
|
+
# Combine results: text labels take priority over affiliate tracking
|
|
94
|
+
filecrypt_mirrors = []
|
|
95
|
+
seen_urls = set()
|
|
96
|
+
for link in all_links:
|
|
97
|
+
href = link.get("href", "")
|
|
98
|
+
if FILECRYPT_REGEX.match(href) and href not in seen_urls:
|
|
99
|
+
seen_urls.add(href)
|
|
100
|
+
# Priority: text label > affiliate > "filecrypt"
|
|
101
|
+
mirror_name = (
|
|
102
|
+
text_labeled_mirrors.get(href)
|
|
103
|
+
or affiliate_mirrors.get(href)
|
|
104
|
+
or "filecrypt"
|
|
105
|
+
)
|
|
106
|
+
filecrypt_mirrors.append((href, mirror_name))
|
|
107
|
+
|
|
108
|
+
# Filter by requested mirror and deduplicate
|
|
109
|
+
seen_urls = set()
|
|
110
|
+
for fc_url, fc_mirror in filecrypt_mirrors:
|
|
111
|
+
if fc_url in seen_urls:
|
|
112
|
+
continue
|
|
113
|
+
seen_urls.add(fc_url)
|
|
114
|
+
|
|
115
|
+
# Filter by requested mirror if specified
|
|
116
|
+
if mirror_lower:
|
|
117
|
+
if mirror_lower != fc_mirror:
|
|
118
|
+
debug(f"Skipping {fc_mirror} link (requested mirror: {mirror})")
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
# Store [url, mirror_name] - mirror_name is used by CAPTCHA page for filtering
|
|
122
|
+
links.append([fc_url, fc_mirror])
|
|
123
|
+
|
|
124
|
+
if not links:
|
|
125
|
+
debug(f"No filecrypt links found on {url} for {title}")
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
info(f"Error loading HS download links: {e}")
|
|
129
|
+
mark_hostname_issue(hostname, "download", str(e))
|
|
130
|
+
|
|
131
|
+
return {"links": links}
|
|
@@ -143,6 +143,7 @@ def render_centered_html(inner_content, footer_content=""):
|
|
|
143
143
|
justify-content: center;
|
|
144
144
|
margin-bottom: 0.5rem;
|
|
145
145
|
font-size: 2rem;
|
|
146
|
+
cursor: pointer;
|
|
146
147
|
}
|
|
147
148
|
.logo {
|
|
148
149
|
width: 48px;
|
|
@@ -333,6 +334,16 @@ def render_centered_html(inner_content, footer_content=""):
|
|
|
333
334
|
justify-content: flex-end;
|
|
334
335
|
}
|
|
335
336
|
</style>
|
|
337
|
+
<script>
|
|
338
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
339
|
+
const h1 = document.querySelector('h1');
|
|
340
|
+
if (h1) {
|
|
341
|
+
h1.onclick = function() {
|
|
342
|
+
window.location.href = '/';
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
</script>
|
|
336
347
|
</head>"""
|
|
337
348
|
)
|
|
338
349
|
|
|
@@ -417,7 +428,7 @@ def render_button(text, button_type="primary", attributes=None):
|
|
|
417
428
|
|
|
418
429
|
def render_form(header, form="", script="", footer_content=""):
|
|
419
430
|
content = f'''
|
|
420
|
-
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
431
|
+
<h1 onclick="window.location.href='/'"><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
421
432
|
<h2>{header}</h2>
|
|
422
433
|
{form}
|
|
423
434
|
{script}
|
|
@@ -446,7 +457,7 @@ def render_success(message, timeout=10, optional_text=""):
|
|
|
446
457
|
}}, 1000);
|
|
447
458
|
</script>
|
|
448
459
|
"""
|
|
449
|
-
content = f'''<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
460
|
+
content = f'''<h1 onclick="window.location.href='/'"><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
450
461
|
<h2>{message}</h2>
|
|
451
462
|
{optional_text}
|
|
452
463
|
{button_html}
|
|
@@ -459,7 +470,7 @@ def render_fail(message):
|
|
|
459
470
|
button_html = render_button(
|
|
460
471
|
"Back", "secondary", {"onclick": "window.location.href='/'"}
|
|
461
472
|
)
|
|
462
|
-
return render_centered_html(f"""<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
473
|
+
return render_centered_html(f"""<h1 onclick="window.location.href='/'"><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
463
474
|
<h2>{message}</h2>
|
|
464
475
|
{button_html}
|
|
465
476
|
""")
|
quasarr/providers/sessions/al.py
CHANGED
|
@@ -371,6 +371,7 @@ def fetch_via_requests_session(
|
|
|
371
371
|
target_url: str,
|
|
372
372
|
post_data: dict = None,
|
|
373
373
|
timeout: int = 30,
|
|
374
|
+
year: int = None,
|
|
374
375
|
):
|
|
375
376
|
"""
|
|
376
377
|
- method: "GET" or "POST"
|
|
@@ -383,6 +384,9 @@ def fetch_via_requests_session(
|
|
|
383
384
|
f"{hostname}: site not usable (login skipped or no credentials)"
|
|
384
385
|
)
|
|
385
386
|
|
|
387
|
+
if year:
|
|
388
|
+
sess.cookies["filter"] = f'{{"year":{{"from":{year},"to":{year}}}}}'
|
|
389
|
+
|
|
386
390
|
# Execute request
|
|
387
391
|
if method.upper() == "GET":
|
|
388
392
|
r = sess.get(target_url, timeout=timeout)
|
|
@@ -171,19 +171,18 @@ def set_device_from_config():
|
|
|
171
171
|
|
|
172
172
|
def check_device(device):
|
|
173
173
|
try:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return valid
|
|
174
|
+
if not isinstance(device, (type, Jddevice)):
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# Trigger a network request to verify connectivity
|
|
178
|
+
# get_current_state() performs an API call to JDownloader
|
|
179
|
+
state = device.downloadcontroller.get_current_state()
|
|
180
|
+
|
|
181
|
+
if state:
|
|
182
|
+
return True
|
|
183
|
+
return False
|
|
184
|
+
except Exception:
|
|
185
|
+
return False
|
|
187
186
|
|
|
188
187
|
|
|
189
188
|
def connect_device():
|
|
@@ -627,11 +626,12 @@ def search_string_in_sanitized_title(search_string, title):
|
|
|
627
626
|
sanitized_search_string = sanitize_string(search_string)
|
|
628
627
|
sanitized_title = sanitize_string(title)
|
|
629
628
|
|
|
629
|
+
search_regex = r"\b.+\b".join(
|
|
630
|
+
[re.escape(s) for s in sanitized_search_string.split(" ")]
|
|
631
|
+
)
|
|
630
632
|
# Use word boundaries to ensure full word/phrase match
|
|
631
|
-
if re.search(rf"\b{
|
|
632
|
-
debug(
|
|
633
|
-
f"Matched search string: {sanitized_search_string} with title: {sanitized_title}"
|
|
634
|
-
)
|
|
633
|
+
if re.search(rf"\b{search_regex}\b", sanitized_title):
|
|
634
|
+
debug(f"Matched search string: {search_regex} with title: {sanitized_title}")
|
|
635
635
|
return True
|
|
636
636
|
else:
|
|
637
637
|
debug(
|