Add Stream/Download buttons to global search results
This commit is contained in:
@@ -1153,6 +1153,23 @@ def api_download_file(did):
|
|||||||
return send_file(p, as_attachment=True, download_name=download_name)
|
return send_file(p, as_attachment=True, download_name=download_name)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/downloads/<did>/save')
|
||||||
|
def api_download_save(did):
|
||||||
|
"""Speichern: Datei senden und aus Liste entfernen"""
|
||||||
|
d = DM.get(did)
|
||||||
|
if not d or d.get('status') != 'complete':
|
||||||
|
return jsonify({'error': 'not ready'}), 404
|
||||||
|
p = d.get('path')
|
||||||
|
if not p or not os.path.exists(p):
|
||||||
|
return jsonify({'error': 'file missing'}), 404
|
||||||
|
# Sauberer Titel als Dateiname
|
||||||
|
safe_title = re.sub(r'[^\w\-\. ]', '_', d.get('title', 'video'))[:80]
|
||||||
|
download_name = f"{safe_title}.mp4"
|
||||||
|
# Aus Liste entfernen
|
||||||
|
DM.delete(did)
|
||||||
|
return send_file(p, as_attachment=True, download_name=download_name)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/downloads/<did>', methods=['DELETE'])
|
@app.route('/api/downloads/<did>', methods=['DELETE'])
|
||||||
def api_download_delete(did):
|
def api_download_delete(did):
|
||||||
if DM.delete(did):
|
if DM.delete(did):
|
||||||
|
|||||||
+49
-5
@@ -31,6 +31,12 @@ header button { padding: 8px 16px; background: #e94560; border: none; border-rad
|
|||||||
/* Suchergebnisse Liste */
|
/* Suchergebnisse Liste */
|
||||||
.search-results { max-width: 700px; margin: 0 auto; }
|
.search-results { max-width: 700px; margin: 0 auto; }
|
||||||
.entry-list { display: flex; flex-direction: column; gap: 6px; }
|
.entry-list { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.entry-actions { display: flex; gap: 4px; margin-left: auto; }
|
||||||
|
.btn-stream-sm, .btn-download-sm { width: 28px; height: 28px; border-radius: 4px; border: none; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.btn-stream-sm { background: #0f3460; color: #0f0; }
|
||||||
|
.btn-download-sm { background: #0f3460; color: #ff0; }
|
||||||
|
.btn-stream-sm:hover { background: #0f0; color: #000; }
|
||||||
|
.btn-download-sm:hover { background: #ff0; color: #000; }
|
||||||
.entry-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: #16213e; border-radius: 8px; cursor: pointer; transition: background 0.15s; }
|
.entry-item { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: #16213e; border-radius: 8px; cursor: pointer; transition: background 0.15s; }
|
||||||
.entry-item:hover { background: #1a2744; }
|
.entry-item:hover { background: #1a2744; }
|
||||||
.entry-item .entry-thumb { width: 50px; height: 70px; object-fit: cover; border-radius: 4px; background: #0f3460; flex-shrink: 0; }
|
.entry-item .entry-thumb { width: 50px; height: 70px; object-fit: cover; border-radius: 4px; background: #0f3460; flex-shrink: 0; }
|
||||||
@@ -161,16 +167,19 @@ function renderEntries() {
|
|||||||
<h2 class="section-title">Suchergebnisse (${state.entries.length})</h2>
|
<h2 class="section-title">Suchergebnisse (${state.entries.length})</h2>
|
||||||
<div class="entry-list">
|
<div class="entry-list">
|
||||||
${state.entries.map((e, i) => `
|
${state.entries.map((e, i) => `
|
||||||
<div class="entry-item" onclick="openSearchResult(${i})">
|
<div class="entry-item">
|
||||||
${e.thumb ? `<img src="${e.thumb}" class="entry-thumb" onerror="this.style.display='none'">` : '<div class="entry-thumb placeholder">🎬</div>'}
|
${e.thumb ? `<img src="${e.thumb}" class="entry-thumb" onerror="this.style.display='none'">` : '<div class="entry-thumb placeholder">🎬</div>'}
|
||||||
<div class="entry-info">
|
<div class="entry-info" onclick="openSearchResult(${i})">
|
||||||
<div class="entry-title">${e.title || 'Unnamed'}</div>
|
<div class="entry-title">${e.title || 'Unnamed'}</div>
|
||||||
<div class="entry-meta">
|
<div class="entry-meta">
|
||||||
${e.year ? `<span class="entry-year">${e.year}</span>` : ''}
|
${e.year ? `<span class="entry-year">${e.year}</span>` : ''}
|
||||||
${e._site ? `<span class="entry-site">${e._site.name}</span>` : ''}
|
${e._site ? `<span class="entry-site">${e._site.name}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="entry-arrow">▶</div>
|
<div class="entry-actions">
|
||||||
|
<button class="btn-stream-sm" onclick="openSearchStream(${i})" title="Stream">▶</button>
|
||||||
|
<button class="btn-download-sm" onclick="addSearchDownload(${i})" title="Download">⬇</button>
|
||||||
|
</div>
|
||||||
</div>`).join('')}
|
</div>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -313,6 +322,41 @@ async function openSearchResult(idx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openSearchStream(idx) {
|
||||||
|
// Stream in neuem Tab - bleibt auf der Suchseite
|
||||||
|
await openSearchResult(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSearchDownload(idx) {
|
||||||
|
// Download starten
|
||||||
|
const entry = state.entries[idx];
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
const site = entry._site;
|
||||||
|
if (!site) {
|
||||||
|
alert('Site nicht gefunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({ url: entry.url || '' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await api(`/sites/${site.id}/hosters?${params}`);
|
||||||
|
const hosters = (data.hosters || []).filter(h => typeof h === 'object');
|
||||||
|
|
||||||
|
if (hosters.length > 0) {
|
||||||
|
const link = hosters[0].link || hosters[0].url;
|
||||||
|
const did = await api(`/download?url=${encodeURIComponent(link)}&title=${encodeURIComponent(entry.title)}`);
|
||||||
|
alert('Download gestartet: ' + entry.title);
|
||||||
|
showDownloads();
|
||||||
|
} else {
|
||||||
|
alert('Keine Streams gefunden für "' + entry.title + '"');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
alert('Fehler beim Starten des Downloads: ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function pollDownload(did, idx, title) {
|
async function pollDownload(did, idx, title) {
|
||||||
const btn = document.getElementById(`dlbtn${idx}`);
|
const btn = document.getElementById(`dlbtn${idx}`);
|
||||||
const maxWait = 120;
|
const maxWait = 120;
|
||||||
@@ -463,10 +507,10 @@ async function deleteDownload(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveDownload(id, btn) {
|
function saveDownload(id, btn) {
|
||||||
// Download starten und Button deaktivieren
|
// Download starten - Backend löscht danach aus Liste
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.style.opacity = '0.5';
|
btn.style.opacity = '0.5';
|
||||||
location.href = `/api/downloads/${id}/file`;
|
location.href = `/api/downloads/${id}/save`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBC(parts) {
|
function setBC(parts) {
|
||||||
|
|||||||
Reference in New Issue
Block a user