mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 08:56:17 +00:00
Basic (untested) jukebox interface based on an external command
This commit is contained in:
parent
d92c5019d9
commit
75b89e5f45
@ -45,6 +45,7 @@ class DefaultConfig(object):
|
||||
"socket": os.path.join(tempdir, "supysonic.sock"),
|
||||
"run_watcher": True,
|
||||
"wait_delay": 5,
|
||||
"jukebox_command": None,
|
||||
"log_file": None,
|
||||
"log_level": "WARNING",
|
||||
}
|
||||
|
@ -59,6 +59,45 @@ class ScannerStartCommand(ScannerCommand):
|
||||
daemon.start_scan(self.__folders, self.__force)
|
||||
|
||||
|
||||
class JukeboxCommand(DaemonCommand):
|
||||
def __init__(self, action, arg):
|
||||
self.__action = action
|
||||
self.__arg = arg
|
||||
|
||||
def apply(self, connection, daemon):
|
||||
if daemon.jukebox is None:
|
||||
connection.send(JukeboxResult(None))
|
||||
return
|
||||
|
||||
playlist = None
|
||||
if self.__action == "get":
|
||||
playlist = daemon.jukebox.playlist
|
||||
elif self.__action == "status":
|
||||
pass
|
||||
elif self.__action == "set":
|
||||
daemon.jukebox.set(self.__arg)
|
||||
elif self.__action == "start":
|
||||
daemon.jukebox.start()
|
||||
elif self.__action == "stop":
|
||||
daemon.jukebox.stop()
|
||||
elif self.__action == "skip":
|
||||
daemon.jukebox.skip(self.__arg)
|
||||
elif self.__action == "add":
|
||||
daemon.jukebox.add(self.__arg)
|
||||
elif self.__action == "clear":
|
||||
daemon.jukebox.clear()
|
||||
elif self.__action == "remove":
|
||||
daemon.jukebox.remove(self.__arg)
|
||||
elif self.__action == "shuffle":
|
||||
daemon.jukebox.shuffle()
|
||||
elif self.__action == "setGain":
|
||||
daemon.jukebox.setgain(self.__arg)
|
||||
|
||||
rv = JukeboxResult(daemon.jukebox)
|
||||
rv.playlist = playlist
|
||||
connection.send(rv)
|
||||
|
||||
|
||||
class DaemonCommandResult(object):
|
||||
pass
|
||||
|
||||
@ -70,6 +109,18 @@ class ScannerProgressResult(DaemonCommandResult):
|
||||
scanned = property(lambda self: self.__scanned)
|
||||
|
||||
|
||||
class JukeboxResult(DaemonCommandResult):
|
||||
def __init__(self, jukebox):
|
||||
if jukebox is None:
|
||||
self.playing = False
|
||||
self.index = -1
|
||||
self.gain = 1.0
|
||||
else:
|
||||
self.playing = jukebox.playing
|
||||
self.index = jukebox.index
|
||||
self.gain = jukebox.gain
|
||||
|
||||
|
||||
class DaemonClient(object):
|
||||
def __init__(self, address=None):
|
||||
self.__address = address or get_current_config().DAEMON["socket"]
|
||||
@ -107,3 +158,10 @@ class DaemonClient(object):
|
||||
raise TypeError("Expecting list, got " + str(type(folders)))
|
||||
with self.__get_connection() as c:
|
||||
c.send(ScannerStartCommand(folders, force))
|
||||
|
||||
def jukebox_control(self, action, *args):
|
||||
if not isinstance(action, strtype):
|
||||
raise TypeError("Expecting string, got " + str(type(action)))
|
||||
with self.__get_connection() as c:
|
||||
c.send(JukeboxCommand(action, args))
|
||||
return c.recv()
|
||||
|
@ -16,6 +16,7 @@ from threading import Thread, Event
|
||||
|
||||
from .client import DaemonCommand
|
||||
from ..db import Folder
|
||||
from ..jukebox import Jukebox
|
||||
from ..scanner import Scanner
|
||||
from ..utils import get_secret_key
|
||||
from ..watcher import SupysonicWatcher
|
||||
@ -31,10 +32,12 @@ class Daemon(object):
|
||||
self.__listener = None
|
||||
self.__watcher = None
|
||||
self.__scanner = None
|
||||
self.__jukebox = None
|
||||
self.__stopped = Event()
|
||||
|
||||
watcher = property(lambda self: self.__watcher)
|
||||
scanner = property(lambda self: self.__scanner)
|
||||
jukbox = property(lambda self: self.__jukebox)
|
||||
|
||||
def __handle_connection(self, connection):
|
||||
cmd = connection.recv()
|
||||
@ -56,6 +59,9 @@ class Daemon(object):
|
||||
self.__watcher = SupysonicWatcher(self.__config)
|
||||
self.__watcher.start()
|
||||
|
||||
if self.__config.DAEMON["jukebox_command"]:
|
||||
self.__jukebox = Jukebox(self.__config.DAEMON["jukebox_command"])
|
||||
|
||||
Thread(target=self.__listen).start()
|
||||
while not self.__stopped.is_set():
|
||||
time.sleep(1)
|
||||
@ -109,3 +115,5 @@ class Daemon(object):
|
||||
self.__scanner.join()
|
||||
if self.__watcher is not None:
|
||||
self.__watcher.stop()
|
||||
if self.__jukebox is not None:
|
||||
self.__jukebox.terminate()
|
||||
|
@ -394,7 +394,7 @@ class User(db.Entity):
|
||||
commentRole=False,
|
||||
podcastRole=False,
|
||||
streamRole=True,
|
||||
jukeboxRole=False,
|
||||
jukeboxRole=True,
|
||||
shareRole=False,
|
||||
)
|
||||
|
||||
|
132
supysonic/jukebox.py
Normal file
132
supysonic/jukebox.py
Normal file
@ -0,0 +1,132 @@
|
||||
# coding: utf-8
|
||||
#
|
||||
# This file is part of Supysonic.
|
||||
# Supysonic is a Python implementation of the Subsonic server API.
|
||||
#
|
||||
# Copyright (C) 2019 Alban 'spl0k' Féron
|
||||
#
|
||||
# Distributed under terms of the GNU AGPLv3 license.
|
||||
|
||||
import logging
|
||||
import shlex
|
||||
import time
|
||||
|
||||
from pony.orm import select
|
||||
from random import shuffle
|
||||
from subprocess import Popen, DEVNULL
|
||||
from threading import Thread, Event, RLock
|
||||
|
||||
from .db import Track
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Jukebox(object):
|
||||
def __init__(self, cmd):
|
||||
self.__cmd = shlex.split(cmd)
|
||||
self.__playlist = []
|
||||
self.__index = -1
|
||||
|
||||
self.__thread = None
|
||||
self.__lock = RLock()
|
||||
self.__skip = Event()
|
||||
self.__stop = Event()
|
||||
|
||||
playing = property(
|
||||
lambda self: self.__thread is not None and self.__thread.is_alive()
|
||||
)
|
||||
index = property(lambda self: self.__index)
|
||||
gain = property(lambda self: 1.0)
|
||||
playlist = property(lambda self: list(self.__playlist))
|
||||
|
||||
def set(self, tracks):
|
||||
self.clear()
|
||||
self.add(tracks)
|
||||
|
||||
def start(self):
|
||||
if self.playing or not self.__playlist:
|
||||
return
|
||||
|
||||
self.__skip.clear()
|
||||
self.__stop.clear()
|
||||
self.__thread = Thread(target=self.__play_thread)
|
||||
self.__thread.start()
|
||||
|
||||
def stop(self):
|
||||
if not self.playing:
|
||||
return
|
||||
|
||||
self.__stop.set()
|
||||
|
||||
def skip(self, index):
|
||||
if not self.playing:
|
||||
return
|
||||
|
||||
if index < 0 or index >= len(self.__playlist):
|
||||
raise IndexError()
|
||||
|
||||
with self.__lock:
|
||||
self.__index = index - 1
|
||||
self.__skip.set()
|
||||
|
||||
def add(self, tracks):
|
||||
paths = select(t.path for t in Track if t.id in tracks)
|
||||
with self.__lock:
|
||||
self.__playlist += paths[:]
|
||||
|
||||
def clear(self):
|
||||
with self.__lock:
|
||||
self.__playlist.clear()
|
||||
self.__index = -1
|
||||
|
||||
def remove(self, index):
|
||||
try:
|
||||
with self.__lock:
|
||||
self.__playlist.pop(index)
|
||||
if index < self.__index:
|
||||
self.__index -= 1
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def shuffle(self):
|
||||
with self.__lock:
|
||||
shuffle(self.__playlist)
|
||||
|
||||
def setgain(self, gain):
|
||||
raise NotImplementedError()
|
||||
|
||||
def terminate(self):
|
||||
self.__stop.set()
|
||||
if self.__thread is not None:
|
||||
self.__thread.join()
|
||||
|
||||
def __play_thread(self):
|
||||
while not self.__stop.is_set():
|
||||
if self.__skip.is_set():
|
||||
proc.terminate()
|
||||
proc.join()
|
||||
self.__skip.clear()
|
||||
|
||||
if proc is None or proc.poll() is not None:
|
||||
with self.__lock:
|
||||
self.__index += 1
|
||||
if self.__index >= len(self.__playlist):
|
||||
break
|
||||
|
||||
proc = self.__play_file()
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
proc.terminate()
|
||||
proc.wait()
|
||||
|
||||
def __play_file(self):
|
||||
path = self.__playlist[self.__index]
|
||||
args = [a.replace("%path", path) for a in self.__cmd]
|
||||
|
||||
logger.debug("Start playing with command %s", args)
|
||||
try:
|
||||
return Popen(args, stdin=DEVNULL, stdout=DEVNULL, stderr=DEVNULL)
|
||||
except:
|
||||
logger.exception("Failed running play command")
|
||||
return None
|
Loading…
Reference in New Issue
Block a user