quasarr 2.4.7__py3-none-any.whl → 2.4.9__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.
- quasarr/__init__.py +134 -70
- quasarr/api/__init__.py +40 -31
- quasarr/api/arr/__init__.py +116 -108
- quasarr/api/captcha/__init__.py +262 -137
- quasarr/api/config/__init__.py +76 -46
- quasarr/api/packages/__init__.py +138 -102
- quasarr/api/sponsors_helper/__init__.py +29 -16
- quasarr/api/statistics/__init__.py +19 -19
- quasarr/downloads/__init__.py +165 -72
- quasarr/downloads/linkcrypters/al.py +35 -18
- quasarr/downloads/linkcrypters/filecrypt.py +107 -52
- quasarr/downloads/linkcrypters/hide.py +5 -6
- quasarr/downloads/packages/__init__.py +342 -177
- quasarr/downloads/sources/al.py +191 -100
- quasarr/downloads/sources/by.py +31 -13
- quasarr/downloads/sources/dd.py +27 -14
- quasarr/downloads/sources/dj.py +1 -3
- quasarr/downloads/sources/dl.py +126 -71
- quasarr/downloads/sources/dt.py +11 -5
- quasarr/downloads/sources/dw.py +28 -14
- quasarr/downloads/sources/he.py +32 -24
- quasarr/downloads/sources/mb.py +19 -9
- quasarr/downloads/sources/nk.py +14 -10
- quasarr/downloads/sources/nx.py +8 -18
- quasarr/downloads/sources/sf.py +45 -20
- quasarr/downloads/sources/sj.py +1 -3
- quasarr/downloads/sources/sl.py +9 -5
- quasarr/downloads/sources/wd.py +32 -12
- quasarr/downloads/sources/wx.py +35 -21
- quasarr/providers/auth.py +42 -37
- quasarr/providers/cloudflare.py +28 -30
- quasarr/providers/hostname_issues.py +2 -1
- quasarr/providers/html_images.py +2 -2
- quasarr/providers/html_templates.py +22 -14
- quasarr/providers/imdb_metadata.py +149 -80
- quasarr/providers/jd_cache.py +131 -39
- quasarr/providers/log.py +1 -1
- quasarr/providers/myjd_api.py +260 -196
- quasarr/providers/notifications.py +53 -41
- quasarr/providers/obfuscated.py +9 -4
- quasarr/providers/sessions/al.py +71 -55
- quasarr/providers/sessions/dd.py +21 -14
- quasarr/providers/sessions/dl.py +30 -19
- quasarr/providers/sessions/nx.py +23 -14
- quasarr/providers/shared_state.py +292 -141
- quasarr/providers/statistics.py +75 -43
- quasarr/providers/utils.py +33 -27
- quasarr/providers/version.py +45 -14
- quasarr/providers/web_server.py +10 -5
- quasarr/search/__init__.py +30 -18
- quasarr/search/sources/al.py +124 -73
- quasarr/search/sources/by.py +110 -59
- quasarr/search/sources/dd.py +57 -35
- quasarr/search/sources/dj.py +69 -48
- quasarr/search/sources/dl.py +159 -100
- quasarr/search/sources/dt.py +110 -74
- quasarr/search/sources/dw.py +121 -61
- quasarr/search/sources/fx.py +108 -62
- quasarr/search/sources/he.py +78 -49
- quasarr/search/sources/mb.py +96 -48
- quasarr/search/sources/nk.py +80 -50
- quasarr/search/sources/nx.py +91 -62
- quasarr/search/sources/sf.py +171 -106
- quasarr/search/sources/sj.py +69 -48
- quasarr/search/sources/sl.py +115 -71
- quasarr/search/sources/wd.py +67 -44
- quasarr/search/sources/wx.py +188 -123
- quasarr/storage/config.py +65 -52
- quasarr/storage/setup.py +238 -140
- quasarr/storage/sqlite_database.py +10 -4
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/METADATA +2 -2
- quasarr-2.4.9.dist-info/RECORD +81 -0
- quasarr-2.4.7.dist-info/RECORD +0 -81
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/WHEEL +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.7.dist-info → quasarr-2.4.9.dist-info}/licenses/LICENSE +0 -0
quasarr/providers/sessions/al.py
CHANGED
|
@@ -10,20 +10,22 @@ import urllib.parse
|
|
|
10
10
|
|
|
11
11
|
import requests
|
|
12
12
|
from bs4 import BeautifulSoup
|
|
13
|
-
from requests.exceptions import
|
|
13
|
+
from requests.exceptions import RequestException, Timeout
|
|
14
14
|
|
|
15
|
-
from quasarr.providers.hostname_issues import
|
|
16
|
-
from quasarr.providers.log import
|
|
17
|
-
from quasarr.providers.utils import
|
|
15
|
+
from quasarr.providers.hostname_issues import clear_hostname_issue, mark_hostname_issue
|
|
16
|
+
from quasarr.providers.log import debug, info
|
|
17
|
+
from quasarr.providers.utils import is_flaresolverr_available, is_site_usable
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class SkippedSiteError(Exception):
|
|
21
21
|
"""Raised when a site is skipped due to missing credentials or login being skipped."""
|
|
22
|
+
|
|
22
23
|
pass
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class FlareSolverrNotAvailableError(Exception):
|
|
26
27
|
"""Raised when FlareSolverr is required but not available."""
|
|
28
|
+
|
|
27
29
|
pass
|
|
28
30
|
|
|
29
31
|
|
|
@@ -35,9 +37,13 @@ SESSION_MAX_AGE_SECONDS = 24 * 60 * 60 # 24 hours
|
|
|
35
37
|
def create_and_persist_session(shared_state):
|
|
36
38
|
# AL requires FlareSolverr - check availability first
|
|
37
39
|
if not is_flaresolverr_available(shared_state):
|
|
38
|
-
info(
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
info(
|
|
41
|
+
f'"{hostname.upper()}" requires FlareSolverr which is not configured. '
|
|
42
|
+
f"Please configure FlareSolverr in the web UI to use this site."
|
|
43
|
+
)
|
|
44
|
+
mark_hostname_issue(
|
|
45
|
+
hostname, "session", "FlareSolverr required but not configured"
|
|
46
|
+
)
|
|
41
47
|
return None
|
|
42
48
|
|
|
43
49
|
cfg = shared_state.values["config"]("Hostnames")
|
|
@@ -46,7 +52,7 @@ def create_and_persist_session(shared_state):
|
|
|
46
52
|
user = credentials_cfg.get("user")
|
|
47
53
|
pw = credentials_cfg.get("password")
|
|
48
54
|
|
|
49
|
-
flaresolverr_url = shared_state.values["config"](
|
|
55
|
+
flaresolverr_url = shared_state.values["config"]("FlareSolverr").get("url")
|
|
50
56
|
|
|
51
57
|
sess = requests.Session()
|
|
52
58
|
|
|
@@ -57,11 +63,13 @@ def create_and_persist_session(shared_state):
|
|
|
57
63
|
fs_payload = {
|
|
58
64
|
"cmd": "request.get",
|
|
59
65
|
"url": f"https://www.{host}/",
|
|
60
|
-
"maxTimeout": 60000
|
|
66
|
+
"maxTimeout": 60000,
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
try:
|
|
64
|
-
fs_resp = requests.post(
|
|
70
|
+
fs_resp = requests.post(
|
|
71
|
+
flaresolverr_url, headers=fs_headers, json=fs_payload, timeout=30
|
|
72
|
+
)
|
|
65
73
|
fs_resp.raise_for_status()
|
|
66
74
|
except Timeout:
|
|
67
75
|
info(f"{hostname}: FlareSolverr request timed out")
|
|
@@ -77,14 +85,16 @@ def create_and_persist_session(shared_state):
|
|
|
77
85
|
# Check if FlareSolverr actually solved the challenge
|
|
78
86
|
if fs_json.get("status") != "ok" or "solution" not in fs_json:
|
|
79
87
|
info(f"{hostname}: FlareSolverr did not return a valid solution")
|
|
80
|
-
mark_hostname_issue(
|
|
88
|
+
mark_hostname_issue(
|
|
89
|
+
hostname, "session", "FlareSolverr did not return a valid solution"
|
|
90
|
+
)
|
|
81
91
|
return None
|
|
82
92
|
|
|
83
93
|
solution = fs_json["solution"]
|
|
84
94
|
# store FlareSolverr's UA into our requests.Session
|
|
85
95
|
fl_ua = solution.get("userAgent")
|
|
86
96
|
if fl_ua:
|
|
87
|
-
sess.headers.update({
|
|
97
|
+
sess.headers.update({"User-Agent": fl_ua})
|
|
88
98
|
|
|
89
99
|
# Extract any cookies returned by FlareSolverr and add them into our session
|
|
90
100
|
for ck in solution.get("cookies", []):
|
|
@@ -101,21 +111,17 @@ def create_and_persist_session(shared_state):
|
|
|
101
111
|
return None
|
|
102
112
|
|
|
103
113
|
if user and pw:
|
|
104
|
-
data = {
|
|
105
|
-
"identity": user,
|
|
106
|
-
"password": pw,
|
|
107
|
-
"remember": "1"
|
|
108
|
-
}
|
|
114
|
+
data = {"identity": user, "password": pw, "remember": "1"}
|
|
109
115
|
encoded_data = urllib.parse.urlencode(data)
|
|
110
116
|
|
|
111
|
-
login_headers = {
|
|
112
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
113
|
-
}
|
|
117
|
+
login_headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
|
114
118
|
|
|
115
|
-
r = sess.post(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
r = sess.post(
|
|
120
|
+
f"https://www.{host}/auth/signin",
|
|
121
|
+
data=encoded_data,
|
|
122
|
+
headers=login_headers,
|
|
123
|
+
timeout=30,
|
|
124
|
+
)
|
|
119
125
|
|
|
120
126
|
if r.status_code != 200 or "invalid" in r.text.lower():
|
|
121
127
|
info(f'Login failed: "{hostname}" - {r.status_code} - {r.text}')
|
|
@@ -194,10 +200,7 @@ def _persist_session_to_db(shared_state, sess):
|
|
|
194
200
|
"""
|
|
195
201
|
blob = pickle.dumps(sess)
|
|
196
202
|
token = base64.b64encode(blob).decode("utf-8")
|
|
197
|
-
session_data = json.dumps({
|
|
198
|
-
"token": token,
|
|
199
|
-
"created_at": time.time()
|
|
200
|
-
})
|
|
203
|
+
session_data = json.dumps({"token": token, "created_at": time.time()})
|
|
201
204
|
shared_state.values["database"]("sessions").update_store(hostname, session_data)
|
|
202
205
|
|
|
203
206
|
|
|
@@ -207,12 +210,14 @@ def _load_session_cookies_for_flaresolverr(sess):
|
|
|
207
210
|
"""
|
|
208
211
|
cookie_list = []
|
|
209
212
|
for ck in sess.cookies:
|
|
210
|
-
cookie_list.append(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
cookie_list.append(
|
|
214
|
+
{
|
|
215
|
+
"name": ck.name,
|
|
216
|
+
"value": ck.value,
|
|
217
|
+
"domain": ck.domain,
|
|
218
|
+
"path": ck.path or "/",
|
|
219
|
+
}
|
|
220
|
+
)
|
|
216
221
|
return cookie_list
|
|
217
222
|
|
|
218
223
|
|
|
@@ -232,11 +237,13 @@ def unwrap_flaresolverr_body(raw_text: str) -> str:
|
|
|
232
237
|
return text
|
|
233
238
|
|
|
234
239
|
|
|
235
|
-
def fetch_via_flaresolverr(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
+
def fetch_via_flaresolverr(
|
|
241
|
+
shared_state,
|
|
242
|
+
method: str,
|
|
243
|
+
target_url: str,
|
|
244
|
+
post_data: dict = None,
|
|
245
|
+
timeout: int = 60,
|
|
246
|
+
):
|
|
240
247
|
"""
|
|
241
248
|
Load (or recreate) the requests.Session from DB.
|
|
242
249
|
Package its cookies into FlareSolverr payload.
|
|
@@ -251,18 +258,20 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
251
258
|
"""
|
|
252
259
|
# Check if FlareSolverr is available
|
|
253
260
|
if not is_flaresolverr_available(shared_state):
|
|
254
|
-
info(
|
|
255
|
-
|
|
261
|
+
info(
|
|
262
|
+
f'"{hostname.upper()}" requires FlareSolverr which is not configured. '
|
|
263
|
+
f"Please configure FlareSolverr in the web UI."
|
|
264
|
+
)
|
|
256
265
|
return {
|
|
257
266
|
"status_code": None,
|
|
258
267
|
"headers": {},
|
|
259
268
|
"json": None,
|
|
260
269
|
"text": "",
|
|
261
270
|
"cookies": [],
|
|
262
|
-
"error": "FlareSolverr is not configured"
|
|
271
|
+
"error": "FlareSolverr is not configured",
|
|
263
272
|
}
|
|
264
273
|
|
|
265
|
-
flaresolverr_url = shared_state.values["config"](
|
|
274
|
+
flaresolverr_url = shared_state.values["config"]("FlareSolverr").get("url")
|
|
266
275
|
|
|
267
276
|
sess = retrieve_and_validate_session(shared_state)
|
|
268
277
|
if not sess:
|
|
@@ -273,7 +282,7 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
273
282
|
"json": None,
|
|
274
283
|
"text": "",
|
|
275
284
|
"cookies": [],
|
|
276
|
-
"error": f"Site '{hostname}' is not usable (login skipped or no credentials)"
|
|
285
|
+
"error": f"Site '{hostname}' is not usable (login skipped or no credentials)",
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
cmd = "request.get" if method.upper() == "GET" else "request.post"
|
|
@@ -282,7 +291,7 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
282
291
|
"url": target_url,
|
|
283
292
|
"maxTimeout": timeout * 1000,
|
|
284
293
|
# Inject every cookie from our Python session into FlareSolverr
|
|
285
|
-
"cookies": _load_session_cookies_for_flaresolverr(sess)
|
|
294
|
+
"cookies": _load_session_cookies_for_flaresolverr(sess),
|
|
286
295
|
}
|
|
287
296
|
|
|
288
297
|
if method.upper() == "POST":
|
|
@@ -294,10 +303,7 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
294
303
|
fs_headers = {"Content-Type": "application/json"}
|
|
295
304
|
try:
|
|
296
305
|
resp = requests.post(
|
|
297
|
-
flaresolverr_url,
|
|
298
|
-
headers=fs_headers,
|
|
299
|
-
json=fs_payload,
|
|
300
|
-
timeout=timeout + 10
|
|
306
|
+
flaresolverr_url, headers=fs_headers, json=fs_payload, timeout=timeout + 10
|
|
301
307
|
)
|
|
302
308
|
except requests.exceptions.RequestException as e:
|
|
303
309
|
info(f"Could not reach FlareSolverr: {e}")
|
|
@@ -308,7 +314,7 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
308
314
|
"json": None,
|
|
309
315
|
"text": "",
|
|
310
316
|
"cookies": [],
|
|
311
|
-
"error": f"FlareSolverr request failed: {e}"
|
|
317
|
+
"error": f"FlareSolverr request failed: {e}",
|
|
312
318
|
}
|
|
313
319
|
except Exception as e:
|
|
314
320
|
raise RuntimeError(f"Could not reach FlareSolverr: {e}")
|
|
@@ -319,7 +325,9 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
319
325
|
|
|
320
326
|
fs_json = resp.json()
|
|
321
327
|
if fs_json.get("status") != "ok" or "solution" not in fs_json:
|
|
322
|
-
raise RuntimeError(
|
|
328
|
+
raise RuntimeError(
|
|
329
|
+
f"FlareSolverr did not return a valid solution: {fs_json.get('message', '<no message>')}"
|
|
330
|
+
)
|
|
323
331
|
|
|
324
332
|
solution = fs_json["solution"]
|
|
325
333
|
|
|
@@ -341,7 +349,7 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
341
349
|
ck.get("name"),
|
|
342
350
|
ck.get("value"),
|
|
343
351
|
domain=ck.get("domain"),
|
|
344
|
-
path=ck.get("path", "/")
|
|
352
|
+
path=ck.get("path", "/"),
|
|
345
353
|
)
|
|
346
354
|
|
|
347
355
|
# Persist the updated Session back into your DB
|
|
@@ -353,11 +361,17 @@ def fetch_via_flaresolverr(shared_state,
|
|
|
353
361
|
"headers": solution.get("headers", {}),
|
|
354
362
|
"json": parsed_json,
|
|
355
363
|
"text": raw_body,
|
|
356
|
-
"cookies": solution.get("cookies", [])
|
|
364
|
+
"cookies": solution.get("cookies", []),
|
|
357
365
|
}
|
|
358
366
|
|
|
359
367
|
|
|
360
|
-
def fetch_via_requests_session(
|
|
368
|
+
def fetch_via_requests_session(
|
|
369
|
+
shared_state,
|
|
370
|
+
method: str,
|
|
371
|
+
target_url: str,
|
|
372
|
+
post_data: dict = None,
|
|
373
|
+
timeout: int = 30,
|
|
374
|
+
):
|
|
361
375
|
"""
|
|
362
376
|
- method: "GET" or "POST"
|
|
363
377
|
- post_data: for POST only (will be sent as form-data unless you explicitly JSON-encode)
|
|
@@ -365,7 +379,9 @@ def fetch_via_requests_session(shared_state, method: str, target_url: str, post_
|
|
|
365
379
|
"""
|
|
366
380
|
sess = retrieve_and_validate_session(shared_state)
|
|
367
381
|
if not sess:
|
|
368
|
-
raise SkippedSiteError(
|
|
382
|
+
raise SkippedSiteError(
|
|
383
|
+
f"{hostname}: site not usable (login skipped or no credentials)"
|
|
384
|
+
)
|
|
369
385
|
|
|
370
386
|
# Execute request
|
|
371
387
|
if method.upper() == "GET":
|
quasarr/providers/sessions/dd.py
CHANGED
|
@@ -7,8 +7,8 @@ import pickle
|
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
|
-
from quasarr.providers.hostname_issues import
|
|
11
|
-
from quasarr.providers.log import
|
|
10
|
+
from quasarr.providers.hostname_issues import clear_hostname_issue, mark_hostname_issue
|
|
11
|
+
from quasarr.providers.log import debug, info
|
|
12
12
|
from quasarr.providers.utils import is_site_usable
|
|
13
13
|
|
|
14
14
|
hostname = "dd"
|
|
@@ -21,31 +21,36 @@ def create_and_persist_session(shared_state):
|
|
|
21
21
|
|
|
22
22
|
cookies = {}
|
|
23
23
|
headers = {
|
|
24
|
-
|
|
24
|
+
"User-Agent": shared_state.values["user_agent"],
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
data = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
"username": shared_state.values["config"]("DD").get("user"),
|
|
29
|
+
"password": shared_state.values["config"]("DD").get("password"),
|
|
30
|
+
"ajax": "true",
|
|
31
|
+
"Login": "true",
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
r = dd_session.post(
|
|
35
|
-
|
|
34
|
+
r = dd_session.post(
|
|
35
|
+
f"https://{dd}/index/index",
|
|
36
|
+
cookies=cookies,
|
|
37
|
+
headers=headers,
|
|
38
|
+
data=data,
|
|
39
|
+
timeout=10,
|
|
40
|
+
)
|
|
36
41
|
r.raise_for_status()
|
|
37
42
|
|
|
38
43
|
error = False
|
|
39
44
|
if r.status_code == 200:
|
|
40
45
|
try:
|
|
41
46
|
response_data = r.json()
|
|
42
|
-
if not response_data.get(
|
|
47
|
+
if not response_data.get("loggedin"):
|
|
43
48
|
info("DD rejected login.")
|
|
44
49
|
mark_hostname_issue(hostname, "session", "Login rejected")
|
|
45
50
|
raise ValueError
|
|
46
51
|
session_id = r.cookies.get("PHPSESSID")
|
|
47
52
|
if session_id:
|
|
48
|
-
dd_session.cookies.set(
|
|
53
|
+
dd_session.cookies.set("PHPSESSID", session_id, domain=dd)
|
|
49
54
|
else:
|
|
50
55
|
info("Invalid DD response on login.")
|
|
51
56
|
mark_hostname_issue(hostname, "session", "Invalid login response")
|
|
@@ -61,7 +66,7 @@ def create_and_persist_session(shared_state):
|
|
|
61
66
|
return None
|
|
62
67
|
|
|
63
68
|
serialized_session = pickle.dumps(dd_session)
|
|
64
|
-
session_string = base64.b64encode(serialized_session).decode(
|
|
69
|
+
session_string = base64.b64encode(serialized_session).decode("utf-8")
|
|
65
70
|
shared_state.values["database"]("sessions").update_store("dd", session_string)
|
|
66
71
|
clear_hostname_issue(hostname)
|
|
67
72
|
return dd_session
|
|
@@ -81,10 +86,12 @@ def retrieve_and_validate_session(shared_state):
|
|
|
81
86
|
dd_session = create_and_persist_session(shared_state)
|
|
82
87
|
else:
|
|
83
88
|
try:
|
|
84
|
-
serialized_session = base64.b64decode(session_string.encode(
|
|
89
|
+
serialized_session = base64.b64decode(session_string.encode("utf-8"))
|
|
85
90
|
dd_session = pickle.loads(serialized_session)
|
|
86
91
|
if not isinstance(dd_session, requests.Session):
|
|
87
|
-
raise ValueError(
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"Retrieved object is not a valid requests.Session instance."
|
|
94
|
+
)
|
|
88
95
|
except Exception as e:
|
|
89
96
|
info(f"Session retrieval failed: {e}")
|
|
90
97
|
mark_hostname_issue(hostname, "session", str(e))
|
quasarr/providers/sessions/dl.py
CHANGED
|
@@ -8,13 +8,14 @@ import pickle
|
|
|
8
8
|
import requests
|
|
9
9
|
from bs4 import BeautifulSoup
|
|
10
10
|
|
|
11
|
-
from quasarr.providers.hostname_issues import
|
|
12
|
-
from quasarr.providers.log import
|
|
11
|
+
from quasarr.providers.hostname_issues import clear_hostname_issue, mark_hostname_issue
|
|
12
|
+
from quasarr.providers.log import debug, info
|
|
13
13
|
from quasarr.providers.utils import is_site_usable
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class SkippedSiteError(Exception):
|
|
17
17
|
"""Raised when a site is skipped due to missing credentials or login being skipped."""
|
|
18
|
+
|
|
18
19
|
pass
|
|
19
20
|
|
|
20
21
|
|
|
@@ -47,46 +48,48 @@ def create_and_persist_session(shared_state):
|
|
|
47
48
|
|
|
48
49
|
# Set user agent
|
|
49
50
|
ua = shared_state.values["user_agent"]
|
|
50
|
-
sess.headers.update({
|
|
51
|
+
sess.headers.update({"User-Agent": ua})
|
|
51
52
|
|
|
52
53
|
try:
|
|
53
54
|
# Step 1: Get login page to retrieve CSRF token
|
|
54
|
-
login_page_url = f
|
|
55
|
+
login_page_url = f"https://www.{host}/login/"
|
|
55
56
|
login_r = sess.get(login_page_url, timeout=30)
|
|
56
57
|
|
|
57
58
|
login_r.raise_for_status()
|
|
58
59
|
|
|
59
60
|
# Extract CSRF token from login form
|
|
60
|
-
soup = BeautifulSoup(login_r.text,
|
|
61
|
-
csrf_input = soup.find(
|
|
61
|
+
soup = BeautifulSoup(login_r.text, "html.parser")
|
|
62
|
+
csrf_input = soup.find("input", {"name": "_xfToken"})
|
|
62
63
|
|
|
63
|
-
if not csrf_input or not csrf_input.get(
|
|
64
|
+
if not csrf_input or not csrf_input.get("value"):
|
|
64
65
|
info(f'Could not find CSRF token on login page for: "{hostname}"')
|
|
65
66
|
mark_hostname_issue(hostname, "session", "Could not find CSRF token")
|
|
66
67
|
return None
|
|
67
68
|
|
|
68
|
-
csrf_token = csrf_input[
|
|
69
|
+
csrf_token = csrf_input["value"]
|
|
69
70
|
|
|
70
71
|
# Step 2: Submit login form
|
|
71
72
|
login_data = {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
"login": user,
|
|
74
|
+
"password": password,
|
|
75
|
+
"_xfToken": csrf_token,
|
|
76
|
+
"remember": "1",
|
|
77
|
+
"_xfRedirect": f"https://www.{host}/",
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
login_url = f
|
|
80
|
+
login_url = f"https://www.{host}/login/login"
|
|
80
81
|
submit_r = sess.post(login_url, data=login_data, timeout=30)
|
|
81
82
|
submit_r.raise_for_status()
|
|
82
83
|
|
|
83
84
|
# Step 3: Verify login success
|
|
84
85
|
# Check if we're logged in by accessing the main page
|
|
85
|
-
verify_r = sess.get(f
|
|
86
|
+
verify_r = sess.get(f"https://www.{host}/", timeout=30)
|
|
86
87
|
verify_r.raise_for_status()
|
|
87
88
|
|
|
88
89
|
if 'data-logged-in="true"' not in verify_r.text:
|
|
89
|
-
info(
|
|
90
|
+
info(
|
|
91
|
+
f'Login verification failed for: "{hostname}" - invalid credentials or login failed'
|
|
92
|
+
)
|
|
90
93
|
mark_hostname_issue(hostname, "session", "Login verification failed")
|
|
91
94
|
return None
|
|
92
95
|
|
|
@@ -160,8 +163,14 @@ def _persist_session_to_db(shared_state, sess):
|
|
|
160
163
|
shared_state.values["database"]("sessions").update_store(hostname, token)
|
|
161
164
|
|
|
162
165
|
|
|
163
|
-
def fetch_via_requests_session(
|
|
164
|
-
|
|
166
|
+
def fetch_via_requests_session(
|
|
167
|
+
shared_state,
|
|
168
|
+
method: str,
|
|
169
|
+
target_url: str,
|
|
170
|
+
post_data: dict = None,
|
|
171
|
+
get_params: dict = None,
|
|
172
|
+
timeout: int = 30,
|
|
173
|
+
):
|
|
165
174
|
"""
|
|
166
175
|
Execute request using the session.
|
|
167
176
|
|
|
@@ -178,7 +187,9 @@ def fetch_via_requests_session(shared_state, method: str, target_url: str, post_
|
|
|
178
187
|
"""
|
|
179
188
|
sess = retrieve_and_validate_session(shared_state)
|
|
180
189
|
if not sess:
|
|
181
|
-
raise SkippedSiteError(
|
|
190
|
+
raise SkippedSiteError(
|
|
191
|
+
f"{hostname}: site not usable (login skipped or no credentials)"
|
|
192
|
+
)
|
|
182
193
|
|
|
183
194
|
# Execute request
|
|
184
195
|
if method.upper() == "GET":
|
quasarr/providers/sessions/nx.py
CHANGED
|
@@ -7,8 +7,8 @@ import pickle
|
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
9
|
|
|
10
|
-
from quasarr.providers.hostname_issues import
|
|
11
|
-
from quasarr.providers.log import
|
|
10
|
+
from quasarr.providers.hostname_issues import clear_hostname_issue, mark_hostname_issue
|
|
11
|
+
from quasarr.providers.log import debug, info
|
|
12
12
|
from quasarr.providers.utils import is_site_usable
|
|
13
13
|
|
|
14
14
|
hostname = "nx"
|
|
@@ -21,33 +21,40 @@ def create_and_persist_session(shared_state):
|
|
|
21
21
|
|
|
22
22
|
cookies = {}
|
|
23
23
|
headers = {
|
|
24
|
-
|
|
24
|
+
"User-Agent": shared_state.values["user_agent"],
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
json_data = {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
"username": shared_state.values["config"]("NX").get("user"),
|
|
29
|
+
"password": shared_state.values["config"]("NX").get("password"),
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
r = nx_session.post(
|
|
33
|
-
|
|
32
|
+
r = nx_session.post(
|
|
33
|
+
f"https://{nx}/api/user/auth",
|
|
34
|
+
cookies=cookies,
|
|
35
|
+
headers=headers,
|
|
36
|
+
json=json_data,
|
|
37
|
+
timeout=10,
|
|
38
|
+
)
|
|
34
39
|
r.raise_for_status()
|
|
35
40
|
|
|
36
41
|
error = False
|
|
37
42
|
if r.status_code == 200:
|
|
38
43
|
try:
|
|
39
44
|
response_data = r.json()
|
|
40
|
-
if response_data.get(
|
|
45
|
+
if response_data.get("err", {}).get("status") == 403:
|
|
41
46
|
info("Invalid NX credentials provided.")
|
|
42
47
|
mark_hostname_issue(hostname, "session", "Invalid credentials")
|
|
43
48
|
error = True
|
|
44
|
-
elif response_data.get(
|
|
49
|
+
elif response_data.get("user").get("username") != shared_state.values[
|
|
50
|
+
"config"
|
|
51
|
+
]("NX").get("user"):
|
|
45
52
|
info("Invalid NX response on login.")
|
|
46
53
|
mark_hostname_issue(hostname, "session", "Invalid login response")
|
|
47
54
|
error = True
|
|
48
55
|
else:
|
|
49
|
-
sessiontoken = response_data.get(
|
|
50
|
-
nx_session.cookies.set(
|
|
56
|
+
sessiontoken = response_data.get("user").get("sessiontoken")
|
|
57
|
+
nx_session.cookies.set("sessiontoken", sessiontoken, domain=nx)
|
|
51
58
|
except ValueError:
|
|
52
59
|
info("Could not parse NX response on login.")
|
|
53
60
|
mark_hostname_issue(hostname, "session", "Could not parse login response")
|
|
@@ -59,7 +66,7 @@ def create_and_persist_session(shared_state):
|
|
|
59
66
|
return None
|
|
60
67
|
|
|
61
68
|
serialized_session = pickle.dumps(nx_session)
|
|
62
|
-
session_string = base64.b64encode(serialized_session).decode(
|
|
69
|
+
session_string = base64.b64encode(serialized_session).decode("utf-8")
|
|
63
70
|
shared_state.values["database"]("sessions").update_store("nx", session_string)
|
|
64
71
|
clear_hostname_issue(hostname)
|
|
65
72
|
return nx_session
|
|
@@ -79,10 +86,12 @@ def retrieve_and_validate_session(shared_state):
|
|
|
79
86
|
nx_session = create_and_persist_session(shared_state)
|
|
80
87
|
else:
|
|
81
88
|
try:
|
|
82
|
-
serialized_session = base64.b64decode(session_string.encode(
|
|
89
|
+
serialized_session = base64.b64decode(session_string.encode("utf-8"))
|
|
83
90
|
nx_session = pickle.loads(serialized_session)
|
|
84
91
|
if not isinstance(nx_session, requests.Session):
|
|
85
|
-
raise ValueError(
|
|
92
|
+
raise ValueError(
|
|
93
|
+
"Retrieved object is not a valid requests.Session instance."
|
|
94
|
+
)
|
|
86
95
|
except Exception as e:
|
|
87
96
|
info(f"Session retrieval failed: {e}")
|
|
88
97
|
mark_hostname_issue(hostname, "session", str(e))
|