add small player widget
This commit is contained in:
		@ -1,5 +1,7 @@
 | 
				
			|||||||
 | 
					import importlib.resources
 | 
				
			||||||
import panel as pn
 | 
					import panel as pn
 | 
				
			||||||
import argparse 
 | 
					import argparse 
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist
 | 
					from hipsterfy.hipsterfy import Hipsterfy, HipsterfyPlaylist
 | 
				
			||||||
from hipsterfy.panel_page import create_panel_page
 | 
					from hipsterfy.panel_page import create_panel_page
 | 
				
			||||||
@ -14,17 +16,19 @@ def parse_args():
 | 
				
			|||||||
def panel_main():
 | 
					def panel_main():
 | 
				
			||||||
    args = parse_args()
 | 
					    args = parse_args()
 | 
				
			||||||
    hipsterfy = Hipsterfy(args.spotify_client_id, args.spotify_client_secret)
 | 
					    hipsterfy = Hipsterfy(args.spotify_client_id, args.spotify_client_secret)
 | 
				
			||||||
    
 | 
					    playlist_uri = 'https://open.spotify.com/playlist/5grJs3PKyLE0cL5NYmkGIF'
 | 
				
			||||||
    # Create a Panel app
 | 
					 | 
				
			||||||
    pn.extension()
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    # Example usage of Hipsterfy
 | 
					 | 
				
			||||||
    playlist_uri = 'https://open.spotify.com/playlist/5grJs3PKyLE0cL5NYmkGIF'  # Replace with your playlist URI
 | 
					 | 
				
			||||||
    app = lambda: create_panel_page(hipsterfy, playlist_uri)
 | 
					    app = lambda: create_panel_page(hipsterfy, playlist_uri)
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    # Serve the Panel app
 | 
					    static_dir = os.path.join(os.path.dirname(__file__), "static")
 | 
				
			||||||
    pn.serve(app, port=args.port, websocket_origin='*', show=False)
 | 
					    pn.serve(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "/": app,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        static_dirs={"qr": static_dir},
 | 
				
			||||||
 | 
					        port=args.port,
 | 
				
			||||||
 | 
					        websocket_origin='*',
 | 
				
			||||||
 | 
					        show=False,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    panel_main()
 | 
					    panel_main()
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
							
								
								
									
										175
									
								
								hipsterfy/static/scan.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								hipsterfy/static/scan.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="de">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
 | 
					  <title>Spotify QR-Scanner</title>
 | 
				
			||||||
 | 
					  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
 | 
					  <style>
 | 
				
			||||||
 | 
					    html, body {
 | 
				
			||||||
 | 
					      margin: 0; padding: 0;
 | 
				
			||||||
 | 
					      font-family: sans-serif;
 | 
				
			||||||
 | 
					      background: #181818;
 | 
				
			||||||
 | 
					      color: #fff;
 | 
				
			||||||
 | 
					      height: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    #app {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: flex-start;
 | 
				
			||||||
 | 
					      min-height: 100vh;
 | 
				
			||||||
 | 
					      padding: 1em;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    #video {
 | 
				
			||||||
 | 
					      width: 90vw;
 | 
				
			||||||
 | 
					      max-width: 400px;
 | 
				
			||||||
 | 
					      border-radius: 12px;
 | 
				
			||||||
 | 
					      margin: 1em 0;
 | 
				
			||||||
 | 
					      background: #222;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    #result {
 | 
				
			||||||
 | 
					      margin: 1em 0;
 | 
				
			||||||
 | 
					      word-break: break-all;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    #backBtn {
 | 
				
			||||||
 | 
					      margin-top: 1em;
 | 
				
			||||||
 | 
					      padding: 0.7em 1.5em;
 | 
				
			||||||
 | 
					      border: none;
 | 
				
			||||||
 | 
					      border-radius: 8px;
 | 
				
			||||||
 | 
					      background: #1DB954;
 | 
				
			||||||
 | 
					      color: #fff;
 | 
				
			||||||
 | 
					      font-size: 1.1em;
 | 
				
			||||||
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    iframe {
 | 
				
			||||||
 | 
					      border: none;
 | 
				
			||||||
 | 
					      border-radius: 12px;
 | 
				
			||||||
 | 
					      margin-top: 1em;
 | 
				
			||||||
 | 
					      width: 90vw;
 | 
				
			||||||
 | 
					      max-width: 400px;
 | 
				
			||||||
 | 
					      height: 152px; /* Spotify empfiehlt 152px für volle Vorschau */
 | 
				
			||||||
 | 
					      background: #222;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  </style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					  <div id="app">
 | 
				
			||||||
 | 
					    <h2>Spotify QR-Scanner</h2>
 | 
				
			||||||
 | 
					    <video id="video" autoplay playsinline></video>
 | 
				
			||||||
 | 
					    <div id="result"></div>
 | 
				
			||||||
 | 
					    <button id="backBtn">Zurück zum Scanner</button>
 | 
				
			||||||
 | 
					    <div id="embed"></div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <script src="https://unpkg.com/jsqr/dist/jsQR.js"></script>
 | 
				
			||||||
 | 
					  <script>
 | 
				
			||||||
 | 
					    const video = document.getElementById('video');
 | 
				
			||||||
 | 
					    const resultDiv = document.getElementById('result');
 | 
				
			||||||
 | 
					    const embedDiv = document.getElementById('embed');
 | 
				
			||||||
 | 
					    const backBtn = document.getElementById('backBtn');
 | 
				
			||||||
 | 
					    let scanning = true;
 | 
				
			||||||
 | 
					    let stream = null;
 | 
				
			||||||
 | 
					    let animationId = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Extrahiere die Track-ID aus beliebigen Spotify-Links
 | 
				
			||||||
 | 
					    function extractSpotifyTrackId(url) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        // Suche nach /track/<id> oder /embed/track/<id>
 | 
				
			||||||
 | 
					        const match = url.match(/(?:embed\/)?track\/([a-zA-Z0-9]+)/);
 | 
				
			||||||
 | 
					        if (match) {
 | 
				
			||||||
 | 
					          return match[1];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch {}
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function isSpotifyUrl(url) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const u = new URL(url);
 | 
				
			||||||
 | 
					        return u.hostname.endsWith('spotify.com');
 | 
				
			||||||
 | 
					      } catch {
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function startScanner() {
 | 
				
			||||||
 | 
					      scanning = true;
 | 
				
			||||||
 | 
					      embedDiv.innerHTML = '';
 | 
				
			||||||
 | 
					      resultDiv.textContent = '';
 | 
				
			||||||
 | 
					      backBtn.style.display = 'none';
 | 
				
			||||||
 | 
					      if (!stream) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } });
 | 
				
			||||||
 | 
					          video.srcObject = stream;
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          resultDiv.textContent = "Kamera konnte nicht gestartet werden.";
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      video.play();
 | 
				
			||||||
 | 
					      requestAnimationFrame(tick);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function stopScanner() {
 | 
				
			||||||
 | 
					      scanning = false;
 | 
				
			||||||
 | 
					      if (animationId) cancelAnimationFrame(animationId);
 | 
				
			||||||
 | 
					      video.pause();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function tick() {
 | 
				
			||||||
 | 
					      if (!scanning) return;
 | 
				
			||||||
 | 
					      if (video.readyState === video.HAVE_ENOUGH_DATA) {
 | 
				
			||||||
 | 
					        const canvas = document.createElement('canvas');
 | 
				
			||||||
 | 
					        canvas.width = video.videoWidth;
 | 
				
			||||||
 | 
					        canvas.height = video.videoHeight;
 | 
				
			||||||
 | 
					        const ctx = canvas.getContext('2d');
 | 
				
			||||||
 | 
					        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
 | 
				
			||||||
 | 
					        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
 | 
				
			||||||
 | 
					        const code = jsQR(imageData.data, imageData.width, imageData.height);
 | 
				
			||||||
 | 
					        if (code && code.data) {
 | 
				
			||||||
 | 
					          handleResult(code.data);
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      animationId = requestAnimationFrame(tick);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function handleResult(data) {
 | 
				
			||||||
 | 
					      stopScanner();
 | 
				
			||||||
 | 
					      if (isSpotifyUrl(data)) {
 | 
				
			||||||
 | 
					        const embedUrl = getSpotifyEmbed(data);
 | 
				
			||||||
 | 
					        if (embedUrl) {
 | 
				
			||||||
 | 
					          resultDiv.innerHTML = `<span>Spotify-Link erkannt:</span><br><a href="${data}" target="_blank" style="color:#1DB954">${data}</a>`;
 | 
				
			||||||
 | 
					          embedDiv.innerHTML = `<iframe src="${embedUrl}" allow="encrypted-media"></iframe>`;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          resultDiv.textContent = "Spotify-Link erkannt, aber kein unterstützter Typ.";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        resultDiv.textContent = "Kein gültiger Spotify-Link erkannt.";
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      backBtn.style.display = 'inline-block';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    backBtn.onclick = () => {
 | 
				
			||||||
 | 
					      startScanner();
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    window.onload = startScanner;
 | 
				
			||||||
 | 
					    window.onpagehide = () => {
 | 
				
			||||||
 | 
					      if (stream) {
 | 
				
			||||||
 | 
					        stream.getTracks().forEach(track => track.stop());
 | 
				
			||||||
 | 
					        stream = null;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  </script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
@ -22,6 +22,9 @@ authors = [
 | 
				
			|||||||
    "Jonas Weinz"
 | 
					    "Jonas Weinz"
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
readme = "README.md"
 | 
					readme = "README.md"
 | 
				
			||||||
 | 
					include = [
 | 
				
			||||||
 | 
					    "hipsterfy/static/*"
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[build-system]
 | 
					[build-system]
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user