quasarr 1.3.5__py3-none-any.whl → 1.20.4__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 +157 -56
- quasarr/api/__init__.py +141 -36
- quasarr/api/arr/__init__.py +197 -78
- quasarr/api/captcha/__init__.py +897 -42
- quasarr/api/config/__init__.py +23 -0
- quasarr/api/sponsors_helper/__init__.py +84 -22
- quasarr/api/statistics/__init__.py +196 -0
- quasarr/downloads/__init__.py +237 -434
- quasarr/downloads/linkcrypters/al.py +237 -0
- quasarr/downloads/linkcrypters/filecrypt.py +178 -31
- quasarr/downloads/linkcrypters/hide.py +123 -0
- quasarr/downloads/packages/__init__.py +461 -0
- quasarr/downloads/sources/al.py +697 -0
- quasarr/downloads/sources/by.py +106 -0
- quasarr/downloads/sources/dd.py +6 -78
- quasarr/downloads/sources/dj.py +7 -0
- quasarr/downloads/sources/dt.py +1 -1
- quasarr/downloads/sources/dw.py +2 -2
- quasarr/downloads/sources/he.py +112 -0
- quasarr/downloads/sources/mb.py +47 -0
- quasarr/downloads/sources/nk.py +51 -0
- quasarr/downloads/sources/nx.py +36 -81
- quasarr/downloads/sources/sf.py +27 -4
- quasarr/downloads/sources/sj.py +7 -0
- quasarr/downloads/sources/sl.py +90 -0
- quasarr/downloads/sources/wd.py +110 -0
- quasarr/providers/cloudflare.py +204 -0
- quasarr/providers/html_images.py +20 -0
- quasarr/providers/html_templates.py +210 -108
- quasarr/providers/imdb_metadata.py +15 -2
- quasarr/providers/myjd_api.py +36 -5
- quasarr/providers/notifications.py +30 -5
- quasarr/providers/obfuscated.py +35 -0
- quasarr/providers/sessions/__init__.py +0 -0
- quasarr/providers/sessions/al.py +286 -0
- quasarr/providers/sessions/dd.py +78 -0
- quasarr/providers/sessions/nx.py +76 -0
- quasarr/providers/shared_state.py +368 -23
- quasarr/providers/statistics.py +154 -0
- quasarr/providers/version.py +60 -1
- quasarr/search/__init__.py +112 -36
- quasarr/search/sources/al.py +448 -0
- quasarr/search/sources/by.py +203 -0
- quasarr/search/sources/dd.py +17 -6
- quasarr/search/sources/dj.py +213 -0
- quasarr/search/sources/dt.py +37 -7
- quasarr/search/sources/dw.py +27 -47
- quasarr/search/sources/fx.py +27 -29
- quasarr/search/sources/he.py +196 -0
- quasarr/search/sources/mb.py +195 -0
- quasarr/search/sources/nk.py +188 -0
- quasarr/search/sources/nx.py +22 -6
- quasarr/search/sources/sf.py +143 -151
- quasarr/search/sources/sj.py +213 -0
- quasarr/search/sources/sl.py +246 -0
- quasarr/search/sources/wd.py +208 -0
- quasarr/storage/config.py +20 -4
- quasarr/storage/setup.py +224 -56
- quasarr-1.20.4.dist-info/METADATA +304 -0
- quasarr-1.20.4.dist-info/RECORD +72 -0
- {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/WHEEL +1 -1
- quasarr/providers/tvmaze_metadata.py +0 -23
- quasarr-1.3.5.dist-info/METADATA +0 -174
- quasarr-1.3.5.dist-info/RECORD +0 -43
- {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/entry_points.txt +0 -0
- {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.3.5.dist-info → quasarr-1.20.4.dist-info}/top_level.txt +0 -0
quasarr/api/arr/__init__.py
CHANGED
|
@@ -7,14 +7,16 @@ import xml.sax.saxutils as sax_utils
|
|
|
7
7
|
from base64 import urlsafe_b64decode
|
|
8
8
|
from datetime import datetime
|
|
9
9
|
from functools import wraps
|
|
10
|
+
from urllib.parse import urlparse, parse_qs
|
|
10
11
|
from xml.etree import ElementTree
|
|
11
12
|
|
|
12
13
|
from bottle import abort, request
|
|
13
14
|
|
|
14
|
-
from quasarr.downloads import download
|
|
15
|
+
from quasarr.downloads import download
|
|
16
|
+
from quasarr.downloads.packages import get_packages, delete_package
|
|
15
17
|
from quasarr.providers import shared_state
|
|
16
18
|
from quasarr.providers.log import info, debug
|
|
17
|
-
from quasarr.providers.
|
|
19
|
+
from quasarr.providers.version import get_version
|
|
18
20
|
from quasarr.search import get_search_results
|
|
19
21
|
from quasarr.storage.config import Config
|
|
20
22
|
|
|
@@ -64,15 +66,21 @@ def setup_arr_routes(app):
|
|
|
64
66
|
password = root.find(".//file").attrib.get("password")
|
|
65
67
|
imdb_id = root.find(".//file").attrib.get("imdb_id")
|
|
66
68
|
|
|
67
|
-
info(f
|
|
69
|
+
info(f'Attempting download for "{title}"')
|
|
68
70
|
request_from = request.headers.get('User-Agent')
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
downloaded = download(shared_state, request_from, title, url, mirror, size_mb, password, imdb_id)
|
|
72
|
+
try:
|
|
73
|
+
success = downloaded["success"]
|
|
74
|
+
package_id = downloaded["package_id"]
|
|
75
|
+
title = downloaded["title"]
|
|
76
|
+
|
|
77
|
+
if success:
|
|
78
|
+
info(f'"{title}" added successfully!')
|
|
79
|
+
else:
|
|
80
|
+
info(f'"{title}" added unsuccessfully! See log for details.')
|
|
73
81
|
nzo_ids.append(package_id)
|
|
74
|
-
|
|
75
|
-
info(f"{title}
|
|
82
|
+
except KeyError:
|
|
83
|
+
info(f'Failed to download "{title}" - no package_id returned')
|
|
76
84
|
|
|
77
85
|
return {
|
|
78
86
|
"status": True,
|
|
@@ -86,12 +94,25 @@ def setup_arr_routes(app):
|
|
|
86
94
|
api_type = 'arr_download_client' if request.query.mode else 'arr_indexer' if request.query.t else None
|
|
87
95
|
|
|
88
96
|
if api_type == 'arr_download_client':
|
|
89
|
-
# This
|
|
97
|
+
# This builds a mock SABnzbd API response based on the My JDownloader integration
|
|
90
98
|
try:
|
|
91
99
|
mode = request.query.mode
|
|
92
|
-
if mode == "
|
|
100
|
+
if mode == "auth":
|
|
101
|
+
return {
|
|
102
|
+
"auth": "apikey"
|
|
103
|
+
}
|
|
104
|
+
elif mode == "version":
|
|
93
105
|
return {
|
|
94
|
-
"version": "
|
|
106
|
+
"version": f"Quasarr {get_version()}"
|
|
107
|
+
}
|
|
108
|
+
elif mode == "get_cats":
|
|
109
|
+
return {
|
|
110
|
+
"categories": [
|
|
111
|
+
"*",
|
|
112
|
+
"movies",
|
|
113
|
+
"tv",
|
|
114
|
+
"docs"
|
|
115
|
+
]
|
|
95
116
|
}
|
|
96
117
|
elif mode == "get_config":
|
|
97
118
|
return {
|
|
@@ -115,7 +136,12 @@ def setup_arr_routes(app):
|
|
|
115
136
|
"name": "tv",
|
|
116
137
|
"order": 2,
|
|
117
138
|
"dir": "",
|
|
118
|
-
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"name": "docs",
|
|
142
|
+
"order": 3,
|
|
143
|
+
"dir": "",
|
|
144
|
+
},
|
|
119
145
|
]
|
|
120
146
|
}
|
|
121
147
|
}
|
|
@@ -125,6 +151,66 @@ def setup_arr_routes(app):
|
|
|
125
151
|
"quasarr": True
|
|
126
152
|
}
|
|
127
153
|
}
|
|
154
|
+
elif mode == "addurl":
|
|
155
|
+
raw_name = getattr(request.query, "name", None)
|
|
156
|
+
if not raw_name:
|
|
157
|
+
abort(400, "missing or empty 'name' parameter")
|
|
158
|
+
|
|
159
|
+
payload = False
|
|
160
|
+
try:
|
|
161
|
+
parsed = urlparse(raw_name)
|
|
162
|
+
qs = parse_qs(parsed.query)
|
|
163
|
+
payload = qs.get("payload", [None])[0]
|
|
164
|
+
except Exception as e:
|
|
165
|
+
abort(400, f"invalid URL in 'name': {e}")
|
|
166
|
+
if not payload:
|
|
167
|
+
abort(400, "missing 'payload' parameter in URL")
|
|
168
|
+
|
|
169
|
+
title = url = mirror = size_mb = password = imdb_id = None
|
|
170
|
+
try:
|
|
171
|
+
decoded = urlsafe_b64decode(payload.encode()).decode()
|
|
172
|
+
parts = decoded.split("|")
|
|
173
|
+
if len(parts) != 6:
|
|
174
|
+
raise ValueError(f"expected 6 fields, got {len(parts)}")
|
|
175
|
+
title, url, mirror, size_mb, password, imdb_id = parts
|
|
176
|
+
except Exception as e:
|
|
177
|
+
abort(400, f"invalid payload format: {e}")
|
|
178
|
+
|
|
179
|
+
mirror = None if mirror == "None" else mirror
|
|
180
|
+
|
|
181
|
+
nzo_ids = []
|
|
182
|
+
info(f'Attempting download for "{title}"')
|
|
183
|
+
request_from = "lazylibrarian"
|
|
184
|
+
|
|
185
|
+
downloaded = download(
|
|
186
|
+
shared_state,
|
|
187
|
+
request_from,
|
|
188
|
+
title,
|
|
189
|
+
url,
|
|
190
|
+
mirror,
|
|
191
|
+
size_mb,
|
|
192
|
+
password or None,
|
|
193
|
+
imdb_id or None,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
success = downloaded["success"]
|
|
198
|
+
package_id = downloaded["package_id"]
|
|
199
|
+
title = downloaded.get("title", title)
|
|
200
|
+
|
|
201
|
+
if success:
|
|
202
|
+
info(f'"{title}" added successfully!')
|
|
203
|
+
else:
|
|
204
|
+
info(f'"{title}" added unsuccessfully! See log for details.')
|
|
205
|
+
nzo_ids.append(package_id)
|
|
206
|
+
except KeyError:
|
|
207
|
+
info(f'Failed to download "{title}" - no package_id returned')
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"status": True,
|
|
211
|
+
"nzo_ids": nzo_ids
|
|
212
|
+
}
|
|
213
|
+
|
|
128
214
|
elif mode == "queue" or mode == "history":
|
|
129
215
|
if request.query.name and request.query.name == "delete":
|
|
130
216
|
package_id = request.query.value
|
|
@@ -139,109 +225,133 @@ def setup_arr_routes(app):
|
|
|
139
225
|
return {
|
|
140
226
|
"queue": {
|
|
141
227
|
"paused": False,
|
|
142
|
-
"slots": packages
|
|
228
|
+
"slots": packages.get("queue", [])
|
|
143
229
|
}
|
|
144
230
|
}
|
|
145
231
|
elif mode == "history":
|
|
146
232
|
return {
|
|
147
233
|
"history": {
|
|
148
234
|
"paused": False,
|
|
149
|
-
"slots": packages
|
|
235
|
+
"slots": packages.get("history", [])
|
|
150
236
|
}
|
|
151
237
|
}
|
|
152
238
|
except Exception as e:
|
|
153
239
|
info(f"Error loading packages: {e}")
|
|
154
240
|
info(traceback.format_exc())
|
|
241
|
+
info(f"[ERROR] Unknown download client request: {dict(request.query)}")
|
|
155
242
|
return {
|
|
156
243
|
"status": False
|
|
157
244
|
}
|
|
158
245
|
|
|
159
246
|
elif api_type == 'arr_indexer':
|
|
160
|
-
# this
|
|
247
|
+
# this builds a mock Newznab API response based on Quasarr search
|
|
161
248
|
try:
|
|
162
249
|
if mirror:
|
|
163
250
|
debug(f'Search will only return releases that match this mirror: "{mirror}"')
|
|
164
251
|
|
|
165
252
|
mode = request.query.t
|
|
253
|
+
request_from = request.headers.get('User-Agent')
|
|
254
|
+
|
|
166
255
|
if mode == 'caps':
|
|
256
|
+
info(f"Providing indexer capability information to {request_from}")
|
|
167
257
|
return '''<?xml version="1.0" encoding="UTF-8"?>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
258
|
+
<caps>
|
|
259
|
+
<server
|
|
260
|
+
version="1.33.7"
|
|
261
|
+
title="Quasarr"
|
|
262
|
+
url="https://quasarr.indexer/"
|
|
263
|
+
email="support@quasarr.indexer"
|
|
264
|
+
/>
|
|
265
|
+
<limits max="9999" default="9999" />
|
|
266
|
+
<registration available="no" open="no" />
|
|
267
|
+
<searching>
|
|
268
|
+
<search available="yes" supportedParams="q" />
|
|
269
|
+
<tv-search available="yes" supportedParams="imdbid,season,ep" />
|
|
270
|
+
<movie-search available="yes" supportedParams="imdbid" />
|
|
271
|
+
</searching>
|
|
272
|
+
<categories>
|
|
273
|
+
<category id="5000" name="TV" />
|
|
274
|
+
<category id="2000" name="Movies" />
|
|
275
|
+
<category id="7000" name="Books">
|
|
276
|
+
</category>
|
|
277
|
+
</categories>
|
|
278
|
+
</caps>'''
|
|
279
|
+
elif mode in ['movie', 'tvsearch', 'book', 'search']:
|
|
179
280
|
releases = []
|
|
180
281
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
elif mode == 'tvsearch':
|
|
195
|
-
season = getattr(request.query, 'season', "")
|
|
196
|
-
episode = getattr(request.query, 'ep', "")
|
|
197
|
-
# only plain search string and tvrage id is implemented
|
|
198
|
-
search_param = getattr(request.query, 'q', "")
|
|
199
|
-
if not search_param:
|
|
200
|
-
tvrage_id = getattr(request.query, 'rid', "")
|
|
201
|
-
if tvrage_id:
|
|
202
|
-
search_param = get_title_from_tvrage_id(tvrage_id)
|
|
203
|
-
|
|
204
|
-
offset = getattr(request.query, 'offset', "") # ignoring offset higher than 0 on purpose
|
|
205
|
-
if int(offset) == 0:
|
|
282
|
+
try:
|
|
283
|
+
offset = int(getattr(request.query, 'offset', 0))
|
|
284
|
+
except (AttributeError, ValueError):
|
|
285
|
+
offset = 0
|
|
286
|
+
|
|
287
|
+
if offset > 0:
|
|
288
|
+
debug(f"Ignoring offset parameter: {offset} - it leads to redundant requests")
|
|
289
|
+
|
|
290
|
+
else:
|
|
291
|
+
if mode == 'movie':
|
|
292
|
+
# supported params: imdbid
|
|
293
|
+
imdb_id = getattr(request.query, 'imdbid', '')
|
|
294
|
+
|
|
206
295
|
releases = get_search_results(shared_state, request_from,
|
|
207
|
-
|
|
296
|
+
imdb_id=imdb_id,
|
|
297
|
+
mirror=mirror
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
elif mode == 'tvsearch':
|
|
301
|
+
# supported params: imdbid, season, ep
|
|
302
|
+
imdb_id = getattr(request.query, 'imdbid', '')
|
|
303
|
+
season = getattr(request.query, 'season', None)
|
|
304
|
+
episode = getattr(request.query, 'ep', None)
|
|
305
|
+
releases = get_search_results(shared_state, request_from,
|
|
306
|
+
imdb_id=imdb_id,
|
|
208
307
|
mirror=mirror,
|
|
209
308
|
season=season,
|
|
210
309
|
episode=episode
|
|
211
310
|
)
|
|
212
|
-
|
|
213
|
-
|
|
311
|
+
elif mode == 'book':
|
|
312
|
+
author = getattr(request.query, 'author', '')
|
|
313
|
+
title = getattr(request.query, 'title', '')
|
|
314
|
+
search_phrase = " ".join(filter(None, [author, title]))
|
|
315
|
+
releases = get_search_results(shared_state, request_from,
|
|
316
|
+
search_phrase=search_phrase,
|
|
317
|
+
mirror=mirror
|
|
318
|
+
)
|
|
214
319
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
</item>'''
|
|
320
|
+
elif mode == 'search':
|
|
321
|
+
if "lazylibrarian" in request_from.lower():
|
|
322
|
+
search_phrase = getattr(request.query, 'q', '')
|
|
323
|
+
releases = get_search_results(shared_state, request_from,
|
|
324
|
+
search_phrase=search_phrase,
|
|
325
|
+
mirror=mirror
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
info(
|
|
329
|
+
f'Ignoring search request from {request_from} - only imdbid searches are supported')
|
|
330
|
+
releases = [{}] # sonarr expects this but we will not support non-imdbid searches
|
|
227
331
|
|
|
332
|
+
items = ""
|
|
228
333
|
for release in releases:
|
|
229
|
-
release = release
|
|
334
|
+
release = release.get("details", {})
|
|
335
|
+
|
|
336
|
+
# Ensure clean XML output
|
|
337
|
+
title = sax_utils.escape(release.get("title", ""))
|
|
338
|
+
source = sax_utils.escape(release.get("source", ""))
|
|
230
339
|
|
|
231
|
-
|
|
340
|
+
if not "lazylibrarian" in request_from.lower():
|
|
341
|
+
title = f'[{release.get("hostname", "").upper()}] {title}'
|
|
232
342
|
|
|
233
343
|
items += f'''
|
|
234
344
|
<item>
|
|
235
|
-
<title>{
|
|
236
|
-
<guid isPermaLink="True">{release
|
|
237
|
-
<link>{release
|
|
238
|
-
<comments>{
|
|
239
|
-
<pubDate>{release
|
|
240
|
-
<enclosure url="{release
|
|
345
|
+
<title>{title}</title>
|
|
346
|
+
<guid isPermaLink="True">{release.get("link", "")}</guid>
|
|
347
|
+
<link>{release.get("link", "")}</link>
|
|
348
|
+
<comments>{source}</comments>
|
|
349
|
+
<pubDate>{release.get("date", datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000"))}</pubDate>
|
|
350
|
+
<enclosure url="{release.get("link", "")}" length="{release.get("size", 0)}" type="application/x-nzb" />
|
|
241
351
|
</item>'''
|
|
242
352
|
|
|
243
353
|
return f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
244
|
-
<rss
|
|
354
|
+
<rss>
|
|
245
355
|
<channel>
|
|
246
356
|
{items}
|
|
247
357
|
</channel>
|
|
@@ -249,6 +359,15 @@ def setup_arr_routes(app):
|
|
|
249
359
|
except Exception as e:
|
|
250
360
|
info(f"Error loading search results: {e}")
|
|
251
361
|
info(traceback.format_exc())
|
|
252
|
-
|
|
253
|
-
|
|
362
|
+
info(f"[ERROR] Unknown indexer request: {dict(request.query)}")
|
|
363
|
+
return '''<?xml version="1.0" encoding="UTF-8"?>
|
|
364
|
+
<rss>
|
|
365
|
+
<channel>
|
|
366
|
+
<title>Quasarr Indexer</title>
|
|
367
|
+
<description>Quasarr Indexer API</description>
|
|
368
|
+
<link>https://quasarr.indexer/</link>
|
|
369
|
+
</channel>
|
|
370
|
+
</rss>'''
|
|
371
|
+
|
|
372
|
+
info(f"[ERROR] Unknown general request: {dict(request.query)}")
|
|
254
373
|
return {"error": True}
|