1
0
mirror of https://github.com/spl0k/supysonic.git synced 2024-11-10 04:02:17 +00:00

Basic (untested) jukebox interface based on an external command

This commit is contained in:
Alban Féron 2019-09-01 14:55:44 +02:00
parent d92c5019d9
commit 75b89e5f45
No known key found for this signature in database
GPG Key ID: 8CE0313646D16165
5 changed files with 200 additions and 1 deletions

View File

@ -45,6 +45,7 @@ class DefaultConfig(object):
"socket": os.path.join(tempdir, "supysonic.sock"), "socket": os.path.join(tempdir, "supysonic.sock"),
"run_watcher": True, "run_watcher": True,
"wait_delay": 5, "wait_delay": 5,
"jukebox_command": None,
"log_file": None, "log_file": None,
"log_level": "WARNING", "log_level": "WARNING",
} }

View File

@ -59,6 +59,45 @@ class ScannerStartCommand(ScannerCommand):
daemon.start_scan(self.__folders, self.__force) 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): class DaemonCommandResult(object):
pass pass
@ -70,6 +109,18 @@ class ScannerProgressResult(DaemonCommandResult):
scanned = property(lambda self: self.__scanned) 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): class DaemonClient(object):
def __init__(self, address=None): def __init__(self, address=None):
self.__address = address or get_current_config().DAEMON["socket"] self.__address = address or get_current_config().DAEMON["socket"]
@ -107,3 +158,10 @@ class DaemonClient(object):
raise TypeError("Expecting list, got " + str(type(folders))) raise TypeError("Expecting list, got " + str(type(folders)))
with self.__get_connection() as c: with self.__get_connection() as c:
c.send(ScannerStartCommand(folders, force)) 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()

View File

@ -16,6 +16,7 @@ from threading import Thread, Event
from .client import DaemonCommand from .client import DaemonCommand
from ..db import Folder from ..db import Folder
from ..jukebox import Jukebox
from ..scanner import Scanner from ..scanner import Scanner
from ..utils import get_secret_key from ..utils import get_secret_key
from ..watcher import SupysonicWatcher from ..watcher import SupysonicWatcher
@ -31,10 +32,12 @@ class Daemon(object):
self.__listener = None self.__listener = None
self.__watcher = None self.__watcher = None
self.__scanner = None self.__scanner = None
self.__jukebox = None
self.__stopped = Event() self.__stopped = Event()
watcher = property(lambda self: self.__watcher) watcher = property(lambda self: self.__watcher)
scanner = property(lambda self: self.__scanner) scanner = property(lambda self: self.__scanner)
jukbox = property(lambda self: self.__jukebox)
def __handle_connection(self, connection): def __handle_connection(self, connection):
cmd = connection.recv() cmd = connection.recv()
@ -56,6 +59,9 @@ class Daemon(object):
self.__watcher = SupysonicWatcher(self.__config) self.__watcher = SupysonicWatcher(self.__config)
self.__watcher.start() self.__watcher.start()
if self.__config.DAEMON["jukebox_command"]:
self.__jukebox = Jukebox(self.__config.DAEMON["jukebox_command"])
Thread(target=self.__listen).start() Thread(target=self.__listen).start()
while not self.__stopped.is_set(): while not self.__stopped.is_set():
time.sleep(1) time.sleep(1)
@ -109,3 +115,5 @@ class Daemon(object):
self.__scanner.join() self.__scanner.join()
if self.__watcher is not None: if self.__watcher is not None:
self.__watcher.stop() self.__watcher.stop()
if self.__jukebox is not None:
self.__jukebox.terminate()

View File

@ -394,7 +394,7 @@ class User(db.Entity):
commentRole=False, commentRole=False,
podcastRole=False, podcastRole=False,
streamRole=True, streamRole=True,
jukeboxRole=False, jukeboxRole=True,
shareRole=False, shareRole=False,
) )

132
supysonic/jukebox.py Normal file
View 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