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'
'
+
+ # 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 = '';
};