Files
ZonePlayer/jack_mp3_player.py
2025-06-30 20:24:59 -05:00

126 lines
4.4 KiB
Python

import jack
import time
import numpy as np
from pydub import AudioSegment
import random
import sys
import json
import os
import yaml
import paho.mqtt.client as mqtt
import threading
import re
# === load and shuffle .m3u playlist ===
def load_playlist(m3u_path):
with open(m3u_path, "r") as f:
lines = [line.strip() for line in f if line.strip() and not line.startswith("#")]
random.shuffle(lines)
return lines
# === load mp3 song file ===
def decode_mp3(path):
song = AudioSegment.from_file(path)
song = song.set_frame_rate(48000).set_channels(2).set_sample_width(4)
samples = np.array(song.get_array_of_samples()).astype(np.float32) / (2**31)
return samples.reshape((-1, 2)) # stereo: L R
def update_now_playing(playlist_name, song_title):
path = "/home/media/Music/now_playing.json"
try:
with open(path, "r") as f:
data = json.load(f)
except FileNotFoundError:
data = {}
data[playlist_name.lower()] = os.path.basename(song_title)
with open(path, "w") as f:
json.dump(data, f, indent=2)
# MQTT title broadcast
mqtt_topic = f"music/now_playing/{playlist_name.lower()}"
mqtt_payload = os.path.splitext(os.path.basename(song_title))[0]
mqtt_payload = re.sub(r'\s*\[[^\]]+\]$', '', mqtt_payload)
try:
mqtt_client = mqtt.Client(protocol=mqtt.MQTTv311)
mqtt_client.connect("10.40.0.2", 1883)
result = mqtt_client.publish(mqtt_topic, mqtt_payload, retain=True)
result.wait_for_publish()
if result.rc != mqtt.MQTT_ERR_SUCCESS:
raise Exception(f"Publish failed with code {result.rc}")
mqtt_client.disconnect()
except Exception as e:
print(f"[MQTT] failed to publish: {e}")
# === YAML config ===
def load_config():
with open("config.yaml") as f:
return yaml.safe_load(f)
# === JACK + player logic ===
def run_playlist(playlist_name, playlist_file):
client = jack.Client(playlist_name)
out_l = client.outports.register("left")
out_r = client.outports.register("right")
playlist = load_playlist(playlist_file)
current_index = 0
samples = decode_mp3(playlist[current_index])
buffer_pos = [0]
@client.set_process_callback
def process(frames):
bufsize = frames
end = buffer_pos[0] + bufsize
if buffer_pos[0] >= len(samples):
out_l.get_buffer()[:] = np.zeros(bufsize, dtype=np.float32)
out_r.get_buffer()[:] = np.zeros(bufsize, dtype=np.float32)
return
chunk = samples[buffer_pos[0]:end]
if len(chunk) < bufsize:
pad = np.zeros((bufsize - len(chunk), 2), dtype=np.float32)
chunk = np.vstack((chunk, pad))
out_l.get_buffer()[:] = chunk[:, 0].copy()
out_r.get_buffer()[:] = chunk[:, 1].copy()
buffer_pos[0] += bufsize
with client:
print(f"[{playlist_name}] Playing: {playlist[current_index]}")
update_now_playing(playlist_name, playlist[current_index])
while True:
if buffer_pos[0] >= len(samples):
current_index += 1
if current_index >= len(playlist):
playlist = load_playlist(playlist_file)
current_index = 0
print(f"[{playlist_name}] Playing: {playlist[current_index]}")
update_now_playing(playlist_name, playlist[current_index])
samples = decode_mp3(playlist[current_index])
buffer_pos[0] = 0
time.sleep(0.1)
# === Main logic ===
if __name__ == "__main__":
config = load_config()
sources = config.get("sources", {})
if len(sys.argv) > 1:
playlist_name = sys.argv[1]
if playlist_name in sources and sources[playlist_name].get("active", True):
playlist_file = f"/home/media/Music/{playlist_name}.m3u"
run_playlist(playlist_name, playlist_file)
else:
print(f"[!] Source '{playlist_name}' not found or not active.")
else:
threads = []
for playlist_name, source_data in sources.items():
if source_data.get("active", True):
playlist_file = f"/home/media/Music/{playlist_name}.m3u"
t = threading.Thread(target=run_playlist, args=(playlist_name, playlist_file), daemon=True)
t.start()
threads.append(t)
for t in threads:
t.join()