quasarr 1.27.0__py3-none-any.whl → 1.28.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/__init__.py +22 -4
- quasarr/api/__init__.py +1 -1
- quasarr/api/arr/__init__.py +2 -2
- quasarr/api/config/__init__.py +195 -3
- quasarr/downloads/linkcrypters/al.py +8 -2
- quasarr/downloads/sources/al.py +7 -0
- quasarr/downloads/sources/wd.py +12 -2
- quasarr/providers/cloudflare.py +20 -1
- quasarr/providers/sessions/al.py +31 -1
- quasarr/providers/utils.py +22 -0
- quasarr/providers/version.py +1 -1
- quasarr/search/sources/al.py +0 -2
- quasarr/storage/setup.py +77 -4
- {quasarr-1.27.0.dist-info → quasarr-1.28.1.dist-info}/METADATA +12 -6
- {quasarr-1.27.0.dist-info → quasarr-1.28.1.dist-info}/RECORD +19 -19
- {quasarr-1.27.0.dist-info → quasarr-1.28.1.dist-info}/WHEEL +0 -0
- {quasarr-1.27.0.dist-info → quasarr-1.28.1.dist-info}/entry_points.txt +0 -0
- {quasarr-1.27.0.dist-info → quasarr-1.28.1.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.27.0.dist-info → quasarr-1.28.1.dist-info}/top_level.txt +0 -0
quasarr/__init__.py
CHANGED
|
@@ -17,7 +17,7 @@ from quasarr.providers import shared_state, version
|
|
|
17
17
|
from quasarr.providers.log import info, debug
|
|
18
18
|
from quasarr.providers.notifications import send_discord_message
|
|
19
19
|
from quasarr.providers.utils import extract_allowed_keys, extract_kv_pairs, is_valid_url, check_ip, check_flaresolverr, \
|
|
20
|
-
validate_address, Unbuffered
|
|
20
|
+
validate_address, Unbuffered, FALLBACK_USER_AGENT
|
|
21
21
|
from quasarr.storage.config import Config, get_clean_hostnames
|
|
22
22
|
from quasarr.storage.setup import path_config, hostnames_config, hostname_credentials_config, flaresolverr_config, \
|
|
23
23
|
jdownloader_config
|
|
@@ -100,19 +100,37 @@ def run():
|
|
|
100
100
|
shared_state.update("database", DataBase)
|
|
101
101
|
supported_hostnames = extract_allowed_keys(Config._DEFAULT_CONFIG, 'Hostnames')
|
|
102
102
|
shared_state.update("sites", [key.upper() for key in supported_hostnames])
|
|
103
|
-
shared_state.update("user_agent", "") # will be set by FlareSolverr
|
|
103
|
+
shared_state.update("user_agent", "") # will be set by FlareSolverr or fallback
|
|
104
104
|
shared_state.update("helper_active", False)
|
|
105
105
|
|
|
106
106
|
print(f'Config path: "{config_path}"')
|
|
107
107
|
|
|
108
|
+
# Check if FlareSolverr was previously skipped
|
|
109
|
+
skip_flaresolverr_db = DataBase("skip_flaresolverr")
|
|
110
|
+
flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
|
|
111
|
+
|
|
108
112
|
flaresolverr_url = Config('FlareSolverr').get('url')
|
|
109
|
-
if not flaresolverr_url:
|
|
113
|
+
if not flaresolverr_url and not flaresolverr_skipped:
|
|
110
114
|
flaresolverr_config(shared_state)
|
|
111
|
-
|
|
115
|
+
# Re-check after config - user may have skipped
|
|
116
|
+
flaresolverr_skipped = skip_flaresolverr_db.retrieve("skipped")
|
|
117
|
+
flaresolverr_url = Config('FlareSolverr').get('url')
|
|
118
|
+
|
|
119
|
+
if flaresolverr_skipped:
|
|
120
|
+
info('FlareSolverr setup skipped by user preference')
|
|
121
|
+
info('Some sites (AL) will not work without FlareSolverr. Configure it later in the web UI.')
|
|
122
|
+
# Set fallback user agent
|
|
123
|
+
shared_state.update("user_agent", FALLBACK_USER_AGENT)
|
|
124
|
+
print(f'User Agent (fallback): "{FALLBACK_USER_AGENT}"')
|
|
125
|
+
elif flaresolverr_url:
|
|
112
126
|
print(f'Flaresolverr URL: "{flaresolverr_url}"')
|
|
113
127
|
flaresolverr_check = check_flaresolverr(shared_state, flaresolverr_url)
|
|
114
128
|
if flaresolverr_check:
|
|
115
129
|
print(f'User Agent: "{shared_state.values["user_agent"]}"')
|
|
130
|
+
else:
|
|
131
|
+
info('FlareSolverr check failed - using fallback user agent')
|
|
132
|
+
shared_state.update("user_agent", FALLBACK_USER_AGENT)
|
|
133
|
+
print(f'User Agent (fallback): "{FALLBACK_USER_AGENT}"')
|
|
116
134
|
|
|
117
135
|
print("\n===== Hostnames =====")
|
|
118
136
|
try:
|
quasarr/api/__init__.py
CHANGED
|
@@ -31,7 +31,6 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
31
31
|
def index():
|
|
32
32
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
33
33
|
api_key = Config('API').get('key')
|
|
34
|
-
|
|
35
34
|
captcha_hint = ""
|
|
36
35
|
if protected:
|
|
37
36
|
plural = 's' if len(protected) > 1 else ''
|
|
@@ -103,6 +102,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
103
102
|
<div class="section">
|
|
104
103
|
<h2>🔧 Quick Actions</h2>
|
|
105
104
|
<p><button class="btn-primary" onclick="location.href='/hostnames'">Update Hostnames</button></p>
|
|
105
|
+
<p><button class="btn-primary" onclick="location.href='/flaresolverr'">Configure FlareSolverr</button></p>
|
|
106
106
|
<p><button class="btn-primary" onclick="location.href='/statistics'">View Statistics</button></p>
|
|
107
107
|
</div>
|
|
108
108
|
|
quasarr/api/arr/__init__.py
CHANGED
|
@@ -327,7 +327,7 @@ def setup_arr_routes(app):
|
|
|
327
327
|
else:
|
|
328
328
|
info(
|
|
329
329
|
f'Ignoring search request from {request_from} - only imdbid searches are supported')
|
|
330
|
-
releases = [
|
|
330
|
+
releases = [] # sonarr expects this but we will not support non-imdbid searches
|
|
331
331
|
|
|
332
332
|
items = ""
|
|
333
333
|
for release in releases:
|
|
@@ -353,7 +353,7 @@ def setup_arr_routes(app):
|
|
|
353
353
|
<enclosure url="{release.get("link", "")}" length="{release.get("size", 0)}" type="application/x-nzb" />
|
|
354
354
|
</item>'''
|
|
355
355
|
|
|
356
|
-
is_feed_request = not getattr(request.query, 'imdbid', '')
|
|
356
|
+
is_feed_request = not getattr(request.query, 'imdbid', '') and not getattr(request.query, 'q', '')
|
|
357
357
|
if is_feed_request and not items:
|
|
358
358
|
items = f'''
|
|
359
359
|
<item>
|
quasarr/api/config/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import signal
|
|
7
8
|
import threading
|
|
8
9
|
import time
|
|
@@ -11,12 +12,12 @@ from urllib.parse import urlparse
|
|
|
11
12
|
import requests
|
|
12
13
|
from bottle import request, response
|
|
13
14
|
|
|
14
|
-
from quasarr.providers.html_templates import render_form, render_button
|
|
15
|
+
from quasarr.providers.html_templates import render_form, render_button, render_fail
|
|
15
16
|
from quasarr.providers.log import info
|
|
16
17
|
from quasarr.providers.shared_state import extract_valid_hostname
|
|
17
|
-
from quasarr.providers.utils import extract_kv_pairs, extract_allowed_keys
|
|
18
|
+
from quasarr.providers.utils import extract_kv_pairs, extract_allowed_keys, check_flaresolverr
|
|
18
19
|
from quasarr.storage.config import Config
|
|
19
|
-
from quasarr.storage.setup import hostname_form_html, save_hostnames
|
|
20
|
+
from quasarr.storage.setup import hostname_form_html, save_hostnames, render_reconnect_success
|
|
20
21
|
from quasarr.storage.sqlite_database import DataBase
|
|
21
22
|
|
|
22
23
|
|
|
@@ -120,6 +121,197 @@ def setup_config(app, shared_state):
|
|
|
120
121
|
info(f'Skip login preference cleared for "{shorthand.upper()}"')
|
|
121
122
|
return {"success": True}
|
|
122
123
|
|
|
124
|
+
@app.get('/flaresolverr')
|
|
125
|
+
def flaresolverr_ui():
|
|
126
|
+
"""Web UI page for configuring FlareSolverr."""
|
|
127
|
+
skip_db = DataBase("skip_flaresolverr")
|
|
128
|
+
is_skipped = skip_db.retrieve("skipped")
|
|
129
|
+
current_url = Config('FlareSolverr').get('url') or ""
|
|
130
|
+
|
|
131
|
+
skip_indicator = ""
|
|
132
|
+
if is_skipped:
|
|
133
|
+
skip_indicator = '''
|
|
134
|
+
<div class="skip-indicator" style="margin-bottom:1rem; padding:0.75rem; background:var(--code-bg, #f8f9fa); border-radius:0.25rem; font-size:0.875rem;">
|
|
135
|
+
<span style="color:#dc3545;">⚠️ FlareSolverr setup was skipped</span>
|
|
136
|
+
<p style="margin:0.5rem 0 0 0; font-size:0.75rem; color:var(--secondary, #6c757d);">
|
|
137
|
+
Some sites (like AL) won't work until FlareSolverr is configured.
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
'''
|
|
141
|
+
|
|
142
|
+
form_content = f'''
|
|
143
|
+
{skip_indicator}
|
|
144
|
+
<span><a href="https://github.com/FlareSolverr/FlareSolverr?tab=readme-ov-file#installation" target="_blank">FlareSolverr</a>
|
|
145
|
+
must be running and reachable to Quasarr for some sites to work.</span><br><br>
|
|
146
|
+
<label for="url">FlareSolverr URL</label>
|
|
147
|
+
<input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1" value="{current_url}"><br>
|
|
148
|
+
'''
|
|
149
|
+
|
|
150
|
+
form_html = f'''
|
|
151
|
+
<form action="/api/flaresolverr" method="post" onsubmit="return handleSubmit(this)">
|
|
152
|
+
{form_content}
|
|
153
|
+
{render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
|
|
154
|
+
</form>
|
|
155
|
+
<p style="font-size:0.875rem; color:var(--secondary, #6c757d); margin-top:1rem;">
|
|
156
|
+
A restart is recommended after configuring FlareSolverr.
|
|
157
|
+
</p>
|
|
158
|
+
<div class="section-divider" style="margin-top:1.5rem; padding-top:1rem; border-top:1px solid var(--divider-color, #dee2e6);">
|
|
159
|
+
{render_button("Restart Quasarr", "secondary", {"type": "button", "onclick": "confirmRestart()"})}
|
|
160
|
+
</div>
|
|
161
|
+
<p>{render_button("Back", "secondary", {"onclick": "location.href='/';"})}</p>
|
|
162
|
+
<script>
|
|
163
|
+
var formSubmitted = false;
|
|
164
|
+
function handleSubmit(form) {{
|
|
165
|
+
if (formSubmitted) return false;
|
|
166
|
+
formSubmitted = true;
|
|
167
|
+
var btn = document.getElementById('submitBtn');
|
|
168
|
+
if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
|
|
169
|
+
return true;
|
|
170
|
+
}}
|
|
171
|
+
function confirmRestart() {{
|
|
172
|
+
if (confirm('Restart Quasarr now?')) {{
|
|
173
|
+
fetch('/api/restart', {{ method: 'POST' }})
|
|
174
|
+
.then(response => response.json())
|
|
175
|
+
.then(data => {{
|
|
176
|
+
if (data.success) {{
|
|
177
|
+
showRestartOverlay();
|
|
178
|
+
}}
|
|
179
|
+
}})
|
|
180
|
+
.catch(error => {{
|
|
181
|
+
showRestartOverlay();
|
|
182
|
+
}});
|
|
183
|
+
}}
|
|
184
|
+
}}
|
|
185
|
+
function showRestartOverlay() {{
|
|
186
|
+
document.body.innerHTML = `
|
|
187
|
+
<div style="text-align:center; padding:2rem; font-family:system-ui,-apple-system,sans-serif;">
|
|
188
|
+
<h2>Restarting Quasarr...</h2>
|
|
189
|
+
<p id="restartStatus">Waiting <span id="countdown">10</span> seconds...</p>
|
|
190
|
+
<div id="spinner" style="display:none; margin-top:1rem;">
|
|
191
|
+
<div style="display:inline-block; width:24px; height:24px; border:3px solid #ccc; border-top-color:#333; border-radius:50%; animation:spin 1s linear infinite;"></div>
|
|
192
|
+
<style>@keyframes spin {{ to {{ transform: rotate(360deg); }} }}</style>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
`;
|
|
196
|
+
startCountdown(10);
|
|
197
|
+
}}
|
|
198
|
+
function startCountdown(seconds) {{
|
|
199
|
+
var countdownEl = document.getElementById('countdown');
|
|
200
|
+
var statusEl = document.getElementById('restartStatus');
|
|
201
|
+
var spinnerEl = document.getElementById('spinner');
|
|
202
|
+
var remaining = seconds;
|
|
203
|
+
var interval = setInterval(function() {{
|
|
204
|
+
remaining--;
|
|
205
|
+
if (countdownEl) countdownEl.textContent = remaining;
|
|
206
|
+
if (remaining <= 0) {{
|
|
207
|
+
clearInterval(interval);
|
|
208
|
+
statusEl.textContent = 'Reconnecting...';
|
|
209
|
+
spinnerEl.style.display = 'block';
|
|
210
|
+
tryReconnect();
|
|
211
|
+
}}
|
|
212
|
+
}}, 1000);
|
|
213
|
+
}}
|
|
214
|
+
function tryReconnect() {{
|
|
215
|
+
var statusEl = document.getElementById('restartStatus');
|
|
216
|
+
var attempts = 0;
|
|
217
|
+
function attempt() {{
|
|
218
|
+
attempts++;
|
|
219
|
+
fetch('/', {{ method: 'HEAD', cache: 'no-store' }})
|
|
220
|
+
.then(response => {{
|
|
221
|
+
if (response.ok) {{
|
|
222
|
+
statusEl.textContent = 'Connected! Reloading...';
|
|
223
|
+
setTimeout(function() {{ window.location.href = '/'; }}, 500);
|
|
224
|
+
}} else {{
|
|
225
|
+
scheduleRetry();
|
|
226
|
+
}}
|
|
227
|
+
}})
|
|
228
|
+
.catch(function() {{
|
|
229
|
+
scheduleRetry();
|
|
230
|
+
}});
|
|
231
|
+
}}
|
|
232
|
+
function scheduleRetry() {{
|
|
233
|
+
statusEl.textContent = 'Reconnecting... (attempt ' + attempts + ')';
|
|
234
|
+
setTimeout(attempt, 1000);
|
|
235
|
+
}}
|
|
236
|
+
attempt();
|
|
237
|
+
}}
|
|
238
|
+
</script>
|
|
239
|
+
'''
|
|
240
|
+
return render_form("Configure FlareSolverr", form_html)
|
|
241
|
+
|
|
242
|
+
@app.post('/api/flaresolverr')
|
|
243
|
+
def set_flaresolverr_url():
|
|
244
|
+
"""Save FlareSolverr URL from web UI."""
|
|
245
|
+
url = request.forms.get('url', '').strip()
|
|
246
|
+
config = Config("FlareSolverr")
|
|
247
|
+
|
|
248
|
+
if not url:
|
|
249
|
+
return render_fail("Please provide a FlareSolverr URL.")
|
|
250
|
+
|
|
251
|
+
if not url.startswith("http://") and not url.startswith("https://"):
|
|
252
|
+
url = "http://" + url
|
|
253
|
+
|
|
254
|
+
# Validate URL format
|
|
255
|
+
if not re.search(r"/v\d+$", url):
|
|
256
|
+
return render_fail("FlareSolverr URL must end with /v1 (or similar version path).")
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
headers = {"Content-Type": "application/json"}
|
|
260
|
+
data = {
|
|
261
|
+
"cmd": "request.get",
|
|
262
|
+
"url": "http://www.google.com/",
|
|
263
|
+
"maxTimeout": 30000
|
|
264
|
+
}
|
|
265
|
+
resp = requests.post(url, headers=headers, json=data, timeout=30)
|
|
266
|
+
if resp.status_code == 200:
|
|
267
|
+
json_data = resp.json()
|
|
268
|
+
if json_data.get("status") == "ok":
|
|
269
|
+
config.save("url", url)
|
|
270
|
+
# Clear skip preference since we now have a working URL
|
|
271
|
+
DataBase("skip_flaresolverr").delete("skipped")
|
|
272
|
+
# Update user agent from FlareSolverr response
|
|
273
|
+
solution = json_data.get("solution", {})
|
|
274
|
+
solution_ua = solution.get("userAgent")
|
|
275
|
+
if solution_ua:
|
|
276
|
+
shared_state.update("user_agent", solution_ua)
|
|
277
|
+
info(f'FlareSolverr URL configured: "{url}"')
|
|
278
|
+
return render_reconnect_success(
|
|
279
|
+
"FlareSolverr URL saved successfully! A restart is recommended.")
|
|
280
|
+
else:
|
|
281
|
+
return render_fail(f"FlareSolverr returned unexpected status: {json_data.get('status')}")
|
|
282
|
+
except requests.RequestException as e:
|
|
283
|
+
return render_fail(f"Could not reach FlareSolverr: {str(e)}")
|
|
284
|
+
|
|
285
|
+
return render_fail("Could not reach FlareSolverr at that URL (expected HTTP 200).")
|
|
286
|
+
|
|
287
|
+
@app.get("/api/flaresolverr/status")
|
|
288
|
+
def get_flaresolverr_status():
|
|
289
|
+
"""Return FlareSolverr configuration status."""
|
|
290
|
+
response.content_type = 'application/json'
|
|
291
|
+
skip_db = DataBase("skip_flaresolverr")
|
|
292
|
+
is_skipped = bool(skip_db.retrieve("skipped"))
|
|
293
|
+
current_url = Config('FlareSolverr').get('url') or ""
|
|
294
|
+
|
|
295
|
+
# Test connection if URL is set
|
|
296
|
+
is_working = False
|
|
297
|
+
if current_url and not is_skipped:
|
|
298
|
+
is_working = check_flaresolverr(shared_state, current_url)
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
"skipped": is_skipped,
|
|
302
|
+
"url": current_url,
|
|
303
|
+
"working": is_working
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
@app.delete("/api/skip-flaresolverr")
|
|
307
|
+
def clear_skip_flaresolverr():
|
|
308
|
+
"""Clear skip FlareSolverr preference."""
|
|
309
|
+
response.content_type = 'application/json'
|
|
310
|
+
skip_db = DataBase("skip_flaresolverr")
|
|
311
|
+
skip_db.delete("skipped")
|
|
312
|
+
info('Skip FlareSolverr preference cleared')
|
|
313
|
+
return {"success": True}
|
|
314
|
+
|
|
123
315
|
@app.post("/api/restart")
|
|
124
316
|
def restart_quasarr():
|
|
125
317
|
"""Restart Quasarr. In Docker with the restart loop, exit(0) triggers restart."""
|
|
@@ -9,6 +9,7 @@ from Cryptodome.Cipher import AES
|
|
|
9
9
|
from PIL import Image, ImageChops
|
|
10
10
|
|
|
11
11
|
from quasarr.providers.log import info, debug
|
|
12
|
+
from quasarr.providers.utils import is_flaresolverr_available
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class CNL:
|
|
@@ -137,7 +138,7 @@ def decrypt_content(content_items: list[dict], mirror: str | None) -> list[str]:
|
|
|
137
138
|
decrypted_links.extend(urls)
|
|
138
139
|
debug(f"[Item {idx} | hoster={hoster_name}] Decrypted {len(urls)} URLs")
|
|
139
140
|
except Exception as e:
|
|
140
|
-
# Log and keep going; one bad item won
|
|
141
|
+
# Log and keep going; one bad item won't stop the rest.
|
|
141
142
|
info(f"[Item {idx} | hoster={hoster_name}] Error during decryption: {e}")
|
|
142
143
|
|
|
143
144
|
return decrypted_links
|
|
@@ -160,6 +161,11 @@ def calculate_pixel_based_difference(img1, img2):
|
|
|
160
161
|
|
|
161
162
|
|
|
162
163
|
def solve_captcha(hostname, shared_state, fetch_via_flaresolverr, fetch_via_requests_session):
|
|
164
|
+
# Check if FlareSolverr is available
|
|
165
|
+
if not is_flaresolverr_available(shared_state):
|
|
166
|
+
raise RuntimeError("FlareSolverr is required for CAPTCHA solving but is not configured. "
|
|
167
|
+
"Please configure FlareSolverr in the web UI.")
|
|
168
|
+
|
|
163
169
|
al = shared_state.values["config"]("Hostnames").get(hostname)
|
|
164
170
|
captcha_base = f"https://www.{al}/files/captcha"
|
|
165
171
|
|
|
@@ -195,7 +201,7 @@ def solve_captcha(hostname, shared_state, fetch_via_flaresolverr, fetch_via_requ
|
|
|
195
201
|
for image_id, raw_bytes in images:
|
|
196
202
|
img = Image.open(BytesIO(raw_bytes))
|
|
197
203
|
|
|
198
|
-
# if it
|
|
204
|
+
# if it's a palette (P) image with an indexed transparency, go through RGBA
|
|
199
205
|
if img.mode == "P" and "transparency" in img.info:
|
|
200
206
|
img = img.convert("RGBA")
|
|
201
207
|
|
quasarr/downloads/sources/al.py
CHANGED
|
@@ -17,6 +17,7 @@ from quasarr.providers.log import info, debug
|
|
|
17
17
|
from quasarr.providers.sessions.al import retrieve_and_validate_session, invalidate_session, unwrap_flaresolverr_body, \
|
|
18
18
|
fetch_via_flaresolverr, fetch_via_requests_session
|
|
19
19
|
from quasarr.providers.statistics import StatsHelper
|
|
20
|
+
from quasarr.providers.utils import is_flaresolverr_available
|
|
20
21
|
|
|
21
22
|
hostname = "al"
|
|
22
23
|
|
|
@@ -552,6 +553,12 @@ def get_al_download_links(shared_state, url, mirror, title, password):
|
|
|
552
553
|
This is set by the search module, not a user password.
|
|
553
554
|
"""
|
|
554
555
|
|
|
556
|
+
# Check if FlareSolverr is available - AL requires it
|
|
557
|
+
if not is_flaresolverr_available(shared_state):
|
|
558
|
+
info(f'"{hostname.upper()}" requires FlareSolverr which is not configured. '
|
|
559
|
+
f'Please configure FlareSolverr in the web UI to use this site.')
|
|
560
|
+
return {}
|
|
561
|
+
|
|
555
562
|
release_id = password # password field carries release_id for AL
|
|
556
563
|
|
|
557
564
|
al = shared_state.values["config"]("Hostnames").get(hostname)
|
quasarr/downloads/sources/wd.py
CHANGED
|
@@ -10,6 +10,7 @@ from bs4 import BeautifulSoup
|
|
|
10
10
|
|
|
11
11
|
from quasarr.providers.cloudflare import flaresolverr_get, is_cloudflare_challenge
|
|
12
12
|
from quasarr.providers.log import info, debug
|
|
13
|
+
from quasarr.providers.utils import is_flaresolverr_available
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def resolve_wd_redirect(url, user_agent):
|
|
@@ -47,8 +48,13 @@ def get_wd_download_links(shared_state, url, mirror, title, password):
|
|
|
47
48
|
try:
|
|
48
49
|
output = requests.get(url)
|
|
49
50
|
if output.status_code == 403 or is_cloudflare_challenge(output.text):
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if is_flaresolverr_available(shared_state):
|
|
52
|
+
info("WD is protected by Cloudflare. Using FlareSolverr to bypass protection.")
|
|
53
|
+
output = flaresolverr_get(shared_state, url)
|
|
54
|
+
else:
|
|
55
|
+
info("WD is protected by Cloudflare but FlareSolverr is not configured. "
|
|
56
|
+
"Please configure FlareSolverr in the web UI to access this site.")
|
|
57
|
+
return {"links": [], "imdb_id": None}
|
|
52
58
|
|
|
53
59
|
soup = BeautifulSoup(output.text, "html.parser")
|
|
54
60
|
|
|
@@ -76,6 +82,10 @@ def get_wd_download_links(shared_state, url, mirror, title, password):
|
|
|
76
82
|
link_tags = body.find_all(
|
|
77
83
|
"a", href=True, class_=lambda c: c and "background-" in c
|
|
78
84
|
)
|
|
85
|
+
except RuntimeError as e:
|
|
86
|
+
# Catch FlareSolverr not configured error
|
|
87
|
+
info(f"WD access failed: {e}")
|
|
88
|
+
return {"links": [], "imdb_id": None}
|
|
79
89
|
except Exception:
|
|
80
90
|
info(f"WD site has been updated. Grabbing download links for {title} not possible!")
|
|
81
91
|
return {"links": [], "imdb_id": None}
|
quasarr/providers/cloudflare.py
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import requests
|
|
6
6
|
from bs4 import BeautifulSoup
|
|
7
7
|
|
|
8
|
+
from quasarr.providers.utils import is_flaresolverr_available
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def is_cloudflare_challenge(html: str) -> bool:
|
|
10
12
|
soup = BeautifulSoup(html, "html.parser")
|
|
@@ -39,9 +41,14 @@ def update_session_via_flaresolverr(info,
|
|
|
39
41
|
sess,
|
|
40
42
|
target_url: str,
|
|
41
43
|
timeout: int = 60):
|
|
44
|
+
# Check if FlareSolverr is available
|
|
45
|
+
if not is_flaresolverr_available(shared_state):
|
|
46
|
+
info("FlareSolverr is not configured. Cannot bypass Cloudflare protection.")
|
|
47
|
+
return False
|
|
48
|
+
|
|
42
49
|
flaresolverr_url = shared_state.values["config"]('FlareSolverr').get('url')
|
|
43
50
|
if not flaresolverr_url:
|
|
44
|
-
info("Cannot proceed without FlareSolverr. Please
|
|
51
|
+
info("Cannot proceed without FlareSolverr. Please configure it in the web UI!")
|
|
45
52
|
return False
|
|
46
53
|
|
|
47
54
|
fs_payload = {
|
|
@@ -104,6 +111,12 @@ def ensure_session_cf_bypassed(info, shared_state, session, url, headers):
|
|
|
104
111
|
|
|
105
112
|
# If page is protected, try FlareSolverr
|
|
106
113
|
if resp.status_code == 403 or is_cloudflare_challenge(resp.text):
|
|
114
|
+
# Check if FlareSolverr is available before attempting bypass
|
|
115
|
+
if not is_flaresolverr_available(shared_state):
|
|
116
|
+
info("Cloudflare protection detected but FlareSolverr is not configured. "
|
|
117
|
+
"Please configure FlareSolverr in the web UI to access this site.")
|
|
118
|
+
return None, None, None
|
|
119
|
+
|
|
107
120
|
info("Encountered Cloudflare protection. Solving challenge with FlareSolverr...")
|
|
108
121
|
flaresolverr_result = update_session_via_flaresolverr(info, shared_state, session, url)
|
|
109
122
|
if not flaresolverr_result:
|
|
@@ -156,7 +169,13 @@ def flaresolverr_get(shared_state, url, timeout=60):
|
|
|
156
169
|
"""
|
|
157
170
|
Core function for performing a GET request via FlareSolverr only.
|
|
158
171
|
Used internally by FlareSolverrSession.get()
|
|
172
|
+
|
|
173
|
+
Returns None if FlareSolverr is not available.
|
|
159
174
|
"""
|
|
175
|
+
# Check if FlareSolverr is available
|
|
176
|
+
if not is_flaresolverr_available(shared_state):
|
|
177
|
+
raise RuntimeError("FlareSolverr is not configured. Please configure it in the web UI.")
|
|
178
|
+
|
|
160
179
|
flaresolverr_url = shared_state.values["config"]('FlareSolverr').get('url')
|
|
161
180
|
if not flaresolverr_url:
|
|
162
181
|
raise RuntimeError("FlareSolverr URL not configured in shared_state.")
|
quasarr/providers/sessions/al.py
CHANGED
|
@@ -13,19 +13,31 @@ from bs4 import BeautifulSoup
|
|
|
13
13
|
from requests.exceptions import Timeout, RequestException
|
|
14
14
|
|
|
15
15
|
from quasarr.providers.log import info, debug
|
|
16
|
-
from quasarr.providers.utils import is_site_usable
|
|
16
|
+
from quasarr.providers.utils import is_site_usable, is_flaresolverr_available
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class SkippedSiteError(Exception):
|
|
20
20
|
"""Raised when a site is skipped due to missing credentials or login being skipped."""
|
|
21
21
|
pass
|
|
22
22
|
|
|
23
|
+
|
|
24
|
+
class FlareSolverrNotAvailableError(Exception):
|
|
25
|
+
"""Raised when FlareSolverr is required but not available."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
23
29
|
hostname = "al"
|
|
24
30
|
|
|
25
31
|
SESSION_MAX_AGE_SECONDS = 24 * 60 * 60 # 24 hours
|
|
26
32
|
|
|
27
33
|
|
|
28
34
|
def create_and_persist_session(shared_state):
|
|
35
|
+
# AL requires FlareSolverr - check availability first
|
|
36
|
+
if not is_flaresolverr_available(shared_state):
|
|
37
|
+
info(f'"{hostname.upper()}" requires FlareSolverr which is not configured. '
|
|
38
|
+
f'Please configure FlareSolverr in the web UI to use this site.')
|
|
39
|
+
return None
|
|
40
|
+
|
|
29
41
|
cfg = shared_state.values["config"]("Hostnames")
|
|
30
42
|
host = cfg.get(hostname)
|
|
31
43
|
credentials_cfg = shared_state.values["config"](hostname.upper())
|
|
@@ -115,6 +127,11 @@ def retrieve_and_validate_session(shared_state):
|
|
|
115
127
|
if not is_site_usable(shared_state, hostname):
|
|
116
128
|
return None
|
|
117
129
|
|
|
130
|
+
# AL requires FlareSolverr - check availability
|
|
131
|
+
if not is_flaresolverr_available(shared_state):
|
|
132
|
+
info(f'"{hostname.upper()}" requires FlareSolverr which is not configured')
|
|
133
|
+
return None
|
|
134
|
+
|
|
118
135
|
db = shared_state.values["database"]("sessions")
|
|
119
136
|
stored = db.retrieve(hostname)
|
|
120
137
|
if not stored:
|
|
@@ -222,6 +239,19 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
222
239
|
– post_data: dict of form‐fields if method=="POST"
|
|
223
240
|
– timeout: seconds (FlareSolverr's internal maxTimeout = timeout*1000 ms)
|
|
224
241
|
"""
|
|
242
|
+
# Check if FlareSolverr is available
|
|
243
|
+
if not is_flaresolverr_available(shared_state):
|
|
244
|
+
info(f'"{hostname.upper()}" requires FlareSolverr which is not configured. '
|
|
245
|
+
f'Please configure FlareSolverr in the web UI.')
|
|
246
|
+
return {
|
|
247
|
+
"status_code": None,
|
|
248
|
+
"headers": {},
|
|
249
|
+
"json": None,
|
|
250
|
+
"text": "",
|
|
251
|
+
"cookies": [],
|
|
252
|
+
"error": "FlareSolverr is not configured"
|
|
253
|
+
}
|
|
254
|
+
|
|
225
255
|
flaresolverr_url = shared_state.values["config"]('FlareSolverr').get('url')
|
|
226
256
|
|
|
227
257
|
sess = retrieve_and_validate_session(shared_state)
|
quasarr/providers/utils.py
CHANGED
|
@@ -9,6 +9,9 @@ from urllib.parse import urlparse
|
|
|
9
9
|
|
|
10
10
|
import requests
|
|
11
11
|
|
|
12
|
+
# Fallback user agent when FlareSolverr is not available
|
|
13
|
+
FALLBACK_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class Unbuffered(object):
|
|
14
17
|
def __init__(self, stream):
|
|
@@ -130,6 +133,25 @@ def validate_address(address, name):
|
|
|
130
133
|
f"Error: {name} '{address}' is invalid. It must contain 1 or 2 colons, but it has {colon_count}.")
|
|
131
134
|
|
|
132
135
|
|
|
136
|
+
def is_flaresolverr_available(shared_state):
|
|
137
|
+
"""
|
|
138
|
+
Check if FlareSolverr is configured and available.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
bool: True if FlareSolverr URL is set and not skipped, False otherwise
|
|
142
|
+
"""
|
|
143
|
+
# Check if FlareSolverr was skipped
|
|
144
|
+
if shared_state.values["database"]("skip_flaresolverr").retrieve("skipped"):
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# Check if FlareSolverr URL is configured
|
|
148
|
+
flaresolverr_url = shared_state.values["config"]('FlareSolverr').get('url')
|
|
149
|
+
if not flaresolverr_url:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
|
|
133
155
|
def is_site_usable(shared_state, shorthand):
|
|
134
156
|
"""
|
|
135
157
|
Check if a site is fully configured and usable.
|
quasarr/providers/version.py
CHANGED
quasarr/search/sources/al.py
CHANGED
quasarr/storage/setup.py
CHANGED
|
@@ -19,7 +19,7 @@ from quasarr.providers.html_templates import render_button, render_form, render_
|
|
|
19
19
|
render_centered_html
|
|
20
20
|
from quasarr.providers.log import info
|
|
21
21
|
from quasarr.providers.shared_state import extract_valid_hostname
|
|
22
|
-
from quasarr.providers.utils import extract_kv_pairs, extract_allowed_keys
|
|
22
|
+
from quasarr.providers.utils import extract_kv_pairs, extract_allowed_keys, FALLBACK_USER_AGENT
|
|
23
23
|
from quasarr.providers.web_server import Server
|
|
24
24
|
from quasarr.storage.config import Config
|
|
25
25
|
from quasarr.storage.sqlite_database import DataBase
|
|
@@ -789,10 +789,39 @@ def flaresolverr_config(shared_state):
|
|
|
789
789
|
<input type="text" id="url" name="url" placeholder="http://192.168.0.1:8191/v1"><br>
|
|
790
790
|
'''
|
|
791
791
|
form_html = f'''
|
|
792
|
+
<style>
|
|
793
|
+
.button-row {{
|
|
794
|
+
display: flex;
|
|
795
|
+
gap: 0.75rem;
|
|
796
|
+
justify-content: center;
|
|
797
|
+
flex-wrap: wrap;
|
|
798
|
+
margin-top: 1rem;
|
|
799
|
+
}}
|
|
800
|
+
.btn-warning {{
|
|
801
|
+
background-color: #ffc107;
|
|
802
|
+
color: #212529;
|
|
803
|
+
border: 1.5px solid #d39e00;
|
|
804
|
+
padding: 0.5rem 1rem;
|
|
805
|
+
font-size: 1rem;
|
|
806
|
+
border-radius: 0.5rem;
|
|
807
|
+
font-weight: 500;
|
|
808
|
+
cursor: pointer;
|
|
809
|
+
}}
|
|
810
|
+
.btn-warning:hover {{
|
|
811
|
+
background-color: #e0a800;
|
|
812
|
+
border-color: #c69500;
|
|
813
|
+
}}
|
|
814
|
+
</style>
|
|
792
815
|
<form action="/api/flaresolverr" method="post" onsubmit="return handleSubmit(this)">
|
|
793
816
|
{form_content}
|
|
794
|
-
|
|
817
|
+
<div class="button-row">
|
|
818
|
+
{render_button("Save", "primary", {"type": "submit", "id": "submitBtn"})}
|
|
819
|
+
<button type="button" class="btn-warning" id="skipBtn" onclick="skipFlaresolverr()">Skip for now</button>
|
|
820
|
+
</div>
|
|
795
821
|
</form>
|
|
822
|
+
<p style="font-size:0.875rem; color:var(--secondary, #6c757d); margin-top:1rem;">
|
|
823
|
+
Skipping will allow Quasarr to start, but some sites (like AL) won't work without FlareSolverr.
|
|
824
|
+
</p>
|
|
796
825
|
<script>
|
|
797
826
|
var formSubmitted = false;
|
|
798
827
|
function handleSubmit(form) {{
|
|
@@ -800,12 +829,54 @@ def flaresolverr_config(shared_state):
|
|
|
800
829
|
formSubmitted = true;
|
|
801
830
|
var btn = document.getElementById('submitBtn');
|
|
802
831
|
if (btn) {{ btn.disabled = true; btn.textContent = 'Saving...'; }}
|
|
832
|
+
document.getElementById('skipBtn').disabled = true;
|
|
803
833
|
return true;
|
|
804
834
|
}}
|
|
835
|
+
function skipFlaresolverr() {{
|
|
836
|
+
if (formSubmitted) return;
|
|
837
|
+
formSubmitted = true;
|
|
838
|
+
var skipBtn = document.getElementById('skipBtn');
|
|
839
|
+
var submitBtn = document.getElementById('submitBtn');
|
|
840
|
+
if (skipBtn) {{ skipBtn.disabled = true; skipBtn.textContent = 'Skipping...'; }}
|
|
841
|
+
if (submitBtn) {{ submitBtn.disabled = true; }}
|
|
842
|
+
|
|
843
|
+
fetch('/api/flaresolverr/skip', {{ method: 'POST' }})
|
|
844
|
+
.then(response => {{
|
|
845
|
+
if (response.ok) {{
|
|
846
|
+
window.location.href = '/skip-success';
|
|
847
|
+
}} else {{
|
|
848
|
+
alert('Failed to skip FlareSolverr setup');
|
|
849
|
+
formSubmitted = false;
|
|
850
|
+
if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
|
|
851
|
+
if (submitBtn) {{ submitBtn.disabled = false; }}
|
|
852
|
+
}}
|
|
853
|
+
}})
|
|
854
|
+
.catch(error => {{
|
|
855
|
+
alert('Error: ' + error.message);
|
|
856
|
+
formSubmitted = false;
|
|
857
|
+
if (skipBtn) {{ skipBtn.disabled = false; skipBtn.textContent = 'Skip for now'; }}
|
|
858
|
+
if (submitBtn) {{ submitBtn.disabled = false; }}
|
|
859
|
+
}});
|
|
860
|
+
}}
|
|
805
861
|
</script>
|
|
806
862
|
'''
|
|
807
863
|
return render_form("Set FlareSolverr URL", form_html)
|
|
808
864
|
|
|
865
|
+
@app.get('/skip-success')
|
|
866
|
+
def skip_success():
|
|
867
|
+
return render_reconnect_success(
|
|
868
|
+
"FlareSolverr setup skipped. Some sites (like AL) won't work. You can configure it later in the web UI.")
|
|
869
|
+
|
|
870
|
+
@app.post('/api/flaresolverr/skip')
|
|
871
|
+
def skip_flaresolverr():
|
|
872
|
+
"""Skip FlareSolverr setup and continue startup."""
|
|
873
|
+
DataBase("skip_flaresolverr").update_store("skipped", "true")
|
|
874
|
+
# Set fallback user agent
|
|
875
|
+
shared_state.update("user_agent", FALLBACK_USER_AGENT)
|
|
876
|
+
info('FlareSolverr setup skipped by user choice')
|
|
877
|
+
quasarr.providers.web_server.temp_server_success = True
|
|
878
|
+
return {"success": True}
|
|
879
|
+
|
|
809
880
|
@app.post('/api/flaresolverr')
|
|
810
881
|
def set_flaresolverr_url():
|
|
811
882
|
url = request.forms.get('url').strip()
|
|
@@ -825,6 +896,8 @@ def flaresolverr_config(shared_state):
|
|
|
825
896
|
resp = requests.post(url, headers=headers, json=data, timeout=30)
|
|
826
897
|
if resp.status_code == 200:
|
|
827
898
|
config.save("url", url)
|
|
899
|
+
# Clear skip preference since we now have a working URL
|
|
900
|
+
DataBase("skip_flaresolverr").delete("skipped")
|
|
828
901
|
print(f'Using Flaresolverr URL: "{url}"')
|
|
829
902
|
quasarr.providers.web_server.temp_server_success = True
|
|
830
903
|
return render_reconnect_success("FlareSolverr URL saved successfully!")
|
|
@@ -836,10 +909,10 @@ def flaresolverr_config(shared_state):
|
|
|
836
909
|
return render_fail("Could not reach FlareSolverr at that URL (expected HTTP 200).")
|
|
837
910
|
|
|
838
911
|
info(
|
|
839
|
-
'"flaresolverr" URL is required for
|
|
912
|
+
'"flaresolverr" URL is required for some sites (like AL). '
|
|
840
913
|
f'Starting web server for config at: "{shared_state.values["internal_address"]}".'
|
|
841
914
|
)
|
|
842
|
-
info("Please enter your FlareSolverr URL now
|
|
915
|
+
info("Please enter your FlareSolverr URL now, or skip to allow Quasarr to launch!")
|
|
843
916
|
return Server(app, listen='0.0.0.0', port=shared_state.values['port']).serve_temporarily()
|
|
844
917
|
|
|
845
918
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quasarr
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.28.1
|
|
4
4
|
Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
|
|
5
5
|
Home-page: https://github.com/rix1337/Quasarr
|
|
6
6
|
Author: rix1337
|
|
@@ -45,22 +45,28 @@ Quasarr will confidently handle the rest. Some CAPTCHA types require [Tampermonk
|
|
|
45
45
|
|
|
46
46
|
# Instructions
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
# Instructions
|
|
49
|
+
|
|
50
|
+
1. Set up and run [JDownloader 2](https://jdownloader.org/download/index)
|
|
51
|
+
2. Configure the integrations below
|
|
52
|
+
3. (Optional) Set up [FlareSolverr 3](https://github.com/FlareSolverr/FlareSolverr) for sites that require it
|
|
51
53
|
|
|
52
54
|
> **Finding your Quasarr URL and API Key**
|
|
53
55
|
> Both values are shown in the console output under **API Information**, or in the Quasarr web UI.
|
|
54
56
|
|
|
55
57
|
---
|
|
56
58
|
|
|
57
|
-
## FlareSolverr
|
|
59
|
+
## FlareSolverr (Optional)
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
FlareSolverr is **optional** but **required for some sites** (e.g., AL) that use Cloudflare protection. You can skip FlareSolverr during setup and configure it later via the web UI.
|
|
62
|
+
|
|
63
|
+
If using FlareSolverr, provide your URL including the version path:
|
|
60
64
|
```
|
|
61
65
|
http://192.168.1.1:8191/v1
|
|
62
66
|
```
|
|
63
67
|
|
|
68
|
+
> **Note:** Sites requiring FlareSolverr will show a warning in the console when it's not configured.
|
|
69
|
+
|
|
64
70
|
---
|
|
65
71
|
|
|
66
72
|
## Quasarr
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
quasarr/__init__.py,sha256=
|
|
2
|
-
quasarr/api/__init__.py,sha256=
|
|
3
|
-
quasarr/api/arr/__init__.py,sha256=
|
|
1
|
+
quasarr/__init__.py,sha256=cEtxN2AuwKvrxpIvAR7UL997VtYQ4iN3Eo3ZnP-WjZQ,14682
|
|
2
|
+
quasarr/api/__init__.py,sha256=UOyyuOjF2WN6Um2wwQNHjFA-Rj0prb11z8SCjbifKJU,6940
|
|
3
|
+
quasarr/api/arr/__init__.py,sha256=TTu67lbAPYERHltby_BZ5GzhUOe9T78K3LzU1R9-3S8,17498
|
|
4
4
|
quasarr/api/captcha/__init__.py,sha256=IhJVn9iWtb01P2yfoqtOF7wSsiXizES7HNn29BX1uHk,60268
|
|
5
|
-
quasarr/api/config/__init__.py,sha256=
|
|
5
|
+
quasarr/api/config/__init__.py,sha256=0oN7-2uev6K3SSLEv7kixBY5_kS9vT6kiaQRS2frbgA,13749
|
|
6
6
|
quasarr/api/sponsors_helper/__init__.py,sha256=kAZabPlplPYRG6Uw7ZHTk5uypualwvhs-NoTOjQhhhA,6369
|
|
7
7
|
quasarr/api/statistics/__init__.py,sha256=NrBAjjHkIUE95HhPUGIfNqh2IqBqJ_zm00S90Y-Qnus,7038
|
|
8
8
|
quasarr/downloads/__init__.py,sha256=bpNg6LNqoqpnA-U7uVDhq9jM6VYB2bkekCw1XxZRpWM,11613
|
|
9
9
|
quasarr/downloads/linkcrypters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
quasarr/downloads/linkcrypters/al.py,sha256=
|
|
10
|
+
quasarr/downloads/linkcrypters/al.py,sha256=mfUG5VclC_-FcGoZL9zHYD7dz7X_YpaNmoKkgiyl9-0,8812
|
|
11
11
|
quasarr/downloads/linkcrypters/filecrypt.py,sha256=GT51x_MG_hW4IpOF6OvL5r-2mTnMijI8K7_1D5Bfn4U,18884
|
|
12
12
|
quasarr/downloads/linkcrypters/hide.py,sha256=8YmNm49JmVa1zZdTHpjK9gnQrX435Cq5fo4JTNsIpds,4850
|
|
13
13
|
quasarr/downloads/packages/__init__.py,sha256=Cub3ztyFYBm30HprvZl7qvfYnjaOH9FsRWDLEyCPHkE,18305
|
|
14
14
|
quasarr/downloads/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
quasarr/downloads/sources/al.py,sha256=
|
|
15
|
+
quasarr/downloads/sources/al.py,sha256=g587VESZRZHZ03uxHKpufEr5qAtzbyGLmoijksU35jk,27297
|
|
16
16
|
quasarr/downloads/sources/by.py,sha256=kmUTn3izayRCV7W-t0E4kYE8qTbt3L3reCLozfvRGcU,3807
|
|
17
17
|
quasarr/downloads/sources/dd.py,sha256=8X2tOle3qTq0b60Aa3o0uqp2vNELDHYYj99ERI7U_X0,2971
|
|
18
18
|
quasarr/downloads/sources/dj.py,sha256=wY00hVRNhucZBG1hfExKqayhP1ISD8FFQm7wHYxutOk,404
|
|
@@ -26,10 +26,10 @@ quasarr/downloads/sources/nx.py,sha256=ESWGDz07m2kntvTGoNlL9Gleld-HUl9ckphaJA9PU
|
|
|
26
26
|
quasarr/downloads/sources/sf.py,sha256=ecPHNsNiRNXTfQX9MBLzJKqrEc1IpkrKkBXpihTPhkE,6352
|
|
27
27
|
quasarr/downloads/sources/sj.py,sha256=Bkv0c14AXct50n_viaTNK3bYG-Bpvx8x2D0UN_6gm78,404
|
|
28
28
|
quasarr/downloads/sources/sl.py,sha256=jWprFt1Hew1T67fB1O_pc9YWgc3NVh30KXSwSyS50Pc,3186
|
|
29
|
-
quasarr/downloads/sources/wd.py,sha256=
|
|
29
|
+
quasarr/downloads/sources/wd.py,sha256=kr1I1uJa7ZkEPH2LA6alXTJEn0LBPgLCwIh3wLXwCv8,4447
|
|
30
30
|
quasarr/downloads/sources/wx.py,sha256=EygMfkgBMZYj3tSk4gvj5DcojkRswGhY_y8FMPNnVeU,4834
|
|
31
31
|
quasarr/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
|
-
quasarr/providers/cloudflare.py,sha256=
|
|
32
|
+
quasarr/providers/cloudflare.py,sha256=9iet8runc2VHVcA0_2z1qkrL6D5JKqz1ndktqCgsJFs,7873
|
|
33
33
|
quasarr/providers/html_images.py,sha256=2n82gTJg7E7q2ytPFN4FWouYTIlmPYu_iHFtG7uktIA,28482
|
|
34
34
|
quasarr/providers/html_templates.py,sha256=YMwdi7l_tHL0-qsUnwi4aPrE5Q6ZDxbjsPIfr-6uemY,10265
|
|
35
35
|
quasarr/providers/imdb_metadata.py,sha256=10L4kZkt6Fg0HGdNcc6KCtIQHRYEqdarLyaMVN6mT8w,4843
|
|
@@ -39,17 +39,17 @@ quasarr/providers/notifications.py,sha256=bohT-6yudmFnmZMc3BwCGX0n1HdzSVgQG_LDZm
|
|
|
39
39
|
quasarr/providers/obfuscated.py,sha256=xPI3WrteOiZN5BgNDp0CURcYfkRrdnRCz_cT7BpzIJU,1363310
|
|
40
40
|
quasarr/providers/shared_state.py,sha256=-TIiH2lkCfovq7bzUZicpUjXEjS87ZHCcevsFgySOqw,29944
|
|
41
41
|
quasarr/providers/statistics.py,sha256=cEQixYnDMDqtm5wWe40E_2ucyo4mD0n3SrfelhQi1L8,6452
|
|
42
|
-
quasarr/providers/utils.py,sha256=
|
|
43
|
-
quasarr/providers/version.py,sha256=
|
|
42
|
+
quasarr/providers/utils.py,sha256=TpNuuUfH811CfROf41uraKbFBvQ6on-7dCz7IK5i3iI,5836
|
|
43
|
+
quasarr/providers/version.py,sha256=vDqxZ2LfAhgNMpwNMDQElc1VOqSdZH_Ub6AwG-JPRVY,4004
|
|
44
44
|
quasarr/providers/web_server.py,sha256=AYd0KRxdDWMBr87BP8wlSMuL4zZo0I_rY-vHBai6Pfg,1688
|
|
45
45
|
quasarr/providers/sessions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
-
quasarr/providers/sessions/al.py,sha256=
|
|
46
|
+
quasarr/providers/sessions/al.py,sha256=WXue9LaT4y0BzsbKtHbN6bb_72c4AZZWR9NP-vg9-cg,12462
|
|
47
47
|
quasarr/providers/sessions/dd.py,sha256=I9tQCdxmhtbdmRUhKlkM1ZJjja1N1bdRSEZdfbSRWkc,2832
|
|
48
48
|
quasarr/providers/sessions/dl.py,sha256=6tch4QzkdwbU6XoNQE22T5nz5eoKlBVGa0eyjXJHPyA,5574
|
|
49
49
|
quasarr/providers/sessions/nx.py,sha256=vZDVnu4sKizx1wyKmrTr8itGURSEIVOtzMI5efjF6Oo,2924
|
|
50
50
|
quasarr/search/__init__.py,sha256=V59LIiC75mQvasDdTjiWZRbPD1jXO1lhXlKeNVX0iOc,5726
|
|
51
51
|
quasarr/search/sources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
-
quasarr/search/sources/al.py,sha256=
|
|
52
|
+
quasarr/search/sources/al.py,sha256=AmTQDc6voMXh8Sh_IrJX_3gQ5UYKMri-aGaE-wbI3ik,17152
|
|
53
53
|
quasarr/search/sources/by.py,sha256=vnE3L43V8suPhPHcn6LVxKO1e3mJaDRqIIMg2BGxr_g,7915
|
|
54
54
|
quasarr/search/sources/dd.py,sha256=pVpdHLZlw2CYklBf_YLkeDWbCNsDLR2iecccR2c2RyI,4889
|
|
55
55
|
quasarr/search/sources/dj.py,sha256=2HIdg5ddXP4DtjHlyXmuQ8QVhOPt3Hh2kL4uxhFJK-8,7074
|
|
@@ -68,11 +68,11 @@ quasarr/search/sources/wd.py,sha256=O02j3irSlVw2qES82g_qHuavAk-njjSRH1dHSCnOUas,
|
|
|
68
68
|
quasarr/search/sources/wx.py,sha256=_h1M6GhkJzixwHscrt0lMOnPSEDP1Xl24OypEe8Jy7c,12906
|
|
69
69
|
quasarr/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
70
|
quasarr/storage/config.py,sha256=SSTgIce2FVYoVTK_6OCU3msknhxuLA3EC4Kcrrf_dxQ,6378
|
|
71
|
-
quasarr/storage/setup.py,sha256
|
|
71
|
+
quasarr/storage/setup.py,sha256=-EZotV31hqJpd40sXYvvt0XwemLyb_7f4oFurCBEqZA,41436
|
|
72
72
|
quasarr/storage/sqlite_database.py,sha256=yMqFQfKf0k7YS-6Z3_7pj4z1GwWSXJ8uvF4IydXsuTE,3554
|
|
73
|
-
quasarr-1.
|
|
74
|
-
quasarr-1.
|
|
75
|
-
quasarr-1.
|
|
76
|
-
quasarr-1.
|
|
77
|
-
quasarr-1.
|
|
78
|
-
quasarr-1.
|
|
73
|
+
quasarr-1.28.1.dist-info/licenses/LICENSE,sha256=QQFCAfDgt7lSA8oSWDHIZ9aTjFbZaBJdjnGOHkuhK7k,1060
|
|
74
|
+
quasarr-1.28.1.dist-info/METADATA,sha256=RGCsUV1FPs0Qi6uuPzcoYpt9CFXaMf6lkQThp86gXSw,11019
|
|
75
|
+
quasarr-1.28.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
76
|
+
quasarr-1.28.1.dist-info/entry_points.txt,sha256=gXi8mUKsIqKVvn-bOc8E5f04sK_KoMCC-ty6b2Hf-jc,40
|
|
77
|
+
quasarr-1.28.1.dist-info/top_level.txt,sha256=dipJdaRda5ruTZkoGfZU60bY4l9dtPlmOWwxK_oGSF0,8
|
|
78
|
+
quasarr-1.28.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|