126 lines
4.4 KiB
Python
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()
|