quasarr 2.5.0__tar.gz → 2.6.1__tar.gz
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-2.5.0 → quasarr-2.6.1}/.github/workflows/PullRequests.yml +5 -5
- {quasarr-2.5.0 → quasarr-2.6.1}/.github/workflows/Release.yml +14 -2
- {quasarr-2.5.0 → quasarr-2.6.1}/PKG-INFO +1 -1
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/__init__.py +30 -35
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/api/__init__.py +17 -9
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/api/arr/__init__.py +15 -6
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/api/captcha/__init__.py +2 -9
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/api/config/__init__.py +10 -0
- quasarr-2.6.1/quasarr/api/jdownloader/__init__.py +239 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/api/packages/__init__.py +2 -12
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/__init__.py +2 -0
- quasarr-2.6.1/quasarr/downloads/sources/hs.py +131 -0
- quasarr-2.6.1/quasarr/downloads/sources/wd.py +169 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/cloudflare.py +46 -1
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/html_templates.py +14 -3
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/sessions/al.py +4 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/shared_state.py +17 -17
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/version.py +1 -1
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/__init__.py +4 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/al.py +17 -13
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/by.py +4 -1
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/dd.py +16 -4
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/dl.py +13 -1
- quasarr-2.6.1/quasarr/search/sources/hs.py +515 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/mb.py +1 -7
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/nx.py +4 -1
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/wd.py +33 -6
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/wx.py +10 -8
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/storage/config.py +1 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/storage/setup.py +90 -64
- quasarr-2.5.0/quasarr/downloads/sources/wd.py +0 -155
- {quasarr-2.5.0 → quasarr-2.6.1}/.github/FUNDING.yml +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/.github/workflows/HostnameRedaction.yml +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/.gitignore +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/.pre-commit-config.yaml +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/CONTRIBUTING.md +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/LICENSE +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/Quasarr.png +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/Quasarr.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/README.md +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/docker/Dockerfile +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/docker/dev-services-compose.yml +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/docker/docker-compose.yml +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/pre-commit.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/pyproject.toml +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/api/sponsors_helper/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/api/statistics/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/linkcrypters/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/linkcrypters/al.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/linkcrypters/filecrypt.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/linkcrypters/hide.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/packages/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/al.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/by.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/dd.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/dj.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/dl.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/dt.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/dw.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/he.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/mb.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/nk.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/nx.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/sf.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/sj.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/sl.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/downloads/sources/wx.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/auth.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/hostname_issues.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/html_images.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/imdb_metadata.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/jd_cache.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/log.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/myjd_api.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/notifications.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/obfuscated.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/sessions/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/sessions/dd.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/sessions/dl.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/sessions/nx.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/statistics.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/utils.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/providers/web_server.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/dj.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/dt.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/dw.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/fx.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/he.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/nk.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/sf.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/sj.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/search/sources/sl.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/storage/__init__.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/quasarr/storage/sqlite_database.py +0 -0
- {quasarr-2.5.0 → quasarr-2.6.1}/uv.lock +0 -0
|
@@ -53,7 +53,7 @@ jobs:
|
|
|
53
53
|
|
|
54
54
|
version:
|
|
55
55
|
needs: [ quality-check ]
|
|
56
|
-
if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name
|
|
56
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
|
|
57
57
|
runs-on: ubuntu-latest
|
|
58
58
|
outputs:
|
|
59
59
|
version: ${{ steps.version.outputs.version }}
|
|
@@ -76,7 +76,7 @@ jobs:
|
|
|
76
76
|
|
|
77
77
|
build-wheel:
|
|
78
78
|
needs: [ quality-check, version ]
|
|
79
|
-
if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name
|
|
79
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
|
|
80
80
|
runs-on: ubuntu-latest
|
|
81
81
|
outputs:
|
|
82
82
|
attestation-id: ${{ steps.attest.outputs.attestation-id }}
|
|
@@ -106,7 +106,7 @@ jobs:
|
|
|
106
106
|
|
|
107
107
|
build-exe:
|
|
108
108
|
needs: [ quality-check, version ]
|
|
109
|
-
if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name
|
|
109
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
|
|
110
110
|
runs-on: windows-latest
|
|
111
111
|
env:
|
|
112
112
|
TMP: "D:\\a\\temp"
|
|
@@ -194,7 +194,7 @@ jobs:
|
|
|
194
194
|
|
|
195
195
|
build-docker-amd64:
|
|
196
196
|
needs: [ quality-check, version, build-wheel ]
|
|
197
|
-
if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name
|
|
197
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
|
|
198
198
|
runs-on: ubuntu-latest
|
|
199
199
|
steps:
|
|
200
200
|
- uses: actions/checkout@v6
|
|
@@ -226,7 +226,7 @@ jobs:
|
|
|
226
226
|
|
|
227
227
|
build-docker-arm64:
|
|
228
228
|
needs: [ quality-check, version, build-wheel ]
|
|
229
|
-
if: needs.quality-check.outputs.changes_pushed != 'true' && (github.head_ref || github.ref_name
|
|
229
|
+
if: needs.quality-check.outputs.changes_pushed != 'true' && ((github.head_ref == 'dev' && github.base_ref == 'main') || (github.event_name == 'workflow_dispatch' && github.ref_name == 'dev'))
|
|
230
230
|
runs-on: ubuntu-24.04-arm
|
|
231
231
|
steps:
|
|
232
232
|
- uses: actions/checkout@v6
|
|
@@ -254,18 +254,24 @@ jobs:
|
|
|
254
254
|
uses: metcalfc/changelog-generator@v4.6.2
|
|
255
255
|
with:
|
|
256
256
|
myToken: ${{ secrets.GITHUB_TOKEN }}
|
|
257
|
-
- name:
|
|
257
|
+
- name: Get PR Body
|
|
258
258
|
env:
|
|
259
259
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
260
260
|
run: |
|
|
261
261
|
PR_BODY=$(gh pr list --search "${{ github.sha }}" --state merged --json body --jq '.[0].body // empty')
|
|
262
|
-
if [ -n "$PR_BODY" ]; then
|
|
262
|
+
if [ -n "$PR_BODY" ]; then
|
|
263
|
+
echo "$PR_BODY" > pr_body.txt
|
|
264
|
+
fi
|
|
263
265
|
- name: Create Release Body
|
|
264
266
|
run: |
|
|
265
267
|
echo "### Docker:" > release_body.md
|
|
266
268
|
echo "\`docker pull ${{ env.GHCR_ENDPOINT }}:latest\`" >> release_body.md
|
|
267
269
|
echo "### Python:" >> release_body.md
|
|
268
270
|
echo "\`uv tool upgrade quasarr\`" >> release_body.md
|
|
271
|
+
if [ -f pr_body.txt ]; then
|
|
272
|
+
cat pr_body.txt >> release_body.md
|
|
273
|
+
echo "" >> release_body.md
|
|
274
|
+
fi
|
|
269
275
|
echo "### Changelog:" >> release_body.md
|
|
270
276
|
echo "${{ steps.changelog.outputs.changelog }}" >> release_body.md
|
|
271
277
|
echo "[Attestation](https://github.com/${{ github.repository }}/attestations/${{ needs.build-wheel.outputs.attestation-id }})" >> release_body.md
|
|
@@ -293,6 +299,12 @@ jobs:
|
|
|
293
299
|
run: |
|
|
294
300
|
TAG="v.$VERSION"
|
|
295
301
|
RELEASE_BODY=$(gh release view "$TAG" --json body --jq .body)
|
|
302
|
+
|
|
303
|
+
# Truncate to 4000 characters to avoid Discord limits
|
|
304
|
+
if [ ${#RELEASE_BODY} -gt 4000 ]; then
|
|
305
|
+
RELEASE_BODY="${RELEASE_BODY:0:4000}..."
|
|
306
|
+
fi
|
|
307
|
+
|
|
296
308
|
if [ -n "$DISCORD_WEBHOOK" ]; then
|
|
297
309
|
jq -n --arg title "🚀 New Release: $TAG" --arg desc "$RELEASE_BODY" --arg url "https://github.com/$REPO/releases/tag/$TAG" '{content: null, embeds: [{title: $title, description: $desc, url: $url, color: 5763719}]}' > discord_payload.json
|
|
298
310
|
curl -H "Content-Type: application/json" -d @discord_payload.json "$DISCORD_WEBHOOK"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quasarr
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.6.1
|
|
4
4
|
Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
|
|
5
5
|
Author-email: rix1337 <rix1337@users.noreply.github.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -386,48 +386,43 @@ def jdownloader_connection(shared_state_dict, shared_state_lock):
|
|
|
386
386
|
try:
|
|
387
387
|
shared_state.set_state(shared_state_dict, shared_state_lock)
|
|
388
388
|
|
|
389
|
-
|
|
389
|
+
while True:
|
|
390
|
+
shared_state.set_device_from_config()
|
|
390
391
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if not connection_established:
|
|
395
|
-
i = 0
|
|
396
|
-
while i < 10:
|
|
397
|
-
i += 1
|
|
392
|
+
device = shared_state.get_device()
|
|
393
|
+
|
|
394
|
+
try:
|
|
398
395
|
info(
|
|
399
|
-
f'Connection
|
|
396
|
+
f'Connection to JDownloader successful. Device name: "{device.name}"'
|
|
400
397
|
)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
shared_state.get_device() and shared_state.get_device().name
|
|
405
|
-
)
|
|
406
|
-
if connection_established:
|
|
407
|
-
break
|
|
398
|
+
except Exception as e:
|
|
399
|
+
info(f"Error connecting to JDownloader: {e}! Stopping Quasarr...")
|
|
400
|
+
sys.exit(1)
|
|
408
401
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
except Exception as e:
|
|
414
|
-
info(f"Error connecting to JDownloader: {e}! Stopping Quasarr!")
|
|
415
|
-
sys.exit(1)
|
|
402
|
+
try:
|
|
403
|
+
shared_state.set_device_settings()
|
|
404
|
+
except Exception as e:
|
|
405
|
+
print(f"Error checking settings: {e}")
|
|
416
406
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
407
|
+
try:
|
|
408
|
+
shared_state.update_jdownloader()
|
|
409
|
+
except Exception as e:
|
|
410
|
+
print(f"Error updating JDownloader: {e}")
|
|
421
411
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
412
|
+
try:
|
|
413
|
+
shared_state.start_downloads()
|
|
414
|
+
except Exception as e:
|
|
415
|
+
print(f"Error starting downloads: {e}")
|
|
426
416
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
417
|
+
while True:
|
|
418
|
+
time.sleep(300)
|
|
419
|
+
device_state = shared_state.check_device(
|
|
420
|
+
shared_state.values.get("device")
|
|
421
|
+
)
|
|
422
|
+
if not device_state:
|
|
423
|
+
info("Lost connection to JDownloader. Reconnecting...")
|
|
424
|
+
shared_state.update("device", False)
|
|
425
|
+
break
|
|
431
426
|
|
|
432
427
|
except KeyboardInterrupt:
|
|
433
428
|
pass
|
|
@@ -8,6 +8,7 @@ import quasarr.providers.html_images as images
|
|
|
8
8
|
from quasarr.api.arr import setup_arr_routes
|
|
9
9
|
from quasarr.api.captcha import setup_captcha_routes
|
|
10
10
|
from quasarr.api.config import setup_config
|
|
11
|
+
from quasarr.api.jdownloader import get_jdownloader_modal_script, get_jdownloader_status
|
|
11
12
|
from quasarr.api.packages import setup_packages_routes
|
|
12
13
|
from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
|
|
13
14
|
from quasarr.api.statistics import setup_statistics
|
|
@@ -49,12 +50,9 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
49
50
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
50
51
|
api_key = Config("API").get("key")
|
|
51
52
|
|
|
52
|
-
# Get
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
jd_connected = device is not None and device is not False
|
|
56
|
-
except:
|
|
57
|
-
jd_connected = False
|
|
53
|
+
# Get JDownloader status and modal script
|
|
54
|
+
jd_status = get_jdownloader_status(shared_state)
|
|
55
|
+
jd_modal_script = get_jdownloader_modal_script()
|
|
58
56
|
|
|
59
57
|
# Calculate hostname status
|
|
60
58
|
hostnames_config = Config("Hostnames")
|
|
@@ -126,10 +124,14 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
126
124
|
# Status bars
|
|
127
125
|
status_bars = f"""
|
|
128
126
|
<div class="status-bar">
|
|
129
|
-
<span class="status-pill {"
|
|
130
|
-
|
|
127
|
+
<span class="status-pill {jd_status["status_class"]}"
|
|
128
|
+
onclick="openJDownloaderModal()"
|
|
129
|
+
title="Click to configure JDownloader">
|
|
130
|
+
{jd_status["status_text"]}
|
|
131
131
|
</span>
|
|
132
|
-
<span class="status-pill {hostname_status_class}"
|
|
132
|
+
<span class="status-pill {hostname_status_class}"
|
|
133
|
+
onclick="location.href='/hostnames'"
|
|
134
|
+
title="Click to configure Hostnames">
|
|
133
135
|
{hostname_status_emoji} {hostname_status_text}
|
|
134
136
|
</span>
|
|
135
137
|
</div>
|
|
@@ -209,6 +211,11 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
209
211
|
padding: 8px 16px;
|
|
210
212
|
border-radius: 0.5rem;
|
|
211
213
|
font-weight: 500;
|
|
214
|
+
transition: transform 0.1s ease;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
}}
|
|
217
|
+
.status-pill:hover {{
|
|
218
|
+
transform: scale(1.05);
|
|
212
219
|
}}
|
|
213
220
|
.status-pill.success {{
|
|
214
221
|
background: var(--status-success-bg, #e8f5e9);
|
|
@@ -478,6 +485,7 @@ def get_api(shared_state_dict, shared_state_lock):
|
|
|
478
485
|
);
|
|
479
486
|
}}
|
|
480
487
|
</script>
|
|
488
|
+
{jd_modal_script}
|
|
481
489
|
"""
|
|
482
490
|
# Add logout link for form auth
|
|
483
491
|
logout_html = '<a href="/logout">Logout</a>' if show_logout_link() else ""
|
|
@@ -56,6 +56,16 @@ def parse_payload(payload_str):
|
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
def setup_arr_routes(app):
|
|
59
|
+
def check_user_agent():
|
|
60
|
+
user_agent = request.headers.get("User-Agent") or ""
|
|
61
|
+
if not any(
|
|
62
|
+
tool in user_agent.lower() for tool in ["radarr", "sonarr", "lazylibrarian"]
|
|
63
|
+
):
|
|
64
|
+
msg = f"Unsupported User-Agent: {user_agent}. Quasarr as a compatibility layer must be called by Radarr, Sonarr or LazyLibrarian directly."
|
|
65
|
+
info(msg)
|
|
66
|
+
abort(406, msg)
|
|
67
|
+
return user_agent
|
|
68
|
+
|
|
59
69
|
@app.get("/download/")
|
|
60
70
|
def fake_nzb_file():
|
|
61
71
|
payload = request.query.payload
|
|
@@ -75,6 +85,7 @@ def setup_arr_routes(app):
|
|
|
75
85
|
@app.post("/api")
|
|
76
86
|
@require_api_key
|
|
77
87
|
def download_fake_nzb_file():
|
|
88
|
+
request_from = check_user_agent()
|
|
78
89
|
downloads = request.files.getall("name")
|
|
79
90
|
nzo_ids = [] # naming structure for package IDs expected in newznab
|
|
80
91
|
|
|
@@ -97,7 +108,6 @@ def setup_arr_routes(app):
|
|
|
97
108
|
source_key = root.find(".//file").attrib.get("source_key") or None
|
|
98
109
|
|
|
99
110
|
info(f'Attempting download for "{title}"')
|
|
100
|
-
request_from = request.headers.get("User-Agent")
|
|
101
111
|
downloaded = download(
|
|
102
112
|
shared_state,
|
|
103
113
|
request_from,
|
|
@@ -128,6 +138,8 @@ def setup_arr_routes(app):
|
|
|
128
138
|
@app.get("/api/<mirror>")
|
|
129
139
|
@require_api_key
|
|
130
140
|
def quasarr_api(mirror=None):
|
|
141
|
+
request_from = check_user_agent()
|
|
142
|
+
|
|
131
143
|
api_type = (
|
|
132
144
|
"arr_download_client"
|
|
133
145
|
if request.query.mode
|
|
@@ -198,7 +210,6 @@ def setup_arr_routes(app):
|
|
|
198
210
|
|
|
199
211
|
nzo_ids = []
|
|
200
212
|
info(f'Attempting download for "{parsed_payload["title"]}"')
|
|
201
|
-
request_from = "lazylibrarian"
|
|
202
213
|
|
|
203
214
|
downloaded = download(
|
|
204
215
|
shared_state,
|
|
@@ -267,8 +278,6 @@ def setup_arr_routes(app):
|
|
|
267
278
|
)
|
|
268
279
|
|
|
269
280
|
mode = request.query.t
|
|
270
|
-
request_from = request.headers.get("User-Agent")
|
|
271
|
-
|
|
272
281
|
if mode == "caps":
|
|
273
282
|
info(f"Providing indexer capability information to {request_from}")
|
|
274
283
|
return """<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -352,10 +361,10 @@ def setup_arr_routes(app):
|
|
|
352
361
|
mirror=mirror,
|
|
353
362
|
)
|
|
354
363
|
else:
|
|
355
|
-
|
|
364
|
+
# sonarr expects this but we will not support non-imdbid searches
|
|
365
|
+
debug(
|
|
356
366
|
f"Ignoring search request from {request_from} - only imdbid searches are supported"
|
|
357
367
|
)
|
|
358
|
-
releases = [] # sonarr expects this but we will not support non-imdbid searches
|
|
359
368
|
|
|
360
369
|
items = ""
|
|
361
370
|
for release in releases:
|
|
@@ -11,6 +11,7 @@ import requests
|
|
|
11
11
|
from bottle import HTTPResponse, redirect, request, response
|
|
12
12
|
|
|
13
13
|
import quasarr.providers.html_images as images
|
|
14
|
+
from quasarr.api.jdownloader import get_jdownloader_disconnected_page
|
|
14
15
|
from quasarr.downloads.linkcrypters.filecrypt import DLC, get_filecrypt_links
|
|
15
16
|
from quasarr.downloads.packages import delete_package
|
|
16
17
|
from quasarr.providers import obfuscated, shared_state
|
|
@@ -46,15 +47,7 @@ def setup_captcha_routes(app):
|
|
|
46
47
|
except KeyError:
|
|
47
48
|
device = None
|
|
48
49
|
if not device:
|
|
49
|
-
return
|
|
50
|
-
<div class="status-bar">
|
|
51
|
-
<span class="status-pill error">
|
|
52
|
-
❌ JDownloader disconnected
|
|
53
|
-
</span>
|
|
54
|
-
</div>
|
|
55
|
-
<p>
|
|
56
|
-
{render_button("Back", "secondary", {"onclick": "location.href='/'"})}
|
|
57
|
-
</p>''')
|
|
50
|
+
return get_jdownloader_disconnected_page(shared_state)
|
|
58
51
|
|
|
59
52
|
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
60
53
|
if not protected:
|
|
@@ -22,6 +22,8 @@ from quasarr.storage.setup import (
|
|
|
22
22
|
import_hostnames_from_url,
|
|
23
23
|
save_flaresolverr_url,
|
|
24
24
|
save_hostnames,
|
|
25
|
+
save_jdownloader_settings,
|
|
26
|
+
verify_jdownloader_credentials,
|
|
25
27
|
)
|
|
26
28
|
from quasarr.storage.sqlite_database import DataBase
|
|
27
29
|
|
|
@@ -217,3 +219,11 @@ def setup_config(app, shared_state):
|
|
|
217
219
|
|
|
218
220
|
threading.Thread(target=delayed_exit, daemon=True).start()
|
|
219
221
|
return {"success": True, "message": "Restarting..."}
|
|
222
|
+
|
|
223
|
+
@app.post("/api/jdownloader/verify")
|
|
224
|
+
def verify_jdownloader_api():
|
|
225
|
+
return verify_jdownloader_credentials(shared_state)
|
|
226
|
+
|
|
227
|
+
@app.post("/api/jdownloader/save")
|
|
228
|
+
def save_jdownloader_api():
|
|
229
|
+
return save_jdownloader_settings(shared_state, is_setup=False)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
from quasarr.providers.html_templates import render_button
|
|
6
|
+
from quasarr.storage.config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_jdownloader_status(shared_state):
|
|
10
|
+
"""Get JDownloader connection status and device name."""
|
|
11
|
+
try:
|
|
12
|
+
device = shared_state.values.get("device")
|
|
13
|
+
jd_connected = device is not None and device is not False
|
|
14
|
+
except:
|
|
15
|
+
jd_connected = False
|
|
16
|
+
|
|
17
|
+
jd_config = Config("JDownloader")
|
|
18
|
+
jd_device = jd_config.get("device") or ""
|
|
19
|
+
|
|
20
|
+
dev_name = jd_device if jd_device else "JDownloader"
|
|
21
|
+
dev_name_safe = (
|
|
22
|
+
dev_name.replace("&", "&")
|
|
23
|
+
.replace("<", "<")
|
|
24
|
+
.replace(">", ">")
|
|
25
|
+
.replace('"', """)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if jd_connected:
|
|
29
|
+
status_text = f"✅ {dev_name_safe} connected"
|
|
30
|
+
status_class = "success"
|
|
31
|
+
elif jd_device:
|
|
32
|
+
status_text = f"❌ {dev_name_safe} disconnected"
|
|
33
|
+
status_class = "error"
|
|
34
|
+
else:
|
|
35
|
+
status_text = "❌ JDownloader disconnected"
|
|
36
|
+
status_class = "error"
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"connected": jd_connected,
|
|
40
|
+
"device_name": jd_device,
|
|
41
|
+
"status_text": status_text,
|
|
42
|
+
"status_class": status_class,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_jdownloader_modal_script():
|
|
47
|
+
"""Return the JavaScript for the JDownloader configuration modal."""
|
|
48
|
+
jd_config = Config("JDownloader")
|
|
49
|
+
jd_user = jd_config.get("user") or ""
|
|
50
|
+
jd_pass = jd_config.get("password") or ""
|
|
51
|
+
jd_device = jd_config.get("device") or ""
|
|
52
|
+
|
|
53
|
+
jd_user_js = jd_user.replace("\\", "\\\\").replace("'", "\\'")
|
|
54
|
+
jd_pass_js = jd_pass.replace("\\", "\\\\").replace("'", "\\'")
|
|
55
|
+
jd_device_js = jd_device.replace("\\", "\\\\").replace("'", "\\'")
|
|
56
|
+
|
|
57
|
+
return f"""
|
|
58
|
+
<script>
|
|
59
|
+
function openJDownloaderModal() {{
|
|
60
|
+
var currentUser = '{jd_user_js}';
|
|
61
|
+
var currentPass = '{jd_pass_js}';
|
|
62
|
+
var currentDevice = '{jd_device_js}';
|
|
63
|
+
|
|
64
|
+
var content = `
|
|
65
|
+
<div id="jd-step-1">
|
|
66
|
+
<input type="hidden" id="jd-current-device" value="${{currentDevice}}">
|
|
67
|
+
<p><strong>JDownloader must be running and connected to My JDownloader!</strong></p>
|
|
68
|
+
<div style="margin-bottom: 1rem;">
|
|
69
|
+
<label style="display:block; font-size: 0.875rem;">E-Mail</label>
|
|
70
|
+
<input type="text" id="jd-user" value="${{currentUser}}" placeholder="user@example.org" style="width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem;">
|
|
71
|
+
</div>
|
|
72
|
+
<div style="margin-bottom: 1rem;">
|
|
73
|
+
<label style="display:block; font-size: 0.875rem;">Password</label>
|
|
74
|
+
<input type="password" id="jd-pass" value="${{currentPass}}" placeholder="Password" style="width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem;">
|
|
75
|
+
</div>
|
|
76
|
+
<div id="jd-status" style="margin-bottom: 0.5rem; font-size: 0.875rem; min-height: 1.25em;"></div>
|
|
77
|
+
<button class="btn-primary" onclick="verifyJDCredentials()">Verify Credentials</button>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div id="jd-step-2" style="display:none;">
|
|
81
|
+
<p>Select your JDownloader instance:</p>
|
|
82
|
+
<div style="margin-bottom: 1rem;">
|
|
83
|
+
<select id="jd-device" style="width: 100%; padding: 0.375rem 0.75rem; border: 1px solid #ced4da; border-radius: 0.25rem;"></select>
|
|
84
|
+
</div>
|
|
85
|
+
<div id="jd-save-status" style="margin-bottom: 0.5rem; font-size: 0.875rem; min-height: 1.25em;"></div>
|
|
86
|
+
<button class="btn-primary" onclick="saveJDSettings()">Save</button>
|
|
87
|
+
</div>
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
showModal('Configure JDownloader', content, '<button class="btn-secondary" onclick="closeModal()">Close</button>');
|
|
91
|
+
}}
|
|
92
|
+
|
|
93
|
+
function verifyJDCredentials() {{
|
|
94
|
+
var user = document.getElementById('jd-user').value;
|
|
95
|
+
var pass = document.getElementById('jd-pass').value;
|
|
96
|
+
var statusDiv = document.getElementById('jd-status');
|
|
97
|
+
|
|
98
|
+
statusDiv.innerHTML = 'Verifying...';
|
|
99
|
+
statusDiv.style.color = 'var(--secondary, #6c757d)';
|
|
100
|
+
|
|
101
|
+
fetch('/api/jdownloader/verify', {{
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {{ 'Content-Type': 'application/json' }},
|
|
104
|
+
body: JSON.stringify({{ user: user, pass: pass }})
|
|
105
|
+
}})
|
|
106
|
+
.then(response => response.json())
|
|
107
|
+
.then(data => {{
|
|
108
|
+
if (data.success) {{
|
|
109
|
+
var select = document.getElementById('jd-device');
|
|
110
|
+
select.innerHTML = '';
|
|
111
|
+
var currentDevice = document.getElementById('jd-current-device').value;
|
|
112
|
+
data.devices.forEach(device => {{
|
|
113
|
+
var opt = document.createElement('option');
|
|
114
|
+
opt.value = device;
|
|
115
|
+
opt.innerHTML = device;
|
|
116
|
+
if (device === currentDevice) {{
|
|
117
|
+
opt.selected = true;
|
|
118
|
+
}}
|
|
119
|
+
select.appendChild(opt);
|
|
120
|
+
}});
|
|
121
|
+
|
|
122
|
+
document.getElementById('jd-step-1').style.display = 'none';
|
|
123
|
+
document.getElementById('jd-step-2').style.display = 'block';
|
|
124
|
+
}} else {{
|
|
125
|
+
statusDiv.innerHTML = '❌ ' + (data.message || 'Verification failed');
|
|
126
|
+
statusDiv.style.color = '#dc3545';
|
|
127
|
+
}}
|
|
128
|
+
}})
|
|
129
|
+
.catch(error => {{
|
|
130
|
+
statusDiv.innerHTML = '❌ Error: ' + error.message;
|
|
131
|
+
statusDiv.style.color = '#dc3545';
|
|
132
|
+
}});
|
|
133
|
+
}}
|
|
134
|
+
|
|
135
|
+
function saveJDSettings() {{
|
|
136
|
+
var user = document.getElementById('jd-user').value;
|
|
137
|
+
var pass = document.getElementById('jd-pass').value;
|
|
138
|
+
var device = document.getElementById('jd-device').value;
|
|
139
|
+
var statusDiv = document.getElementById('jd-save-status');
|
|
140
|
+
|
|
141
|
+
statusDiv.innerHTML = 'Saving...';
|
|
142
|
+
statusDiv.style.color = 'var(--secondary, #6c757d)';
|
|
143
|
+
|
|
144
|
+
fetch('/api/jdownloader/save', {{
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: {{ 'Content-Type': 'application/json' }},
|
|
147
|
+
body: JSON.stringify({{ user: user, pass: pass, device: device }})
|
|
148
|
+
}})
|
|
149
|
+
.then(response => response.json())
|
|
150
|
+
.then(data => {{
|
|
151
|
+
if (data.success) {{
|
|
152
|
+
statusDiv.innerHTML = '✅ ' + data.message;
|
|
153
|
+
statusDiv.style.color = '#198754';
|
|
154
|
+
setTimeout(function() {{
|
|
155
|
+
window.location.reload();
|
|
156
|
+
}}, 1000);
|
|
157
|
+
}} else {{
|
|
158
|
+
statusDiv.innerHTML = '❌ ' + data.message;
|
|
159
|
+
statusDiv.style.color = '#dc3545';
|
|
160
|
+
}}
|
|
161
|
+
}})
|
|
162
|
+
.catch(error => {{
|
|
163
|
+
statusDiv.innerHTML = '❌ Error: ' + error.message;
|
|
164
|
+
statusDiv.style.color = '#dc3545';
|
|
165
|
+
}});
|
|
166
|
+
}}
|
|
167
|
+
</script>
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_jdownloader_status_pill(shared_state):
|
|
172
|
+
"""Return the HTML for the JDownloader status pill."""
|
|
173
|
+
status = get_jdownloader_status(shared_state)
|
|
174
|
+
|
|
175
|
+
return f"""
|
|
176
|
+
<span class="status-pill {status["status_class"]}"
|
|
177
|
+
onclick="openJDownloaderModal()"
|
|
178
|
+
style="cursor: pointer;"
|
|
179
|
+
title="Click to configure JDownloader">
|
|
180
|
+
{status["status_text"]}
|
|
181
|
+
</span>
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_jdownloader_disconnected_page(shared_state, back_url="/"):
|
|
186
|
+
"""Return a full error page when JDownloader is disconnected."""
|
|
187
|
+
import quasarr.providers.html_images as images
|
|
188
|
+
from quasarr.providers.html_templates import render_centered_html
|
|
189
|
+
|
|
190
|
+
status_pill = get_jdownloader_status_pill(shared_state)
|
|
191
|
+
modal_script = get_jdownloader_modal_script()
|
|
192
|
+
|
|
193
|
+
back_btn = render_button(
|
|
194
|
+
"Back", "secondary", {"onclick": f"location.href='{back_url}'"}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
content = f'''
|
|
198
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
199
|
+
<div class="status-bar">
|
|
200
|
+
{status_pill}
|
|
201
|
+
</div>
|
|
202
|
+
<p>{back_btn}</p>
|
|
203
|
+
<style>
|
|
204
|
+
.status-pill {{
|
|
205
|
+
font-size: 0.9em;
|
|
206
|
+
padding: 8px 16px;
|
|
207
|
+
border-radius: 0.5rem;
|
|
208
|
+
font-weight: 500;
|
|
209
|
+
transition: transform 0.1s ease;
|
|
210
|
+
}}
|
|
211
|
+
.status-pill:hover {{
|
|
212
|
+
transform: scale(1.05);
|
|
213
|
+
}}
|
|
214
|
+
.status-pill.success {{
|
|
215
|
+
background: var(--status-success-bg, #e8f5e9);
|
|
216
|
+
color: var(--status-success-color, #2e7d32);
|
|
217
|
+
border: 1px solid var(--status-success-border, #a5d6a7);
|
|
218
|
+
}}
|
|
219
|
+
.status-pill.error {{
|
|
220
|
+
background: var(--status-error-bg, #ffebee);
|
|
221
|
+
color: var(--status-error-color, #c62828);
|
|
222
|
+
border: 1px solid var(--status-error-border, #ef9a9a);
|
|
223
|
+
}}
|
|
224
|
+
/* Dark mode */
|
|
225
|
+
@media (prefers-color-scheme: dark) {{
|
|
226
|
+
:root {{
|
|
227
|
+
--status-success-bg: #1c4532;
|
|
228
|
+
--status-success-color: #68d391;
|
|
229
|
+
--status-success-border: #276749;
|
|
230
|
+
--status-error-bg: #3d2d2d;
|
|
231
|
+
--status-error-color: #fc8181;
|
|
232
|
+
--status-error-border: #c53030;
|
|
233
|
+
}}
|
|
234
|
+
}}
|
|
235
|
+
</style>
|
|
236
|
+
{modal_script}
|
|
237
|
+
'''
|
|
238
|
+
|
|
239
|
+
return render_centered_html(content)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Project by https://github.com/rix1337
|
|
4
4
|
|
|
5
5
|
import quasarr.providers.html_images as images
|
|
6
|
+
from quasarr.api.jdownloader import get_jdownloader_disconnected_page
|
|
6
7
|
from quasarr.downloads.packages import delete_package, get_packages
|
|
7
8
|
from quasarr.providers import shared_state
|
|
8
9
|
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
@@ -349,18 +350,7 @@ def setup_packages_routes(app):
|
|
|
349
350
|
device = None
|
|
350
351
|
|
|
351
352
|
if not device:
|
|
352
|
-
|
|
353
|
-
"Back", "secondary", {"onclick": "location.href='/'"}
|
|
354
|
-
)
|
|
355
|
-
return render_centered_html(f'''
|
|
356
|
-
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
357
|
-
<div class="status-bar">
|
|
358
|
-
<span class="status-pill error">
|
|
359
|
-
❌ JDownloader disconnected
|
|
360
|
-
</span>
|
|
361
|
-
</div>
|
|
362
|
-
<p>{back_btn}</p>
|
|
363
|
-
''')
|
|
353
|
+
return get_jdownloader_disconnected_page(shared_state)
|
|
364
354
|
|
|
365
355
|
# Check for delete status from redirect
|
|
366
356
|
deleted = request.query.get("deleted")
|
|
@@ -16,6 +16,7 @@ from quasarr.downloads.sources.dl import get_dl_download_links
|
|
|
16
16
|
from quasarr.downloads.sources.dt import get_dt_download_links
|
|
17
17
|
from quasarr.downloads.sources.dw import get_dw_download_links
|
|
18
18
|
from quasarr.downloads.sources.he import get_he_download_links
|
|
19
|
+
from quasarr.downloads.sources.hs import get_hs_download_links
|
|
19
20
|
from quasarr.downloads.sources.mb import get_mb_download_links
|
|
20
21
|
from quasarr.downloads.sources.nk import get_nk_download_links
|
|
21
22
|
from quasarr.downloads.sources.nx import get_nx_download_links
|
|
@@ -57,6 +58,7 @@ SOURCE_GETTERS = {
|
|
|
57
58
|
"dt": get_dt_download_links,
|
|
58
59
|
"dw": get_dw_download_links,
|
|
59
60
|
"he": get_he_download_links,
|
|
61
|
+
"hs": get_hs_download_links,
|
|
60
62
|
"mb": get_mb_download_links,
|
|
61
63
|
"nk": get_nk_download_links,
|
|
62
64
|
"nx": get_nx_download_links,
|