diff --git a/spotify_widget.py b/spotify_widget.py index 8beb308..ef104d0 100644 --- a/spotify_widget.py +++ b/spotify_widget.py @@ -1,243 +1,714 @@ -import tkinter as tk -import requests -import threading -import time -import os -import random -from io import BytesIO -from PIL import Image, ImageTk - -SERVER_URL = "http://localhost:8080/endpoint" -CYCLE_INTERVAL_MS = 10000 # Změna produktu každých 10 sekund - -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") - -class FullscreenJarmarkDisplay: - def __init__(self, root): - self.root = root - self.root.title("RP + 1602 Display Jarmark") - - self.root.attributes("-fullscreen", True) - self.root.bind("", lambda e: self.root.destroy()) - - self.canvas = tk.Canvas(root, width=1920, height=1080, bg="#0d031b", highlightthickness=0) - self.canvas.pack(fill=tk.BOTH, expand=True) - - self.load_background() - - # Název produktu nahoře - self.title_text_id = self.canvas.create_text(960, 120, text="", fill="white", font=("Impact", 75, "bold")) - - # Obrázek produktu uprostřed - self.prod_image_id = self.canvas.create_image(920, 510, anchor="center") - - # --- DESIGN CENOVKY (Vpravo) --- - # 1. Podkladový badge/karta pro cenu - self.price_bg_id = self.canvas.create_rectangle(1380, 450, 1840, 570, fill="#150826", outline="#9d4edd", width=3) - - # 2. Stín ceny (3D efekt) - self.price_shadow_id = self.canvas.create_text(1613, 513, text="", fill="#000000", font=("Trebuchet MS", 62, "bold"), anchor="center") - - # 3. Hlavní text ceny (Zářivě zlatá) - self.price_text_id = self.canvas.create_text(1610, 510, text="", fill="#FFD700", font=("Trebuchet MS", 62, "bold"), anchor="center") - - - # Spodní lišta přehrávače - self.canvas.create_rectangle(0, 910, 1920, 1080, fill="#000000", outline="") - - self.cover_img_id = self.canvas.create_image(80, 995, anchor="center") - - self.spotify_title_id = self.canvas.create_text(160, 955, text="Načítám...", fill="white", font=("Arial", 22, "bold"), anchor="w") - self.spotify_subtitle_id = self.canvas.create_text(160, 990, text="Hudobné video • Čekám na Spotify", fill="#bbbbbb", font=("Arial", 16), anchor="w") - - self.time_curr_id = self.canvas.create_text(160, 1040, text="0:00", fill="#aaaaaa", font=("Arial", 14), anchor="w") - self.time_total_id = self.canvas.create_text(1860, 1040, text="0:00", fill="#aaaaaa", font=("Arial", 14), anchor="e") - - self.pbar_bg = self.canvas.create_line(220, 1040, 1800, 1040, fill="#444444", width=5) - self.pbar_fg = self.canvas.create_line(220, 1040, 220, 1040, fill="#ffffff", width=5) - - self.running = True - self.current_img_url = None - self.photo_cover = None - self.current_prod_photo = None - - self.catalog_items = [] - self.current_catalog_index = 0 - - self.load_catalog() - self.cycle_product() - - threading.Thread(target=self.poll_data, daemon=True).start() - - def load_background(self): - bg_candidates = [ - "backgroung.png", - "background.png", - "background.jpg", - "backgroung.jpg" - ] - - bg_path = None - for filename in bg_candidates: - full_path = os.path.join(SCRIPT_DIR, filename) - if os.path.exists(full_path): - bg_path = full_path - break - - if bg_path: - try: - bg_img = Image.open(bg_path).convert("RGBA").resize((1920, 1080), Image.Resampling.LANCZOS) - self.bg_photo = ImageTk.PhotoImage(bg_img) - self.canvas.create_image(0, 0, image=self.bg_photo, anchor="nw") - except Exception as e: - print(f"Chyba při načítání pozadí {bg_path}: {e}") - else: - colors = ["#2b0845", "#410b66", "#581287", "#731aa8", "#8b24ca"] - random.seed(42) - for _ in range(40): - x = random.randint(0, 1920) - y = random.randint(0, 1080) - r = random.randint(40, 150) - c = random.choice(colors) - self.canvas.create_oval(x-r, y-r, x+r, y+r, fill=c, outline="") - - 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): - print(f"Adresář 'katalog' neexistuje na cestě: {KATALOG_DIR}") - return - - 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") - image_path = os.path.join(folder_path, "image.png") - - title = folder_name - price = "" - - 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] - except Exception as e: - print(f"Chyba při čtení {info_path}: {e}") - - self.catalog_items.append({ - "title": title, - "price": price, - "image_path": image_path if os.path.exists(image_path) else None - }) - - def cycle_product(self): - if self.current_catalog_index == 0: - self.load_catalog() - - if self.catalog_items: - self.current_catalog_index %= len(self.catalog_items) - item = self.catalog_items[self.current_catalog_index] - - self.canvas.itemconfig(self.title_text_id, text=item["title"]) - - # Nastavení ceny i stínu ceny - price_val = item["price"] - self.canvas.itemconfig(self.price_text_id, text=price_val) - self.canvas.itemconfig(self.price_shadow_id, text=price_val) - - # Pokud cenovka nemá text, schováme podkladový obdélník - if price_val: - self.canvas.itemconfig(self.price_bg_id, state="normal") - else: - self.canvas.itemconfig(self.price_bg_id, state="hidden") - - if item["image_path"]: - try: - raw_img = Image.open(item["image_path"]).convert("RGBA") - raw_img.thumbnail((700, 460), 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í obrázku {item['image_path']}: {e}") - self.canvas.itemconfig(self.prod_image_id, image="") - else: - self.canvas.itemconfig(self.prod_image_id, image="") - - self.current_catalog_index = (self.current_catalog_index + 1) % len(self.catalog_items) - - self.root.after(CYCLE_INTERVAL_MS, self.cycle_product) - - def poll_data(self): - while self.running: - try: - res = requests.get(SERVER_URL, timeout=1) - if res.status_code == 200: - data = res.json() - artist = data.get("artist", "Neznámý interpret") - title = data.get("title", "Čekám na skladbu") - progress = data.get("progressTime", "0:00/0:00") - img_url = data.get("coverImg", None) - - self.root.after(0, self.update_gui, artist, title, progress, img_url) - except Exception: - pass - time.sleep(0.8) - - 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): - self.canvas.itemconfig(self.spotify_title_id, text=title) - self.canvas.itemconfig(self.spotify_subtitle_id, text=f"Hudobné video • {artist}") - - 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 - - start_x = 220 - max_w = 1800 - start_x - fill_x = start_x + int(max_w * percent) - self.canvas.coords(self.pbar_fg, start_x, 1040, fill_x, 1040) - - 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: - raw_img = Image.open(BytesIO(res.content)).convert("RGBA") - raw_img = raw_img.resize((100, 100), Image.Resampling.LANCZOS) - - self.photo_cover = ImageTk.PhotoImage(raw_img) - self.root.after(0, lambda: self.canvas.itemconfig(self.cover_img_id, image=self.photo_cover)) - except Exception: - pass - -if __name__ == "__main__": - root = tk.Tk() - app = FullscreenJarmarkDisplay(root) +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("", lambda e: self.root.destroy()) + self.root.bind("", lambda e: self.next_product()) + self.root.bind("", lambda e: self.prev_product()) + self.root.bind("", lambda e: self.toggle_pause()) + self.root.bind("", lambda e: self.reload_catalog_action()) + self.root.bind("", 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("", 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() \ No newline at end of file