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.

Files changed (89) hide show
  1. {quasarr-1.32.0 → quasarr-2.0.0}/PKG-INFO +12 -2
  2. {quasarr-1.32.0 → quasarr-2.0.0}/README.md +11 -1
  3. quasarr-2.0.0/quasarr/api/__init__.py +405 -0
  4. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/captcha/__init__.py +26 -1
  5. quasarr-2.0.0/quasarr/api/packages/__init__.py +374 -0
  6. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/sponsors_helper/__init__.py +4 -0
  7. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/__init__.py +2 -0
  8. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/linkcrypters/hide.py +45 -6
  9. quasarr-2.0.0/quasarr/providers/auth.py +250 -0
  10. quasarr-2.0.0/quasarr/providers/obfuscated.py +121 -0
  11. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/shared_state.py +24 -0
  12. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/version.py +1 -1
  13. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dl.py +3 -2
  14. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/storage/setup.py +12 -0
  15. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/PKG-INFO +12 -2
  16. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/SOURCES.txt +2 -0
  17. quasarr-1.32.0/quasarr/api/__init__.py +0 -187
  18. quasarr-1.32.0/quasarr/providers/obfuscated.py +0 -119
  19. {quasarr-1.32.0 → quasarr-2.0.0}/LICENSE +0 -0
  20. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/__init__.py +0 -0
  21. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/arr/__init__.py +0 -0
  22. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/config/__init__.py +0 -0
  23. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/api/statistics/__init__.py +0 -0
  24. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/linkcrypters/__init__.py +0 -0
  25. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/linkcrypters/al.py +0 -0
  26. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/linkcrypters/filecrypt.py +0 -0
  27. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/packages/__init__.py +0 -0
  28. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/__init__.py +0 -0
  29. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/al.py +0 -0
  30. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/by.py +0 -0
  31. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dd.py +0 -0
  32. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dj.py +0 -0
  33. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dl.py +0 -0
  34. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dt.py +0 -0
  35. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/dw.py +0 -0
  36. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/he.py +0 -0
  37. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/mb.py +0 -0
  38. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/nk.py +0 -0
  39. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/nx.py +0 -0
  40. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/sf.py +0 -0
  41. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/sj.py +0 -0
  42. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/sl.py +0 -0
  43. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/wd.py +0 -0
  44. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/downloads/sources/wx.py +0 -0
  45. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/__init__.py +0 -0
  46. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/cloudflare.py +0 -0
  47. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/html_images.py +0 -0
  48. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/html_templates.py +0 -0
  49. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/imdb_metadata.py +0 -0
  50. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/jd_cache.py +0 -0
  51. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/log.py +0 -0
  52. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/myjd_api.py +0 -0
  53. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/notifications.py +0 -0
  54. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/__init__.py +0 -0
  55. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/al.py +0 -0
  56. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/dd.py +0 -0
  57. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/dl.py +0 -0
  58. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/sessions/nx.py +0 -0
  59. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/statistics.py +0 -0
  60. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/utils.py +0 -0
  61. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/providers/web_server.py +0 -0
  62. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/__init__.py +0 -0
  63. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/__init__.py +0 -0
  64. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/al.py +0 -0
  65. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/by.py +0 -0
  66. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dd.py +0 -0
  67. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dj.py +0 -0
  68. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dt.py +0 -0
  69. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/dw.py +0 -0
  70. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/fx.py +0 -0
  71. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/he.py +0 -0
  72. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/mb.py +0 -0
  73. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/nk.py +0 -0
  74. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/nx.py +0 -0
  75. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/sf.py +0 -0
  76. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/sj.py +0 -0
  77. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/sl.py +0 -0
  78. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/wd.py +0 -0
  79. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/search/sources/wx.py +0 -0
  80. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/storage/__init__.py +0 -0
  81. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/storage/config.py +0 -0
  82. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr/storage/sqlite_database.py +0 -0
  83. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/dependency_links.txt +0 -0
  84. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/entry_points.txt +0 -0
  85. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/not-zip-safe +0 -0
  86. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/requires.txt +0 -0
  87. {quasarr-1.32.0 → quasarr-2.0.0}/quasarr.egg-info/top_level.txt +0 -0
  88. {quasarr-1.32.0 → quasarr-2.0.0}/setup.cfg +0 -0
  89. {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: 1.32.0
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
- package = protected[0]
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