From a71428dc57df728ef56b7f1c57c27a2975e6e6d5 Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Mon, 4 Aug 2025 17:14:34 +0200 Subject: [PATCH] add mobile scanner --- hipsterfy/panel_page.py | 26 +++++++ hipsterfy/static/scan.html | 143 +++++++++++++++++++++++++++++-------- 2 files changed, 140 insertions(+), 29 deletions(-) diff --git a/hipsterfy/panel_page.py b/hipsterfy/panel_page.py index 7f42fd8..08c94e7 100644 --- a/hipsterfy/panel_page.py +++ b/hipsterfy/panel_page.py @@ -2,6 +2,7 @@ import panel as pn import base64 from io import BytesIO from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist, HipsterfyPlaylistItem, CardStyle +import qrcode pn.extension("filedownload", "notifications", "location") @@ -167,9 +168,34 @@ def create_panel_page(hipsterfy: Hipsterfy, playlist_uri: str=None) -> pn.Templa create_preview_button.on_click(create_preview) + # Dynamischer QR-Code für die Player-App + def generate_qr_code_base64(url): + qr = qrcode.QRCode(box_size=4, border=2) + qr.add_data(url) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + buffered = BytesIO() + img.save(buffered, format="PNG") + img_str = base64.b64encode(buffered.getvalue()).decode() + return f'
Mobile QR-Scanner:
QR Code
{url}
' + + # Dynamisch: QR-Code aktualisiert sich, wenn die Seite unter einer anderen URL läuft + def player_qr_html(): + # pn.state.location.href ist erst nach dem ersten Request gesetzt + base_url = pn.state.location.href or main_url + # Basis-URL ohne evtl. Pfad/Query + from urllib.parse import urlparse + parsed = urlparse(base_url) + base = f"{parsed.scheme}://{parsed.netloc}" + url = base + "/qr/scan.html" + return generate_qr_code_base64(url) + + player_qr_pane = pn.bind(lambda: pn.pane.HTML(player_qr_html(), sizing_mode='stretch_width')) + template = pn.template.FastListTemplate( title='Hipsterfy', sidebar=[ + player_qr_pane, # <-- Dynamischer QR-Code ganz oben in der Sidebar playlist_instructions, playlist_uri, primary_item, secondary_item, additional_items, card_style, cards_per_row_widget, create_preview_button, download_front_html_button, download_back_html_button, print_instructions diff --git a/hipsterfy/static/scan.html b/hipsterfy/static/scan.html index f038ac6..cf56d67 100644 --- a/hipsterfy/static/scan.html +++ b/hipsterfy/static/scan.html @@ -1,16 +1,17 @@ - + - Spotify QR-Scanner + Hipsterfy QR-Scanner @@ -58,7 +113,8 @@

Spotify QR-Scanner

- + +
@@ -67,14 +123,24 @@ const resultDiv = document.getElementById('result'); const embedDiv = document.getElementById('embed'); const backBtn = document.getElementById('backBtn'); + const previewAudio = document.getElementById('preview'); let scanning = true; let stream = null; let animationId = null; - // Extrahiere die Track-ID aus beliebigen Spotify-Links + function extractPreviewUrl(url) { + try { + if (url.endsWith('.mp3')) return url; + const u = new URL(url); + if (u.searchParams.has('preview_url')) { + return u.searchParams.get('preview_url'); + } + } catch {} + return null; + } + function extractSpotifyTrackId(url) { try { - // Suche nach /track/ oder /embed/track/ const match = url.match(/(?:embed\/)?track\/([a-zA-Z0-9]+)/); if (match) { return match[1]; @@ -95,7 +161,6 @@ function getSpotifyEmbed(url) { const trackId = extractSpotifyTrackId(url); if (trackId) { - // Die Vorschau ist immer das offizielle Spotify-Embed, keine App-Credentials! return `https://open.spotify.com/embed/track/${trackId}`; } return null; @@ -106,12 +171,15 @@ embedDiv.innerHTML = ''; resultDiv.textContent = ''; backBtn.style.display = 'none'; + previewAudio.style.display = 'none'; + previewAudio.pause(); + previewAudio.src = ''; if (!stream) { try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }); video.srcObject = stream; } catch (e) { - resultDiv.textContent = "Kamera konnte nicht gestartet werden."; + resultDiv.textContent = "Could not start camera."; return; } } @@ -145,16 +213,31 @@ function handleResult(data) { stopScanner(); - if (isSpotifyUrl(data)) { + const previewUrl = extractPreviewUrl(data); + if (previewUrl) { + resultDiv.innerHTML = `Spotify preview found and playing.`; + previewAudio.src = previewUrl; + previewAudio.style.display = 'block'; + previewAudio.play(); + embedDiv.innerHTML = ''; + } else if (isSpotifyUrl(data)) { const embedUrl = getSpotifyEmbed(data); if (embedUrl) { - resultDiv.innerHTML = `Spotify-Link erkannt:
${data}`; + resultDiv.innerHTML = `Spotify link found and loaded below.`; embedDiv.innerHTML = ``; } else { - resultDiv.textContent = "Spotify-Link erkannt, aber kein unterstützter Typ."; + resultDiv.textContent = "Spotify link found, but not a supported type."; + embedDiv.innerHTML = ''; } + previewAudio.style.display = 'none'; + previewAudio.pause(); + previewAudio.src = ''; } else { - resultDiv.textContent = "Kein gültiger Spotify-Link erkannt."; + resultDiv.textContent = "No valid Spotify link found."; + embedDiv.innerHTML = ''; + previewAudio.style.display = 'none'; + previewAudio.pause(); + previewAudio.src = ''; } backBtn.style.display = 'inline-block'; } @@ -169,6 +252,8 @@ stream.getTracks().forEach(track => track.stop()); stream = null; } + previewAudio.pause(); + previewAudio.src = ''; };