Initial commit of ZonePlayer
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import jack
|
||||
|
||||
client = jack.Client("praise_out")
|
||||
client.outports.register("left")
|
||||
client.outports.register("right")
|
||||
|
||||
client.activate()
|
||||
|
||||
try:
|
||||
while True:
|
||||
pass # Keep running
|
||||
except KeyboardInterrupt:
|
||||
print("Exiting...")
|
||||
@@ -0,0 +1,83 @@
|
||||
import os
|
||||
import random
|
||||
import threading
|
||||
from mpv import MPV # type: ignore
|
||||
|
||||
class PersistentPlayer:
|
||||
def __init__(self, name: str, folder: str):
|
||||
self.name = name
|
||||
self.folder = folder
|
||||
self.lock = threading.RLock()
|
||||
self.tracks = []
|
||||
self.current_index = 0
|
||||
self.reload_tracks()
|
||||
|
||||
# LET python-mpv start its own event thread
|
||||
self.player = MPV(
|
||||
ao="jack",
|
||||
jack_name=f"{name}_out",
|
||||
jack_connect="no",
|
||||
keep_open="yes",
|
||||
idle="yes",
|
||||
terminal=False,
|
||||
log_handler=lambda level, prefix, text: print(f"[{self.name}][mpv][{level}] {text}"),
|
||||
loglevel="info"
|
||||
|
||||
)
|
||||
|
||||
@self.player.event_callback("end-file")
|
||||
def _on_end(event):
|
||||
print(f"[{self.name}] ↩ EVENT end-file fired: {event}")
|
||||
self._next_track()
|
||||
|
||||
print(f"[{self.name}] ▶️ Registered end-file handler")
|
||||
|
||||
|
||||
def reload_tracks(self):
|
||||
with self.lock:
|
||||
files = [
|
||||
os.path.join(self.folder, f)
|
||||
for f in os.listdir(self.folder)
|
||||
if f.lower().endswith(".mp3")
|
||||
]
|
||||
random.shuffle(files)
|
||||
self.tracks = files
|
||||
if self.tracks:
|
||||
self.current_index %= len(self.tracks)
|
||||
else:
|
||||
self.current_index = 0
|
||||
print(f"[{self.name}] playlist loaded ({len(self.tracks)} tracks)")
|
||||
|
||||
def add_track(self, path: str):
|
||||
if not path.lower().endswith(".mp3") or not os.path.isfile(path):
|
||||
return print(f"[{self.name}] invalid mp3: {path}")
|
||||
with self.lock:
|
||||
if path in self.tracks:
|
||||
return print(f"[{self.name}] already in playlist")
|
||||
self.tracks.append(path)
|
||||
print(f"[{self.name}] added: {os.path.basename(path)}")
|
||||
|
||||
def remove_track(self, path: str):
|
||||
with self.lock:
|
||||
if path not in self.tracks:
|
||||
return print(f"[{self.name}] not in playlist")
|
||||
idx = self.tracks.index(path)
|
||||
if idx < self.current_index:
|
||||
self.current_index -= 1
|
||||
self.tracks.pop(idx)
|
||||
print(f"[{self.name}] removed: {os.path.basename(path)}")
|
||||
|
||||
def play_current(self):
|
||||
with self.lock:
|
||||
if not self.tracks:
|
||||
return print(f"[{self.name}] no tracks!")
|
||||
track = self.tracks[self.current_index]
|
||||
print(f"[{self.name}] ▶ Playing: {os.path.basename(track)}")
|
||||
self.player.command("loadfile", track, "replace")
|
||||
|
||||
def _next_track(self):
|
||||
with self.lock:
|
||||
if not self.tracks:
|
||||
return
|
||||
self.current_index = (self.current_index + 1) % len(self.tracks)
|
||||
self.play_current()
|
||||
@@ -0,0 +1,17 @@
|
||||
from Scripts.ZonePlayer.OLD.persistent_player import PersistentPlayer
|
||||
import time
|
||||
|
||||
# create and start three players
|
||||
praise = PersistentPlayer("praise", "/home/media/Music/Praise")
|
||||
worship = PersistentPlayer("worship", "/home/media/Music/Worship")
|
||||
instrumental = PersistentPlayer("instrumental", "/home/media/Music/Instrumental")
|
||||
|
||||
for p in (praise, worship, instrumental):
|
||||
p.play_current()
|
||||
|
||||
# just keep Python alive; python-mpv's internal thread will fire your callbacks
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping…")
|
||||
@@ -0,0 +1,125 @@
|
||||
import os
|
||||
import random
|
||||
import gi
|
||||
gi.require_version('Gst','1.0')
|
||||
from gi.repository import Gst, GObject, GLib
|
||||
import time
|
||||
|
||||
|
||||
class Playlist:
|
||||
def __init__(self, name, folder_path, shuffle=True):
|
||||
self.name = name
|
||||
self.folder_path = folder_path
|
||||
self.shuffle = shuffle
|
||||
self.tracks = self.load_tracks()
|
||||
self.current_index = 0
|
||||
|
||||
def load_tracks(self):
|
||||
files = [
|
||||
os.path.join(self.folder_path, f)
|
||||
for f in os.listdir(self.folder_path)
|
||||
if f.lower().endswith(".mp3")
|
||||
]
|
||||
if self.shuffle:
|
||||
random.shuffle(files)
|
||||
return files
|
||||
|
||||
def next_track(self):
|
||||
if not self.tracks:
|
||||
return None
|
||||
self.current_index = (self.current_index + 1) % len(self.tracks)
|
||||
return self.tracks[self.current_index]
|
||||
|
||||
def current_track(self):
|
||||
if not self.tracks:
|
||||
return None
|
||||
return self.tracks[self.current_index]
|
||||
|
||||
Gst.init(None)
|
||||
|
||||
class Player:
|
||||
def __init__(self, playlist: Playlist):
|
||||
self.playlist = playlist
|
||||
self.pipeline = None
|
||||
|
||||
def play_current(self):
|
||||
track = self.playlist.current_track()
|
||||
if not track:
|
||||
print(f"No track found in playlist: {self.playlist.name}")
|
||||
return
|
||||
|
||||
if self.pipeline:
|
||||
self.pipeline.set_state(Gst.State.NULL)
|
||||
self.pipeline = None
|
||||
|
||||
print(f"[{self.playlist.name}] Playing: {os.path.basename(track)}")
|
||||
if not self.create_pipeline(track):
|
||||
print(f"[{self.playlist.name}] Failed to create pipeline.")
|
||||
return
|
||||
|
||||
def create_pipeline(self, track_path):
|
||||
pipeline_str = (
|
||||
f'filesrc location="{track_path}" ! \
|
||||
decodebin ! \
|
||||
audioconvert ! audioresample ! \
|
||||
audio/x-raw,channels=2 ! \
|
||||
jackaudiosink client-name={self.playlist.name} connect=1'
|
||||
)
|
||||
|
||||
try:
|
||||
self.pipeline = Gst.parse_launch(pipeline_str)
|
||||
except Exception as e:
|
||||
print(f"[{self.playlist.name}] Failed to create pipeline: {e}")
|
||||
return False
|
||||
|
||||
#listen for end of track
|
||||
bus = self.pipeline.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect("message", self.on_message)
|
||||
|
||||
ret = self.pipeline.set_state(Gst.State.PLAYING)
|
||||
if ret == Gst.StateChangeReturn.FAILURE:
|
||||
print(f"[{self.playlist.name}] Failed to set pipeline to PLAYING")
|
||||
return False
|
||||
|
||||
print(f"[{self.playlist.name}] Pipeline is playing")
|
||||
return True
|
||||
|
||||
def on_message(self, bus, message):
|
||||
if message.type == Gst.MessageType.EOS:
|
||||
self.on_end_of_stream(bus, message)
|
||||
|
||||
|
||||
def on_end_of_stream(self, bus, msg):
|
||||
print(f"[{self.playlist.name}] Track finished")
|
||||
self.pipeline.set_state(Gst.State.NULL)
|
||||
self.playlist.next_track()
|
||||
self.play_current()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
praise = Playlist("praise", "/home/media/Music/Praise")
|
||||
worship = Playlist("worship", "/home/media/Music/Worship")
|
||||
instrumental = Playlist("instrumental", "/home/media/Music/Instrumental")
|
||||
|
||||
praise_player = Player(praise)
|
||||
worship_player = Player(worship)
|
||||
instrumental_player = Player(instrumental)
|
||||
|
||||
praise_player.play_current()
|
||||
worship_player.play_current()
|
||||
instrumental_player.play_current()
|
||||
|
||||
loop = GLib.MainLoop()
|
||||
try:
|
||||
loop.run()
|
||||
except KeyboardInterrupt:
|
||||
print("Stopping playback")
|
||||
for player in [praise_player, worship_player, instrumental_player]:
|
||||
if player.pipeline:
|
||||
player.pipeline.set_state(Gst.State.NULL)
|
||||
loop.quit()
|
||||
|
||||
#print("Praise:",praise.current_track())
|
||||
#print("Worship:", worship.current_track())
|
||||
#print("Instrumenal:", instrumental.current_track())
|
||||
Reference in New Issue
Block a user