quasarr 1.32.0__py3-none-any.whl → 2.0.0__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/api/__init__.py +324 -106
- quasarr/api/captcha/__init__.py +26 -1
- quasarr/api/packages/__init__.py +374 -0
- quasarr/api/sponsors_helper/__init__.py +4 -0
- quasarr/downloads/__init__.py +2 -0
- quasarr/downloads/linkcrypters/hide.py +45 -6
- quasarr/providers/auth.py +250 -0
- quasarr/providers/obfuscated.py +9 -7
- quasarr/providers/shared_state.py +24 -0
- quasarr/providers/version.py +1 -1
- quasarr/search/sources/dl.py +3 -2
- quasarr/storage/setup.py +12 -0
- {quasarr-1.32.0.dist-info → quasarr-2.0.0.dist-info}/METADATA +12 -2
- {quasarr-1.32.0.dist-info → quasarr-2.0.0.dist-info}/RECORD +18 -16
- {quasarr-1.32.0.dist-info → quasarr-2.0.0.dist-info}/WHEEL +0 -0
- {quasarr-1.32.0.dist-info → quasarr-2.0.0.dist-info}/entry_points.txt +0 -0
- {quasarr-1.32.0.dist-info → quasarr-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {quasarr-1.32.0.dist-info → quasarr-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Quasarr
|
|
3
|
+
# Project by https://github.com/rix1337
|
|
4
|
+
|
|
5
|
+
import quasarr.providers.html_images as images
|
|
6
|
+
from quasarr.downloads.packages import get_packages, delete_package
|
|
7
|
+
from quasarr.providers import shared_state
|
|
8
|
+
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_packages_routes(app):
|
|
12
|
+
@app.get('/packages/delete/<package_id>')
|
|
13
|
+
def delete_package_route(package_id):
|
|
14
|
+
success = delete_package(shared_state, package_id)
|
|
15
|
+
|
|
16
|
+
if success:
|
|
17
|
+
return render_centered_html(f'''
|
|
18
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
19
|
+
<p>✅ Package deleted successfully.</p>
|
|
20
|
+
<p>{render_button("Back to Packages", "primary", {"onclick": "location.href='/packages'"})}</p>
|
|
21
|
+
''')
|
|
22
|
+
else:
|
|
23
|
+
return render_centered_html(f'''
|
|
24
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
25
|
+
<p>❌ Failed to delete package.</p>
|
|
26
|
+
<p>{render_button("Back to Packages", "secondary", {"onclick": "location.href='/packages'"})}</p>
|
|
27
|
+
''')
|
|
28
|
+
|
|
29
|
+
@app.get('/packages')
|
|
30
|
+
def packages_status():
|
|
31
|
+
try:
|
|
32
|
+
device = shared_state.values["device"]
|
|
33
|
+
except KeyError:
|
|
34
|
+
device = None
|
|
35
|
+
|
|
36
|
+
if not device:
|
|
37
|
+
back_btn = render_button("Back", "secondary", {"onclick": "location.href='/'"})
|
|
38
|
+
return render_centered_html(f'''
|
|
39
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
40
|
+
<p>JDownloader connection not established.</p>
|
|
41
|
+
<p>{back_btn}</p>
|
|
42
|
+
''')
|
|
43
|
+
|
|
44
|
+
# Get packages data
|
|
45
|
+
downloads = get_packages(shared_state)
|
|
46
|
+
queue = downloads.get('queue', [])
|
|
47
|
+
history = downloads.get('history', [])
|
|
48
|
+
|
|
49
|
+
# Separate Quasarr packages from others
|
|
50
|
+
quasarr_queue = [p for p in queue if p.get('cat') != 'not_quasarr']
|
|
51
|
+
other_queue = [p for p in queue if p.get('cat') == 'not_quasarr']
|
|
52
|
+
quasarr_history = [p for p in history if p.get('category') != 'not_quasarr']
|
|
53
|
+
other_history = [p for p in history if p.get('category') == 'not_quasarr']
|
|
54
|
+
|
|
55
|
+
def get_category_emoji(cat):
|
|
56
|
+
return {'movies': '🎬', 'tv': '📺', 'docs': '📄', 'not_quasarr': '📦'}.get(cat, '📦')
|
|
57
|
+
|
|
58
|
+
def format_size(mb=None, bytes_val=None):
|
|
59
|
+
if bytes_val is not None:
|
|
60
|
+
mb = bytes_val / (1024 * 1024)
|
|
61
|
+
if mb is None or mb == 0:
|
|
62
|
+
return "? MB"
|
|
63
|
+
if mb < 1024:
|
|
64
|
+
return f"{mb:.0f} MB"
|
|
65
|
+
return f"{mb / 1024:.1f} GB"
|
|
66
|
+
|
|
67
|
+
def escape_js(s):
|
|
68
|
+
return s.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n')
|
|
69
|
+
|
|
70
|
+
def render_queue_item(item):
|
|
71
|
+
filename = item.get('filename', 'Unknown')
|
|
72
|
+
percentage = item.get('percentage', 0)
|
|
73
|
+
timeleft = item.get('timeleft', '??:??:??')
|
|
74
|
+
mb = item.get('mb', 0)
|
|
75
|
+
cat = item.get('cat', 'not_quasarr')
|
|
76
|
+
is_archive = item.get('is_archive', False)
|
|
77
|
+
nzo_id = item.get('nzo_id', '')
|
|
78
|
+
|
|
79
|
+
is_captcha = '[CAPTCHA' in filename
|
|
80
|
+
if is_captcha:
|
|
81
|
+
status_emoji = '🔒'
|
|
82
|
+
elif '[Extracting]' in filename:
|
|
83
|
+
status_emoji = '📦'
|
|
84
|
+
elif '[Paused]' in filename:
|
|
85
|
+
status_emoji = '⏸️'
|
|
86
|
+
elif '[Linkgrabber]' in filename:
|
|
87
|
+
status_emoji = '🔗'
|
|
88
|
+
else:
|
|
89
|
+
status_emoji = '⬇️'
|
|
90
|
+
|
|
91
|
+
display_name = filename
|
|
92
|
+
for prefix in ['[Downloading] ', '[Extracting] ', '[Paused] ', '[Linkgrabber] ', '[CAPTCHA not solved!] ']:
|
|
93
|
+
display_name = display_name.replace(prefix, '')
|
|
94
|
+
|
|
95
|
+
archive_badge = '<span class="badge archive">📁 ARCHIVE</span>' if is_archive else ''
|
|
96
|
+
cat_emoji = get_category_emoji(cat)
|
|
97
|
+
size_str = format_size(mb=mb)
|
|
98
|
+
|
|
99
|
+
# Progress bar - show "waiting..." for 0%
|
|
100
|
+
if percentage == 0:
|
|
101
|
+
progress_html = '<span class="progress-waiting"></span>'
|
|
102
|
+
else:
|
|
103
|
+
progress_html = f'<div class="progress-track"><div class="progress-fill" style="width: {percentage}%"></div></div>'
|
|
104
|
+
|
|
105
|
+
# Action buttons - CAPTCHA left, delete right
|
|
106
|
+
if is_captcha and nzo_id:
|
|
107
|
+
actions = f'''
|
|
108
|
+
<div class="package-actions">
|
|
109
|
+
<button class="btn-small primary" onclick="location.href='/captcha?package_id={nzo_id}'">🔓 Solve CAPTCHA</button>
|
|
110
|
+
<span class="spacer"></span>
|
|
111
|
+
<button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{escape_js(display_name)}')">🗑️</button>
|
|
112
|
+
</div>
|
|
113
|
+
'''
|
|
114
|
+
elif nzo_id:
|
|
115
|
+
actions = f'''
|
|
116
|
+
<div class="package-actions right-only">
|
|
117
|
+
<button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{escape_js(display_name)}')">🗑️</button>
|
|
118
|
+
</div>
|
|
119
|
+
'''
|
|
120
|
+
else:
|
|
121
|
+
actions = ''
|
|
122
|
+
|
|
123
|
+
return f'''
|
|
124
|
+
<div class="package-card">
|
|
125
|
+
<div class="package-header">
|
|
126
|
+
<span class="status-emoji">{status_emoji}</span>
|
|
127
|
+
<span class="package-name">{display_name}</span>
|
|
128
|
+
{archive_badge}
|
|
129
|
+
</div>
|
|
130
|
+
<div class="package-progress">
|
|
131
|
+
{progress_html}
|
|
132
|
+
<span class="progress-percent">{percentage}%</span>
|
|
133
|
+
</div>
|
|
134
|
+
<div class="package-details">
|
|
135
|
+
<span>⏱️ {timeleft}</span>
|
|
136
|
+
<span>💾 {size_str}</span>
|
|
137
|
+
<span>{cat_emoji} {cat}</span>
|
|
138
|
+
</div>
|
|
139
|
+
{actions}
|
|
140
|
+
</div>
|
|
141
|
+
'''
|
|
142
|
+
|
|
143
|
+
def render_history_item(item):
|
|
144
|
+
name = item.get('name', 'Unknown')
|
|
145
|
+
status = item.get('status', 'Unknown')
|
|
146
|
+
category = item.get('category', 'not_quasarr')
|
|
147
|
+
bytes_val = item.get('bytes', 0)
|
|
148
|
+
is_archive = item.get('is_archive')
|
|
149
|
+
extraction_ok = item.get('extraction_ok', False)
|
|
150
|
+
fail_message = item.get('fail_message', '')
|
|
151
|
+
nzo_id = item.get('nzo_id', '')
|
|
152
|
+
|
|
153
|
+
status_emoji = '✅' if status == 'Completed' else '❌'
|
|
154
|
+
|
|
155
|
+
archive_badge = ''
|
|
156
|
+
if is_archive:
|
|
157
|
+
archive_badge = '<span class="badge extracted">📁 EXTRACTED</span>' if extraction_ok else '<span class="badge pending">📁 NOT EXTRACTED</span>'
|
|
158
|
+
|
|
159
|
+
cat_emoji = get_category_emoji(category)
|
|
160
|
+
size_str = format_size(bytes_val=bytes_val)
|
|
161
|
+
|
|
162
|
+
error_html = f'<div class="package-error">⚠️ {fail_message}</div>' if fail_message else ''
|
|
163
|
+
error_class = 'error' if status != 'Completed' else ''
|
|
164
|
+
|
|
165
|
+
delete_btn = f'<div class="package-actions right-only"><button class="btn-small danger" onclick="confirmDelete(\'{nzo_id}\', \'{escape_js(name)}\')">🗑️</button></div>' if nzo_id else ''
|
|
166
|
+
|
|
167
|
+
return f'''
|
|
168
|
+
<div class="package-card {error_class}">
|
|
169
|
+
<div class="package-header">
|
|
170
|
+
<span class="status-emoji">{status_emoji}</span>
|
|
171
|
+
<span class="package-name">{name}</span>
|
|
172
|
+
{archive_badge}
|
|
173
|
+
</div>
|
|
174
|
+
<div class="package-details">
|
|
175
|
+
<span>💾 {size_str}</span>
|
|
176
|
+
<span>{cat_emoji} {category}</span>
|
|
177
|
+
</div>
|
|
178
|
+
{error_html}
|
|
179
|
+
{delete_btn}
|
|
180
|
+
</div>
|
|
181
|
+
'''
|
|
182
|
+
|
|
183
|
+
# Build HTML sections
|
|
184
|
+
queue_html = ''.join(render_queue_item(item) for item in
|
|
185
|
+
quasarr_queue) if quasarr_queue else '<p class="empty-message">No Quasarr packages in queue</p>'
|
|
186
|
+
history_html = ''.join(render_history_item(item) for item in
|
|
187
|
+
quasarr_history) if quasarr_history else '<p class="empty-message">No Quasarr packages in history</p>'
|
|
188
|
+
|
|
189
|
+
other_html = ''
|
|
190
|
+
if other_queue or other_history:
|
|
191
|
+
other_count = len(other_queue) + len(other_history)
|
|
192
|
+
other_items = ''
|
|
193
|
+
if other_queue:
|
|
194
|
+
other_items += '<h4>⏳ Queue</h4>' + ''.join(render_queue_item(item) for item in other_queue)
|
|
195
|
+
if other_history:
|
|
196
|
+
other_items += '<h4>📜 History</h4>' + ''.join(render_history_item(item) for item in other_history)
|
|
197
|
+
plural = 's' if other_count != 1 else ''
|
|
198
|
+
other_html = f'''
|
|
199
|
+
<div class="other-packages-section">
|
|
200
|
+
<details id="otherPackagesDetails">
|
|
201
|
+
<summary id="otherPackagesSummary">Show {other_count} non-Quasarr package{plural}</summary>
|
|
202
|
+
<div class="other-packages-content">{other_items}</div>
|
|
203
|
+
</details>
|
|
204
|
+
</div>
|
|
205
|
+
'''
|
|
206
|
+
|
|
207
|
+
queue_extra = f" + {len(other_queue)} other" if other_queue else ""
|
|
208
|
+
history_extra = f" + {len(other_history)} other" if other_history else ""
|
|
209
|
+
|
|
210
|
+
refresh_btn = render_button("Refresh Now", "primary", {"onclick": "location.reload()"})
|
|
211
|
+
back_btn = render_button("Back", "secondary", {"onclick": "location.href='/'"})
|
|
212
|
+
|
|
213
|
+
packages_html = f'''
|
|
214
|
+
<h1><img src="{images.logo}" type="image/png" alt="Quasarr logo" class="logo"/>Quasarr</h1>
|
|
215
|
+
<h2>📦 Package Status</h2>
|
|
216
|
+
<div class="refresh-indicator"><span id="countdown">30</span>s until refresh</div>
|
|
217
|
+
|
|
218
|
+
<div class="packages-container">
|
|
219
|
+
<div class="section">
|
|
220
|
+
<h3>⏳ Queue ({len(quasarr_queue)}{queue_extra})</h3>
|
|
221
|
+
<div class="packages-list">{queue_html}</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="section">
|
|
224
|
+
<h3>📜 History ({len(quasarr_history)}{history_extra})</h3>
|
|
225
|
+
<div class="packages-list">{history_html}</div>
|
|
226
|
+
</div>
|
|
227
|
+
{other_html}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<p>{refresh_btn}</p>
|
|
231
|
+
<p>{back_btn}</p>
|
|
232
|
+
|
|
233
|
+
<!-- Delete Confirmation Modal -->
|
|
234
|
+
<div id="deleteModal" class="modal">
|
|
235
|
+
<div class="modal-content">
|
|
236
|
+
<h3>⚠️ Delete Package?</h3>
|
|
237
|
+
<p class="modal-package-name" id="modalPackageName"></p>
|
|
238
|
+
<div class="modal-warning">
|
|
239
|
+
<strong>⛔ Warning:</strong> This will permanently delete the package AND all associated files from disk. This action cannot be undone!
|
|
240
|
+
</div>
|
|
241
|
+
<div class="modal-buttons">
|
|
242
|
+
<button class="btn-secondary" onclick="closeModal()">Cancel</button>
|
|
243
|
+
<button class="btn-danger" id="confirmDeleteBtn">🗑️ Delete Package & Files</button>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<style>
|
|
249
|
+
.packages-container {{ max-width: 600px; margin: 0 auto; }}
|
|
250
|
+
.section {{ margin: 20px 0; }}
|
|
251
|
+
.section h3 {{ margin-bottom: 15px; padding-bottom: 8px; border-bottom: 1px solid var(--border-color, #ddd); }}
|
|
252
|
+
.packages-list {{ display: flex; flex-direction: column; gap: 10px; }}
|
|
253
|
+
|
|
254
|
+
.package-card {{
|
|
255
|
+
background: var(--card-bg, #f8f9fa);
|
|
256
|
+
border: 1px solid var(--card-border, #dee2e6);
|
|
257
|
+
border-radius: 8px;
|
|
258
|
+
padding: 12px 15px;
|
|
259
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
260
|
+
}}
|
|
261
|
+
.package-card:hover {{ transform: translateY(-1px); box-shadow: 0 2px 8px var(--card-shadow, rgba(0,0,0,0.1)); }}
|
|
262
|
+
.package-card.error {{ border-color: var(--error-border, #dc3545); background: var(--error-bg, #fff5f5); }}
|
|
263
|
+
|
|
264
|
+
.package-header {{ display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }}
|
|
265
|
+
.status-emoji {{ font-size: 1.2em; flex-shrink: 0; }}
|
|
266
|
+
.package-name {{ flex: 1; font-weight: 500; word-break: break-word; line-height: 1.3; }}
|
|
267
|
+
|
|
268
|
+
.badge {{ font-size: 0.75em; padding: 2px 6px; border-radius: 4px; white-space: nowrap; flex-shrink: 0; }}
|
|
269
|
+
.badge.archive {{ background: var(--badge-archive-bg, #e3f2fd); color: var(--badge-archive-color, #1565c0); }}
|
|
270
|
+
.badge.extracted {{ background: var(--badge-success-bg, #e8f5e9); color: var(--badge-success-color, #2e7d32); }}
|
|
271
|
+
.badge.pending {{ background: var(--badge-warning-bg, #fff3e0); color: var(--badge-warning-color, #e65100); }}
|
|
272
|
+
|
|
273
|
+
.package-progress {{ display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }}
|
|
274
|
+
.progress-track {{ flex: 1; height: 8px; background: var(--progress-track, #e0e0e0); border-radius: 4px; overflow: hidden; }}
|
|
275
|
+
.progress-fill {{ height: 100%; background: var(--progress-fill, #4caf50); border-radius: 4px; min-width: 4px; }}
|
|
276
|
+
.progress-waiting {{ flex: 1; color: var(--text-muted, #888); font-style: italic; font-size: 0.85em; }}
|
|
277
|
+
.progress-percent {{ font-weight: bold; min-width: 40px; text-align: right; font-size: 0.9em; }}
|
|
278
|
+
|
|
279
|
+
.package-details {{ display: flex; flex-wrap: wrap; gap: 15px; font-size: 0.85em; color: var(--text-muted, #666); }}
|
|
280
|
+
.package-error {{ margin-top: 8px; padding: 8px; background: var(--error-msg-bg, #ffebee); border-radius: 4px; font-size: 0.85em; color: var(--error-msg-color, #c62828); }}
|
|
281
|
+
|
|
282
|
+
.package-actions {{ margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-color, #eee); display: flex; gap: 8px; align-items: center; }}
|
|
283
|
+
.package-actions .spacer {{ flex: 1; }}
|
|
284
|
+
.package-actions.right-only {{ justify-content: flex-end; }}
|
|
285
|
+
.btn-small {{ padding: 5px 12px; font-size: 0.8em; border-radius: 4px; cursor: pointer; transition: all 0.2s; }}
|
|
286
|
+
.btn-small.primary {{ background: var(--btn-primary-bg, #007bff); color: white; border: none; }}
|
|
287
|
+
.btn-small.primary:hover {{ background: var(--btn-primary-hover, #0056b3); }}
|
|
288
|
+
.btn-small.danger {{ background: transparent; color: var(--btn-danger-text, #dc3545); border: 1px solid var(--btn-danger-border, #dc3545); }}
|
|
289
|
+
.btn-small.danger:hover {{ background: var(--btn-danger-hover-bg, #dc3545); color: white; }}
|
|
290
|
+
|
|
291
|
+
.empty-message {{ color: var(--text-muted, #888); font-style: italic; text-align: center; padding: 20px; }}
|
|
292
|
+
.refresh-indicator {{ text-align: center; font-size: 0.85em; color: var(--text-muted, #888); margin-bottom: 15px; }}
|
|
293
|
+
|
|
294
|
+
.other-packages-section {{ margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-color, #ddd); }}
|
|
295
|
+
.other-packages-section summary {{ cursor: pointer; padding: 8px 0; color: var(--text-muted, #666); }}
|
|
296
|
+
.other-packages-section summary:hover {{ color: var(--link-color, #0066cc); }}
|
|
297
|
+
.other-packages-content {{ margin-top: 15px; }}
|
|
298
|
+
.other-packages-content h4 {{ margin: 15px 0 10px 0; font-size: 0.95em; color: var(--text-muted, #666); }}
|
|
299
|
+
|
|
300
|
+
/* Modal */
|
|
301
|
+
.modal {{ display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; }}
|
|
302
|
+
.modal.show {{ display: flex; }}
|
|
303
|
+
.modal-content {{ background: var(--modal-bg, white); padding: 25px; border-radius: 12px; max-width: 400px; width: 90%; text-align: center; }}
|
|
304
|
+
.modal-content h3 {{ margin: 0 0 15px 0; color: var(--error-msg-color, #c62828); }}
|
|
305
|
+
.modal-package-name {{ font-weight: 500; word-break: break-word; padding: 10px; background: var(--card-bg, #f5f5f5); border-radius: 6px; margin: 10px 0; }}
|
|
306
|
+
.modal-warning {{ background: var(--error-msg-bg, #ffebee); color: var(--error-msg-color, #c62828); padding: 12px; border-radius: 6px; margin: 15px 0; font-size: 0.9em; text-align: left; }}
|
|
307
|
+
.modal-buttons {{ display: flex; gap: 10px; justify-content: center; margin-top: 20px; }}
|
|
308
|
+
.btn-danger {{ background: var(--btn-danger-bg, #dc3545); color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-weight: 500; }}
|
|
309
|
+
.btn-danger:hover {{ opacity: 0.9; }}
|
|
310
|
+
|
|
311
|
+
/* Dark mode */
|
|
312
|
+
@media (prefers-color-scheme: dark) {{
|
|
313
|
+
:root {{
|
|
314
|
+
--card-bg: #2d3748; --card-border: #4a5568; --card-shadow: rgba(0,0,0,0.3);
|
|
315
|
+
--border-color: #4a5568; --text-muted: #a0aec0;
|
|
316
|
+
--progress-track: #4a5568; --progress-fill: #68d391;
|
|
317
|
+
--error-border: #fc8181; --error-bg: #2d2d2d; --error-msg-bg: #3d2d2d; --error-msg-color: #fc8181;
|
|
318
|
+
--badge-archive-bg: #1a365d; --badge-archive-color: #63b3ed;
|
|
319
|
+
--badge-success-bg: #1c4532; --badge-success-color: #68d391;
|
|
320
|
+
--badge-warning-bg: #3d2d1a; --badge-warning-color: #f6ad55;
|
|
321
|
+
--link-color: #63b3ed; --modal-bg: #2d3748;
|
|
322
|
+
--btn-primary-bg: #3182ce; --btn-primary-hover: #2c5282;
|
|
323
|
+
--btn-danger-text: #fc8181; --btn-danger-border: #fc8181; --btn-danger-hover-bg: #e53e3e;
|
|
324
|
+
}}
|
|
325
|
+
}}
|
|
326
|
+
</style>
|
|
327
|
+
|
|
328
|
+
<script>
|
|
329
|
+
let countdown = 30;
|
|
330
|
+
const countdownEl = document.getElementById('countdown');
|
|
331
|
+
const refreshInterval = setInterval(() => {{
|
|
332
|
+
countdown--;
|
|
333
|
+
if (countdownEl) countdownEl.textContent = countdown;
|
|
334
|
+
if (countdown <= 0) location.reload();
|
|
335
|
+
}}, 1000);
|
|
336
|
+
|
|
337
|
+
// Restore collapse state from localStorage
|
|
338
|
+
const otherDetails = document.getElementById('otherPackagesDetails');
|
|
339
|
+
const otherSummary = document.getElementById('otherPackagesSummary');
|
|
340
|
+
if (otherDetails && otherSummary) {{
|
|
341
|
+
const count = otherSummary.textContent.match(/\\d+/)?.[0] || '0';
|
|
342
|
+
const plural = count !== '1' ? 's' : '';
|
|
343
|
+
if (localStorage.getItem('otherPackagesOpen') === 'true') {{
|
|
344
|
+
otherDetails.open = true;
|
|
345
|
+
otherSummary.textContent = 'Hide ' + count + ' non-Quasarr package' + plural;
|
|
346
|
+
}}
|
|
347
|
+
otherDetails.addEventListener('toggle', () => {{
|
|
348
|
+
localStorage.setItem('otherPackagesOpen', otherDetails.open);
|
|
349
|
+
otherSummary.textContent = (otherDetails.open ? 'Hide ' : 'Show ') + count + ' non-Quasarr package' + plural;
|
|
350
|
+
}});
|
|
351
|
+
}}
|
|
352
|
+
|
|
353
|
+
// Delete modal
|
|
354
|
+
let deletePackageId = null;
|
|
355
|
+
function confirmDelete(packageId, packageName) {{
|
|
356
|
+
deletePackageId = packageId;
|
|
357
|
+
document.getElementById('modalPackageName').textContent = packageName;
|
|
358
|
+
document.getElementById('deleteModal').classList.add('show');
|
|
359
|
+
clearInterval(refreshInterval);
|
|
360
|
+
}}
|
|
361
|
+
function closeModal() {{
|
|
362
|
+
document.getElementById('deleteModal').classList.remove('show');
|
|
363
|
+
deletePackageId = null;
|
|
364
|
+
location.reload();
|
|
365
|
+
}}
|
|
366
|
+
document.getElementById('confirmDeleteBtn').onclick = function() {{
|
|
367
|
+
if (deletePackageId) location.href = '/packages/delete/' + encodeURIComponent(deletePackageId);
|
|
368
|
+
}};
|
|
369
|
+
document.getElementById('deleteModal').onclick = function(e) {{ if (e.target === this) closeModal(); }};
|
|
370
|
+
document.addEventListener('keydown', function(e) {{ if (e.key === 'Escape') closeModal(); }});
|
|
371
|
+
</script>
|
|
372
|
+
'''
|
|
373
|
+
|
|
374
|
+
return render_centered_html(packages_html)
|
quasarr/downloads/__init__.py
CHANGED
|
@@ -232,6 +232,8 @@ def download(shared_state, request_from, title, url, mirror, size_mb, password,
|
|
|
232
232
|
"""Main download entry point."""
|
|
233
233
|
category = "docs" if "lazylibrarian" in request_from.lower() else \
|
|
234
234
|
"movies" if "radarr" in request_from.lower() else "tv"
|
|
235
|
+
|
|
236
|
+
# Problem, we should make this id deterministic, so same source and same request_from (radarr / sonarr, not their version!) must yield same hash
|
|
235
237
|
package_id = f"Quasarr_{category}_{str(hash(title + url)).replace('-', '')}"
|
|
236
238
|
|
|
237
239
|
if imdb_id and imdb_id.lower() == "none":
|
|
@@ -12,22 +12,58 @@ from quasarr.providers.log import info, debug
|
|
|
12
12
|
from quasarr.providers.statistics import StatsHelper
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
def unhide_links(shared_state, url):
|
|
15
|
+
def unhide_links(shared_state, url, session):
|
|
16
16
|
try:
|
|
17
17
|
links = []
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
# Support both formats:
|
|
20
|
+
# - https://hide.cx/container/{id}
|
|
21
|
+
# - https://hide.cx/fc/Container/{id}.html
|
|
22
|
+
match = re.search(
|
|
23
|
+
r"/(?:fc/)?container/([a-z0-9A-Z\-]+)(?:\.html)?",
|
|
24
|
+
url,
|
|
25
|
+
re.IGNORECASE,
|
|
26
|
+
)
|
|
27
|
+
|
|
20
28
|
if not match:
|
|
21
29
|
info(f"Invalid hide.cx URL: {url}")
|
|
22
30
|
return []
|
|
23
31
|
|
|
24
32
|
container_id = match.group(1)
|
|
33
|
+
is_fc = "/fc/" in url.lower()
|
|
34
|
+
# resolve fc foreign ID to canonical container ID
|
|
35
|
+
if is_fc:
|
|
36
|
+
headers = {
|
|
37
|
+
"User-Agent": shared_state.values["user_agent"],
|
|
38
|
+
"Accept": "application/json",
|
|
39
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
info(f"Resolving hide.cx foreign container ID: {container_id}")
|
|
43
|
+
resolve_url = f"https://api.hide.cx/fc/Container/{container_id}"
|
|
44
|
+
resp = session.get(resolve_url, headers=headers, timeout=30)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
resolved = resp.json()
|
|
48
|
+
except Exception:
|
|
49
|
+
debug(f"Failed to resolve foreign container {container_id}")
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
canonical_id = resolved.get("id")
|
|
53
|
+
if not canonical_id:
|
|
54
|
+
debug(f"No canonical container ID found for {container_id}")
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
container_id = canonical_id
|
|
58
|
+
debug(f"Resolved to canonical container ID: {container_id}")
|
|
59
|
+
|
|
60
|
+
headers = {'User-Agent': shared_state.values["user_agent"]}
|
|
25
61
|
info(f"Fetching hide.cx container with ID: {container_id}")
|
|
26
62
|
|
|
27
63
|
headers = {'User-Agent': shared_state.values["user_agent"]}
|
|
28
64
|
|
|
29
65
|
container_url = f"https://api.hide.cx/containers/{container_id}"
|
|
30
|
-
response =
|
|
66
|
+
response = session.get(container_url, headers=headers)
|
|
31
67
|
data = response.json()
|
|
32
68
|
|
|
33
69
|
link_ids = [link.get("id") for link in data.get("links", []) if link.get("id")]
|
|
@@ -39,7 +75,7 @@ def unhide_links(shared_state, url):
|
|
|
39
75
|
def fetch_link(link_id):
|
|
40
76
|
debug(f"Fetching hide.cx link with ID: {link_id}")
|
|
41
77
|
link_url = f"https://api.hide.cx/containers/{container_id}/links/{link_id}"
|
|
42
|
-
link_data =
|
|
78
|
+
link_data = session.get(link_url, headers=headers).json()
|
|
43
79
|
return link_data.get("url")
|
|
44
80
|
|
|
45
81
|
# Process links in batches of 10
|
|
@@ -103,12 +139,15 @@ def decrypt_links_if_hide(shared_state: Any, items: List[List[str]]) -> Dict[str
|
|
|
103
139
|
resp = session.get(original_url, allow_redirects=True, timeout=10)
|
|
104
140
|
|
|
105
141
|
final_url = resp.url
|
|
106
|
-
|
|
142
|
+
|
|
143
|
+
# accept hide.cx even if it did not redirect
|
|
144
|
+
if "hide.cx" in final_url or "hide.cx" in original_url:
|
|
107
145
|
debug(f"Identified hide.cx link: {final_url}")
|
|
108
146
|
hide_urls.append(final_url)
|
|
109
147
|
else:
|
|
110
148
|
debug(f"Not a hide.cx link (skipped): {final_url}")
|
|
111
149
|
|
|
150
|
+
|
|
112
151
|
except requests.RequestException as e:
|
|
113
152
|
info(f"Error resolving URL {original_url}: {e}")
|
|
114
153
|
continue
|
|
@@ -121,7 +160,7 @@ def decrypt_links_if_hide(shared_state: Any, items: List[List[str]]) -> Dict[str
|
|
|
121
160
|
decrypted_links: List[str] = []
|
|
122
161
|
for url in hide_urls:
|
|
123
162
|
try:
|
|
124
|
-
links = unhide_links(shared_state, url)
|
|
163
|
+
links = unhide_links(shared_state, url, session)
|
|
125
164
|
if not links:
|
|
126
165
|
debug(f"No links decrypted for {url}")
|
|
127
166
|
continue
|