Files
2026-07-22 14:32:22 +02:00

786 lines
31 KiB
Python

#!/usr/bin/env python3
import socket
import time
import tkinter as tk
from tkinter import ttk, scrolledtext
import threading
import subprocess
import re
import tempfile
import os
import sys
ANDROID_HOST = os.environ.get("ANDROID_HOST_OVERRIDE", "192.168.0.190")
ANDROID_PORT = 8023
WINDOWS_IP = os.environ.get("WINDOWS_IP_OVERRIDE", "192.168.0.122")
AUDIO_STREAM_PORT = 12345
COL_BG = "#fff5f7"
COL_PANEL = "#ffe4ed"
COL_PANEL_LIGHT = "#ffd9e4"
COL_ACCENT = "#ff69b4"
COL_ACCENT_DARK = "#ff1493"
COL_TEXT = "#4a2c3a"
COL_SUBTEXT = "#8b5e72"
COL_SUCCESS = "#2ecc71"
COL_WARNING = "#f39c12"
COL_ERROR = "#e74c3c"
COL_INFO = "#3498db"
class AudioStreamer:
def __init__(self):
self.process = None
self.sock = None
self.conn = None
self.log_callback = None
self.total_bytes = 0
self.stop_flag = False
self.stream_id = None
self.lock = threading.Lock()
def set_log_callback(self, callback):
self.log_callback = callback
def start_receiver(self, stream_id):
"""Receive entire file into memory, save to temp file, then play with ffplay."""
self.stream_id = stream_id
temp_path = None
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.settimeout(1.0)
self.sock.bind(('0.0.0.0', AUDIO_STREAM_PORT))
self.sock.listen(1)
self.conn = None
while not self.stop_flag and self.conn is None:
try:
self.conn, addr = self.sock.accept()
print(f"[Audio {stream_id}] Connected from {addr}")
except socket.timeout:
continue
except Exception as e:
if not self.stop_flag:
print(f"[Audio {stream_id}] Accept error: {e}")
break
if self.stop_flag or self.conn is None:
print(f"[Audio {stream_id}] Stopped before connection")
return None
# Receive all data into memory
print(f"[Audio {stream_id}] Receiving file...")
file_data = bytearray()
bytes_received = 0
last_log = time.time()
while not self.stop_flag:
try:
data = self.conn.recv(65536)
if not data:
print(f"[Audio {stream_id}] Connection closed by sender")
break
file_data.extend(data)
bytes_received += len(data)
self.total_bytes = bytes_received
if time.time() - last_log >= 1:
mb = bytes_received / (1024 * 1024)
if self.log_callback:
self.log_callback(mb, stream_id)
last_log = time.time()
except socket.timeout:
continue
except Exception as e:
if not self.stop_flag:
print(f"[Audio {stream_id}] Receive error: {e}")
break
print(f"[Audio {stream_id}] Received {bytes_received/(1024*1024):.2f} MB")
if self.stop_flag or bytes_received == 0:
print(f"[Audio {stream_id}] Stopped or no data")
return None
# Save to temp file
suffix = ".mp3"
fd, temp_path = tempfile.mkstemp(suffix=suffix)
try:
os.write(fd, file_data)
finally:
os.close(fd)
print(f"[Audio {stream_id}] Saved to {temp_path}")
# Play the file
self.process = subprocess.Popen(
['ffplay', '-nodisp', '-autoexit', '-loglevel', 'quiet', temp_path],
stdin=subprocess.DEVNULL
)
# Wait for ffplay to finish
while not self.stop_flag and self.process.poll() is None:
time.sleep(0.5)
print(f"[Audio {stream_id}] Playback finished")
return temp_path
except Exception as e:
print(f"AudioStreamer error: {e}")
return None
finally:
self.cleanup()
if temp_path and os.path.exists(temp_path):
try:
os.remove(temp_path)
print(f"[Audio {stream_id}] Removed temp file")
except:
pass
def cleanup(self):
with self.lock:
if self.process:
try:
self.process.terminate()
self.process.wait(timeout=2)
except:
pass
self.process = None
if self.conn:
try:
self.conn.close()
except:
pass
self.conn = None
if self.sock:
try:
self.sock.close()
except:
pass
self.sock = None
def stop(self):
print(f"[Audio {self.stream_id}] Stop requested")
self.stop_flag = True
if self.conn:
try:
self.conn.shutdown(socket.SHUT_RDWR)
except:
pass
class PowerampApp:
def __init__(self, root):
self.root = root
self.root.title("Poweramp Streamer")
self.root.geometry("900x750")
self.root.configure(bg=COL_BG)
self.current_song_path = None
self.current_folder = None
self.playlist = []
self.playlist_index = -1
self.is_playing = False
self.auto_advance = tk.BooleanVar(value=True)
self.streamer = None
self.current_mb = 0
self.stream_generation = 0
self.active_stream_thread = None
self.playing_lock = threading.Lock()
self.android_ip_var = tk.StringVar(value=ANDROID_HOST)
self.windows_ip_var = tk.StringVar(value=WINDOWS_IP)
self.ticker_enabled = tk.BooleanVar(value=False)
self.ticker_window = None
self.ticker_after_id = None
self.ticker_title_label = None
self.ticker_song_label = None
self.setup_styles()
self.setup_ui()
self.setup_bindings()
self.root.after(500, self.get_song_and_folder)
def setup_styles(self):
style = ttk.Style()
try:
style.theme_use("clam")
except tk.TclError:
pass
default_font = ("Segoe UI", 10)
bold_font = ("Segoe UI", 10, "bold")
style.configure(".", background=COL_BG, foreground=COL_TEXT, font=default_font)
style.configure("TFrame", background=COL_BG)
style.configure("TLabel", background=COL_BG, foreground=COL_TEXT, font=default_font)
style.configure("TLabelframe", background=COL_PANEL, foreground=COL_TEXT, borderwidth=1, relief="flat")
style.configure("TLabelframe.Label", background=COL_PANEL, foreground=COL_ACCENT_DARK, font=bold_font)
style.configure("Pill.TButton", background=COL_ACCENT, foreground="#ffffff", font=("Segoe UI", 10, "bold"),
borderwidth=0, focusthickness=0, focuscolor=COL_ACCENT, padding=(14, 8), relief="flat")
style.map("Pill.TButton", background=[("active", COL_ACCENT_DARK), ("pressed", COL_ACCENT_DARK)])
style.configure("Accent.TButton", background=COL_ACCENT_DARK, foreground="#ffffff", font=("Segoe UI", 10, "bold"),
borderwidth=0, focusthickness=0, focuscolor=COL_ACCENT_DARK, padding=(16, 8), relief="flat")
style.map("Accent.TButton", background=[("active", "#db0f8b"), ("pressed", "#db0f8b")])
style.configure("Danger.TButton", background="#d44c7a", foreground="#ffffff", font=("Segoe UI", 10, "bold"),
borderwidth=0, focusthickness=0, focuscolor="#d44c7a", padding=(14, 8), relief="flat")
style.map("Danger.TButton", background=[("active", "#c41e6a"), ("pressed", "#c41e6a")])
style.configure("Nav.TButton", background=COL_ACCENT, foreground="#ffffff", font=("Segoe UI", 12, "bold"),
borderwidth=0, focusthickness=0, focuscolor=COL_ACCENT, padding=(10, 6), relief="flat")
style.map("Nav.TButton", background=[("active", COL_ACCENT_DARK), ("pressed", COL_ACCENT_DARK)])
style.configure("TCheckbutton", background=COL_BG, foreground=COL_SUBTEXT, font=default_font)
style.map("TCheckbutton", foreground=[("active", COL_TEXT)])
style.configure("Accent.Horizontal.TProgressbar", troughcolor=COL_PANEL_LIGHT, background=COL_ACCENT,
borderwidth=0, lightcolor=COL_ACCENT, darkcolor=COL_ACCENT, thickness=14)
def setup_ui(self):
main_frame = ttk.Frame(self.root, padding="12")
main_frame.pack(fill=tk.BOTH, expand=True)
# --- Address override panel (Ctrl+1 / Ctrl+2 to focus) ---
addr_frame = ttk.LabelFrame(main_frame, text="Connection Addresses", padding="10")
addr_frame.pack(fill=tk.X, pady=(0, 8))
ttk.Label(addr_frame, text="Android IP (Ctrl+1):").grid(row=0, column=0, sticky=tk.W, padx=(0, 6), pady=4)
self.android_ip_entry = ttk.Entry(addr_frame, textvariable=self.android_ip_var, width=18)
self.android_ip_entry.grid(row=0, column=1, sticky=tk.W, padx=(0, 16), pady=4)
ttk.Label(addr_frame, text="Windows IP (Ctrl+2):").grid(row=0, column=2, sticky=tk.W, padx=(0, 6), pady=4)
self.windows_ip_entry = ttk.Entry(addr_frame, textvariable=self.windows_ip_var, width=18)
self.windows_ip_entry.grid(row=0, column=3, sticky=tk.W, padx=(0, 16), pady=4)
ttk.Button(addr_frame, text="Restart script with new addresses", style="Pill.TButton",
command=self.restart_with_new_addresses).grid(row=0, column=4, sticky=tk.W, pady=4)
self.status_label = ttk.Label(main_frame, text="Status: Auto-loading...", foreground=COL_WARNING,
font=("Segoe UI", 11, "bold"))
self.status_label.pack(anchor=tk.W, pady=(0, 8))
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(pady=5, fill=tk.X)
ttk.Button(btn_frame, text="Get Current Song & Folder", style="Pill.TButton",
command=self.get_song_and_folder).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_frame, text="▶ Play", style="Accent.TButton", command=self.play_song).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_frame, text="■ Stop", style="Danger.TButton", command=self.stop_song).pack(side=tk.LEFT, padx=4)
ttk.Button(btn_frame, text="", width=3, style="Nav.TButton", command=self.prev_song).pack(side=tk.LEFT, padx=(16, 2))
ttk.Button(btn_frame, text="", width=3, style="Nav.TButton", command=self.next_song).pack(side=tk.LEFT, padx=2)
ttk.Checkbutton(btn_frame, text="Auto-advance", variable=self.auto_advance).pack(side=tk.LEFT, padx=16)
ttk.Checkbutton(btn_frame, text="Ticker", variable=self.ticker_enabled).pack(side=tk.LEFT, padx=8)
progress_frame = ttk.LabelFrame(main_frame, text="Stream Progress", padding="10")
progress_frame.pack(fill=tk.X, pady=8)
self.progress_bar = ttk.Progressbar(progress_frame, mode='determinate', style="Accent.Horizontal.TProgressbar")
self.progress_bar.pack(fill=tk.X, pady=4)
self.progress_label = ttk.Label(progress_frame, text="Waiting...", foreground=COL_SUBTEXT)
self.progress_label.pack(pady=2)
playlist_frame = ttk.LabelFrame(main_frame, text="Playlist (Double-click to play)", padding="10")
playlist_frame.pack(fill=tk.BOTH, expand=True, pady=10)
list_frame = ttk.Frame(playlist_frame)
list_frame.pack(fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.playlist_box = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, font=("Consolas", 9),
bg=COL_PANEL_LIGHT, fg=COL_TEXT, selectbackground=COL_ACCENT,
selectforeground="#ffffff", borderwidth=0, highlightthickness=0, activestyle="none")
self.playlist_box.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.playlist_box.yview)
self.playlist_box.bind('<Double-Button-1>', self.on_playlist_double_click)
song_frame = ttk.LabelFrame(main_frame, text="Current Song", padding="10")
song_frame.pack(fill=tk.X, pady=5)
self.song_text = tk.Text(song_frame, height=2, wrap=tk.WORD, bg=COL_PANEL_LIGHT, fg=COL_TEXT,
insertbackground=COL_TEXT, borderwidth=0, highlightthickness=0, font=("Consolas", 10))
self.song_text.pack(fill=tk.X)
console_frame = ttk.LabelFrame(main_frame, text="Console", padding="10")
console_frame.pack(fill=tk.BOTH, expand=True)
self.console = scrolledtext.ScrolledText(console_frame, height=8, font=("Consolas", 9),
bg=COL_PANEL_LIGHT, fg=COL_SUBTEXT, insertbackground=COL_TEXT,
borderwidth=0, highlightthickness=0)
self.console.pack(fill=tk.BOTH, expand=True)
def setup_bindings(self):
# Ctrl+1 / Ctrl+2 focus the address textboxes
self.root.bind_all("<Control-Key-1>", lambda e: self.android_ip_entry.focus_set())
self.root.bind_all("<Control-Key-2>", lambda e: self.windows_ip_entry.focus_set())
# Media keys: play/pause toggles play/stop, next/previous move in playlist
media_bindings = {
"<XF86AudioPlay>": lambda e: self.toggle_play_pause(),
"<XF86AudioPause>": lambda e: self.toggle_play_pause(),
"<XF86AudioNext>": lambda e: self.next_song(),
"<XF86AudioPrev>": lambda e: self.prev_song(),
"<MediaPlayPause>": lambda e: self.toggle_play_pause(),
"<MediaNextTrack>": lambda e: self.next_song(),
"<MediaPreviousTrack>": lambda e: self.prev_song(),
}
for keysym, handler in media_bindings.items():
try:
self.root.bind_all(keysym, handler)
except tk.TclError:
pass
def restart_with_new_addresses(self):
android_ip = self.android_ip_var.get().strip()
windows_ip = self.windows_ip_var.get().strip()
if not android_ip or not windows_ip:
self.log("Please enter both IP addresses before restarting.")
return
self.log(f"Restarting with Android IP={android_ip}, Windows IP={windows_ip}...")
try:
if self.streamer:
self.streamer.stop()
except Exception:
pass
env = os.environ.copy()
env["ANDROID_HOST_OVERRIDE"] = android_ip
env["WINDOWS_IP_OVERRIDE"] = windows_ip
python = sys.executable
args = [python] + sys.argv
os.execve(python, args, env)
def toggle_play_pause(self):
if self.is_playing:
self.stop_song()
else:
self.play_song()
def show_ticker(self, song_name):
if not self.ticker_enabled.get():
return
if self.ticker_after_id:
try:
self.root.after_cancel(self.ticker_after_id)
except Exception:
pass
self.ticker_after_id = None
if self.ticker_window is None or not self.ticker_window.winfo_exists():
self.ticker_window = tk.Toplevel(self.root)
self.ticker_window.overrideredirect(True)
try:
self.ticker_window.attributes("-topmost", True)
except Exception:
pass
try:
self.ticker_window.attributes("-alpha", 0.95)
except Exception:
pass
self.ticker_window.configure(bg=COL_ACCENT_DARK)
frame = tk.Frame(self.ticker_window, bg=COL_ACCENT_DARK, padx=12, pady=8)
frame.pack()
self.ticker_title_label = tk.Label(frame, text="Now Playing", bg=COL_ACCENT_DARK, fg="#ffffff",
font=("Segoe UI", 8, "bold"))
self.ticker_title_label.pack(anchor=tk.W)
self.ticker_song_label = tk.Label(frame, text="", bg=COL_ACCENT_DARK, fg="#ffffff",
font=("Segoe UI", 10, "bold"), wraplength=280, justify=tk.LEFT)
self.ticker_song_label.pack(anchor=tk.W)
else:
self.ticker_window.deiconify()
self.ticker_song_label.config(text=song_name)
self.ticker_window.update_idletasks()
self._position_ticker()
self.ticker_window.lift()
self.ticker_after_id = self.root.after(4000, self._hide_ticker)
def _position_ticker(self):
if not self.ticker_window or not self.ticker_window.winfo_exists():
return
self.ticker_window.update_idletasks()
w = self.ticker_window.winfo_width()
h = self.ticker_window.winfo_height()
screen_w = self.ticker_window.winfo_screenwidth()
screen_h = self.ticker_window.winfo_screenheight()
x = screen_w - w - 20
y = screen_h - h - 60
self.ticker_window.geometry(f"+{x}+{y}")
def _hide_ticker(self):
self.ticker_after_id = None
if self.ticker_window and self.ticker_window.winfo_exists():
self.ticker_window.withdraw()
def log(self, msg):
def _do_log():
self.console.insert(tk.END, f"[{time.strftime('%H:%M:%S')}] {msg}\n")
self.console.see(tk.END)
try:
if threading.current_thread() is threading.main_thread():
_do_log()
else:
self.root.after(0, _do_log)
except Exception:
pass
def update_progress(self, mb, stream_id):
if stream_id != self.stream_generation:
return
self.current_mb = mb
self.progress_label.config(text=f"Receiving: {mb:.2f} MB")
percent = min(int((mb / 5) * 100), 100)
self.progress_bar['value'] = percent
def clean_filename(self, filename):
if not filename:
return ""
filename = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', filename)
filename = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', filename)
filename = filename.strip()
return filename
def send_telnet_command(self, cmd):
sock = None
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
sock.connect((ANDROID_HOST, ANDROID_PORT))
time.sleep(0.5)
try:
sock.recv(4096)
except:
pass
sock.send(f"{cmd}\n".encode())
time.sleep(1)
response = b''
sock.settimeout(2)
try:
while True:
data = sock.recv(4096)
if not data:
break
response += data
if b'~ $' in data or b'#' in data:
break
except socket.timeout:
pass
return response.decode('utf-8', errors='ignore')
except Exception as e:
self.log(f"Telnet error: {e}")
return ""
finally:
if sock:
try:
sock.close()
except:
pass
def _get_poweramp_pid(self):
response = self.send_telnet_command('su -c "pidof com.maxmpz.audioplayer"')
cleaned = self.clean_filename(response)
match = re.search(r'\d+', cleaned)
if match:
return match.group(0)
return None
def get_song_and_folder(self):
self.status_label.config(text="Status: Getting song and folder...", foreground=COL_WARNING)
threading.Thread(target=self._get_song_and_folder, daemon=True).start()
def _get_song_and_folder(self):
try:
self.log("Getting Poweramp PID...")
pid = self._get_poweramp_pid()
if not pid:
self.log("Could not find Poweramp PID")
self.status_label.config(text="Status: No PID", foreground=COL_ERROR)
return
self.log(f"Poweramp PID: {pid}")
self.log("Getting current song path...")
response = self.send_telnet_command(
f'su -c "ls -la /proc/{pid}/fd/ 2>/dev/null | grep /mobile/ | sed \'s/.*-> //\' | tail -1"'
)
response = self.clean_filename(response)
start = response.find('/storage/')
if start != -1:
end = response.find('.mp3', start)
if end != -1:
self.current_song_path = response[start:end + 4]
self.current_folder = '/'.join(self.current_song_path.split('/')[:-1]) + '/'
filename = self.current_song_path.split('/')[-1]
self.log(f"Found: {filename}")
self.log(f"Folder: {self.current_folder}")
self.log("Listing folder contents...")
ls_cmd = f'ls -1 "{self.current_folder}" | grep -E "\\.(mp3|flac|wav|m4a|ogg|opus)"'
ls_response = self.send_telnet_command(ls_cmd)
self.playlist = []
for line in ls_response.split('\n'):
line = self.clean_filename(line)
if line and re.search(r'\.(mp3|flac|wav|m4a|ogg|opus)$', line, re.IGNORECASE):
self.playlist.append(line)
self.playlist = sorted(self.playlist)
clean_filename = self.clean_filename(filename)
for i, song in enumerate(self.playlist):
if song == clean_filename:
self.playlist_index = i
break
self.playlist_box.delete(0, tk.END)
for song in self.playlist:
self.playlist_box.insert(tk.END, song)
if self.playlist_index >= 0:
self.playlist_box.selection_set(self.playlist_index)
self.playlist_box.see(self.playlist_index)
self.log(f"Found {len(self.playlist)} songs in folder")
self.song_text.delete(1.0, tk.END)
self.song_text.insert(1.0, self.current_song_path)
self.status_label.config(text="Status: Ready", foreground=COL_SUCCESS)
return
self.log("No song found")
self.status_label.config(text="Status: No song", foreground=COL_ERROR)
except Exception as e:
self.log(f"ERROR: {e}")
self.status_label.config(text="Status: Error", foreground=COL_ERROR)
def _wait_for_receiver_stop(self, timeout=5):
if self.active_stream_thread and self.active_stream_thread.is_alive():
self.active_stream_thread.join(timeout=timeout)
def _do_stop_current_stream(self):
"""Stop current stream. Must be called WITH playing_lock held."""
if self.streamer:
old = self.streamer
self.streamer = None
old.stop()
self.playing_lock.release()
try:
self._wait_for_receiver_stop(timeout=3)
time.sleep(0.5)
finally:
self.playing_lock.acquire()
def play_song(self):
if not self.current_song_path:
self.log("No song loaded. Click 'Get Current Song & Folder' first.")
return
threading.Thread(target=self._play_song_worker, daemon=True).start()
def _play_song_worker(self):
with self.playing_lock:
self.root.after(0, lambda: self.progress_bar.configure(value=0))
self.root.after(0, lambda: self.progress_label.config(text="Starting..."))
self.stream_generation += 1
my_gen = self.stream_generation
self.current_mb = 0
if self.streamer:
self.log("Stopping previous stream...")
old = self.streamer
self.streamer = None
old.stop()
self.playing_lock.release()
try:
self._wait_for_receiver_stop(timeout=3)
time.sleep(0.5)
finally:
self.playing_lock.acquire()
self.log(f"Starting audio receiver (stream {my_gen})...")
self.streamer = AudioStreamer()
self.streamer.set_log_callback(lambda mb, sid: self.root.after(0, self.update_progress, mb, sid))
def run_receiver():
try:
self.streamer.start_receiver(my_gen)
except Exception as e:
self.log(f"Receiver error: {e}")
finally:
self.root.after(100, lambda: self.on_song_ended(my_gen))
self.active_stream_thread = threading.Thread(target=run_receiver, daemon=True)
self.active_stream_thread.start()
# Wait for receiver to be listening
ready = False
for _ in range(50):
if self.streamer and self.streamer.sock:
try:
self.streamer.sock.getsockname()
ready = True
break
except:
pass
time.sleep(0.1)
if not ready:
self.log("ERROR: Receiver did not start in time!")
self.root.after(0, lambda: self.status_label.config(text="Status: Receiver failed", foreground=COL_ERROR))
self.streamer = None
return
clean_filename = self.clean_filename(self.current_song_path.split('/')[-1])
self.log(f"Streaming: {clean_filename} (stream {my_gen})")
self.root.after(0, lambda: self.show_ticker(clean_filename))
escaped_filename = clean_filename.replace("'", "'\\''")
cmd = f"cat '{self.current_folder}{escaped_filename}' | nc {WINDOWS_IP} {AUDIO_STREAM_PORT}"
self.is_playing = True
self.root.after(0, lambda: self.status_label.config(text="Status: Playing", foreground=COL_SUCCESS))
# Send command OUTSIDE lock so we don't block
try:
self.send_telnet_command(cmd)
except Exception as e:
self.log(f"Stream send error: {e}")
def on_song_ended(self, gen):
if gen != self.stream_generation:
self.log(f"Ignoring end of stale stream {gen} (current is {self.stream_generation})")
return
with self.playing_lock:
mb = self.current_mb
if mb > 0:
self.log(f"Song ended - Total: {mb:.2f} MB")
else:
self.log("Song ended")
self.progress_bar['value'] = 0
self.progress_label.config(text="Complete")
self.current_mb = 0
self.is_playing = False
self.streamer = None
should_advance = (gen == self.stream_generation and self.auto_advance.get())
if should_advance:
self.log("Auto-advancing...")
self.root.after(500, self._safe_next_song)
def _safe_next_song(self):
with self.playing_lock:
should_advance = self.auto_advance.get() and not self.is_playing
if should_advance:
self.next_song()
def play_song_at_index(self, index):
with self.playing_lock:
if index < 0 or index >= len(self.playlist):
self.log("End of playlist reached")
self.playing_lock.release()
try:
self.stop_song()
finally:
self.playing_lock.acquire()
return False
if self.streamer:
old = self.streamer
self.streamer = None
old.stop()
self.playing_lock.release()
try:
self._wait_for_receiver_stop(timeout=3)
time.sleep(0.5)
finally:
self.playing_lock.acquire()
self.playlist_index = index
clean_song = self.clean_filename(self.playlist[index])
self.current_song_path = self.current_folder + clean_song
self.song_text.delete(1.0, tk.END)
self.song_text.insert(1.0, self.current_song_path)
self.playlist_box.selection_clear(0, tk.END)
self.playlist_box.selection_set(index)
self.playlist_box.see(index)
self.play_song()
return True
def next_song(self):
if not self.playlist:
return
next_index = self.playlist_index + 1
if next_index < len(self.playlist):
self.play_song_at_index(next_index)
else:
self.log("End of playlist - stopping")
self.stop_song()
def prev_song(self):
if not self.playlist:
return
prev_index = self.playlist_index - 1
if prev_index >= 0:
self.play_song_at_index(prev_index)
else:
self.log("Beginning of playlist")
def stop_song(self):
with self.playing_lock:
self.stream_generation += 1
if self.streamer:
old = self.streamer
self.streamer = None
old.stop()
self.playing_lock.release()
try:
self._wait_for_receiver_stop(timeout=3)
finally:
self.playing_lock.acquire()
self.is_playing = False
self.current_mb = 0
self.status_label.config(text="Status: Stopped", foreground=COL_INFO)
self.log("Stopped")
self.progress_bar['value'] = 0
self.progress_label.config(text="Waiting...")
def on_playlist_double_click(self, event):
selection = self.playlist_box.curselection()
if selection:
self.play_song_at_index(selection[0])
def main():
root = tk.Tk()
app = PowerampApp(root)
root.mainloop()
if __name__ == "__main__":
main()