Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working prototype for muting spotify on pause/unmuting on resume #883

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion amplipi/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,17 @@
# start in the web directory
TEMPLATE_DIR = os.path.abspath('web/templates')
STATIC_DIR = os.path.abspath('web/static')
GENERATED_DIR = os.path.abspath('web/generated')
GENERATED_DIR = os.path.abspath(f'{utils.get_folder("web")}/generated') # web/generated is now in the user's home directory with all other runtime-generated files
WEB_DIR = os.path.abspath('web/dist')

# we host docs using rapidoc instead via a custom endpoint, so the default endpoints need to be disabled
app = FastAPI(openapi_url=None, redoc_url=None,)

app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")

uvicorn_access = logging.getLogger("uvicorn.access")
uvicorn_access.disabled = True

# This will get generated as a tmpfs on AmpliPi,
# but won't exist if testing on another machine.
os.makedirs(GENERATED_DIR, exist_ok=True)
Expand Down
41 changes: 2 additions & 39 deletions amplipi/mpris.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class Metadata:
connected: bool = False
state_changed_time: float = 0

# TODO: consider removing the script this starts and doing it all here since we no longer poll


class MPRIS:
"""A class for interfacing with an MPRIS MediaPlayer2 over dbus."""
Expand All @@ -53,14 +55,6 @@ def __init__(self, service_suffix, metadata_path) -> None:
self.metadata_path = metadata_path
self._closing = False

try:
with open(self.metadata_path, "w", encoding='utf-8') as f:
m = Metadata()
m.state = "Stopped"
json.dump(m.__dict__, f)
except Exception as e:
logger.exception(f'Exception clearing metadata file: {e}')

try:
child_args = [sys.executable,
f"{utils.get_folder('streams')}/MPRIS_metadata_reader.py",
Expand Down Expand Up @@ -91,37 +85,6 @@ def play_pause(self) -> None:
"""Plays or pauses depending on current state."""
self.mpris.PlayPause()

def _load_metadata(self) -> Metadata:
try:
with open(self.metadata_path, 'r', encoding='utf-8') as f:
metadata_dict = json.load(f)
metadata_obj = Metadata()

for k in metadata_dict.keys():
metadata_obj.__dict__[k] = metadata_dict[k]

return metadata_obj
except Exception as e:
logger.exception(f"MPRIS loading metadata at {self.metadata_path} failed: {e}")

return Metadata()

def metadata(self) -> Metadata:
"""Returns metadata from MPRIS."""
return self._load_metadata()

def is_playing(self) -> bool:
"""Playing?"""
return self._load_metadata().state == 'Playing'

def is_stopped(self) -> bool:
"""Stopped?"""
return self._load_metadata().state == 'Stopped'

def is_connected(self) -> bool:
"""Returns true if we can talk to the MPRIS dbus object."""
return self._load_metadata().connected

def get_capabilities(self) -> List[CommandTypes]:
"""Returns a list of supported commands."""

Expand Down
87 changes: 36 additions & 51 deletions amplipi/streams/airplay.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ def __init__(self, name: str, ap2: bool, disabled: bool = False, mock: bool = Fa
self._connect_time = 0.0
self._coverart_dir = ''
self._log_file: Optional[io.TextIOBase] = None
self.last_info: Optional[models.SourceInfo] = None
self.change_time = time.time() - self.STATE_TIMEOUT

self.default_image_url = 'static/imgs/shairport.png'
self.stopped_message = f'Nothing is playing, please connect to {self.name} to play music'

def reconfig(self, **kwargs):
self.validate_stream(**kwargs)
Expand Down Expand Up @@ -109,23 +114,21 @@ def _activate(self, vsrc: int):
except FileNotFoundError:
pass
os.makedirs(self._coverart_dir, exist_ok=True)
os.makedirs(src_config_folder, exist_ok=True)
config_file = f'{src_config_folder}/shairport.conf'
config_file = f'{self._get_config_folder()}/shairport.conf'
write_sp_config_file(config_file, config)
self._log_file = open(f'{src_config_folder}/log', mode='w')
self._log_file = open(f'{self._get_config_folder()}/log', mode='w')
shairport_args = f"{utils.get_folder('streams')}/shairport-sync{'-ap2' if self.ap2 else ''} -c {config_file}".split(' ')
logger.info(f'shairport_args: {shairport_args}')

self.proc = subprocess.Popen(args=shairport_args, stdin=subprocess.PIPE,
stdout=self._log_file, stderr=self._log_file)

try:
mpris_name = 'ShairportSync'
# If there are multiple shairport-sync processes, add the pid to the mpris name
# shairport sync only adds the pid to the mpris name if it cannot use the default name
if len(os.popen("pgrep shairport-sync").read().strip().splitlines()) > 1:
mpris_name += f".i{self.proc.pid}"
self.mpris = MPRIS(mpris_name, f'{src_config_folder}/metadata.txt')
self.mpris = MPRIS(mpris_name, f'{self._get_config_folder()}/metadata.json')
except Exception as exc:
logger.exception(f'Error starting airplay MPRIS reader: {exc}')

Expand All @@ -151,69 +154,51 @@ def _deactivate(self):
self._disconnect()
self.proc = None

def _read_info(self) -> models.SourceInfo:
self.change_time = time.time() # keep track of the last time the state changed
return super()._read_info()

def info(self) -> models.SourceInfo:
source = models.SourceInfo(
name=f"Connect to {self.name} on Airplay{'2' if self.ap2 else ''}",
state=self.state,
img_url='static/imgs/shairport.png',
type=self.stream_type
)

source = super().info()

# fake a paused state if the stream has stopped and it hasn't been stopped for too long since airplay doesn't have a paused state
if self.last_info and source.state == 'stopped' and not (time.time() - self.change_time > self.STATE_TIMEOUT):
source = self.last_info
source.state = 'paused'

# if stream is airplay2 and other airplay2s exist show error message
if self.ap2:
if self.ap2_exists:
source.name = 'An Airplay2 stream already exists!\n Please disconnect it and try again.'
source.artist = 'An Airplay2 stream already exists!\n Please disconnect it and try again.'
return source

if not self.mpris:
logger.info(f'Airplay: No MPRIS object for {self.name}!')
return source

try:
md = self.mpris.metadata()

if self.mpris.is_playing():
source.state = 'playing'
else:
# if we've been paused for a while and the state has changed since connecting, then say
# we're stopped since shairport-sync doesn't really differentiate between paused and stopped
if self._connect_time < md.state_changed_time and time.time() - md.state_changed_time < self.STATE_TIMEOUT:
source.state = 'paused'
else:
source.state = 'stopped'

if source.state != 'stopped':
source.artist = md.artist
source.track = md.title
source.album = md.album
source.supported_cmds = list(self.supported_cmds)

if md.title != '':
# if there is a title, attempt to get coverart
images = os.listdir(self._coverart_dir)
if len(images) > 0:
source.img_url = f'generated/v{self.vsrc}/{images[0]}'
else:
source.track = "No metadata available"
if source.track != '':
# if there is a title, attempt to get coverart
images = os.listdir(self._coverart_dir)
logger.info(f'images: {images}')
if len(images) > 0:
source.img_url = f'generated/v{self.vsrc}/{images[0]}'

except Exception as e:
logger.exception(f"error in airplay: {e}")
self.last_info = source

return source

def send_cmd(self, cmd):
super().send_cmd(cmd)
try:
if cmd in self.supported_cmds:
if cmd == 'play':
self.mpris.play_pause()
elif cmd == 'pause':
self.mpris.play_pause()
elif cmd == 'next':
self.mpris.next()
elif cmd == 'prev':
self.mpris.previous()
else:
raise NotImplementedError(f'"{cmd}" is either incorrect or not currently supported')
if cmd == 'play':
self.mpris.play_pause()
elif cmd == 'pause':
self.mpris.play_pause()
elif cmd == 'next':
self.mpris.next()
elif cmd == 'prev':
self.mpris.previous()
except Exception as e:
logger.exception(f"error in shairport: {e}")

Expand Down
Loading
Loading