quasarr 1.32.0__tar.gz → 2.0.0__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-1.32.0 → quasarr-2.0.0}/PKG-INFO +12 -2
- {quasarr-1.32.0 → quasarr-2.0.0}/README.md +11 -1
- quasarr-2.0.0/quasarr/api/__init__.py +405 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/captcha/__init__.py +26 -1
- quasarr-2.0.0/quasarr/api/packages/__init__.py +374 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/sponsors_helper/__init__.py +4 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/__init__.py +2 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/linkcrypters/hide.py +45 -6
- quasarr-2.0.0/quasarr/providers/auth.py +250 -0
- quasarr-2.0.0/quasarr/providers/obfuscated.py +121 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/shared_state.py +24 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/version.py +1 -1
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dl.py +3 -2
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/storage/setup.py +12 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/PKG-INFO +12 -2
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/SOURCES.txt +2 -0
- quasarr-1.32.0/quasarr/api/__init__.py +0 -187
- quasarr-1.32.0/quasarr/providers/obfuscated.py +0 -119
- {quasarr-1.32.0 → quasarr-2.0.0}/LICENSE +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/arr/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/config/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/statistics/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/linkcrypters/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/linkcrypters/al.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/linkcrypters/filecrypt.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/packages/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/al.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/by.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dd.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dj.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dl.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dt.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dw.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/he.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/mb.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/nk.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/nx.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/sf.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/sj.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/sl.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/wd.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/wx.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/cloudflare.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/html_images.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/html_templates.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/imdb_metadata.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/jd_cache.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/log.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/myjd_api.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/notifications.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/al.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/dd.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/dl.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/nx.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/statistics.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/utils.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/web_server.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/al.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/by.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dd.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dj.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dt.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dw.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/fx.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/he.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/mb.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/nk.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/nx.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/sf.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/sj.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/sl.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/wd.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/wx.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/storage/__init__.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/storage/config.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/storage/sqlite_database.py +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/dependency_links.txt +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/entry_points.txt +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/not-zip-safe +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/requires.txt +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/top_level.txt +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/setup.cfg +0 -0
- {quasarr-1.32.0 → quasarr-2.0.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quasarr
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Quasarr connects JDownloader with Radarr, Sonarr and LazyLibrarian. It also decrypts links protected by CAPTCHAs.
|
|
5
5
|
Home-page: https://github.com/rix1337/Quasarr
|
|
6
6
|
Author: rix1337
|
|
@@ -75,6 +75,11 @@ http://192.168.1.1:8191/v1
|
|
|
75
75
|
|
|
76
76
|
📋 Alternatively, browse community suggestions via [pastebin search](https://pastebin.com/search?q=hostnames+quasarr) (login required).
|
|
77
77
|
|
|
78
|
+
> Authentication is optional but strongly recommended.
|
|
79
|
+
>
|
|
80
|
+
> - 🔐 Set `USER` and `PASS` to enable form-based login (30-day session)
|
|
81
|
+
> - 🔑 Set `AUTH=basic` to use HTTP Basic Authentication instead
|
|
82
|
+
|
|
78
83
|
---
|
|
79
84
|
|
|
80
85
|
## JDownloader
|
|
@@ -170,7 +175,10 @@ docker run -d \
|
|
|
170
175
|
-e 'INTERNAL_ADDRESS'='http://192.168.0.1:8080' \
|
|
171
176
|
-e 'EXTERNAL_ADDRESS'='https://foo.bar/' \
|
|
172
177
|
-e 'DISCORD'='https://discord.com/api/webhooks/1234567890/ABCDEFGHIJKLMN' \
|
|
173
|
-
-e 'HOSTNAMES'='https://quasarr-host.name/ini?token=123...'
|
|
178
|
+
-e 'HOSTNAMES'='https://quasarr-host.name/ini?token=123...' \
|
|
179
|
+
-e 'USER'='admin' \
|
|
180
|
+
-e 'PASS'='change-me' \
|
|
181
|
+
-e 'AUTH'='form' \
|
|
174
182
|
-e 'SILENT'='True' \
|
|
175
183
|
-e 'DEBUG'='' \
|
|
176
184
|
-e 'TZ'='Europe/Berlin' \
|
|
@@ -184,6 +192,8 @@ docker run -d \
|
|
|
184
192
|
* Must be a publicly available `HTTP` or `HTTPs` link
|
|
185
193
|
* Must be a raw `.ini` / text file (not HTML or JSON)
|
|
186
194
|
* Must contain at least one valid Hostname per line `ab = xyz`
|
|
195
|
+
* `USER` / `PASS` are credentials to protect the web UI
|
|
196
|
+
* `AUTH` is the authentication mode (`form` or `basic`)
|
|
187
197
|
* `SILENT` is optional and silences all discord notifications except for error messages from SponsorsHelper if `True`.
|
|
188
198
|
* `DEBUG` is optional and enables debug logging if `True`.
|
|
189
199
|
* `TZ` is optional, wrong timezone can cause HTTPS/SSL issues
|
|
@@ -48,6 +48,11 @@ http://192.168.1.1:8191/v1
|
|
|
48
48
|
|
|
49
49
|
📋 Alternatively, browse community suggestions via [pastebin search](https://pastebin.com/search?q=hostnames+quasarr) (login required).
|
|
50
50
|
|
|
51
|
+
> Authentication is optional but strongly recommended.
|
|
52
|
+
>
|
|
53
|
+
> - 🔐 Set `USER` and `PASS` to enable form-based login (30-day session)
|
|
54
|
+
> - 🔑 Set `AUTH=basic` to use HTTP Basic Authentication instead
|
|
55
|
+
|
|
51
56
|
---
|
|
52
57
|
|
|
53
58
|
## JDownloader
|
|
@@ -143,7 +148,10 @@ docker run -d \
|
|
|
143
148
|
-e 'INTERNAL_ADDRESS'='http://192.168.0.1:8080' \
|
|
144
149
|
-e 'EXTERNAL_ADDRESS'='https://foo.bar/' \
|
|
145
150
|
-e 'DISCORD'='https://discord.com/api/webhooks/1234567890/ABCDEFGHIJKLMN' \
|
|
146
|
-
-e 'HOSTNAMES'='https://quasarr-host.name/ini?token=123...'
|
|
151
|
+
-e 'HOSTNAMES'='https://quasarr-host.name/ini?token=123...' \
|
|
152
|
+
-e 'USER'='admin' \
|
|
153
|
+
-e 'PASS'='change-me' \
|
|
154
|
+
-e 'AUTH'='form' \
|
|
147
155
|
-e 'SILENT'='True' \
|
|
148
156
|
-e 'DEBUG'='' \
|
|
149
157
|
-e 'TZ'='Europe/Berlin' \
|
|
@@ -157,6 +165,8 @@ docker run -d \
|
|
|
157
165
|
* Must be a publicly available `HTTP` or `HTTPs` link
|
|
158
166
|
* Must be a raw `.ini` / text file (not HTML or JSON)
|
|
159
167
|
* Must contain at least one valid Hostname per line `ab = xyz`
|
|
168
|
+
* `USER` / `PASS` are credentials to protect the web UI
|
|
169
|
+
* `AUTH` is the authentication mode (`form` or `basic`)
|
|
160
170
|
* `SILENT` is optional and silences all discord notifications except for error messages from SponsorsHelper if `True`.
|
|
161
171
|
* `DEBUG` is optional and enables debug logging if `True`.
|
|
162
172
|
* `TZ` is optional, wrong timezone can cause HTTPS/SSL issues
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
from bottle import Bottle
|
|
6
|
+
|
|
7
|
+
import quasarr.providers.html_images as images
|
|
8
|
+
from quasarr.api.arr import setup_arr_routes
|
|
9
|
+
from quasarr.api.captcha import setup_captcha_routes
|
|
10
|
+
from quasarr.api.config import setup_config
|
|
11
|
+
from quasarr.api.packages import setup_packages_routes
|
|
12
|
+
from quasarr.api.sponsors_helper import setup_sponsors_helper_routes
|
|
13
|
+
from quasarr.api.statistics import setup_statistics
|
|
14
|
+
from quasarr.providers import shared_state
|
|
15
|
+
from quasarr.providers.auth import add_auth_routes, add_auth_hook, show_logout_link
|
|
16
|
+
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
17
|
+
from quasarr.providers.web_server import Server
|
|
18
|
+
from quasarr.storage.config import Config
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_api(shared_state_dict, shared_state_lock):
|
|
22
|
+
shared_state.set_state(shared_state_dict, shared_state_lock)
|
|
23
|
+
|
|
24
|
+
app = Bottle()
|
|
25
|
+
|
|
26
|
+
# Auth: routes must come first, then hook
|
|
27
|
+
add_auth_routes(app)
|
|
28
|
+
add_auth_hook(app, whitelist_prefixes=['/api', '/api/' '/sponsors_helper/', '/download/'])
|
|
29
|
+
|
|
30
|
+
setup_arr_routes(app)
|
|
31
|
+
setup_captcha_routes(app)
|
|
32
|
+
setup_config(app, shared_state)
|
|
33
|
+
setup_statistics(app, shared_state)
|
|
34
|
+
setup_sponsors_helper_routes(app)
|
|
35
|
+
setup_packages_routes(app)
|
|
36
|
+
|
|
37
|
+
@app.get('/')
|
|
38
|
+
def index():
|
|
39
|
+
protected = shared_state.get_db("protected").retrieve_all_titles()
|
|
40
|
+
api_key = Config('API').get('key')
|
|
41
|
+
|
|
42
|
+
# Get quick status summary
|
|
43
|
+
try:
|
|
44
|
+
device = shared_state.values.get("device")
|
|
45
|
+
jd_connected = device is not None
|
|
46
|
+
except:
|
|
47
|
+
jd_connected = False
|
|
48
|
+
|
|
49
|
+
# CAPTCHA banner
|
|
50
|
+
captcha_hint = ""
|
|
51
|
+
if protected:
|
|
52
|
+
plural = 's' if len(protected) > 1 else ''
|
|
53
|
+
captcha_hint = f"""
|
|
54
|
+
<div class="alert alert-warning">
|
|
55
|
+
<span class="alert-icon">🔒</span>
|
|
56
|
+
<div class="alert-content">
|
|
57
|
+
<strong>{len(protected)} link{plural} waiting for CAPTCHA</strong>
|
|
58
|
+
{"" if shared_state.values.get("helper_active") else '<br><a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#sponsorshelper" target="_blank">Sponsors get automated CAPTCHA solutions!</a>'}
|
|
59
|
+
</div>
|
|
60
|
+
<div class="alert-action">
|
|
61
|
+
{render_button(f"Solve CAPTCHA{plural}", 'primary', {'onclick': "location.href='/captcha'"})}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
# JDownloader status
|
|
67
|
+
jd_status = f"""
|
|
68
|
+
<div class="status-bar">
|
|
69
|
+
<span class="status-item {'status-ok' if jd_connected else 'status-error'}">
|
|
70
|
+
{'✅' if jd_connected else '❌'} JDownloader {'Connected' if jd_connected else 'Disconnected'}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
info = f"""
|
|
76
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
77
|
+
|
|
78
|
+
{jd_status}
|
|
79
|
+
{captcha_hint}
|
|
80
|
+
|
|
81
|
+
<div class="quick-actions">
|
|
82
|
+
<a href="/packages" class="action-card">
|
|
83
|
+
<span class="action-icon">📦</span>
|
|
84
|
+
<span class="action-label">Packages</span>
|
|
85
|
+
</a>
|
|
86
|
+
<a href="/statistics" class="action-card">
|
|
87
|
+
<span class="action-icon">📊</span>
|
|
88
|
+
<span class="action-label">Statistics</span>
|
|
89
|
+
</a>
|
|
90
|
+
<a href="/hostnames" class="action-card">
|
|
91
|
+
<span class="action-icon">🌐</span>
|
|
92
|
+
<span class="action-label">Hostnames</span>
|
|
93
|
+
</a>
|
|
94
|
+
<a href="/flaresolverr" class="action-card">
|
|
95
|
+
<span class="action-icon">🛡️</span>
|
|
96
|
+
<span class="action-label">FlareSolverr</span>
|
|
97
|
+
</a>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="section">
|
|
101
|
+
<details id="apiDetails">
|
|
102
|
+
<summary id="apiSummary">⚙️ API Configuration</summary>
|
|
103
|
+
<div class="api-settings">
|
|
104
|
+
<p class="api-hint">Use these settings for <strong>Newznab Indexer</strong> and <strong>SABnzbd Download Client</strong> in Radarr/Sonarr</p>
|
|
105
|
+
|
|
106
|
+
<div class="input-group">
|
|
107
|
+
<label>URL</label>
|
|
108
|
+
<div class="input-row">
|
|
109
|
+
<input id="urlInput" type="text" readonly value="{shared_state.values['internal_address']}" />
|
|
110
|
+
<button id="copyUrl" type="button">Copy</button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<div class="input-group">
|
|
115
|
+
<label>API Key</label>
|
|
116
|
+
<div class="input-row">
|
|
117
|
+
<input id="apiKeyInput" type="password" readonly value="{api_key}" />
|
|
118
|
+
<button id="toggleKey" type="button">Show</button>
|
|
119
|
+
<button id="copyKey" type="button">Copy</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<p style="margin-top: 15px;">
|
|
124
|
+
{render_button("Regenerate API key", "secondary", {"onclick": "if(confirm('Regenerate API key?')) location.href='/regenerate-api-key';"})}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</details>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div class="section help-link">
|
|
131
|
+
<a href="https://github.com/rix1337/Quasarr?tab=readme-ov-file#instructions" target="_blank">
|
|
132
|
+
📖 Setup Instructions & Documentation
|
|
133
|
+
</a>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<style>
|
|
137
|
+
.status-bar {{
|
|
138
|
+
display: flex;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
gap: 20px;
|
|
141
|
+
margin-bottom: 20px;
|
|
142
|
+
flex-wrap: wrap;
|
|
143
|
+
}}
|
|
144
|
+
.status-item {{
|
|
145
|
+
font-size: 0.9em;
|
|
146
|
+
padding: 6px 12px;
|
|
147
|
+
border-radius: 20px;
|
|
148
|
+
background: var(--status-bg, #f5f5f5);
|
|
149
|
+
}}
|
|
150
|
+
.status-ok {{ color: var(--status-ok, #2e7d32); }}
|
|
151
|
+
.status-error {{ color: var(--status-error, #c62828); }}
|
|
152
|
+
|
|
153
|
+
.alert {{
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
align-items: center;
|
|
157
|
+
text-align: center;
|
|
158
|
+
gap: 12px;
|
|
159
|
+
padding: 20px;
|
|
160
|
+
border-radius: 8px;
|
|
161
|
+
margin-bottom: 25px;
|
|
162
|
+
}}
|
|
163
|
+
.alert-warning {{
|
|
164
|
+
background: var(--alert-warning-bg, #fff3e0);
|
|
165
|
+
border: 1px solid var(--alert-warning-border, #ffb74d);
|
|
166
|
+
}}
|
|
167
|
+
.alert-icon {{ font-size: 1.5em; }}
|
|
168
|
+
.alert-content {{ }}
|
|
169
|
+
.alert-content a {{ color: var(--link-color, #0066cc); }}
|
|
170
|
+
.alert-action {{ margin-top: 5px; }}
|
|
171
|
+
|
|
172
|
+
.quick-actions {{
|
|
173
|
+
display: grid;
|
|
174
|
+
grid-template-columns: repeat(4, 1fr);
|
|
175
|
+
gap: 12px;
|
|
176
|
+
max-width: 500px;
|
|
177
|
+
margin: 0 auto 30px auto;
|
|
178
|
+
}}
|
|
179
|
+
@media (max-width: 500px) {{
|
|
180
|
+
.quick-actions {{
|
|
181
|
+
grid-template-columns: repeat(2, 1fr);
|
|
182
|
+
}}
|
|
183
|
+
}}
|
|
184
|
+
.action-card {{
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
align-items: center;
|
|
188
|
+
padding: 15px 10px;
|
|
189
|
+
background: var(--card-bg, #f8f9fa);
|
|
190
|
+
border: 1px solid var(--card-border, #dee2e6);
|
|
191
|
+
border-radius: 10px;
|
|
192
|
+
text-decoration: none;
|
|
193
|
+
color: inherit;
|
|
194
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
195
|
+
}}
|
|
196
|
+
.action-card:hover {{
|
|
197
|
+
transform: translateY(-2px);
|
|
198
|
+
box-shadow: 0 4px 12px var(--card-shadow, rgba(0,0,0,0.1));
|
|
199
|
+
border-color: var(--card-hover-border, #007bff);
|
|
200
|
+
}}
|
|
201
|
+
.action-icon {{ font-size: 1.8em; margin-bottom: 5px; }}
|
|
202
|
+
.action-label {{ font-size: 0.85em; font-weight: 500; }}
|
|
203
|
+
|
|
204
|
+
.section {{ margin: 20px 0; max-width: 500px; margin-left: auto; margin-right: auto; }}
|
|
205
|
+
details {{ background: var(--card-bg, #f8f9fa); border: 1px solid var(--card-border, #dee2e6); border-radius: 8px; }}
|
|
206
|
+
summary {{
|
|
207
|
+
cursor: pointer;
|
|
208
|
+
padding: 12px 15px;
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
list-style: none;
|
|
211
|
+
}}
|
|
212
|
+
summary::-webkit-details-marker {{ display: none; }}
|
|
213
|
+
summary::before {{ content: '▶ '; font-size: 0.8em; }}
|
|
214
|
+
details[open] summary::before {{ content: '▼ '; }}
|
|
215
|
+
summary:hover {{ color: var(--link-color, #0066cc); }}
|
|
216
|
+
|
|
217
|
+
.api-settings {{ padding: 15px; border-top: 1px solid var(--card-border, #dee2e6); }}
|
|
218
|
+
.api-hint {{ font-size: 0.9em; color: var(--text-muted, #666); margin-bottom: 15px; }}
|
|
219
|
+
.input-group {{ margin-bottom: 15px; }}
|
|
220
|
+
.input-group label {{ display: block; font-weight: 500; margin-bottom: 6px; font-size: 0.95em; text-align: left; }}
|
|
221
|
+
.input-row {{
|
|
222
|
+
display: flex;
|
|
223
|
+
gap: 8px;
|
|
224
|
+
align-items: stretch;
|
|
225
|
+
}}
|
|
226
|
+
.input-row input {{
|
|
227
|
+
flex: 1;
|
|
228
|
+
padding: 8px 12px;
|
|
229
|
+
border: 1px solid var(--input-border, #ced4da);
|
|
230
|
+
border-radius: 4px;
|
|
231
|
+
font-family: monospace;
|
|
232
|
+
font-size: 0.9em;
|
|
233
|
+
background: var(--input-bg, #e9ecef);
|
|
234
|
+
color: var(--fg-color, #212529);
|
|
235
|
+
min-width: 0;
|
|
236
|
+
margin: 0;
|
|
237
|
+
}}
|
|
238
|
+
.input-row button {{
|
|
239
|
+
padding: 8px 16px;
|
|
240
|
+
border: none;
|
|
241
|
+
border-radius: 4px;
|
|
242
|
+
cursor: pointer;
|
|
243
|
+
font-size: 0.9em;
|
|
244
|
+
font-weight: 500;
|
|
245
|
+
transition: background 0.2s;
|
|
246
|
+
white-space: nowrap;
|
|
247
|
+
margin: 0;
|
|
248
|
+
flex-shrink: 0;
|
|
249
|
+
}}
|
|
250
|
+
#copyUrl, #copyKey {{
|
|
251
|
+
background: var(--btn-primary-bg, #007bff);
|
|
252
|
+
color: white;
|
|
253
|
+
}}
|
|
254
|
+
#copyUrl:hover, #copyKey:hover {{
|
|
255
|
+
background: var(--btn-primary-hover, #0056b3);
|
|
256
|
+
}}
|
|
257
|
+
#toggleKey {{
|
|
258
|
+
background: var(--btn-secondary-bg, #6c757d);
|
|
259
|
+
color: white;
|
|
260
|
+
}}
|
|
261
|
+
#toggleKey:hover {{
|
|
262
|
+
background: var(--btn-secondary-hover, #545b62);
|
|
263
|
+
}}
|
|
264
|
+
|
|
265
|
+
.help-link {{
|
|
266
|
+
text-align: center;
|
|
267
|
+
padding: 15px;
|
|
268
|
+
background: var(--card-bg, #f8f9fa);
|
|
269
|
+
border: 1px solid var(--card-border, #dee2e6);
|
|
270
|
+
border-radius: 8px;
|
|
271
|
+
}}
|
|
272
|
+
.help-link a {{
|
|
273
|
+
color: var(--link-color, #0066cc);
|
|
274
|
+
text-decoration: none;
|
|
275
|
+
font-weight: 500;
|
|
276
|
+
}}
|
|
277
|
+
.help-link a:hover {{ text-decoration: underline; }}
|
|
278
|
+
|
|
279
|
+
.logout-link {{
|
|
280
|
+
display: block;
|
|
281
|
+
text-align: center;
|
|
282
|
+
margin-top: 20px;
|
|
283
|
+
font-size: 0.85em;
|
|
284
|
+
}}
|
|
285
|
+
.logout-link a {{
|
|
286
|
+
color: var(--text-muted, #666);
|
|
287
|
+
text-decoration: none;
|
|
288
|
+
}}
|
|
289
|
+
.logout-link a:hover {{ text-decoration: underline; }}
|
|
290
|
+
|
|
291
|
+
/* Dark mode */
|
|
292
|
+
@media (prefers-color-scheme: dark) {{
|
|
293
|
+
:root {{
|
|
294
|
+
--status-bg: #2d3748;
|
|
295
|
+
--status-ok: #68d391;
|
|
296
|
+
--status-error: #fc8181;
|
|
297
|
+
--alert-warning-bg: #3d3520;
|
|
298
|
+
--alert-warning-border: #d69e2e;
|
|
299
|
+
--card-bg: #2d3748;
|
|
300
|
+
--card-border: #4a5568;
|
|
301
|
+
--card-shadow: rgba(0,0,0,0.3);
|
|
302
|
+
--card-hover-border: #63b3ed;
|
|
303
|
+
--text-muted: #a0aec0;
|
|
304
|
+
--link-color: #63b3ed;
|
|
305
|
+
--input-bg: #1a202c;
|
|
306
|
+
--input-border: #4a5568;
|
|
307
|
+
--btn-primary-bg: #3182ce;
|
|
308
|
+
--btn-primary-hover: #2c5282;
|
|
309
|
+
--btn-secondary-bg: #4a5568;
|
|
310
|
+
--btn-secondary-hover: #2d3748;
|
|
311
|
+
}}
|
|
312
|
+
}}
|
|
313
|
+
</style>
|
|
314
|
+
|
|
315
|
+
<script>
|
|
316
|
+
(function() {{
|
|
317
|
+
var urlInput = document.getElementById('urlInput');
|
|
318
|
+
var copyUrlBtn = document.getElementById('copyUrl');
|
|
319
|
+
var apiInput = document.getElementById('apiKeyInput');
|
|
320
|
+
var toggleBtn = document.getElementById('toggleKey');
|
|
321
|
+
var copyKeyBtn = document.getElementById('copyKey');
|
|
322
|
+
|
|
323
|
+
function copyToClipboard(text, button, callback) {{
|
|
324
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {{
|
|
325
|
+
navigator.clipboard.writeText(text).then(function() {{
|
|
326
|
+
var originalText = button.innerText;
|
|
327
|
+
button.innerText = 'Copied!';
|
|
328
|
+
setTimeout(function() {{
|
|
329
|
+
button.innerText = originalText;
|
|
330
|
+
if (callback) callback();
|
|
331
|
+
}}, 1500);
|
|
332
|
+
}}).catch(function() {{
|
|
333
|
+
fallbackCopy(text, button, callback);
|
|
334
|
+
}});
|
|
335
|
+
}} else {{
|
|
336
|
+
fallbackCopy(text, button, callback);
|
|
337
|
+
}}
|
|
338
|
+
}}
|
|
339
|
+
|
|
340
|
+
function fallbackCopy(text, button, callback) {{
|
|
341
|
+
var textarea = document.createElement('textarea');
|
|
342
|
+
textarea.value = text;
|
|
343
|
+
textarea.style.position = 'fixed';
|
|
344
|
+
textarea.style.opacity = '0';
|
|
345
|
+
document.body.appendChild(textarea);
|
|
346
|
+
textarea.select();
|
|
347
|
+
try {{
|
|
348
|
+
document.execCommand('copy');
|
|
349
|
+
var originalText = button.innerText;
|
|
350
|
+
button.innerText = 'Copied!';
|
|
351
|
+
setTimeout(function() {{
|
|
352
|
+
button.innerText = originalText;
|
|
353
|
+
if (callback) callback();
|
|
354
|
+
}}, 1500);
|
|
355
|
+
}} catch (e) {{
|
|
356
|
+
alert('Copy failed. Please copy manually.');
|
|
357
|
+
}}
|
|
358
|
+
document.body.removeChild(textarea);
|
|
359
|
+
}}
|
|
360
|
+
|
|
361
|
+
if (copyUrlBtn) {{
|
|
362
|
+
copyUrlBtn.onclick = function() {{
|
|
363
|
+
copyToClipboard(urlInput.value, copyUrlBtn);
|
|
364
|
+
}};
|
|
365
|
+
}}
|
|
366
|
+
|
|
367
|
+
if (copyKeyBtn) {{
|
|
368
|
+
copyKeyBtn.onclick = function() {{
|
|
369
|
+
copyToClipboard(apiInput.value, copyKeyBtn, function() {{
|
|
370
|
+
// Re-hide the API key after copying
|
|
371
|
+
apiInput.type = 'password';
|
|
372
|
+
toggleBtn.innerText = 'Show';
|
|
373
|
+
}});
|
|
374
|
+
}};
|
|
375
|
+
}}
|
|
376
|
+
|
|
377
|
+
if (toggleBtn) {{
|
|
378
|
+
toggleBtn.onclick = function() {{
|
|
379
|
+
if (apiInput.type === 'password') {{
|
|
380
|
+
apiInput.type = 'text';
|
|
381
|
+
toggleBtn.innerText = 'Hide';
|
|
382
|
+
}} else {{
|
|
383
|
+
apiInput.type = 'password';
|
|
384
|
+
toggleBtn.innerText = 'Show';
|
|
385
|
+
}}
|
|
386
|
+
}};
|
|
387
|
+
}}
|
|
388
|
+
}})();
|
|
389
|
+
</script>
|
|
390
|
+
"""
|
|
391
|
+
# Add logout link for form auth
|
|
392
|
+
logout_html = '<div class="logout-link"><a href="/logout">Logout</a></div>' if show_logout_link() else ''
|
|
393
|
+
return render_centered_html(info + logout_html)
|
|
394
|
+
|
|
395
|
+
@app.get('/regenerate-api-key')
|
|
396
|
+
def regenerate_api_key():
|
|
397
|
+
api_key = shared_state.generate_api_key()
|
|
398
|
+
return f"""
|
|
399
|
+
<script>
|
|
400
|
+
alert('API key replaced with: {api_key}');
|
|
401
|
+
window.location.href = '/';
|
|
402
|
+
</script>
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
Server(app, listen='0.0.0.0', port=shared_state.values["port"]).serve_forever()
|
|
@@ -61,7 +61,21 @@ def setup_captcha_routes(app):
|
|
|
61
61
|
{render_button("Confirm", "secondary", {"onclick": "location.href='/'"})}
|
|
62
62
|
</p>''')
|
|
63
63
|
else:
|
|
64
|
-
|
|
64
|
+
# Check if a specific package_id was requested
|
|
65
|
+
requested_package_id = request.query.get('package_id')
|
|
66
|
+
package = None
|
|
67
|
+
|
|
68
|
+
if requested_package_id:
|
|
69
|
+
# Find the specific package
|
|
70
|
+
for p in protected:
|
|
71
|
+
if p[0] == requested_package_id:
|
|
72
|
+
package = p
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
# Fall back to first package if not found or not specified
|
|
76
|
+
if package is None:
|
|
77
|
+
package = protected[0]
|
|
78
|
+
|
|
65
79
|
package_id = package[0]
|
|
66
80
|
data = json.loads(package[1])
|
|
67
81
|
title = data["title"]
|
|
@@ -1209,6 +1223,17 @@ def setup_captcha_routes(app):
|
|
|
1209
1223
|
border-right: none !important;
|
|
1210
1224
|
}
|
|
1211
1225
|
}
|
|
1226
|
+
/* Fix captcha container to shrink-wrap iframe on desktop */
|
|
1227
|
+
.captcha-container {
|
|
1228
|
+
display: inline-block;
|
|
1229
|
+
background-color: var(--secondary);
|
|
1230
|
+
}
|
|
1231
|
+
#puzzle-captcha {
|
|
1232
|
+
display: block;
|
|
1233
|
+
}
|
|
1234
|
+
#puzzle-captcha iframe {
|
|
1235
|
+
display: block;
|
|
1236
|
+
}
|
|
1212
1237
|
</style>
|
|
1213
1238
|
<script type="text/javascript">
|
|
1214
1239
|
// Package title for result display
|