quasarr 2.1.1__py3-none-any.whl → 2.1.2__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/packages/__init__.py +283 -233
- quasarr/providers/auth.py +84 -27
- quasarr/providers/html_templates.py +5 -1
- quasarr/providers/obfuscated.py +10 -10
- quasarr/providers/version.py +1 -1
- {quasarr-2.1.1.dist-info → quasarr-2.1.2.dist-info}/METADATA +1 -1
- {quasarr-2.1.1.dist-info → quasarr-2.1.2.dist-info}/RECORD +11 -11
- {quasarr-2.1.1.dist-info → quasarr-2.1.2.dist-info}/WHEEL +0 -0
- {quasarr-2.1.1.dist-info → quasarr-2.1.2.dist-info}/entry_points.txt +0 -0
- {quasarr-2.1.1.dist-info → quasarr-2.1.2.dist-info}/licenses/LICENSE +0 -0
- {quasarr-2.1.1.dist-info → quasarr-2.1.2.dist-info}/top_level.txt +0 -0
quasarr/api/packages/__init__.py
CHANGED
|
@@ -8,6 +8,220 @@ from quasarr.providers import shared_state
|
|
|
8
8
|
from quasarr.providers.html_templates import render_button, render_centered_html
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def _get_category_emoji(cat):
|
|
12
|
+
return {'movies': '🎬', 'tv': '📺', 'docs': '📄', 'not_quasarr': '📦'}.get(cat, '📦')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _format_size(mb=None, bytes_val=None):
|
|
16
|
+
if bytes_val is not None:
|
|
17
|
+
mb = bytes_val / (1024 * 1024)
|
|
18
|
+
if mb is None or mb == 0:
|
|
19
|
+
return "? MB"
|
|
20
|
+
if mb < 1024:
|
|
21
|
+
return f"{mb:.0f} MB"
|
|
22
|
+
return f"{mb / 1024:.1f} GB"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _escape_js(s):
|
|
26
|
+
return s.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _render_queue_item(item):
|
|
30
|
+
filename = item.get('filename', 'Unknown')
|
|
31
|
+
percentage = item.get('percentage', 0)
|
|
32
|
+
timeleft = item.get('timeleft', '??:??:??')
|
|
33
|
+
mb = item.get('mb', 0)
|
|
34
|
+
cat = item.get('cat', 'not_quasarr')
|
|
35
|
+
is_archive = item.get('is_archive', False)
|
|
36
|
+
nzo_id = item.get('nzo_id', '')
|
|
37
|
+
|
|
38
|
+
is_captcha = '[CAPTCHA' in filename
|
|
39
|
+
if is_captcha:
|
|
40
|
+
status_emoji = '🔒'
|
|
41
|
+
elif '[Extracting]' in filename:
|
|
42
|
+
status_emoji = '📦'
|
|
43
|
+
elif '[Paused]' in filename:
|
|
44
|
+
status_emoji = '⏸️'
|
|
45
|
+
elif '[Linkgrabber]' in filename:
|
|
46
|
+
status_emoji = '🔗'
|
|
47
|
+
else:
|
|
48
|
+
status_emoji = '⬇️'
|
|
49
|
+
|
|
50
|
+
display_name = filename
|
|
51
|
+
for prefix in ['[Downloading] ', '[Extracting] ', '[Paused] ', '[Linkgrabber] ', '[CAPTCHA not solved!] ']:
|
|
52
|
+
display_name = display_name.replace(prefix, '')
|
|
53
|
+
|
|
54
|
+
archive_badge = '<span class="badge archive">📁 ARCHIVE</span>' if is_archive else ''
|
|
55
|
+
cat_emoji = _get_category_emoji(cat)
|
|
56
|
+
size_str = _format_size(mb=mb)
|
|
57
|
+
|
|
58
|
+
# Progress bar - show "waiting..." for 0%
|
|
59
|
+
if percentage == 0:
|
|
60
|
+
progress_html = '<span class="progress-waiting"></span>'
|
|
61
|
+
else:
|
|
62
|
+
progress_html = f'<div class="progress-track"><div class="progress-fill" style="width: {percentage}%"></div></div>'
|
|
63
|
+
|
|
64
|
+
# Action buttons - CAPTCHA left, delete right
|
|
65
|
+
if is_captcha and nzo_id:
|
|
66
|
+
actions = f'''
|
|
67
|
+
<div class="package-actions">
|
|
68
|
+
<button class="btn-small primary" onclick="location.href='/captcha?package_id={nzo_id}'">🔓 Solve CAPTCHA</button>
|
|
69
|
+
<span class="spacer"></span>
|
|
70
|
+
<button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(display_name)}')">🗑️</button>
|
|
71
|
+
</div>
|
|
72
|
+
'''
|
|
73
|
+
elif nzo_id:
|
|
74
|
+
actions = f'''
|
|
75
|
+
<div class="package-actions right-only">
|
|
76
|
+
<button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(display_name)}')">🗑️</button>
|
|
77
|
+
</div>
|
|
78
|
+
'''
|
|
79
|
+
else:
|
|
80
|
+
actions = ''
|
|
81
|
+
|
|
82
|
+
return f'''
|
|
83
|
+
<div class="package-card">
|
|
84
|
+
<div class="package-header">
|
|
85
|
+
<span class="status-emoji">{status_emoji}</span>
|
|
86
|
+
<span class="package-name">{display_name}</span>
|
|
87
|
+
{archive_badge}
|
|
88
|
+
</div>
|
|
89
|
+
<div class="package-progress">
|
|
90
|
+
{progress_html}
|
|
91
|
+
<span class="progress-percent">{percentage}%</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="package-details">
|
|
94
|
+
<span>⏱️ {timeleft}</span>
|
|
95
|
+
<span>💾 {size_str}</span>
|
|
96
|
+
<span>{cat_emoji} {cat}</span>
|
|
97
|
+
</div>
|
|
98
|
+
{actions}
|
|
99
|
+
</div>
|
|
100
|
+
'''
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _render_history_item(item):
|
|
104
|
+
name = item.get('name', 'Unknown')
|
|
105
|
+
status = item.get('status', 'Unknown')
|
|
106
|
+
bytes_val = item.get('bytes', 0)
|
|
107
|
+
category = item.get('category', 'not_quasarr')
|
|
108
|
+
is_archive = item.get('is_archive', False)
|
|
109
|
+
extraction_status = item.get('extraction_status', '')
|
|
110
|
+
fail_message = item.get('fail_message', '')
|
|
111
|
+
nzo_id = item.get('nzo_id', '')
|
|
112
|
+
|
|
113
|
+
is_error = status.lower() in ['failed', 'error'] or fail_message
|
|
114
|
+
card_class = 'package-card error' if is_error else 'package-card'
|
|
115
|
+
|
|
116
|
+
cat_emoji = _get_category_emoji(category)
|
|
117
|
+
size_str = _format_size(bytes_val=bytes_val)
|
|
118
|
+
|
|
119
|
+
archive_badge = ''
|
|
120
|
+
if is_archive:
|
|
121
|
+
if extraction_status == 'SUCCESSFUL':
|
|
122
|
+
archive_badge = '<span class="badge extracted">✅ EXTRACTED</span>'
|
|
123
|
+
elif extraction_status == 'RUNNING':
|
|
124
|
+
archive_badge = '<span class="badge pending">⏳ EXTRACTING</span>'
|
|
125
|
+
else:
|
|
126
|
+
archive_badge = '<span class="badge archive">📁 ARCHIVE</span>'
|
|
127
|
+
|
|
128
|
+
status_emoji = '❌' if is_error else '✅'
|
|
129
|
+
error_html = f'<div class="package-error">⚠️ {fail_message}</div>' if fail_message else ''
|
|
130
|
+
|
|
131
|
+
# Delete button for history items
|
|
132
|
+
if nzo_id:
|
|
133
|
+
actions = f'''
|
|
134
|
+
<div class="package-actions right-only">
|
|
135
|
+
<button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{_escape_js(name)}')">🗑️</button>
|
|
136
|
+
</div>
|
|
137
|
+
'''
|
|
138
|
+
else:
|
|
139
|
+
actions = ''
|
|
140
|
+
|
|
141
|
+
return f'''
|
|
142
|
+
<div class="{card_class}">
|
|
143
|
+
<div class="package-header">
|
|
144
|
+
<span class="status-emoji">{status_emoji}</span>
|
|
145
|
+
<span class="package-name">{name}</span>
|
|
146
|
+
{archive_badge}
|
|
147
|
+
</div>
|
|
148
|
+
<div class="package-details">
|
|
149
|
+
<span>💾 {size_str}</span>
|
|
150
|
+
<span>{cat_emoji} {category}</span>
|
|
151
|
+
</div>
|
|
152
|
+
{error_html}
|
|
153
|
+
{actions}
|
|
154
|
+
</div>
|
|
155
|
+
'''
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _render_packages_content():
|
|
159
|
+
"""Render just the packages content (used for both full page and AJAX refresh)."""
|
|
160
|
+
downloads = get_packages(shared_state)
|
|
161
|
+
queue = downloads.get('queue', [])
|
|
162
|
+
history = downloads.get('history', [])
|
|
163
|
+
|
|
164
|
+
# Separate Quasarr packages from others
|
|
165
|
+
quasarr_queue = [p for p in queue if p.get('cat') != 'not_quasarr']
|
|
166
|
+
other_queue = [p for p in queue if p.get('cat') == 'not_quasarr']
|
|
167
|
+
quasarr_history = [p for p in history if p.get('category') != 'not_quasarr']
|
|
168
|
+
other_history = [p for p in history if p.get('category') == 'not_quasarr']
|
|
169
|
+
|
|
170
|
+
# Build queue section
|
|
171
|
+
queue_html = ''
|
|
172
|
+
if quasarr_queue:
|
|
173
|
+
queue_items = ''.join(_render_queue_item(item) for item in quasarr_queue)
|
|
174
|
+
queue_html = f'''
|
|
175
|
+
<div class="section">
|
|
176
|
+
<h3>⬇️ Downloading</h3>
|
|
177
|
+
<div class="packages-list">{queue_items}</div>
|
|
178
|
+
</div>
|
|
179
|
+
'''
|
|
180
|
+
else:
|
|
181
|
+
queue_html = '<div class="section"><p class="empty-message">No active downloads</p></div>'
|
|
182
|
+
|
|
183
|
+
# Build history section
|
|
184
|
+
history_html = ''
|
|
185
|
+
if quasarr_history:
|
|
186
|
+
history_items = ''.join(_render_history_item(item) for item in quasarr_history[:10])
|
|
187
|
+
history_html = f'''
|
|
188
|
+
<div class="section">
|
|
189
|
+
<h3>📜 Recent History</h3>
|
|
190
|
+
<div class="packages-list">{history_items}</div>
|
|
191
|
+
</div>
|
|
192
|
+
'''
|
|
193
|
+
|
|
194
|
+
# Build "other packages" section (non-Quasarr)
|
|
195
|
+
other_html = ''
|
|
196
|
+
other_count = len(other_queue) + len(other_history)
|
|
197
|
+
if other_count > 0:
|
|
198
|
+
other_items = ''
|
|
199
|
+
if other_queue:
|
|
200
|
+
other_items += f'<h4>Queue ({len(other_queue)})</h4>'
|
|
201
|
+
other_items += ''.join(_render_queue_item(item) for item in other_queue)
|
|
202
|
+
if other_history:
|
|
203
|
+
other_items += f'<h4>History ({len(other_history)})</h4>'
|
|
204
|
+
other_items += ''.join(_render_history_item(item) for item in other_history[:5])
|
|
205
|
+
|
|
206
|
+
plural = 's' if other_count != 1 else ''
|
|
207
|
+
other_html = f'''
|
|
208
|
+
<div class="other-packages-section">
|
|
209
|
+
<details id="otherPackagesDetails">
|
|
210
|
+
<summary id="otherPackagesSummary">Show {other_count} non-Quasarr package{plural}</summary>
|
|
211
|
+
<div class="other-packages-content">{other_items}</div>
|
|
212
|
+
</details>
|
|
213
|
+
</div>
|
|
214
|
+
'''
|
|
215
|
+
|
|
216
|
+
return f'''
|
|
217
|
+
<div class="packages-container">
|
|
218
|
+
{queue_html}
|
|
219
|
+
{history_html}
|
|
220
|
+
{other_html}
|
|
221
|
+
</div>
|
|
222
|
+
'''
|
|
223
|
+
|
|
224
|
+
|
|
11
225
|
def setup_packages_routes(app):
|
|
12
226
|
@app.get('/packages/delete/<package_id>')
|
|
13
227
|
def delete_package_route(package_id):
|
|
@@ -20,6 +234,19 @@ def setup_packages_routes(app):
|
|
|
20
234
|
else:
|
|
21
235
|
redirect('/packages?deleted=0')
|
|
22
236
|
|
|
237
|
+
@app.get('/api/packages/content')
|
|
238
|
+
def packages_content_api():
|
|
239
|
+
"""AJAX endpoint - returns just the packages content HTML for background refresh."""
|
|
240
|
+
try:
|
|
241
|
+
device = shared_state.values["device"]
|
|
242
|
+
except KeyError:
|
|
243
|
+
device = None
|
|
244
|
+
|
|
245
|
+
if not device:
|
|
246
|
+
return '<p class="empty-message">JDownloader connection not established.</p>'
|
|
247
|
+
|
|
248
|
+
return _render_packages_content()
|
|
249
|
+
|
|
23
250
|
@app.get('/packages')
|
|
24
251
|
def packages_status():
|
|
25
252
|
from bottle import request
|
|
@@ -45,204 +272,8 @@ def setup_packages_routes(app):
|
|
|
45
272
|
elif deleted == '0':
|
|
46
273
|
status_message = '<div class="status-message error">❌ Failed to delete package.</div>'
|
|
47
274
|
|
|
48
|
-
# Get packages
|
|
49
|
-
|
|
50
|
-
queue = downloads.get('queue', [])
|
|
51
|
-
history = downloads.get('history', [])
|
|
52
|
-
|
|
53
|
-
# Separate Quasarr packages from others
|
|
54
|
-
quasarr_queue = [p for p in queue if p.get('cat') != 'not_quasarr']
|
|
55
|
-
other_queue = [p for p in queue if p.get('cat') == 'not_quasarr']
|
|
56
|
-
quasarr_history = [p for p in history if p.get('category') != 'not_quasarr']
|
|
57
|
-
other_history = [p for p in history if p.get('category') == 'not_quasarr']
|
|
58
|
-
|
|
59
|
-
def get_category_emoji(cat):
|
|
60
|
-
return {'movies': '🎬', 'tv': '📺', 'docs': '📄', 'not_quasarr': '📦'}.get(cat, '📦')
|
|
61
|
-
|
|
62
|
-
def format_size(mb=None, bytes_val=None):
|
|
63
|
-
if bytes_val is not None:
|
|
64
|
-
mb = bytes_val / (1024 * 1024)
|
|
65
|
-
if mb is None or mb == 0:
|
|
66
|
-
return "? MB"
|
|
67
|
-
if mb < 1024:
|
|
68
|
-
return f"{mb:.0f} MB"
|
|
69
|
-
return f"{mb / 1024:.1f} GB"
|
|
70
|
-
|
|
71
|
-
def escape_js(s):
|
|
72
|
-
return s.replace('\\', '\\\\').replace("'", "\\'").replace('"', '\\"').replace('\n', '\\n')
|
|
73
|
-
|
|
74
|
-
def render_queue_item(item):
|
|
75
|
-
filename = item.get('filename', 'Unknown')
|
|
76
|
-
percentage = item.get('percentage', 0)
|
|
77
|
-
timeleft = item.get('timeleft', '??:??:??')
|
|
78
|
-
mb = item.get('mb', 0)
|
|
79
|
-
cat = item.get('cat', 'not_quasarr')
|
|
80
|
-
is_archive = item.get('is_archive', False)
|
|
81
|
-
nzo_id = item.get('nzo_id', '')
|
|
82
|
-
|
|
83
|
-
is_captcha = '[CAPTCHA' in filename
|
|
84
|
-
if is_captcha:
|
|
85
|
-
status_emoji = '🔒'
|
|
86
|
-
elif '[Extracting]' in filename:
|
|
87
|
-
status_emoji = '📦'
|
|
88
|
-
elif '[Paused]' in filename:
|
|
89
|
-
status_emoji = '⏸️'
|
|
90
|
-
elif '[Linkgrabber]' in filename:
|
|
91
|
-
status_emoji = '🔗'
|
|
92
|
-
else:
|
|
93
|
-
status_emoji = '⬇️'
|
|
94
|
-
|
|
95
|
-
display_name = filename
|
|
96
|
-
for prefix in ['[Downloading] ', '[Extracting] ', '[Paused] ', '[Linkgrabber] ', '[CAPTCHA not solved!] ']:
|
|
97
|
-
display_name = display_name.replace(prefix, '')
|
|
98
|
-
|
|
99
|
-
archive_badge = '<span class="badge archive">📁 ARCHIVE</span>' if is_archive else ''
|
|
100
|
-
cat_emoji = get_category_emoji(cat)
|
|
101
|
-
size_str = format_size(mb=mb)
|
|
102
|
-
|
|
103
|
-
# Progress bar - show "waiting..." for 0%
|
|
104
|
-
if percentage == 0:
|
|
105
|
-
progress_html = '<span class="progress-waiting"></span>'
|
|
106
|
-
else:
|
|
107
|
-
progress_html = f'<div class="progress-track"><div class="progress-fill" style="width: {percentage}%"></div></div>'
|
|
108
|
-
|
|
109
|
-
# Action buttons - CAPTCHA left, delete right
|
|
110
|
-
if is_captcha and nzo_id:
|
|
111
|
-
actions = f'''
|
|
112
|
-
<div class="package-actions">
|
|
113
|
-
<button class="btn-small primary" onclick="location.href='/captcha?package_id={nzo_id}'">🔓 Solve CAPTCHA</button>
|
|
114
|
-
<span class="spacer"></span>
|
|
115
|
-
<button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{escape_js(display_name)}')">🗑️</button>
|
|
116
|
-
</div>
|
|
117
|
-
'''
|
|
118
|
-
elif nzo_id:
|
|
119
|
-
actions = f'''
|
|
120
|
-
<div class="package-actions right-only">
|
|
121
|
-
<button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{escape_js(display_name)}')">🗑️</button>
|
|
122
|
-
</div>
|
|
123
|
-
'''
|
|
124
|
-
else:
|
|
125
|
-
actions = ''
|
|
126
|
-
|
|
127
|
-
return f'''
|
|
128
|
-
<div class="package-card">
|
|
129
|
-
<div class="package-header">
|
|
130
|
-
<span class="status-emoji">{status_emoji}</span>
|
|
131
|
-
<span class="package-name">{display_name}</span>
|
|
132
|
-
{archive_badge}
|
|
133
|
-
</div>
|
|
134
|
-
<div class="package-progress">
|
|
135
|
-
{progress_html}
|
|
136
|
-
<span class="progress-percent">{percentage}%</span>
|
|
137
|
-
</div>
|
|
138
|
-
<div class="package-details">
|
|
139
|
-
<span>⏱️ {timeleft}</span>
|
|
140
|
-
<span>💾 {size_str}</span>
|
|
141
|
-
<span>{cat_emoji} {cat}</span>
|
|
142
|
-
</div>
|
|
143
|
-
{actions}
|
|
144
|
-
</div>
|
|
145
|
-
'''
|
|
146
|
-
|
|
147
|
-
def render_history_item(item):
|
|
148
|
-
name = item.get('name', 'Unknown')
|
|
149
|
-
status = item.get('status', 'Unknown')
|
|
150
|
-
bytes_val = item.get('bytes', 0)
|
|
151
|
-
category = item.get('category', 'not_quasarr')
|
|
152
|
-
is_archive = item.get('is_archive', False)
|
|
153
|
-
extraction_status = item.get('extraction_status', '')
|
|
154
|
-
fail_message = item.get('fail_message', '')
|
|
155
|
-
nzo_id = item.get('nzo_id', '')
|
|
156
|
-
|
|
157
|
-
is_error = status.lower() in ['failed', 'error'] or fail_message
|
|
158
|
-
card_class = 'package-card error' if is_error else 'package-card'
|
|
159
|
-
|
|
160
|
-
cat_emoji = get_category_emoji(category)
|
|
161
|
-
size_str = format_size(bytes_val=bytes_val)
|
|
162
|
-
|
|
163
|
-
archive_badge = ''
|
|
164
|
-
if is_archive:
|
|
165
|
-
if extraction_status == 'SUCCESSFUL':
|
|
166
|
-
archive_badge = '<span class="badge extracted">✅ EXTRACTED</span>'
|
|
167
|
-
elif extraction_status == 'RUNNING':
|
|
168
|
-
archive_badge = '<span class="badge pending">⏳ EXTRACTING</span>'
|
|
169
|
-
else:
|
|
170
|
-
archive_badge = '<span class="badge archive">📁 ARCHIVE</span>'
|
|
171
|
-
|
|
172
|
-
status_emoji = '❌' if is_error else '✅'
|
|
173
|
-
error_html = f'<div class="package-error">⚠️ {fail_message}</div>' if fail_message else ''
|
|
174
|
-
|
|
175
|
-
# Delete button for history items
|
|
176
|
-
if nzo_id:
|
|
177
|
-
actions = f'''
|
|
178
|
-
<div class="package-actions right-only">
|
|
179
|
-
<button class="btn-small danger" onclick="confirmDelete('{nzo_id}', '{escape_js(name)}')">🗑️</button>
|
|
180
|
-
</div>
|
|
181
|
-
'''
|
|
182
|
-
else:
|
|
183
|
-
actions = ''
|
|
184
|
-
|
|
185
|
-
return f'''
|
|
186
|
-
<div class="{card_class}">
|
|
187
|
-
<div class="package-header">
|
|
188
|
-
<span class="status-emoji">{status_emoji}</span>
|
|
189
|
-
<span class="package-name">{name}</span>
|
|
190
|
-
{archive_badge}
|
|
191
|
-
</div>
|
|
192
|
-
<div class="package-details">
|
|
193
|
-
<span>💾 {size_str}</span>
|
|
194
|
-
<span>{cat_emoji} {category}</span>
|
|
195
|
-
</div>
|
|
196
|
-
{error_html}
|
|
197
|
-
{actions}
|
|
198
|
-
</div>
|
|
199
|
-
'''
|
|
200
|
-
|
|
201
|
-
# Build queue section
|
|
202
|
-
queue_html = ''
|
|
203
|
-
if quasarr_queue:
|
|
204
|
-
queue_items = ''.join(render_queue_item(item) for item in quasarr_queue)
|
|
205
|
-
queue_html = f'''
|
|
206
|
-
<div class="section">
|
|
207
|
-
<h3>⬇️ Downloading</h3>
|
|
208
|
-
<div class="packages-list">{queue_items}</div>
|
|
209
|
-
</div>
|
|
210
|
-
'''
|
|
211
|
-
else:
|
|
212
|
-
queue_html = '<div class="section"><p class="empty-message">No active downloads</p></div>'
|
|
213
|
-
|
|
214
|
-
# Build history section
|
|
215
|
-
history_html = ''
|
|
216
|
-
if quasarr_history:
|
|
217
|
-
history_items = ''.join(render_history_item(item) for item in quasarr_history[:10])
|
|
218
|
-
history_html = f'''
|
|
219
|
-
<div class="section">
|
|
220
|
-
<h3>📜 Recent History</h3>
|
|
221
|
-
<div class="packages-list">{history_items}</div>
|
|
222
|
-
</div>
|
|
223
|
-
'''
|
|
224
|
-
|
|
225
|
-
# Build "other packages" section (non-Quasarr)
|
|
226
|
-
other_html = ''
|
|
227
|
-
other_count = len(other_queue) + len(other_history)
|
|
228
|
-
if other_count > 0:
|
|
229
|
-
other_items = ''
|
|
230
|
-
if other_queue:
|
|
231
|
-
other_items += f'<h4>Queue ({len(other_queue)})</h4>'
|
|
232
|
-
other_items += ''.join(render_queue_item(item) for item in other_queue)
|
|
233
|
-
if other_history:
|
|
234
|
-
other_items += f'<h4>History ({len(other_history)})</h4>'
|
|
235
|
-
other_items += ''.join(render_history_item(item) for item in other_history[:5])
|
|
236
|
-
|
|
237
|
-
plural = 's' if other_count != 1 else ''
|
|
238
|
-
other_html = f'''
|
|
239
|
-
<div class="other-packages-section">
|
|
240
|
-
<details id="otherPackagesDetails">
|
|
241
|
-
<summary id="otherPackagesSummary">Show {other_count} non-Quasarr package{plural}</summary>
|
|
242
|
-
<div class="other-packages-content">{other_items}</div>
|
|
243
|
-
</details>
|
|
244
|
-
</div>
|
|
245
|
-
'''
|
|
275
|
+
# Get rendered packages content using shared helper
|
|
276
|
+
packages_content = _render_packages_content()
|
|
246
277
|
|
|
247
278
|
back_btn = render_button("Back", "secondary", {"onclick": "location.href='/'"})
|
|
248
279
|
|
|
@@ -252,14 +283,8 @@ def setup_packages_routes(app):
|
|
|
252
283
|
|
|
253
284
|
{status_message}
|
|
254
285
|
|
|
255
|
-
<div
|
|
256
|
-
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
<div class="packages-container">
|
|
260
|
-
{queue_html}
|
|
261
|
-
{history_html}
|
|
262
|
-
{other_html}
|
|
286
|
+
<div id="packages-content">
|
|
287
|
+
{packages_content}
|
|
263
288
|
</div>
|
|
264
289
|
|
|
265
290
|
<p>{back_btn}</p>
|
|
@@ -323,8 +348,6 @@ def setup_packages_routes(app):
|
|
|
323
348
|
.btn-small.danger:hover {{ background: var(--btn-danger-hover-bg, #dc3545); color: white; }}
|
|
324
349
|
|
|
325
350
|
.empty-message {{ color: var(--text-muted, #888); font-style: italic; text-align: center; padding: 20px; }}
|
|
326
|
-
.refresh-indicator {{ text-align: center; font-size: 0.85em; color: var(--text-muted, #888); margin-bottom: 15px; cursor: pointer; }}
|
|
327
|
-
.refresh-indicator:hover {{ color: var(--link-color, #0066cc); text-decoration: underline; }}
|
|
328
351
|
|
|
329
352
|
.other-packages-section {{ margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border-color, #ddd); }}
|
|
330
353
|
.other-packages-section summary {{ cursor: pointer; padding: 8px 0; color: var(--text-muted, #666); }}
|
|
@@ -380,13 +403,56 @@ def setup_packages_routes(app):
|
|
|
380
403
|
</style>
|
|
381
404
|
|
|
382
405
|
<script>
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
406
|
+
// Background refresh - fetches content via AJAX, waits 5s between refresh cycles
|
|
407
|
+
let refreshPaused = false;
|
|
408
|
+
|
|
409
|
+
async function refreshContent() {{
|
|
410
|
+
if (refreshPaused) return;
|
|
411
|
+
try {{
|
|
412
|
+
const response = await fetch('/api/packages/content');
|
|
413
|
+
if (response.ok) {{
|
|
414
|
+
const html = await response.text();
|
|
415
|
+
const container = document.getElementById('packages-content');
|
|
416
|
+
if (container && html) {{
|
|
417
|
+
container.innerHTML = html;
|
|
418
|
+
// Re-apply collapse state after content update
|
|
419
|
+
restoreCollapseState();
|
|
420
|
+
}}
|
|
421
|
+
}}
|
|
422
|
+
}} catch (e) {{
|
|
423
|
+
// Silent fail - will retry on next cycle
|
|
424
|
+
}}
|
|
425
|
+
// Schedule next refresh 5 seconds after this one completes
|
|
426
|
+
setTimeout(refreshContent, 5000);
|
|
427
|
+
}}
|
|
428
|
+
|
|
429
|
+
function restoreCollapseState() {{
|
|
430
|
+
const otherDetails = document.getElementById('otherPackagesDetails');
|
|
431
|
+
const otherSummary = document.getElementById('otherPackagesSummary');
|
|
432
|
+
if (otherDetails && otherSummary) {{
|
|
433
|
+
const count = otherSummary.textContent.match(/\\d+/)?.[0] || '0';
|
|
434
|
+
const plural = count !== '1' ? 's' : '';
|
|
435
|
+
if (localStorage.getItem('otherPackagesOpen') === 'true') {{
|
|
436
|
+
otherDetails.open = true;
|
|
437
|
+
otherSummary.textContent = 'Hide ' + count + ' non-Quasarr package' + plural;
|
|
438
|
+
}}
|
|
439
|
+
// Re-attach event listener
|
|
440
|
+
otherDetails.onclick = null;
|
|
441
|
+
otherDetails.addEventListener('toggle', function() {{
|
|
442
|
+
localStorage.setItem('otherPackagesOpen', this.open);
|
|
443
|
+
const summaryEl = document.getElementById('otherPackagesSummary');
|
|
444
|
+
if (summaryEl) {{
|
|
445
|
+
summaryEl.textContent = (this.open ? 'Hide ' : 'Show ') + count + ' non-Quasarr package' + plural;
|
|
446
|
+
}}
|
|
447
|
+
}});
|
|
448
|
+
}}
|
|
449
|
+
}}
|
|
450
|
+
|
|
451
|
+
// Start refresh cycle after initial 5s delay
|
|
452
|
+
setTimeout(refreshContent, 5000);
|
|
453
|
+
|
|
454
|
+
// Initial collapse state setup
|
|
455
|
+
restoreCollapseState();
|
|
390
456
|
|
|
391
457
|
// Clear status message from URL after display
|
|
392
458
|
if (window.location.search.includes('deleted=')) {{
|
|
@@ -395,34 +461,18 @@ def setup_packages_routes(app):
|
|
|
395
461
|
window.history.replaceState({{}}, '', url);
|
|
396
462
|
}}
|
|
397
463
|
|
|
398
|
-
// Restore collapse state from localStorage
|
|
399
|
-
const otherDetails = document.getElementById('otherPackagesDetails');
|
|
400
|
-
const otherSummary = document.getElementById('otherPackagesSummary');
|
|
401
|
-
if (otherDetails && otherSummary) {{
|
|
402
|
-
const count = otherSummary.textContent.match(/\\d+/)?.[0] || '0';
|
|
403
|
-
const plural = count !== '1' ? 's' : '';
|
|
404
|
-
if (localStorage.getItem('otherPackagesOpen') === 'true') {{
|
|
405
|
-
otherDetails.open = true;
|
|
406
|
-
otherSummary.textContent = 'Hide ' + count + ' non-Quasarr package' + plural;
|
|
407
|
-
}}
|
|
408
|
-
otherDetails.addEventListener('toggle', () => {{
|
|
409
|
-
localStorage.setItem('otherPackagesOpen', otherDetails.open);
|
|
410
|
-
otherSummary.textContent = (otherDetails.open ? 'Hide ' : 'Show ') + count + ' non-Quasarr package' + plural;
|
|
411
|
-
}});
|
|
412
|
-
}}
|
|
413
|
-
|
|
414
464
|
// Delete modal
|
|
415
465
|
let deletePackageId = null;
|
|
416
466
|
function confirmDelete(packageId, packageName) {{
|
|
417
467
|
deletePackageId = packageId;
|
|
418
468
|
document.getElementById('modalPackageName').textContent = packageName;
|
|
419
469
|
document.getElementById('deleteModal').classList.add('show');
|
|
420
|
-
|
|
470
|
+
refreshPaused = true; // Pause background refresh while modal is open
|
|
421
471
|
}}
|
|
422
472
|
function closeModal() {{
|
|
423
473
|
document.getElementById('deleteModal').classList.remove('show');
|
|
424
474
|
deletePackageId = null;
|
|
425
|
-
|
|
475
|
+
refreshPaused = false; // Resume background refresh
|
|
426
476
|
}}
|
|
427
477
|
document.getElementById('confirmDeleteBtn').onclick = function() {{
|
|
428
478
|
if (deletePackageId) location.href = '/packages/delete/' + encodeURIComponent(deletePackageId);
|