quasarr 2.4.8__py3-none-any.whl → 2.4.10__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 +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.8.dist-info → quasarr-2.4.10.dist-info}/METADATA +4 -3
- quasarr-2.4.10.dist-info/RECORD +81 -0
- quasarr-2.4.8.dist-info/RECORD +0 -81
- {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/WHEEL +0 -0
- {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/entry_points.txt +0 -0
- {quasarr-2.4.8.dist-info → quasarr-2.4.10.dist-info}/licenses/LICENSE +0 -0
quasarr/api/arr/__init__.py
CHANGED
|
@@ -6,16 +6,16 @@ import traceback
|
|
|
6
6
|
import xml.sax.saxutils as sax_utils
|
|
7
7
|
from base64 import urlsafe_b64decode
|
|
8
8
|
from datetime import datetime
|
|
9
|
-
from urllib.parse import
|
|
9
|
+
from urllib.parse import parse_qs, urlparse
|
|
10
10
|
from xml.etree import ElementTree
|
|
11
11
|
|
|
12
12
|
from bottle import abort, request
|
|
13
13
|
|
|
14
14
|
from quasarr.downloads import download
|
|
15
|
-
from quasarr.downloads.packages import
|
|
15
|
+
from quasarr.downloads.packages import delete_package, get_packages
|
|
16
16
|
from quasarr.providers import shared_state
|
|
17
17
|
from quasarr.providers.auth import require_api_key
|
|
18
|
-
from quasarr.providers.log import
|
|
18
|
+
from quasarr.providers.log import debug, info
|
|
19
19
|
from quasarr.providers.version import get_version
|
|
20
20
|
from quasarr.search import get_search_results
|
|
21
21
|
|
|
@@ -51,12 +51,12 @@ def parse_payload(payload_str):
|
|
|
51
51
|
"size_mb": size_mb,
|
|
52
52
|
"password": password if password else None,
|
|
53
53
|
"imdb_id": imdb_id if imdb_id else None,
|
|
54
|
-
"source_key": source_key if source_key else None
|
|
54
|
+
"source_key": source_key if source_key else None,
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def setup_arr_routes(app):
|
|
59
|
-
@app.get(
|
|
59
|
+
@app.get("/download/")
|
|
60
60
|
def fake_nzb_file():
|
|
61
61
|
payload = request.query.payload
|
|
62
62
|
decoded_payload = urlsafe_b64decode(payload).decode("utf-8").split("|")
|
|
@@ -72,10 +72,10 @@ def setup_arr_routes(app):
|
|
|
72
72
|
|
|
73
73
|
return f'<nzb><file title="{title}" url="{url}" mirror="{mirror}" size_mb="{size_mb}" password="{password}" imdb_id="{imdb_id}" source_key="{source_key}"/></nzb>'
|
|
74
74
|
|
|
75
|
-
@app.post(
|
|
75
|
+
@app.post("/api")
|
|
76
76
|
@require_api_key
|
|
77
77
|
def download_fake_nzb_file():
|
|
78
|
-
downloads = request.files.getall(
|
|
78
|
+
downloads = request.files.getall("name")
|
|
79
79
|
nzo_ids = [] # naming structure for package IDs expected in newznab
|
|
80
80
|
|
|
81
81
|
for upload in downloads:
|
|
@@ -85,7 +85,11 @@ def setup_arr_routes(app):
|
|
|
85
85
|
title = sax_utils.unescape(root.find(".//file").attrib["title"])
|
|
86
86
|
|
|
87
87
|
url = root.find(".//file").attrib["url"]
|
|
88
|
-
mirror =
|
|
88
|
+
mirror = (
|
|
89
|
+
None
|
|
90
|
+
if (mirror := root.find(".//file").attrib.get("mirror")) == "None"
|
|
91
|
+
else mirror
|
|
92
|
+
)
|
|
89
93
|
|
|
90
94
|
size_mb = root.find(".//file").attrib["size_mb"]
|
|
91
95
|
password = root.find(".//file").attrib.get("password")
|
|
@@ -93,9 +97,18 @@ def setup_arr_routes(app):
|
|
|
93
97
|
source_key = root.find(".//file").attrib.get("source_key") or None
|
|
94
98
|
|
|
95
99
|
info(f'Attempting download for "{title}"')
|
|
96
|
-
request_from = request.headers.get(
|
|
97
|
-
downloaded = download(
|
|
98
|
-
|
|
100
|
+
request_from = request.headers.get("User-Agent")
|
|
101
|
+
downloaded = download(
|
|
102
|
+
shared_state,
|
|
103
|
+
request_from,
|
|
104
|
+
title,
|
|
105
|
+
url,
|
|
106
|
+
mirror,
|
|
107
|
+
size_mb,
|
|
108
|
+
password,
|
|
109
|
+
imdb_id,
|
|
110
|
+
source_key,
|
|
111
|
+
)
|
|
99
112
|
try:
|
|
100
113
|
success = downloaded["success"]
|
|
101
114
|
package_id = downloaded["package_id"]
|
|
@@ -109,45 +122,34 @@ def setup_arr_routes(app):
|
|
|
109
122
|
except KeyError:
|
|
110
123
|
info(f'Failed to download "{title}" - no package_id returned')
|
|
111
124
|
|
|
112
|
-
return {
|
|
113
|
-
"status": True,
|
|
114
|
-
"nzo_ids": nzo_ids
|
|
115
|
-
}
|
|
125
|
+
return {"status": True, "nzo_ids": nzo_ids}
|
|
116
126
|
|
|
117
|
-
@app.get(
|
|
118
|
-
@app.get(
|
|
127
|
+
@app.get("/api")
|
|
128
|
+
@app.get("/api/<mirror>")
|
|
119
129
|
@require_api_key
|
|
120
130
|
def quasarr_api(mirror=None):
|
|
121
|
-
api_type =
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
api_type = (
|
|
132
|
+
"arr_download_client"
|
|
133
|
+
if request.query.mode
|
|
134
|
+
else "arr_indexer"
|
|
135
|
+
if request.query.t
|
|
136
|
+
else None
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if api_type == "arr_download_client":
|
|
124
140
|
# This builds a mock SABnzbd API response based on the My JDownloader integration
|
|
125
141
|
try:
|
|
126
142
|
mode = request.query.mode
|
|
127
143
|
if mode == "auth":
|
|
128
|
-
return {
|
|
129
|
-
"auth": "apikey"
|
|
130
|
-
}
|
|
144
|
+
return {"auth": "apikey"}
|
|
131
145
|
elif mode == "version":
|
|
132
|
-
return {
|
|
133
|
-
"version": f"Quasarr {get_version()}"
|
|
134
|
-
}
|
|
146
|
+
return {"version": f"Quasarr {get_version()}"}
|
|
135
147
|
elif mode == "get_cats":
|
|
136
|
-
return {
|
|
137
|
-
"categories": [
|
|
138
|
-
"*",
|
|
139
|
-
"movies",
|
|
140
|
-
"tv",
|
|
141
|
-
"docs"
|
|
142
|
-
]
|
|
143
|
-
}
|
|
148
|
+
return {"categories": ["*", "movies", "tv", "docs"]}
|
|
144
149
|
elif mode == "get_config":
|
|
145
150
|
return {
|
|
146
151
|
"config": {
|
|
147
|
-
"misc": {
|
|
148
|
-
"quasarr": True,
|
|
149
|
-
"complete_dir": "/tmp/"
|
|
150
|
-
},
|
|
152
|
+
"misc": {"quasarr": True, "complete_dir": "/tmp/"},
|
|
151
153
|
"categories": [
|
|
152
154
|
{
|
|
153
155
|
"name": "*",
|
|
@@ -169,15 +171,11 @@ def setup_arr_routes(app):
|
|
|
169
171
|
"order": 3,
|
|
170
172
|
"dir": "",
|
|
171
173
|
},
|
|
172
|
-
]
|
|
174
|
+
],
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
elif mode == "fullstatus":
|
|
176
|
-
return {
|
|
177
|
-
"status": {
|
|
178
|
-
"quasarr": True
|
|
179
|
-
}
|
|
180
|
-
}
|
|
178
|
+
return {"status": {"quasarr": True}}
|
|
181
179
|
elif mode == "addurl":
|
|
182
180
|
raw_name = getattr(request.query, "name", None)
|
|
183
181
|
if not raw_name:
|
|
@@ -222,60 +220,58 @@ def setup_arr_routes(app):
|
|
|
222
220
|
if success:
|
|
223
221
|
info(f'"{title}" added successfully!')
|
|
224
222
|
else:
|
|
225
|
-
info(
|
|
223
|
+
info(
|
|
224
|
+
f'"{title}" added unsuccessfully! See log for details.'
|
|
225
|
+
)
|
|
226
226
|
nzo_ids.append(package_id)
|
|
227
227
|
except KeyError:
|
|
228
|
-
info(
|
|
228
|
+
info(
|
|
229
|
+
f'Failed to download "{parsed_payload["title"]}" - no package_id returned'
|
|
230
|
+
)
|
|
229
231
|
|
|
230
|
-
return {
|
|
231
|
-
"status": True,
|
|
232
|
-
"nzo_ids": nzo_ids
|
|
233
|
-
}
|
|
232
|
+
return {"status": True, "nzo_ids": nzo_ids}
|
|
234
233
|
|
|
235
234
|
elif mode == "queue" or mode == "history":
|
|
236
235
|
if request.query.name and request.query.name == "delete":
|
|
237
236
|
package_id = request.query.value
|
|
238
237
|
deleted = delete_package(shared_state, package_id)
|
|
239
|
-
return {
|
|
240
|
-
"status": deleted,
|
|
241
|
-
"nzo_ids": [package_id]
|
|
242
|
-
}
|
|
238
|
+
return {"status": deleted, "nzo_ids": [package_id]}
|
|
243
239
|
|
|
244
240
|
packages = get_packages(shared_state)
|
|
245
241
|
if mode == "queue":
|
|
246
242
|
return {
|
|
247
243
|
"queue": {
|
|
248
244
|
"paused": False,
|
|
249
|
-
"slots": packages.get("queue", [])
|
|
245
|
+
"slots": packages.get("queue", []),
|
|
250
246
|
}
|
|
251
247
|
}
|
|
252
248
|
elif mode == "history":
|
|
253
249
|
return {
|
|
254
250
|
"history": {
|
|
255
251
|
"paused": False,
|
|
256
|
-
"slots": packages.get("history", [])
|
|
252
|
+
"slots": packages.get("history", []),
|
|
257
253
|
}
|
|
258
254
|
}
|
|
259
255
|
except Exception as e:
|
|
260
256
|
info(f"Error loading packages: {e}")
|
|
261
257
|
info(traceback.format_exc())
|
|
262
258
|
info(f"[ERROR] Unknown download client request: {dict(request.query)}")
|
|
263
|
-
return {
|
|
264
|
-
"status": False
|
|
265
|
-
}
|
|
259
|
+
return {"status": False}
|
|
266
260
|
|
|
267
|
-
elif api_type ==
|
|
261
|
+
elif api_type == "arr_indexer":
|
|
268
262
|
# this builds a mock Newznab API response based on Quasarr search
|
|
269
263
|
try:
|
|
270
264
|
if mirror:
|
|
271
|
-
debug(
|
|
265
|
+
debug(
|
|
266
|
+
f'Search will only return releases that match this mirror: "{mirror}"'
|
|
267
|
+
)
|
|
272
268
|
|
|
273
269
|
mode = request.query.t
|
|
274
|
-
request_from = request.headers.get(
|
|
270
|
+
request_from = request.headers.get("User-Agent")
|
|
275
271
|
|
|
276
|
-
if mode ==
|
|
272
|
+
if mode == "caps":
|
|
277
273
|
info(f"Providing indexer capability information to {request_from}")
|
|
278
|
-
return
|
|
274
|
+
return """<?xml version="1.0" encoding="UTF-8"?>
|
|
279
275
|
<caps>
|
|
280
276
|
<server
|
|
281
277
|
version="1.33.7"
|
|
@@ -296,58 +292,69 @@ def setup_arr_routes(app):
|
|
|
296
292
|
<category id="7000" name="Books">
|
|
297
293
|
</category>
|
|
298
294
|
</categories>
|
|
299
|
-
</caps>
|
|
300
|
-
elif mode in [
|
|
295
|
+
</caps>"""
|
|
296
|
+
elif mode in ["movie", "tvsearch", "book", "search"]:
|
|
301
297
|
releases = []
|
|
302
298
|
|
|
303
299
|
try:
|
|
304
|
-
offset = int(getattr(request.query,
|
|
300
|
+
offset = int(getattr(request.query, "offset", 0))
|
|
305
301
|
except (AttributeError, ValueError):
|
|
306
302
|
offset = 0
|
|
307
303
|
|
|
308
304
|
if offset > 0:
|
|
309
|
-
debug(
|
|
305
|
+
debug(
|
|
306
|
+
f"Ignoring offset parameter: {offset} - it leads to redundant requests"
|
|
307
|
+
)
|
|
310
308
|
|
|
311
309
|
else:
|
|
312
|
-
if mode ==
|
|
310
|
+
if mode == "movie":
|
|
313
311
|
# supported params: imdbid
|
|
314
|
-
imdb_id = getattr(request.query,
|
|
312
|
+
imdb_id = getattr(request.query, "imdbid", "")
|
|
315
313
|
|
|
316
|
-
releases = get_search_results(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
314
|
+
releases = get_search_results(
|
|
315
|
+
shared_state,
|
|
316
|
+
request_from,
|
|
317
|
+
imdb_id=imdb_id,
|
|
318
|
+
mirror=mirror,
|
|
319
|
+
)
|
|
320
320
|
|
|
321
|
-
elif mode ==
|
|
321
|
+
elif mode == "tvsearch":
|
|
322
322
|
# supported params: imdbid, season, ep
|
|
323
|
-
imdb_id = getattr(request.query,
|
|
324
|
-
season = getattr(request.query,
|
|
325
|
-
episode = getattr(request.query,
|
|
326
|
-
releases = get_search_results(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
323
|
+
imdb_id = getattr(request.query, "imdbid", "")
|
|
324
|
+
season = getattr(request.query, "season", None)
|
|
325
|
+
episode = getattr(request.query, "ep", None)
|
|
326
|
+
releases = get_search_results(
|
|
327
|
+
shared_state,
|
|
328
|
+
request_from,
|
|
329
|
+
imdb_id=imdb_id,
|
|
330
|
+
mirror=mirror,
|
|
331
|
+
season=season,
|
|
332
|
+
episode=episode,
|
|
333
|
+
)
|
|
334
|
+
elif mode == "book":
|
|
335
|
+
author = getattr(request.query, "author", "")
|
|
336
|
+
title = getattr(request.query, "title", "")
|
|
335
337
|
search_phrase = " ".join(filter(None, [author, title]))
|
|
336
|
-
releases = get_search_results(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
338
|
+
releases = get_search_results(
|
|
339
|
+
shared_state,
|
|
340
|
+
request_from,
|
|
341
|
+
search_phrase=search_phrase,
|
|
342
|
+
mirror=mirror,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
elif mode == "search":
|
|
342
346
|
if "lazylibrarian" in request_from.lower():
|
|
343
|
-
search_phrase = getattr(request.query,
|
|
344
|
-
releases = get_search_results(
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
347
|
+
search_phrase = getattr(request.query, "q", "")
|
|
348
|
+
releases = get_search_results(
|
|
349
|
+
shared_state,
|
|
350
|
+
request_from,
|
|
351
|
+
search_phrase=search_phrase,
|
|
352
|
+
mirror=mirror,
|
|
353
|
+
)
|
|
348
354
|
else:
|
|
349
355
|
info(
|
|
350
|
-
f
|
|
356
|
+
f"Ignoring search request from {request_from} - only imdbid searches are supported"
|
|
357
|
+
)
|
|
351
358
|
releases = [] # sonarr expects this but we will not support non-imdbid searches
|
|
352
359
|
|
|
353
360
|
items = ""
|
|
@@ -359,7 +366,7 @@ def setup_arr_routes(app):
|
|
|
359
366
|
source = sax_utils.escape(release.get("source", ""))
|
|
360
367
|
|
|
361
368
|
if not "lazylibrarian" in request_from.lower():
|
|
362
|
-
title = f
|
|
369
|
+
title = f"[{release.get('hostname', '').upper()}] {title}"
|
|
363
370
|
|
|
364
371
|
# Get publication date - sources should provide valid dates
|
|
365
372
|
pub_date = release.get("date", "").strip()
|
|
@@ -374,10 +381,11 @@ def setup_arr_routes(app):
|
|
|
374
381
|
<enclosure url="{release.get("link", "")}" length="{release.get("size", 0)}" type="application/x-nzb" />
|
|
375
382
|
</item>'''
|
|
376
383
|
|
|
377
|
-
requires_placeholder_item = not getattr(
|
|
378
|
-
|
|
384
|
+
requires_placeholder_item = not getattr(
|
|
385
|
+
request.query, "imdbid", ""
|
|
386
|
+
) and not getattr(request.query, "q", "")
|
|
379
387
|
if requires_placeholder_item and not items:
|
|
380
|
-
items = f
|
|
388
|
+
items = f"""
|
|
381
389
|
<item>
|
|
382
390
|
<title>No results found</title>
|
|
383
391
|
<guid isPermaLink="False">0</guid>
|
|
@@ -385,26 +393,26 @@ def setup_arr_routes(app):
|
|
|
385
393
|
<comments>No results matched your search criteria.</comments>
|
|
386
394
|
<pubDate>{datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000")}</pubDate>
|
|
387
395
|
<enclosure url="https://github.com/rix1337/Quasarr" length="0" type="application/x-nzb" />
|
|
388
|
-
</item>
|
|
396
|
+
</item>"""
|
|
389
397
|
|
|
390
|
-
return f
|
|
398
|
+
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
391
399
|
<rss>
|
|
392
400
|
<channel>
|
|
393
401
|
{items}
|
|
394
402
|
</channel>
|
|
395
|
-
</rss>
|
|
403
|
+
</rss>"""
|
|
396
404
|
except Exception as e:
|
|
397
405
|
info(f"Error loading search results: {e}")
|
|
398
406
|
info(traceback.format_exc())
|
|
399
407
|
info(f"[ERROR] Unknown indexer request: {dict(request.query)}")
|
|
400
|
-
return
|
|
408
|
+
return """<?xml version="1.0" encoding="UTF-8"?>
|
|
401
409
|
<rss>
|
|
402
410
|
<channel>
|
|
403
411
|
<title>Quasarr Indexer</title>
|
|
404
412
|
<description>Quasarr Indexer API</description>
|
|
405
413
|
<link>https://quasarr.indexer/</link>
|
|
406
414
|
</channel>
|
|
407
|
-
</rss>
|
|
415
|
+
</rss>"""
|
|
408
416
|
|
|
409
417
|
info(f"[ERROR] Unknown general request: {dict(request.query)}")
|
|
410
418
|
return {"error": True}
|