quasarr 2.6.0__py3-none-any.whl → 2.7.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 +71 -61
- quasarr/api/__init__.py +3 -4
- quasarr/api/arr/__init__.py +159 -56
- quasarr/api/captcha/__init__.py +203 -154
- quasarr/api/config/__init__.py +1 -1
- quasarr/api/jdownloader/__init__.py +19 -12
- quasarr/downloads/__init__.py +12 -8
- quasarr/downloads/linkcrypters/al.py +3 -3
- quasarr/downloads/linkcrypters/filecrypt.py +1 -2
- quasarr/downloads/packages/__init__.py +62 -88
- quasarr/downloads/sources/al.py +3 -3
- quasarr/downloads/sources/by.py +3 -3
- quasarr/downloads/sources/he.py +8 -9
- quasarr/downloads/sources/nk.py +3 -3
- quasarr/downloads/sources/sl.py +6 -1
- quasarr/downloads/sources/wd.py +132 -62
- quasarr/downloads/sources/wx.py +11 -17
- quasarr/providers/auth.py +9 -13
- quasarr/providers/cloudflare.py +50 -4
- quasarr/providers/imdb_metadata.py +0 -2
- quasarr/providers/jd_cache.py +64 -90
- quasarr/providers/log.py +226 -8
- quasarr/providers/myjd_api.py +116 -94
- quasarr/providers/sessions/al.py +20 -22
- quasarr/providers/sessions/dd.py +1 -1
- quasarr/providers/sessions/dl.py +8 -10
- quasarr/providers/sessions/nx.py +1 -1
- quasarr/providers/shared_state.py +26 -15
- quasarr/providers/utils.py +15 -6
- quasarr/providers/version.py +1 -1
- quasarr/search/__init__.py +91 -78
- quasarr/search/sources/al.py +19 -23
- quasarr/search/sources/by.py +6 -6
- quasarr/search/sources/dd.py +8 -10
- quasarr/search/sources/dj.py +15 -18
- quasarr/search/sources/dl.py +25 -37
- quasarr/search/sources/dt.py +13 -15
- quasarr/search/sources/dw.py +24 -16
- quasarr/search/sources/fx.py +25 -11
- quasarr/search/sources/he.py +16 -14
- quasarr/search/sources/hs.py +7 -7
- quasarr/search/sources/mb.py +7 -7
- quasarr/search/sources/nk.py +24 -25
- quasarr/search/sources/nx.py +22 -15
- quasarr/search/sources/sf.py +18 -9
- quasarr/search/sources/sj.py +7 -7
- quasarr/search/sources/sl.py +26 -14
- quasarr/search/sources/wd.py +63 -9
- quasarr/search/sources/wx.py +33 -47
- quasarr/storage/config.py +1 -3
- quasarr/storage/setup.py +13 -4
- {quasarr-2.6.0.dist-info → quasarr-2.7.0.dist-info}/METADATA +4 -1
- quasarr-2.7.0.dist-info/RECORD +84 -0
- quasarr-2.6.0.dist-info/RECORD +0 -84
- {quasarr-2.6.0.dist-info → quasarr-2.7.0.dist-info}/WHEEL +0 -0
- {quasarr-2.6.0.dist-info → quasarr-2.7.0.dist-info}/entry_points.txt +0 -0
- {quasarr-2.6.0.dist-info → quasarr-2.7.0.dist-info}/licenses/LICENSE +0 -0
quasarr/downloads/sources/wd.py
CHANGED
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
import re
|
|
6
|
+
import uuid
|
|
6
7
|
from urllib.parse import urljoin
|
|
7
8
|
|
|
8
9
|
import requests
|
|
9
10
|
from bs4 import BeautifulSoup
|
|
10
11
|
|
|
11
|
-
from quasarr.providers.cloudflare import
|
|
12
|
+
from quasarr.providers.cloudflare import (
|
|
13
|
+
flaresolverr_create_session,
|
|
14
|
+
flaresolverr_destroy_session,
|
|
15
|
+
flaresolverr_get,
|
|
16
|
+
is_cloudflare_challenge,
|
|
17
|
+
)
|
|
12
18
|
from quasarr.providers.hostname_issues import mark_hostname_issue
|
|
13
19
|
from quasarr.providers.log import debug, info
|
|
14
20
|
from quasarr.providers.utils import is_flaresolverr_available
|
|
@@ -16,11 +22,29 @@ from quasarr.providers.utils import is_flaresolverr_available
|
|
|
16
22
|
hostname = "wd"
|
|
17
23
|
|
|
18
24
|
|
|
19
|
-
def resolve_wd_redirect(url,
|
|
25
|
+
def resolve_wd_redirect(shared_state, url, session_id=None):
|
|
20
26
|
"""
|
|
21
27
|
Follow redirects for a WD mirror URL and return the final destination.
|
|
22
28
|
"""
|
|
29
|
+
# Try FlareSolverr first if available and session_id is provided
|
|
30
|
+
if session_id and is_flaresolverr_available(shared_state):
|
|
31
|
+
try:
|
|
32
|
+
r = flaresolverr_get(shared_state, url, session_id=session_id)
|
|
33
|
+
if r.status_code == 200:
|
|
34
|
+
if r.url.endswith("/404.html"):
|
|
35
|
+
return None
|
|
36
|
+
return r.url
|
|
37
|
+
else:
|
|
38
|
+
info(f"WD blocked attempt to resolve {url}. Status: {r.status_code}")
|
|
39
|
+
except Exception as e:
|
|
40
|
+
info(f"FlareSolverr error fetching redirected URL for {url}: {e}")
|
|
41
|
+
# Fallback to requests if FlareSolverr fails?
|
|
42
|
+
# For now, let's assume if FS is configured we should rely on it or fail.
|
|
43
|
+
# But the user asked to reconsider "without cloudflare".
|
|
44
|
+
|
|
45
|
+
# Fallback to regular requests if FlareSolverr not used or failed/not configured
|
|
23
46
|
try:
|
|
47
|
+
user_agent = shared_state.values["user_agent"]
|
|
24
48
|
r = requests.get(
|
|
25
49
|
url,
|
|
26
50
|
allow_redirects=True,
|
|
@@ -33,6 +57,11 @@ def resolve_wd_redirect(url, user_agent):
|
|
|
33
57
|
debug(f"Redirected from {resp.url} to {r.url}")
|
|
34
58
|
return r.url
|
|
35
59
|
else:
|
|
60
|
+
# If no history, maybe it wasn't a redirect or it blocked us?
|
|
61
|
+
# WD usually redirects. If we get 200 OK but same URL, it might be a block page or direct link?
|
|
62
|
+
# The original code assumed if no history -> blocked.
|
|
63
|
+
# But if it's a direct link, history is empty.
|
|
64
|
+
# Let's trust the original logic: "WD blocked attempt..."
|
|
36
65
|
info(
|
|
37
66
|
f"WD blocked attempt to resolve {url}. Your IP may be banned. Try again later."
|
|
38
67
|
)
|
|
@@ -52,32 +81,69 @@ def get_wd_download_links(shared_state, url, mirror, title, password):
|
|
|
52
81
|
"""
|
|
53
82
|
|
|
54
83
|
wd = shared_state.values["config"]("Hostnames").get("wd")
|
|
55
|
-
|
|
84
|
+
|
|
85
|
+
# Try normal request first
|
|
86
|
+
text = None
|
|
87
|
+
status_code = None
|
|
88
|
+
session_id = None
|
|
56
89
|
|
|
57
90
|
try:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
91
|
+
headers = {"User-Agent": shared_state.values["user_agent"]}
|
|
92
|
+
r = requests.get(url, headers=headers, timeout=10)
|
|
93
|
+
# Don't raise for status yet, check for 403/challenge
|
|
94
|
+
if r.status_code == 403 or is_cloudflare_challenge(r.text):
|
|
95
|
+
raise requests.RequestException("Cloudflare protection detected")
|
|
96
|
+
r.raise_for_status()
|
|
97
|
+
text = r.text
|
|
98
|
+
status_code = r.status_code
|
|
99
|
+
except Exception as e:
|
|
100
|
+
# If blocked or failed, try FlareSolverr
|
|
101
|
+
if is_flaresolverr_available(shared_state):
|
|
102
|
+
debug(
|
|
103
|
+
f"Encountered Cloudflare on {hostname} download. Trying FlareSolverr..."
|
|
104
|
+
)
|
|
105
|
+
# Create a temporary FlareSolverr session for this download attempt
|
|
106
|
+
session_id = str(uuid.uuid4())
|
|
107
|
+
created_session = flaresolverr_create_session(shared_state, session_id)
|
|
108
|
+
if not created_session:
|
|
61
109
|
info(
|
|
62
|
-
"
|
|
110
|
+
"Could not create FlareSolverr session. Proceeding without session..."
|
|
63
111
|
)
|
|
64
|
-
|
|
112
|
+
session_id = None
|
|
65
113
|
else:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
debug(f"Created FlareSolverr session: {session_id}")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
r = flaresolverr_get(shared_state, url, session_id=session_id)
|
|
118
|
+
if r.status_code == 403 or is_cloudflare_challenge(r.text):
|
|
119
|
+
info("Could not bypass Cloudflare protection with FlareSolverr!")
|
|
120
|
+
mark_hostname_issue(
|
|
121
|
+
hostname, "download", "Cloudflare challenge failed"
|
|
122
|
+
)
|
|
123
|
+
if session_id:
|
|
124
|
+
flaresolverr_destroy_session(shared_state, session_id)
|
|
125
|
+
return {"links": [], "imdb_id": None}
|
|
126
|
+
text = r.text
|
|
127
|
+
status_code = r.status_code
|
|
128
|
+
except RuntimeError as fs_err:
|
|
129
|
+
info(f"WD access failed via FlareSolverr: {fs_err}")
|
|
130
|
+
if session_id:
|
|
131
|
+
flaresolverr_destroy_session(shared_state, session_id)
|
|
73
132
|
return {"links": [], "imdb_id": None}
|
|
133
|
+
else:
|
|
134
|
+
info(
|
|
135
|
+
f"WD site has been updated or is protected. Grabbing download links for {title} not possible! ({e})"
|
|
136
|
+
)
|
|
137
|
+
mark_hostname_issue(hostname, "download", str(e))
|
|
138
|
+
return {"links": [], "imdb_id": None}
|
|
74
139
|
|
|
75
|
-
|
|
140
|
+
try:
|
|
141
|
+
if status_code and status_code >= 400:
|
|
76
142
|
mark_hostname_issue(
|
|
77
|
-
hostname, "download", f"Download error: {str(
|
|
143
|
+
hostname, "download", f"Download error: {str(status_code)}"
|
|
78
144
|
)
|
|
79
145
|
|
|
80
|
-
soup = BeautifulSoup(
|
|
146
|
+
soup = BeautifulSoup(text, "html.parser")
|
|
81
147
|
|
|
82
148
|
# extract IMDb id if present
|
|
83
149
|
imdb_id = None
|
|
@@ -105,51 +171,55 @@ def get_wd_download_links(shared_state, url, mirror, title, password):
|
|
|
105
171
|
link_tags = body.find_all(
|
|
106
172
|
"a", href=True, class_=lambda c: c and "background-" in c
|
|
107
173
|
)
|
|
108
|
-
except RuntimeError as e:
|
|
109
|
-
# Catch FlareSolverr not configured error
|
|
110
|
-
info(f"WD access failed: {e}")
|
|
111
|
-
return {"links": [], "imdb_id": None}
|
|
112
|
-
except Exception:
|
|
113
|
-
info(
|
|
114
|
-
f"WD site has been updated. Grabbing download links for {title} not possible!"
|
|
115
|
-
)
|
|
116
|
-
return {"links": [], "imdb_id": None}
|
|
117
174
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if resolved.endswith("/404.html"):
|
|
129
|
-
info(f"Link {resolved} is dead!")
|
|
130
|
-
continue
|
|
131
|
-
|
|
132
|
-
# determine hoster
|
|
133
|
-
hoster = a.get_text(strip=True) or None
|
|
134
|
-
if not hoster:
|
|
135
|
-
for cls in a.get("class", []):
|
|
136
|
-
if cls.startswith("background-"):
|
|
137
|
-
hoster = cls.split("-", 1)[1]
|
|
138
|
-
break
|
|
139
|
-
|
|
140
|
-
if mirror and mirror.lower() not in hoster.lower():
|
|
141
|
-
debug(
|
|
142
|
-
f'Skipping link from "{hoster}" (not the desired mirror "{mirror}")!'
|
|
143
|
-
)
|
|
144
|
-
continue
|
|
175
|
+
results = []
|
|
176
|
+
try:
|
|
177
|
+
for a in link_tags:
|
|
178
|
+
raw_href = a["href"]
|
|
179
|
+
full_link = urljoin(f"https://{wd}", raw_href)
|
|
180
|
+
|
|
181
|
+
# resolve any redirects using the same session (or requests if no session)
|
|
182
|
+
resolved = resolve_wd_redirect(
|
|
183
|
+
shared_state, full_link, session_id=session_id
|
|
184
|
+
)
|
|
145
185
|
|
|
146
|
-
|
|
147
|
-
|
|
186
|
+
if resolved:
|
|
187
|
+
if resolved.endswith("/404.html"):
|
|
188
|
+
info(f"Link {resolved} is dead!")
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# determine hoster
|
|
192
|
+
hoster = a.get_text(strip=True) or None
|
|
193
|
+
if not hoster:
|
|
194
|
+
for cls in a.get("class", []):
|
|
195
|
+
if cls.startswith("background-"):
|
|
196
|
+
hoster = cls.split("-", 1)[1]
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
if mirror and mirror.lower() not in hoster.lower():
|
|
200
|
+
debug(
|
|
201
|
+
f'Skipping link from "{hoster}" (not the desired mirror "{mirror}")!'
|
|
202
|
+
)
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
results.append([resolved, hoster])
|
|
206
|
+
except Exception as e:
|
|
207
|
+
info(
|
|
208
|
+
f"WD site has been updated. Parsing download links for {title} not possible! Error: {e}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
"links": results,
|
|
213
|
+
"imdb_id": imdb_id,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
except Exception as e:
|
|
148
217
|
info(
|
|
149
|
-
f"WD site has been updated.
|
|
218
|
+
f"WD site has been updated. Grabbing download links for {title} not possible! Error: {e}"
|
|
150
219
|
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
220
|
+
return {"links": [], "imdb_id": None}
|
|
221
|
+
finally:
|
|
222
|
+
# Always destroy the session if we created one
|
|
223
|
+
if session_id:
|
|
224
|
+
debug(f"Destroying FlareSolverr session: {session_id}")
|
|
225
|
+
flaresolverr_destroy_session(shared_state, session_id)
|
quasarr/downloads/sources/wx.py
CHANGED
|
@@ -39,7 +39,7 @@ def get_wx_download_links(shared_state, url, mirror, title, password):
|
|
|
39
39
|
# Extract slug from URL
|
|
40
40
|
slug_match = re.search(r"/detail/([^/?]+)", url)
|
|
41
41
|
if not slug_match:
|
|
42
|
-
info(f"
|
|
42
|
+
info(f"Could not extract slug from URL: {url}")
|
|
43
43
|
return {"links": []}
|
|
44
44
|
|
|
45
45
|
api_url = f"https://api.{host}/start/d/{slug_match.group(1)}"
|
|
@@ -50,7 +50,7 @@ def get_wx_download_links(shared_state, url, mirror, title, password):
|
|
|
50
50
|
"Accept": "application/json",
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
debug(f"
|
|
53
|
+
debug(f"Fetching API data from: {api_url}")
|
|
54
54
|
api_r = session.get(api_url, headers=api_headers, timeout=30)
|
|
55
55
|
api_r.raise_for_status()
|
|
56
56
|
|
|
@@ -58,7 +58,7 @@ def get_wx_download_links(shared_state, url, mirror, title, password):
|
|
|
58
58
|
|
|
59
59
|
# Navigate to releases in the API response
|
|
60
60
|
if "item" not in data or "releases" not in data["item"]:
|
|
61
|
-
info(
|
|
61
|
+
info("No releases found in API response")
|
|
62
62
|
return {"links": []}
|
|
63
63
|
|
|
64
64
|
releases = data["item"]["releases"]
|
|
@@ -67,12 +67,10 @@ def get_wx_download_links(shared_state, url, mirror, title, password):
|
|
|
67
67
|
matching_releases = [r for r in releases if r.get("fulltitle") == title]
|
|
68
68
|
|
|
69
69
|
if not matching_releases:
|
|
70
|
-
info(f"
|
|
70
|
+
info(f"No release found matching title: {title}")
|
|
71
71
|
return {"links": []}
|
|
72
72
|
|
|
73
|
-
debug(
|
|
74
|
-
f"{hostname.upper()}: Found {len(matching_releases)} mirror(s) for: {title}"
|
|
75
|
-
)
|
|
73
|
+
debug(f"Found {len(matching_releases)} mirror(s) for: {title}")
|
|
76
74
|
|
|
77
75
|
# Evaluate each mirror and find the best one
|
|
78
76
|
# Track: (online_count, is_hide, online_links)
|
|
@@ -105,14 +103,12 @@ def get_wx_download_links(shared_state, url, mirror, title, password):
|
|
|
105
103
|
hide_total = len(hide_links)
|
|
106
104
|
hide_online = len(online_hide)
|
|
107
105
|
|
|
108
|
-
debug(
|
|
109
|
-
f"{hostname.upper()}: M{idx + 1} hide.cx: {hide_online}/{hide_total} online"
|
|
110
|
-
)
|
|
106
|
+
debug(f"M{idx + 1} hide.cx: {hide_online}/{hide_total} online")
|
|
111
107
|
|
|
112
108
|
# If all hide.cx links are online, use this mirror immediately
|
|
113
109
|
if hide_online == hide_total and hide_online > 0:
|
|
114
110
|
debug(
|
|
115
|
-
f"
|
|
111
|
+
f"M{idx + 1} is complete (all {hide_online} hide.cx links online), using this mirror"
|
|
116
112
|
)
|
|
117
113
|
return {"links": online_hide}
|
|
118
114
|
|
|
@@ -124,9 +120,7 @@ def get_wx_download_links(shared_state, url, mirror, title, password):
|
|
|
124
120
|
other_total = len(other_links)
|
|
125
121
|
other_online = len(online_other)
|
|
126
122
|
|
|
127
|
-
debug(
|
|
128
|
-
f"{hostname.upper()}: M{idx + 1} other crypters: {other_online}/{other_total} online"
|
|
129
|
-
)
|
|
123
|
+
debug(f"M{idx + 1} other crypters: {other_online}/{other_total} online")
|
|
130
124
|
|
|
131
125
|
# Determine best option for this mirror (prefer hide.cx on ties)
|
|
132
126
|
mirror_links = None
|
|
@@ -163,15 +157,15 @@ def get_wx_download_links(shared_state, url, mirror, title, password):
|
|
|
163
157
|
if best_mirror and best_mirror[2]:
|
|
164
158
|
crypter_type = "hide.cx" if best_mirror[1] else "other crypter"
|
|
165
159
|
debug(
|
|
166
|
-
f"
|
|
160
|
+
f"No complete mirror, using best partial with {best_mirror[0]} online {crypter_type} link(s)"
|
|
167
161
|
)
|
|
168
162
|
return {"links": best_mirror[2]}
|
|
169
163
|
|
|
170
|
-
info(f"
|
|
164
|
+
info(f"No online links found for: {title}")
|
|
171
165
|
return {"links": []}
|
|
172
166
|
|
|
173
167
|
except Exception as e:
|
|
174
|
-
info(f"
|
|
168
|
+
info(f"Error extracting download links from {url}: {e}")
|
|
175
169
|
mark_hostname_issue(
|
|
176
170
|
hostname, "download", str(e) if "e" in dir() else "Download error"
|
|
177
171
|
)
|
quasarr/providers/auth.py
CHANGED
|
@@ -278,35 +278,31 @@ def add_auth_routes(app):
|
|
|
278
278
|
return _handle_logout()
|
|
279
279
|
|
|
280
280
|
|
|
281
|
-
def add_auth_hook(app,
|
|
281
|
+
def add_auth_hook(app, whitelist=None):
|
|
282
282
|
"""Add authentication hook to a Bottle app.
|
|
283
283
|
|
|
284
284
|
Args:
|
|
285
285
|
app: Bottle application
|
|
286
|
-
|
|
286
|
+
whitelist: List of path prefixes or suffixes to skip auth
|
|
287
287
|
"""
|
|
288
|
-
if
|
|
289
|
-
|
|
288
|
+
if whitelist is None:
|
|
289
|
+
whitelist = []
|
|
290
290
|
|
|
291
291
|
@app.hook("before_request")
|
|
292
292
|
def auth_hook():
|
|
293
293
|
if not is_auth_enabled():
|
|
294
294
|
return
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
# Strip query parameters for path matching
|
|
297
|
+
path = request.path.split("?")[0]
|
|
297
298
|
|
|
298
299
|
# Always allow login/logout
|
|
299
300
|
if path in ["/login", "/logout"]:
|
|
300
301
|
return
|
|
301
302
|
|
|
302
|
-
# Check whitelist
|
|
303
|
-
for
|
|
304
|
-
if path.startswith(
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
# Check whitelist suffixes:
|
|
308
|
-
for suffix in whitelist_suffixes:
|
|
309
|
-
if path.endswith(suffix):
|
|
303
|
+
# Check whitelist
|
|
304
|
+
for item in whitelist:
|
|
305
|
+
if path.startswith(item) or path.endswith(item):
|
|
310
306
|
return
|
|
311
307
|
|
|
312
308
|
# Check authentication
|
quasarr/providers/cloudflare.py
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import requests
|
|
6
6
|
from bs4 import BeautifulSoup
|
|
7
7
|
|
|
8
|
+
from quasarr.providers.log import debug
|
|
8
9
|
from quasarr.providers.utils import is_flaresolverr_available
|
|
9
10
|
|
|
10
11
|
|
|
@@ -73,7 +74,7 @@ def update_session_via_flaresolverr(
|
|
|
73
74
|
"error": f"FlareSolverr request failed: {e}",
|
|
74
75
|
}
|
|
75
76
|
except Exception as e:
|
|
76
|
-
raise RuntimeError(f"Could not reach FlareSolverr: {e}")
|
|
77
|
+
raise RuntimeError(f"Could not reach FlareSolverr: {e}") from e
|
|
77
78
|
|
|
78
79
|
fs_json = resp.json()
|
|
79
80
|
if fs_json.get("status") != "ok" or "solution" not in fs_json:
|
|
@@ -116,7 +117,7 @@ def ensure_session_cf_bypassed(info, shared_state, session, url, headers):
|
|
|
116
117
|
)
|
|
117
118
|
return None, None, None
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
debug(
|
|
120
121
|
"Encountered Cloudflare protection. Solving challenge with FlareSolverr..."
|
|
121
122
|
)
|
|
122
123
|
flaresolverr_result = update_session_via_flaresolverr(
|
|
@@ -168,7 +169,7 @@ class FlareSolverrResponse:
|
|
|
168
169
|
raise requests.HTTPError(f"{self.status_code} Error at {self.url}")
|
|
169
170
|
|
|
170
171
|
|
|
171
|
-
def flaresolverr_get(shared_state, url, timeout=60):
|
|
172
|
+
def flaresolverr_get(shared_state, url, timeout=60, session_id=None):
|
|
172
173
|
"""
|
|
173
174
|
Core function for performing a GET request via FlareSolverr only.
|
|
174
175
|
Used internally by FlareSolverrSession.get()
|
|
@@ -186,6 +187,8 @@ def flaresolverr_get(shared_state, url, timeout=60):
|
|
|
186
187
|
raise RuntimeError("FlareSolverr URL not configured in shared_state.")
|
|
187
188
|
|
|
188
189
|
payload = {"cmd": "request.get", "url": url, "maxTimeout": timeout * 1000}
|
|
190
|
+
if session_id:
|
|
191
|
+
payload["session"] = session_id
|
|
189
192
|
|
|
190
193
|
try:
|
|
191
194
|
resp = requests.post(
|
|
@@ -196,7 +199,7 @@ def flaresolverr_get(shared_state, url, timeout=60):
|
|
|
196
199
|
)
|
|
197
200
|
resp.raise_for_status()
|
|
198
201
|
except Exception as e:
|
|
199
|
-
raise RuntimeError(f"Error communicating with FlareSolverr: {e}")
|
|
202
|
+
raise RuntimeError(f"Error communicating with FlareSolverr: {e}") from e
|
|
200
203
|
|
|
201
204
|
data = resp.json()
|
|
202
205
|
|
|
@@ -219,3 +222,46 @@ def flaresolverr_get(shared_state, url, timeout=60):
|
|
|
219
222
|
return FlareSolverrResponse(
|
|
220
223
|
url=url, status_code=status_code, headers=fs_headers, text=html
|
|
221
224
|
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def flaresolverr_create_session(shared_state, session_id=None):
|
|
228
|
+
if not is_flaresolverr_available(shared_state):
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
flaresolverr_url = shared_state.values["config"]("FlareSolverr").get("url")
|
|
232
|
+
payload = {"cmd": "sessions.create"}
|
|
233
|
+
if session_id:
|
|
234
|
+
payload["session"] = session_id
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
resp = requests.post(
|
|
238
|
+
flaresolverr_url,
|
|
239
|
+
json=payload,
|
|
240
|
+
headers={"Content-Type": "application/json"},
|
|
241
|
+
timeout=10,
|
|
242
|
+
)
|
|
243
|
+
resp.raise_for_status()
|
|
244
|
+
data = resp.json()
|
|
245
|
+
if data.get("status") == "ok":
|
|
246
|
+
return data.get("session")
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def flaresolverr_destroy_session(shared_state, session_id):
|
|
253
|
+
if not is_flaresolverr_available(shared_state):
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
flaresolverr_url = shared_state.values["config"]("FlareSolverr").get("url")
|
|
257
|
+
payload = {"cmd": "sessions.destroy", "session": session_id}
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
requests.post(
|
|
261
|
+
flaresolverr_url,
|
|
262
|
+
json=payload,
|
|
263
|
+
headers={"Content-Type": "application/json"},
|
|
264
|
+
timeout=10,
|
|
265
|
+
)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|