Compare commits
9 Commits
spotify-se
...
sweb
| Author | SHA1 | Date | |
|---|---|---|---|
| 03fc72f54f | |||
| 8fd9d1f55b | |||
| 7e01723901 | |||
| dcc2697176 | |||
| 83d69f7c2d | |||
| 8d23d0170f | |||
| 0d386b08b6 | |||
| 209a020418 | |||
| 9b9b350629 |
62
content.js
62
content.js
@@ -1,62 +0,0 @@
|
||||
const TARGET_URL = "http://localhost:8080/endpoint";
|
||||
let lastSentSignature = "";
|
||||
|
||||
function extractAndSendSong() {
|
||||
const trackInfo = document.querySelector('[data-testid="now-playing-widget"]');
|
||||
|
||||
if (trackInfo) {
|
||||
const links = trackInfo.querySelectorAll('a');
|
||||
const songTitle = links[0] ? links[0].innerText : null;
|
||||
const artistName = links[1] ? links[1].innerText : "Unknown Artist";
|
||||
|
||||
// Získání obrázku alba
|
||||
const imgElement = trackInfo.querySelector('img');
|
||||
const albumCoverUrl = imgElement ? imgElement.src : null;
|
||||
|
||||
// Získání času
|
||||
const currentTimeEl = document.querySelector('[data-testid="playback-position"]');
|
||||
const durationEl = document.querySelector('[data-testid="playback-duration"]');
|
||||
|
||||
const currentTime = currentTimeEl ? currentTimeEl.innerText : "0:00";
|
||||
const duration = durationEl ? durationEl.innerText : "0:00";
|
||||
|
||||
const progressFull = `${currentTime}/${duration}`;
|
||||
|
||||
// Spolehlivější detekce reklamy:
|
||||
// Reklamy na Spotify trvají vždy přesně 30 vteřin (0:30) nebo nemají běžné odkazy na skladbu
|
||||
const isAd = (duration === "0:30") || (links.length < 2) || (songTitle === null);
|
||||
|
||||
const finalTitle = isAd ? "Reklama" : songTitle;
|
||||
const finalArtist = isAd ? "Spotify" : artistName;
|
||||
|
||||
if (finalTitle || isAd) {
|
||||
const currentSignature = `${finalArtist}-${finalTitle}-${currentTime}-${isAd}`;
|
||||
|
||||
if (currentSignature !== lastSentSignature) {
|
||||
lastSentSignature = currentSignature;
|
||||
|
||||
console.log("Odesílám data:", finalArtist, "-", finalTitle, `[${progressFull}]`, "Reklama:", isAd);
|
||||
|
||||
fetch(TARGET_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
artist: finalArtist,
|
||||
title: finalTitle,
|
||||
coverImg: albumCoverUrl,
|
||||
progressTime: progressFull,
|
||||
ad: isAd ? "true" : "false",
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}).catch(err => {
|
||||
console.error("Chyba při odesílání:", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kontrola každou vteřinu
|
||||
setInterval(extractAndSendSong, 1000);
|
||||
BIN
katalog+zvuky.zip
Normal file
BIN
katalog+zvuky.zip
Normal file
Binary file not shown.
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Spotify IP Sender",
|
||||
"version": "1.0",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://open.spotify.com/*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://open.spotify.com/*"],
|
||||
"js": ["content.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
44
server.js
Normal file
44
server.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const app = express();
|
||||
const PORT = 8080;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Úložiště pro aktuální stav přehrávání
|
||||
let currentSongData = {
|
||||
artist: "Čekám na Spotify...",
|
||||
title: "Spusťte hudbu",
|
||||
coverImg: null,
|
||||
progressTime: "0:00/0:00",
|
||||
isAd: false
|
||||
};
|
||||
|
||||
// 1. Endpoint pro příjem dat z Chrome / Tampermonkey (POST)
|
||||
app.post('/endpoint', (req, res) => {
|
||||
if (req.body) {
|
||||
// Přepíšeme celá přijatá data
|
||||
currentSongData = req.body;
|
||||
|
||||
// Sjednocení klíče pro reklamu (kdyby rozšíření poslalo 'ad' i 'isAd')
|
||||
if (req.body.ad !== undefined && req.body.isAd === undefined) {
|
||||
currentSongData.isAd = (req.body.ad === true || req.body.ad === "true");
|
||||
}
|
||||
|
||||
console.log("--- New Event Received ---");
|
||||
console.log(`Interpret: ${currentSongData.artist}`);
|
||||
console.log(`Skladba: ${currentSongData.title}`);
|
||||
console.log(`Čas: ${currentSongData.progressTime} | Reklama: ${currentSongData.isAd}`);
|
||||
}
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
// 2. Endpoint pro Python widget (GET)
|
||||
app.get('/endpoint', (req, res) => {
|
||||
res.json(currentSongData);
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Server běží a poslouchá na portu ${PORT}...`);
|
||||
});
|
||||
Binary file not shown.
714
spotify_widget.py
Normal file
714
spotify_widget.py
Normal file
@@ -0,0 +1,714 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import math
|
||||
import random
|
||||
import datetime
|
||||
import threading
|
||||
import subprocess
|
||||
import requests
|
||||
from io import BytesIO
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk, ImageDraw
|
||||
import pythoncom
|
||||
|
||||
# Pokus o automatickou instalaci pycaw, pokud chybí
|
||||
try:
|
||||
from pycaw.pycaw import AudioUtilities
|
||||
HAS_PYCAW = True
|
||||
except ImportError:
|
||||
print("📦 Instaluji potřebnou knihovnu 'pycaw' pro okamžité ztlumení Windows...")
|
||||
try:
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "pycaw", "comtypes"],
|
||||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||||
from pycaw.pycaw import AudioUtilities
|
||||
HAS_PYCAW = True
|
||||
print("✅ Knihovna pycaw byla úspěšně nainstalována!")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Automatická instalace pycaw se nezdařila: {e}")
|
||||
HAS_PYCAW = False
|
||||
|
||||
# Pokus o import winsound (standardně na Windows)
|
||||
try:
|
||||
import winsound
|
||||
HAS_WINSOUND = True
|
||||
except ImportError:
|
||||
HAS_WINSOUND = False
|
||||
|
||||
# Pokus o import pygame
|
||||
try:
|
||||
import pygame
|
||||
pygame.mixer.init()
|
||||
HAS_PYGAME = True
|
||||
except Exception:
|
||||
HAS_PYGAME = False
|
||||
|
||||
SERVER_URL = "http://localhost:8080/endpoint"
|
||||
CYCLE_INTERVAL_MS = 10000 # Doba zobrazení produktu (10 sekund)
|
||||
FRAME_INTERVAL_MS = 120 # Rychlost 3D rotace fotek (120 ms)
|
||||
PARTICLE_COUNT = 60 # Počet bokeh částic
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
KATALOG_DIR = os.path.join(SCRIPT_DIR, "katalog")
|
||||
if not os.path.exists(KATALOG_DIR):
|
||||
KATALOG_DIR = os.path.join(SCRIPT_DIR, "documents", "katalog")
|
||||
|
||||
# Složka se zvuky pro reklamy
|
||||
SOUNDS_DIR = os.path.join(SCRIPT_DIR, "zvuky_reklama")
|
||||
if not os.path.exists(SOUNDS_DIR):
|
||||
os.makedirs(SOUNDS_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def natural_sort_key(s):
|
||||
""" Řazení souborů podle čísel (image1, image2, ..., image10) """
|
||||
return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', s)]
|
||||
|
||||
|
||||
def set_spotify_mute(mute=True):
|
||||
""" Bleskově ztlumí VŠECHNY aplikace ve Windows kromě samotného Pythonu """
|
||||
pythoncom.CoInitialize()
|
||||
if HAS_PYCAW:
|
||||
try:
|
||||
sessions = AudioUtilities.GetAllSessions()
|
||||
for session in sessions:
|
||||
try:
|
||||
pname = session.Process.name().lower() if session.Process else ""
|
||||
if not any(keep in pname for keep in ["python", "wmplayer"]):
|
||||
volume = session.SimpleAudioVolume
|
||||
volume.SetMute(1 if mute else 0, None)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"Pycaw chyba: {e}")
|
||||
|
||||
nircmd_path = os.path.join(SCRIPT_DIR, "nircmd.exe")
|
||||
if os.path.exists(nircmd_path):
|
||||
try:
|
||||
val = "1" if mute else "0"
|
||||
for proc in ["spotify.exe", "chrome.exe", "msedge.exe", "brave.exe", "firefox.exe", "opera.exe"]:
|
||||
subprocess.run(
|
||||
[nircmd_path, "muteappvolume", proc, val],
|
||||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"NirCmd chyba: {e}")
|
||||
|
||||
|
||||
class AdvancedParticle:
|
||||
""" Třída pro vícevrstvé animované bokeh částice """
|
||||
def __init__(self, w=1920, h=1080):
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.reset(random_y=True)
|
||||
|
||||
def reset(self, random_y=False):
|
||||
self.x = random.uniform(0, self.w)
|
||||
self.y = random.uniform(0, self.h) if random_y else self.h + random.uniform(20, 80)
|
||||
|
||||
self.p_type = random.choice(['bokeh_large', 'bokeh_medium', 'spark'])
|
||||
|
||||
if self.p_type == 'bokeh_large':
|
||||
self.base_radius = random.uniform(40, 90)
|
||||
self.speed_y = random.uniform(0.2, 0.6)
|
||||
self.sway_amp = random.uniform(1.0, 3.0)
|
||||
self.sway_freq = random.uniform(0.005, 0.015)
|
||||
self.color_outer = random.choice(["#2a0845", "#3c096c", "#1e0034", "#480ca8", "#38040e"])
|
||||
self.color_inner = random.choice(["#5a189a", "#7b2cbf", "#9d4edd", "#7209b7", "#9e2a2b"])
|
||||
elif self.p_type == 'bokeh_medium':
|
||||
self.base_radius = random.uniform(18, 38)
|
||||
self.speed_y = random.uniform(0.5, 1.2)
|
||||
self.sway_amp = random.uniform(1.5, 4.0)
|
||||
self.sway_freq = random.uniform(0.01, 0.025)
|
||||
self.color_outer = random.choice(["#5a189a", "#7209b7", "#b5179e", "#480ca8"])
|
||||
self.color_inner = random.choice(["#e0aaff", "#f72585", "#c77dff", "#ffb703"])
|
||||
else: # spark
|
||||
self.base_radius = random.uniform(2, 5)
|
||||
self.speed_y = random.uniform(0.8, 2.2)
|
||||
self.sway_amp = random.uniform(0.5, 2.0)
|
||||
self.sway_freq = random.uniform(0.02, 0.05)
|
||||
self.color_outer = "#ffffff"
|
||||
self.color_inner = random.choice(["#ffffff", "#ffee83", "#4cc9f0", "#ffb703"])
|
||||
|
||||
self.radius = self.base_radius
|
||||
self.pulse_phase = random.uniform(0, 2 * math.pi)
|
||||
self.pulse_speed = random.uniform(0.015, 0.04)
|
||||
self.phase = random.uniform(0, 2 * math.pi)
|
||||
|
||||
def update(self, w, h):
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.y -= self.speed_y
|
||||
self.phase += self.sway_freq
|
||||
self.x += math.sin(self.phase) * self.sway_amp
|
||||
|
||||
self.pulse_phase += self.pulse_speed
|
||||
pulse_factor = 1.0 + 0.22 * math.sin(self.pulse_phase)
|
||||
self.radius = self.base_radius * pulse_factor
|
||||
|
||||
if self.y < -self.radius * 2 or self.x < -self.radius or self.x > self.w + self.radius:
|
||||
self.reset(random_y=False)
|
||||
|
||||
|
||||
class FullscreenJarmarkDisplay:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Jarmark Display Kiosk")
|
||||
self.root.attributes("-fullscreen", True)
|
||||
|
||||
self.w = 1920
|
||||
self.h = 1080
|
||||
|
||||
# Klávesové zkratky
|
||||
self.root.bind("<Escape>", lambda e: self.root.destroy())
|
||||
self.root.bind("<Right>", lambda e: self.next_product())
|
||||
self.root.bind("<Left>", lambda e: self.prev_product())
|
||||
self.root.bind("<space>", lambda e: self.toggle_pause())
|
||||
self.root.bind("<r>", lambda e: self.reload_catalog_action())
|
||||
self.root.bind("<R>", lambda e: self.reload_catalog_action())
|
||||
|
||||
# Plátno s čistým tmavým pozadím
|
||||
self.canvas = tk.Canvas(root, bg="#07010f", highlightthickness=0)
|
||||
self.canvas.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
self.canvas.bind("<Configure>", self.on_resize)
|
||||
|
||||
# Částice
|
||||
self.particles = [AdvancedParticle(self.w, self.h) for _ in range(PARTICLE_COUNT)]
|
||||
self.particle_draw_ids = []
|
||||
self.init_particles_canvas()
|
||||
|
||||
# Canvas Prvky - Název produktu se stínem pro skvělou čitelnost bez rámčku
|
||||
self.title_shadow_id = self.canvas.create_text(0, 0, text="", fill="#000000", font=("Impact", 42, "bold"), anchor="center")
|
||||
self.title_text_id = self.canvas.create_text(0, 0, text="", fill="#ffffff", font=("Impact", 42, "bold"), anchor="center")
|
||||
|
||||
self.badge_bg_id = self.canvas.create_rectangle(0, 0, 0, 0, fill="", outline="", state="hidden")
|
||||
self.badge_text_id = self.canvas.create_text(0, 0, text="", fill="white", font=("Segoe UI", 13, "bold"), state="hidden")
|
||||
|
||||
# Produkt fotka (zcela bez rámečku, volně na pozadí)
|
||||
self.prod_image_id = self.canvas.create_image(0, 0, anchor="center")
|
||||
|
||||
# Cenovka vpravo
|
||||
self.price_bg_id = self.canvas.create_rectangle(0, 0, 0, 0, fill="#120521", outline="#9d4edd", width=2)
|
||||
self.price_shadow_id = self.canvas.create_text(0, 0, text="", fill="#000000", font=("Segoe UI", 38, "bold"), anchor="center")
|
||||
self.price_text_id = self.canvas.create_text(0, 0, text="", fill="#ffb703", font=("Segoe UI", 38, "bold"), anchor="center")
|
||||
|
||||
# Hodiny & Datum
|
||||
self.clock_time_id = self.canvas.create_text(0, 0, text="", fill="#ffffff", font=("Segoe UI", 26, "bold"), anchor="e")
|
||||
self.clock_date_id = self.canvas.create_text(0, 0, text="", fill="#9d4edd", font=("Segoe UI", 13, "bold"), anchor="e")
|
||||
|
||||
# Indikátor pauzy
|
||||
self.pause_indicator_id = self.canvas.create_text(0, 0, text="", fill="#ff0055", font=("Segoe UI", 13, "bold"), anchor="w")
|
||||
|
||||
# Spotify Karta (ovládaná vaším rozšířením, vlevo dole)
|
||||
self.card_x1, self.card_y1 = 0, 0
|
||||
self.card_w, self.card_h = 360, 150
|
||||
self.card_bg_photo = None
|
||||
self.card_bg_id = self.canvas.create_image(0, 0, anchor="nw")
|
||||
|
||||
self.cover_img_id = self.canvas.create_image(0, 0, anchor="center")
|
||||
self.spotify_title_id = self.canvas.create_text(0, 0, text="Načítám...", fill="white", font=("Segoe UI", 13, "bold"), anchor="w")
|
||||
self.spotify_subtitle_id = self.canvas.create_text(0, 0, text="Čekám na Spotify", fill="#c77dff", font=("Segoe UI", 10), anchor="w")
|
||||
|
||||
self.time_curr_id = self.canvas.create_text(0, 0, text="0:00", fill="#aaaaaa", font=("Segoe UI", 9), anchor="w")
|
||||
self.time_total_id = self.canvas.create_text(0, 0, text="0:00", fill="#aaaaaa", font=("Segoe UI", 9), anchor="e")
|
||||
|
||||
self.pbar_bg = self.canvas.create_line(0, 0, 0, 0, fill="#3c096c", width=5, capstyle=tk.ROUND)
|
||||
self.pbar_fg = self.canvas.create_line(0, 0, 0, 0, fill="#f72585", width=5, capstyle=tk.ROUND)
|
||||
|
||||
# Stavové proměnné
|
||||
self.running = True
|
||||
self.is_paused = False
|
||||
self.is_ad_active = False
|
||||
self.ad_sound_process = None
|
||||
self.current_img_url = None
|
||||
self.photo_cover = None
|
||||
self.raw_cover_img = None
|
||||
self.current_card_rgba = (25, 10, 40, 230)
|
||||
self.current_prod_photo = None
|
||||
|
||||
self.catalog_items = []
|
||||
self.active_item = None
|
||||
self.current_catalog_index = 0
|
||||
self.current_frame_index = 0
|
||||
|
||||
# Spuštění procesů
|
||||
self.load_catalog()
|
||||
self.cycle_product()
|
||||
self.animate_product_3d()
|
||||
self.animate_particles()
|
||||
self.update_clock()
|
||||
|
||||
threading.Thread(target=self.poll_data, daemon=True).start()
|
||||
|
||||
def trigger_ad_sound(self):
|
||||
print("🔇 Ztlumuji vše kromě Pythonu...")
|
||||
set_spotify_mute(True)
|
||||
|
||||
if not os.path.exists(SOUNDS_DIR):
|
||||
return
|
||||
|
||||
valid_exts = ('.wav', '.mp3', '.wma', '.ogg')
|
||||
sounds = [os.path.join(SOUNDS_DIR, f) for f in os.listdir(SOUNDS_DIR) if f.lower().endswith(valid_exts)]
|
||||
|
||||
if not sounds:
|
||||
return
|
||||
|
||||
chosen_sound = random.choice(sounds)
|
||||
|
||||
def _play():
|
||||
try:
|
||||
if HAS_PYGAME:
|
||||
pygame.mixer.music.load(chosen_sound)
|
||||
pygame.mixer.music.play()
|
||||
elif chosen_sound.lower().endswith('.wav') and HAS_WINSOUND:
|
||||
winsound.PlaySound(chosen_sound, winsound.SND_FILENAME | winsound.SND_ASYNC)
|
||||
else:
|
||||
ps_script = f"$m = New-Object -ComObject WMPlayer.OCX; $m.URL = '{chosen_sound}'; $m.controls.play(); Start-Sleep -s 60"
|
||||
self.ad_sound_process = subprocess.Popen(
|
||||
["powershell", "-Command", ps_script],
|
||||
creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Chyba při přehrávání zvuku: {e}")
|
||||
|
||||
threading.Thread(target=_play, daemon=True).start()
|
||||
|
||||
def stop_ad_sound(self):
|
||||
print("🔊 REKLAMA SKONČILA: Vracím hlasitost zpět.")
|
||||
try:
|
||||
if HAS_PYGAME and pygame.mixer.music.get_busy():
|
||||
pygame.mixer.music.stop()
|
||||
if HAS_WINSOUND:
|
||||
winsound.PlaySound(None, winsound.SND_PURGE)
|
||||
if self.ad_sound_process:
|
||||
self.ad_sound_process.kill()
|
||||
self.ad_sound_process = None
|
||||
except Exception as e:
|
||||
print(f"Chyba při zastavování: {e}")
|
||||
|
||||
set_spotify_mute(False)
|
||||
|
||||
def on_resize(self, event):
|
||||
if event.width <= 10 or event.height <= 10:
|
||||
return
|
||||
if self.w == event.width and self.h == event.height:
|
||||
return
|
||||
|
||||
self.w = event.width
|
||||
self.h = event.height
|
||||
self.reposition_all()
|
||||
|
||||
def reposition_all(self):
|
||||
w, h = self.w, self.h
|
||||
|
||||
# 1. Název produktu + stín
|
||||
font_title_sz = max(24, int(h * 0.055))
|
||||
self.canvas.itemconfig(self.title_text_id, font=("Impact", font_title_sz, "bold"))
|
||||
self.canvas.itemconfig(self.title_shadow_id, font=("Impact", font_title_sz, "bold"))
|
||||
self.canvas.coords(self.title_text_id, w * 0.5, h * 0.09)
|
||||
self.canvas.coords(self.title_shadow_id, w * 0.5 + 2, h * 0.09 + 2)
|
||||
|
||||
# 2. Produkt
|
||||
self.canvas.coords(self.prod_image_id, w * 0.5, h * 0.48)
|
||||
self.update_product_frame()
|
||||
|
||||
# 3. Cenovka vpravo
|
||||
font_price_sz = max(22, int(h * 0.050))
|
||||
price_w, price_h = int(w * 0.20), int(h * 0.10)
|
||||
px1, py1 = int(w * 0.74), int(h * 0.46)
|
||||
px2, py2 = px1 + price_w, py1 + price_h
|
||||
|
||||
self.canvas.coords(self.price_bg_id, px1, py1, px2, py2)
|
||||
self.canvas.coords(self.price_shadow_id, px1 + price_w/2 + 2, py1 + price_h/2 + 2)
|
||||
self.canvas.coords(self.price_text_id, px1 + price_w/2, py1 + price_h/2)
|
||||
|
||||
self.canvas.itemconfig(self.price_shadow_id, font=("Segoe UI", font_price_sz, "bold"))
|
||||
self.canvas.itemconfig(self.price_text_id, font=("Segoe UI", font_price_sz, "bold"))
|
||||
|
||||
# 4. Hodiny & Datum
|
||||
font_time_sz = max(18, int(h * 0.032))
|
||||
font_date_sz = max(11, int(h * 0.016))
|
||||
self.canvas.itemconfig(self.clock_time_id, font=("Segoe UI", font_time_sz, "bold"))
|
||||
self.canvas.itemconfig(self.clock_date_id, font=("Segoe UI", font_date_sz, "bold"))
|
||||
self.canvas.coords(self.clock_time_id, w * 0.97, h * 0.04)
|
||||
self.canvas.coords(self.clock_date_id, w * 0.97, h * 0.08)
|
||||
|
||||
# 5. Indikátor pauzy
|
||||
self.canvas.coords(self.pause_indicator_id, w * 0.03, h * 0.04)
|
||||
|
||||
# 6. Spotify Karta vlevo dole
|
||||
self.card_w = max(320, int(w * 0.23))
|
||||
self.card_h = max(135, int(h * 0.16))
|
||||
self.card_x1 = int(w * 0.03)
|
||||
self.card_y1 = int(h - self.card_h - (h * 0.03))
|
||||
|
||||
self.canvas.coords(self.card_bg_id, self.card_x1, self.card_y1)
|
||||
self.update_card_background(self.current_card_rgba)
|
||||
|
||||
cover_size = int(self.card_h * 0.58)
|
||||
cover_cx = self.card_x1 + int(self.card_h * 0.42)
|
||||
cover_cy = self.card_y1 + int(self.card_h * 0.50)
|
||||
self.canvas.coords(self.cover_img_id, cover_cx, cover_cy)
|
||||
|
||||
if self.raw_cover_img:
|
||||
resized_cover = self.raw_cover_img.resize((cover_size, cover_size), Image.Resampling.LANCZOS)
|
||||
self.photo_cover = ImageTk.PhotoImage(resized_cover)
|
||||
self.canvas.itemconfig(self.cover_img_id, image=self.photo_cover)
|
||||
|
||||
text_x = self.card_x1 + int(self.card_h * 0.82)
|
||||
font_sp_title = max(11, int(self.card_h * 0.11))
|
||||
font_sp_sub = max(9, int(self.card_h * 0.085))
|
||||
|
||||
self.canvas.itemconfig(self.spotify_title_id, font=("Segoe UI", font_sp_title, "bold"))
|
||||
self.canvas.itemconfig(self.spotify_subtitle_id, font=("Segoe UI", font_sp_sub, "bold"))
|
||||
|
||||
self.canvas.coords(self.spotify_title_id, text_x, self.card_y1 + int(self.card_h * 0.22))
|
||||
self.canvas.coords(self.spotify_subtitle_id, text_x, self.card_y1 + int(self.card_h * 0.40))
|
||||
|
||||
bar_x1 = text_x
|
||||
bar_x2 = self.card_x1 + self.card_w - int(self.card_h * 0.12)
|
||||
bar_y = self.card_y1 + int(self.card_h * 0.68)
|
||||
|
||||
self.canvas.coords(self.pbar_bg, bar_x1, bar_y, bar_x2, bar_y)
|
||||
self.canvas.coords(self.time_curr_id, bar_x1, bar_y + int(self.card_h * 0.18))
|
||||
self.canvas.coords(self.time_total_id, bar_x2, bar_y + int(self.card_h * 0.18))
|
||||
|
||||
if self.active_item:
|
||||
self.show_product_by_index(self.current_catalog_index - 1)
|
||||
|
||||
def update_clock(self):
|
||||
now = datetime.datetime.now()
|
||||
time_str = now.strftime("%H:%M:%S")
|
||||
|
||||
days_cz = ["Pondělí", "Úterý", "Středa", "Čtvrtek", "Pátek", "Sobota", "Neděle"]
|
||||
months_cz = ["ledna", "února", "března", "dubna", "května", "června", "července", "srpna", "září", "října", "listopadu", "prosince"]
|
||||
|
||||
day_name = days_cz[now.weekday()]
|
||||
month_name = months_cz[now.month - 1]
|
||||
date_str = f"{day_name}, {now.day}. {month_name} {now.year}"
|
||||
|
||||
self.canvas.itemconfig(self.clock_time_id, text=time_str)
|
||||
self.canvas.itemconfig(self.clock_date_id, text=date_str)
|
||||
self.root.after(1000, self.update_clock)
|
||||
|
||||
def init_particles_canvas(self):
|
||||
for p in self.particles:
|
||||
if p.p_type in ['bokeh_large', 'bokeh_medium']:
|
||||
outer_id = self.canvas.create_oval(p.x - p.radius, p.y - p.radius, p.x + p.radius, p.y + p.radius, fill=p.color_outer, outline="")
|
||||
r_in = p.radius * 0.45
|
||||
inner_id = self.canvas.create_oval(p.x - r_in, p.y - r_in, p.x + r_in, p.y + r_in, fill=p.color_inner, outline="")
|
||||
self.particle_draw_ids.append((outer_id, inner_id))
|
||||
else:
|
||||
draw_id = self.canvas.create_oval(p.x - p.radius, p.y - p.radius, p.x + p.radius, p.y + p.radius, fill=p.color_inner, outline="")
|
||||
self.particle_draw_ids.append((draw_id, None))
|
||||
|
||||
def animate_particles(self):
|
||||
for p, draw_ids in zip(self.particles, self.particle_draw_ids):
|
||||
p.update(self.w, self.h)
|
||||
outer_id, inner_id = draw_ids
|
||||
|
||||
if inner_id is not None:
|
||||
self.canvas.coords(outer_id, p.x - p.radius, p.y - p.radius, p.x + p.radius, p.y + p.radius)
|
||||
r_in = p.radius * 0.45
|
||||
self.canvas.coords(inner_id, p.x - r_in, p.y - r_in, p.x + r_in, p.y + r_in)
|
||||
else:
|
||||
self.canvas.coords(outer_id, p.x - p.radius, p.y - p.radius, p.x + p.radius, p.y + p.radius)
|
||||
self.root.after(35, self.animate_particles)
|
||||
|
||||
def toggle_pause(self):
|
||||
self.is_paused = not self.is_paused
|
||||
text = "⏸ PAUZA" if self.is_paused else ""
|
||||
self.canvas.itemconfig(self.pause_indicator_id, text=text)
|
||||
|
||||
def next_product(self):
|
||||
if self.catalog_items:
|
||||
self.show_product_by_index(self.current_catalog_index)
|
||||
self.current_catalog_index = (self.current_catalog_index + 1) % len(self.catalog_items)
|
||||
|
||||
def prev_product(self):
|
||||
if self.catalog_items:
|
||||
prev_idx = (self.current_catalog_index - 2) % len(self.catalog_items)
|
||||
self.show_product_by_index(prev_idx)
|
||||
self.current_catalog_index = (prev_idx + 1) % len(self.catalog_items)
|
||||
|
||||
def reload_catalog_action(self):
|
||||
self.load_catalog()
|
||||
if self.catalog_items:
|
||||
self.show_product_by_index(0)
|
||||
self.current_catalog_index = 1 % len(self.catalog_items)
|
||||
|
||||
def update_card_background(self, rgba_color):
|
||||
self.current_card_rgba = rgba_color
|
||||
img = Image.new("RGBA", (self.card_w, self.card_h), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
r, g, b, a = rgba_color
|
||||
border_color = (min(r + 80, 255), min(g + 80, 255), min(b + 80, 255), 200)
|
||||
|
||||
draw.rounded_rectangle([0, 0, self.card_w - 1, self.card_h - 1], radius=16, fill=rgba_color, outline=border_color, width=2)
|
||||
|
||||
self.card_bg_photo = ImageTk.PhotoImage(img)
|
||||
self.canvas.itemconfig(self.card_bg_id, image=self.card_bg_photo)
|
||||
|
||||
def load_catalog(self):
|
||||
global KATALOG_DIR
|
||||
if not os.path.exists(KATALOG_DIR):
|
||||
KATALOG_DIR = os.path.join(SCRIPT_DIR, "katalog")
|
||||
if not os.path.exists(KATALOG_DIR):
|
||||
KATALOG_DIR = os.path.join(SCRIPT_DIR, "documents", "katalog")
|
||||
|
||||
self.catalog_items = []
|
||||
if not os.path.exists(KATALOG_DIR):
|
||||
return
|
||||
|
||||
valid_extensions = ('.png', '.jpg', '.jpeg', '.webp')
|
||||
|
||||
for folder_name in sorted(os.listdir(KATALOG_DIR)):
|
||||
folder_path = os.path.join(KATALOG_DIR, folder_name)
|
||||
if os.path.isdir(folder_path):
|
||||
info_path = os.path.join(folder_path, "info.txt")
|
||||
|
||||
title = folder_name
|
||||
price = ""
|
||||
badge = ""
|
||||
|
||||
if os.path.exists(info_path):
|
||||
try:
|
||||
with open(info_path, "r", encoding="utf-8") as f:
|
||||
lines = [line.strip() for line in f.readlines() if line.strip()]
|
||||
if len(lines) >= 1:
|
||||
title = lines[0]
|
||||
if len(lines) >= 2:
|
||||
price = lines[1]
|
||||
if len(lines) >= 3:
|
||||
badge = lines[2]
|
||||
except Exception as e:
|
||||
print(f"Chyba při čtení {info_path}: {e}")
|
||||
|
||||
images = [f for f in os.listdir(folder_path) if f.lower().endswith(valid_extensions)]
|
||||
images.sort(key=natural_sort_key)
|
||||
image_paths = [os.path.join(folder_path, img) for img in images]
|
||||
|
||||
self.catalog_items.append({
|
||||
"title": title,
|
||||
"price": price,
|
||||
"badge": badge,
|
||||
"image_paths": image_paths
|
||||
})
|
||||
|
||||
def cycle_product(self):
|
||||
if not self.is_paused:
|
||||
if not self.catalog_items:
|
||||
self.load_catalog()
|
||||
|
||||
if self.catalog_items:
|
||||
self.show_product_by_index(self.current_catalog_index)
|
||||
self.current_catalog_index = (self.current_catalog_index + 1) % len(self.catalog_items)
|
||||
|
||||
self.root.after(CYCLE_INTERVAL_MS, self.cycle_product)
|
||||
|
||||
def show_product_by_index(self, index):
|
||||
if not self.catalog_items:
|
||||
return
|
||||
|
||||
idx = index % len(self.catalog_items)
|
||||
self.active_item = self.catalog_items[idx]
|
||||
|
||||
self.canvas.itemconfig(self.title_text_id, text=self.active_item["title"])
|
||||
self.canvas.itemconfig(self.title_shadow_id, text=self.active_item["title"])
|
||||
|
||||
price_val = self.active_item["price"]
|
||||
self.canvas.itemconfig(self.price_text_id, text=price_val)
|
||||
self.canvas.itemconfig(self.price_shadow_id, text=price_val)
|
||||
if price_val:
|
||||
self.canvas.itemconfig(self.price_bg_id, state="normal")
|
||||
self.canvas.itemconfig(self.price_shadow_id, state="normal")
|
||||
self.canvas.itemconfig(self.price_text_id, state="normal")
|
||||
else:
|
||||
self.canvas.itemconfig(self.price_bg_id, state="hidden")
|
||||
self.canvas.itemconfig(self.price_shadow_id, state="hidden")
|
||||
self.canvas.itemconfig(self.price_text_id, state="hidden")
|
||||
|
||||
badge_val = self.active_item["badge"]
|
||||
if badge_val:
|
||||
badge_upper = badge_val.upper()
|
||||
if any(w in badge_upper for w in ["VYPRODÁNO", "NENÍ", "OUT"]):
|
||||
bg_color, text_color = "#e63946", "#ffffff"
|
||||
elif any(w in badge_upper for w in ["AKCE", "SLEVA", "SALE", "%"]):
|
||||
bg_color, text_color = "#f77f00", "#ffffff"
|
||||
elif any(w in badge_upper for w in ["NOVINKA", "NEW", "TOP"]):
|
||||
bg_color, text_color = "#2a9d8f", "#ffffff"
|
||||
else:
|
||||
bg_color, text_color = "#7209b7", "#ffffff"
|
||||
|
||||
self.canvas.itemconfig(self.badge_text_id, text=f" {badge_val.upper()} ", fill=text_color, state="normal")
|
||||
|
||||
badge_y = self.h * 0.14
|
||||
self.canvas.coords(self.badge_text_id, self.w * 0.5, badge_y)
|
||||
|
||||
bbox = self.canvas.bbox(self.badge_text_id)
|
||||
if bbox:
|
||||
x1, y1, x2, y2 = bbox
|
||||
self.canvas.coords(self.badge_bg_id, x1 - 12, y1 - 5, x2 + 12, y2 + 5)
|
||||
self.canvas.itemconfig(self.badge_bg_id, fill=bg_color, outline="#ffffff", width=1, state="normal")
|
||||
else:
|
||||
self.canvas.itemconfig(self.badge_text_id, state="hidden")
|
||||
self.canvas.itemconfig(self.badge_bg_id, state="hidden")
|
||||
|
||||
self.current_frame_index = 0
|
||||
self.update_product_frame()
|
||||
|
||||
def animate_product_3d(self):
|
||||
if self.active_item:
|
||||
paths = self.active_item["image_paths"]
|
||||
if len(paths) > 1:
|
||||
self.current_frame_index = (self.current_frame_index + 1) % len(paths)
|
||||
self.update_product_frame()
|
||||
|
||||
self.root.after(FRAME_INTERVAL_MS, self.animate_product_3d)
|
||||
|
||||
def update_product_frame(self):
|
||||
if not self.active_item:
|
||||
return
|
||||
|
||||
paths = self.active_item["image_paths"]
|
||||
if paths:
|
||||
img_path = paths[self.current_frame_index % len(paths)]
|
||||
try:
|
||||
raw_img = Image.open(img_path).convert("RGBA")
|
||||
max_w = int(self.w * 0.42)
|
||||
max_h = int(self.h * 0.52)
|
||||
|
||||
raw_img.thumbnail((max_w, max_h), Image.Resampling.LANCZOS)
|
||||
self.current_prod_photo = ImageTk.PhotoImage(raw_img)
|
||||
self.canvas.itemconfig(self.prod_image_id, image=self.current_prod_photo)
|
||||
except Exception as e:
|
||||
print(f"Chyba při načítání snímku {img_path}: {e}")
|
||||
self.canvas.itemconfig(self.prod_image_id, image="")
|
||||
else:
|
||||
self.canvas.itemconfig(self.prod_image_id, image="")
|
||||
|
||||
def poll_data(self):
|
||||
pythoncom.CoInitialize()
|
||||
while self.running:
|
||||
try:
|
||||
res = requests.get(SERVER_URL, timeout=1)
|
||||
if res.status_code == 200:
|
||||
data = res.json()
|
||||
|
||||
artist = str(data.get("artist", "")).strip()
|
||||
title = str(data.get("title", "")).strip()
|
||||
progress = str(data.get("progressTime", "0:00/0:00"))
|
||||
img_url = data.get("coverImg", None)
|
||||
is_ad_flag = bool(data.get("isAd", False) or data.get("is_ad", False))
|
||||
|
||||
# Sloučíme VŠECHNY hodnoty z JSONu do jednoho řetězce (pro jistotu, kdyby rozšíření poslalo text v jiném poli)
|
||||
all_text_values = " ".join([str(v).lower() for v in data.values()])
|
||||
|
||||
# Kompletní a rozšířený seznam frází pro spolehlivou detekci reklam
|
||||
ad_phrases = [
|
||||
"advertisement", "reklama", "přetvoř svůj poslech",
|
||||
"pretvor svoj poslech", "tvoje uši si zaslouží",
|
||||
"spotify premium", "sponzorované", "sponzorovano",
|
||||
"spotify partner", "partner", "commercial", "spot",
|
||||
"poslech", "hudba", "atmosféra", "atmosfera",
|
||||
"1 z 2", "2 z 2", "1 z 1", "2 z 1", "zaslouží",
|
||||
"zaslouzi", "premium", "propagace", "z 2", "z 1"
|
||||
]
|
||||
|
||||
# Detekce shody buď podle frází kdekoli v datech, nebo podle speciálního vzoru Spotify + čítač (např. 2 z 2)
|
||||
matched_phrase = next((p for p in ad_phrases if p in all_text_values), None)
|
||||
is_spotify_ad_pattern = "spotify" in all_text_values and ("z 2" in all_text_values or "z 1" in all_text_values or "reklama" in all_text_values)
|
||||
|
||||
is_ad_detected = is_ad_flag or (matched_phrase is not None) or is_spotify_ad_pattern
|
||||
|
||||
# Debug výpis do konzole, abyste viděl, co přesně přišlo
|
||||
if is_ad_detected:
|
||||
print(f"[REKLAMA DETEKOVÁNA] Zastavuji zvuk. (Shoda s frází: '{matched_phrase}' / vzor Spotify | Všechna data: {all_text_values})")
|
||||
if not self.is_ad_active:
|
||||
self.is_ad_active = True
|
||||
self.trigger_ad_sound()
|
||||
else:
|
||||
if self.is_ad_active:
|
||||
print(f"[KONEC REKLAMY] Vracím hlasitost. | Všechna data: {all_text_values}")
|
||||
self.is_ad_active = False
|
||||
self.stop_ad_sound()
|
||||
|
||||
self.root.after(0, self.update_gui, artist, title, progress, img_url, self.is_ad_active)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.4)
|
||||
|
||||
def parse_time_to_seconds(self, time_str):
|
||||
try:
|
||||
parts = time_str.split(':')
|
||||
return int(parts[0]) * 60 + int(parts[1])
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def update_gui(self, artist, title, progress, img_url, is_ad=False):
|
||||
if is_ad:
|
||||
title_disp = "📢 REKLAMA"
|
||||
artist_disp = "Spotify Partner"
|
||||
else:
|
||||
title_disp = title if len(title) <= 22 else title[:20] + "..."
|
||||
artist_disp = artist if len(artist) <= 26 else artist[:24] + "..."
|
||||
|
||||
self.canvas.itemconfig(self.spotify_title_id, text=title_disp)
|
||||
self.canvas.itemconfig(self.spotify_subtitle_id, text=artist_disp)
|
||||
|
||||
if "/" in progress:
|
||||
curr, dur = progress.split("/")
|
||||
self.canvas.itemconfig(self.time_curr_id, text=curr)
|
||||
self.canvas.itemconfig(self.time_total_id, text=dur)
|
||||
|
||||
c_sec = self.parse_time_to_seconds(curr)
|
||||
d_sec = self.parse_time_to_seconds(dur)
|
||||
percent = (c_sec / d_sec) if d_sec > 0 else 0
|
||||
|
||||
text_x = self.card_x1 + int(self.card_h * 0.82)
|
||||
bar_x1 = text_x
|
||||
bar_x2 = self.card_x1 + self.card_w - int(self.card_h * 0.12)
|
||||
bar_y = self.card_y1 + int(self.card_h * 0.68)
|
||||
|
||||
max_w = bar_x2 - bar_x1
|
||||
fill_x = bar_x1 + int(max_w * percent)
|
||||
self.canvas.coords(self.pbar_fg, bar_x1, bar_y, fill_x, bar_y)
|
||||
|
||||
if img_url and img_url != self.current_img_url:
|
||||
self.current_img_url = img_url
|
||||
threading.Thread(target=self.download_cover, args=(img_url,), daemon=True).start()
|
||||
|
||||
def download_cover(self, url):
|
||||
try:
|
||||
res = requests.get(url, timeout=3)
|
||||
if res.status_code == 200:
|
||||
self.raw_cover_img = Image.open(BytesIO(res.content)).convert("RGBA")
|
||||
|
||||
small_img = self.raw_cover_img.resize((1, 1), Image.Resampling.BILINEAR)
|
||||
dom_color = small_img.getpixel((0, 0))
|
||||
|
||||
dark_r = int(dom_color[0] * 0.35)
|
||||
dark_g = int(dom_color[1] * 0.35)
|
||||
dark_b = int(dom_color[2] * 0.35)
|
||||
card_rgba = (dark_r, dark_g, dark_b, 225)
|
||||
|
||||
cover_size = int(self.card_h * 0.58)
|
||||
cover_img = self.raw_cover_img.resize((cover_size, cover_size), Image.Resampling.LANCZOS)
|
||||
self.photo_cover = ImageTk.PhotoImage(cover_img)
|
||||
|
||||
self.root.after(0, lambda: [
|
||||
self.canvas.itemconfig(self.cover_img_id, image=self.photo_cover),
|
||||
self.update_card_background(card_rgba)
|
||||
])
|
||||
except Exception as e:
|
||||
print(f"Chyba při načítání obalu alba: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = tk.Tk()
|
||||
app = FullscreenJarmarkDisplay(root)
|
||||
root.mainloop()
|
||||
Reference in New Issue
Block a user