mirror of
https://github.com/spl0k/supysonic.git
synced 2024-12-22 17:06: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"),
|
"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",
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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()
|
||||||
|
@ -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
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