#!/usr/bin/python3 # main.py — FULLY WORKING VERSION FOR REMOTE SERVER import os import random import threading import json from urllib.request import urlopen from pathlib import Path from kivy.lang import Builder from kivy.clock import Clock from kivy.uix.screenmanager import Screen from kivymd.app import MDApp from kivymd.uix.list import OneLineAvatarIconListItem from kivymd.uix.button import MDFlatButton import pychromecast # ---------------------------------------------------------------------- # CONFIG — CHANGE THESE # ---------------------------------------------------------------------- SERVER_URL = "http://192.168.0.2:8000" # Your Ubuntu server CAST_NAME_SUBSTRING = "Greg" # Or set via env: CAST_NAME # ---------------------------------------------------------------------- KV = ''' #:import hex kivy.utils.get_color_from_hex : IconLeftWidget: icon: "movie" on_release: app.play_video(self.video_name) Screen: MDBoxLayout: orientation: "vertical" spacing: "12dp" padding: "24dp" MDTopAppBar: title: "Cast Jukebox" elevation: 4 right_action_items: [["refresh", lambda x: app.refresh_list()], ["shuffle", lambda x: app.toggle_shuffle()]] MDLabel: id: status text: "Connecting..." halign: "center" theme_text_color: "Primary" font_style: "H6" MDBoxLayout: orientation: "horizontal" size_hint_y: None height: "56dp" spacing: "12dp" pos_hint: {"center_x": .5} MDRectangleFlatButton: text: "Play/Pause" on_release: app.toggle_playback() MDRectangleFlatButton: text: "Next" on_release: app.next_video() MDRectangleFlatButton: text: "Prev" on_release: app.prev_video() ScrollView: MDList: id: playlist ''' class VideoItem(OneLineAvatarIconListItem): pass class CastApp(MDApp): def build(self): self.theme_cls.theme_style = "Dark" self.theme_cls.primary_palette = "DeepPurple" return Builder.load_string(KV) def on_start(self): threading.Thread(target=self._discover_cast, daemon=True).start() Clock.schedule_once(self.fetch_video_list, 1) # ------------------------------------------------------------------ def _discover_cast(self): casts, _ = pychromecast.get_chromecasts() target = os.getenv("CAST_NAME", CAST_NAME_SUBSTRING) for c in casts: if target.lower() in c.cast_info.friendly_name.lower(): c.wait() self.cast = c self.mc = c.media_controller self.mc.register_status_listener(self.on_cast_status) Clock.schedule_once(lambda dt: self.set_status(f"Connected: {c.cast_info.friendly_name}")) return Clock.schedule_once(lambda dt: self.set_status("Cast not found")) # ------------------------------------------------------------------ def fetch_video_list(self, *_): self.set_status("Fetching video list...") try: with urlopen(f"{SERVER_URL}/") as resp: html = resp.read().decode() # Parse links import re matches = re.findall(r'href="([^"]*\.(mp4|mkv|webm|avi))"', html, re.I) self.videos = sorted(set(matches)) self.index = -1 self.shuffle = True self.rebuild_playlist() if self.videos: self.next_video() else: self.set_status("No videos found on server") except Exception as e: self.set_status(f"Error: {e}") # ------------------------------------------------------------------ def rebuild_playlist(self): pl = self.root.ids.playlist pl.clear_widgets() for name in self.videos: item = VideoItem(text=name, video_name=name) pl.add_widget(item) # ------------------------------------------------------------------ def set_status(self, txt): self.root.ids.status.text = txt # ------------------------------------------------------------------ def play_video(self, video_name): idx = self.videos.index(video_name) self.index = idx self._play_current() # ------------------------------------------------------------------ def _play_current(self): if not (0 <= self.index < len(self.videos)) or not self.mc: return video_name = self.videos[self.index] url = f"{SERVER_URL}/{video_name}" self.set_status(f"Playing: {video_name}") self.mc.play_media(url, "video/mp4") self.highlight_current() # ------------------------------------------------------------------ def highlight_current(self): for child in self.root.ids.playlist.children: if getattr(child, "video_name", "") == self.videos[self.index]: child.theme_text_color = "Custom" child.text_color = self.theme_cls.accent_color else: child.theme_text_color = "Primary" # ------------------------------------------------------------------ def toggle_playback(self): if not self.mc: return if self.mc.status.player_state == "PLAYING": self.mc.pause() else: self.mc.play() # ------------------------------------------------------------------ def next_video(self): if not self.videos: return self.index = (self.index + 1) % len(self.videos) self._play_current() def prev_video(self): if not self.videos: return self.index = (self.index - 1) % len(self.videos) self._play_current() # ------------------------------------------------------------------ def toggle_shuffle(self): self.shuffle = not self.shuffle if self.shuffle: random.shuffle(self.videos) else: self.videos.sort() self.rebuild_playlist() self.set_status("Shuffle " + ("ON" if self.shuffle else "OFF")) # ------------------------------------------------------------------ def refresh_list(self): self.fetch_video_list() # ------------------------------------------------------------------ def on_cast_status(self, status): if status.player_state in ("IDLE", "UNKNOWN") and self.videos: Clock.schedule_once(lambda dt: self.next_video(), 2) CastApp().run()