2020-11-01 02:35:14 +00:00
"""
BAPSicle Server
Next - gen audio playout server for University Radio York playout ,
based on WebStudio interface .
Audio Player
Authors :
Matthew Stratford
Michael Grace
Date :
October , November 2020
"""
2020-10-30 19:31:18 +00:00
# This is the player. Reliability is critical here, so we're catching
# literally every exception possible and handling it.
# It is key that whenever the parent server tells us to do something
# that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting.
2021-04-08 20:15:15 +00:00
# Stop the Pygame Hello message.
import os
os . environ [ " PYGAME_HIDE_SUPPORT_PROMPT " ] = " hide "
2020-10-29 21:23:37 +00:00
from queue import Empty
import multiprocessing
import setproctitle
import copy
import json
import time
2020-12-19 14:57:37 +00:00
from typing import Any , Callable , Dict , List , Optional
2021-04-07 19:14:12 +00:00
from pygame import mixer
2020-10-25 01:23:24 +00:00
from mutagen . mp3 import MP3
2021-04-18 02:14:14 +00:00
from syncer import sync
2021-04-22 22:00:31 +00:00
from threading import Timer
2020-10-30 22:15:51 +00:00
2021-08-16 22:43:09 +00:00
from helpers . normalisation import get_normalised_filename_if_available
2020-11-05 23:53:27 +00:00
from helpers . myradio_api import MyRadioAPI
2020-10-30 22:06:03 +00:00
from helpers . state_manager import StateManager
from helpers . logging_manager import LoggingManager
2021-04-12 21:59:51 +00:00
from baps_types . plan import PlanItem
from baps_types . marker import Marker
2021-05-26 23:23:49 +00:00
import package
2020-10-23 20:10:32 +00:00
2021-03-22 00:33:14 +00:00
# TODO ENUM
2021-04-05 23:32:58 +00:00
VALID_MESSAGE_SOURCES = [ " WEBSOCKET " , " UI " , " CONTROLLER " , " TEST " , " ALL " ]
2021-04-22 22:00:31 +00:00
TRACKLISTING_DELAYED_S = 20
2021-04-08 19:53:51 +00:00
2021-04-08 21:32:16 +00:00
2021-04-08 19:53:51 +00:00
class Player :
2021-04-04 21:34:46 +00:00
out_q : multiprocessing . Queue
last_msg : str
last_msg_source : str
2021-02-14 00:29:47 +00:00
last_time_update = None
2021-04-04 21:34:46 +00:00
state : StateManager
logger : LoggingManager
api : MyRadioAPI
running : bool = False
2021-04-07 19:14:12 +00:00
stopped_manually : bool = False
2020-10-24 20:31:52 +00:00
2021-04-22 22:00:31 +00:00
tracklist_start_timer : Optional [ Timer ] = None
tracklist_end_timer : Optional [ Timer ] = None
2020-10-24 20:31:52 +00:00
__default_state = {
2020-10-30 00:32:34 +00:00
" initialised " : False ,
2020-11-01 02:35:14 +00:00
" loaded_item " : None ,
2020-10-24 20:31:52 +00:00
" channel " : - 1 ,
" playing " : False ,
2020-10-30 19:31:18 +00:00
" paused " : False ,
2020-10-29 22:25:17 +00:00
" loaded " : False ,
2020-10-24 20:31:52 +00:00
" pos " : 0 ,
2020-10-30 19:31:18 +00:00
" pos_offset " : 0 ,
" pos_true " : 0 ,
2020-10-24 20:31:52 +00:00
" remaining " : 0 ,
" length " : 0 ,
2020-11-03 01:07:25 +00:00
" auto_advance " : True ,
2021-02-14 17:53:28 +00:00
" repeat " : " none " , # none, one or all
2020-11-03 01:07:25 +00:00
" play_on_load " : False ,
2020-11-01 02:35:14 +00:00
" output " : None ,
2021-04-08 19:53:51 +00:00
" show_plan " : [ ] ,
2021-06-20 23:22:29 +00:00
" live " : True ,
2021-04-22 22:00:31 +00:00
" tracklist_mode " : " off " ,
" tracklist_id " : None ,
2020-10-24 20:31:52 +00:00
}
2021-04-08 19:53:51 +00:00
__rate_limited_params = [ " pos " , " pos_offset " , " pos_true " , " remaining " ]
2020-11-04 01:19:56 +00:00
2020-10-29 22:25:17 +00:00
@property
2020-10-24 20:31:52 +00:00
def isInit ( self ) :
try :
2020-10-25 01:23:24 +00:00
mixer . music . get_busy ( )
2021-04-08 21:32:16 +00:00
except Exception :
2020-10-24 20:31:52 +00:00
return False
2020-10-29 22:25:17 +00:00
return True
2020-10-24 20:31:52 +00:00
2020-10-29 21:23:37 +00:00
@property
2021-04-12 21:59:51 +00:00
def isPlaying ( self ) - > bool :
2020-10-29 22:25:17 +00:00
if self . isInit :
2020-10-30 19:31:18 +00:00
return ( not self . isPaused ) and bool ( mixer . music . get_busy ( ) )
2020-10-29 22:25:17 +00:00
return False
2020-10-24 20:31:52 +00:00
2020-10-30 19:31:18 +00:00
@property
2020-12-19 14:57:37 +00:00
def isPaused ( self ) - > bool :
2021-04-18 19:27:54 +00:00
return self . state . get ( ) [ " paused " ]
2020-10-30 19:31:18 +00:00
2020-10-29 21:23:37 +00:00
@property
def isLoaded ( self ) :
2021-04-12 21:59:51 +00:00
return self . _isLoaded ( )
def _isLoaded ( self , short_test : bool = False ) :
2021-04-18 19:27:54 +00:00
if not self . state . get ( ) [ " loaded_item " ] :
2020-10-29 22:25:17 +00:00
return False
if self . isPlaying :
return True
2021-04-12 21:59:51 +00:00
# If we don't want to do any testing if it's really loaded, fine.
if short_test :
return True
2020-10-30 00:32:34 +00:00
# Because Pygame/SDL is annoying
# We're not playing now, so we can quickly test run
# If that works, we're loaded.
2020-10-29 22:25:17 +00:00
try :
2020-10-30 00:32:34 +00:00
mixer . music . set_volume ( 0 )
mixer . music . play ( 0 )
2021-04-08 21:32:16 +00:00
except Exception :
2020-10-30 00:32:34 +00:00
try :
mixer . music . set_volume ( 1 )
2021-04-08 21:32:16 +00:00
except Exception :
2021-04-08 19:53:51 +00:00
self . logger . log . exception (
" Failed to reset volume after attempting loaded test. "
)
2020-10-30 00:32:34 +00:00
pass
2020-10-29 22:25:17 +00:00
return False
2021-04-22 22:00:31 +00:00
finally :
mixer . music . stop ( )
2020-10-30 00:32:34 +00:00
mixer . music . set_volume ( 1 )
return True
2020-10-29 22:25:17 +00:00
2021-04-12 21:59:51 +00:00
@property
def isCued ( self ) :
# Don't mess with playback, we only care about if it's supposed to be loaded.
if not self . _isLoaded ( short_test = True ) :
return False
2021-04-18 19:27:54 +00:00
return ( self . state . get ( ) [ " pos_true " ] == self . state . get ( ) [ " loaded_item " ] . cue and not self . isPlaying )
2021-04-12 21:59:51 +00:00
2020-10-30 00:32:34 +00:00
@property
def status ( self ) :
2020-11-01 02:35:14 +00:00
state = copy . copy ( self . state . state )
# Not the biggest fan of this, but maybe I'll get a better solution for this later
2021-04-08 19:53:51 +00:00
state [ " loaded_item " ] = (
state [ " loaded_item " ] . __dict__ if state [ " loaded_item " ] else None
)
2020-11-03 00:32:43 +00:00
state [ " show_plan " ] = [ repr . __dict__ for repr in state [ " show_plan " ] ]
2020-11-01 02:35:14 +00:00
res = json . dumps ( state )
2020-10-30 00:32:34 +00:00
return res
2020-10-29 21:23:37 +00:00
2020-11-15 19:34:13 +00:00
# Audio Playout Related Methods
2020-11-03 01:07:25 +00:00
2020-12-19 14:57:37 +00:00
def play ( self , pos : float = 0 ) :
2021-03-15 20:19:45 +00:00
if not self . isLoaded :
2021-02-14 20:10:32 +00:00
return
2021-05-01 22:08:38 +00:00
self . logger . log . info ( " Playing from pos: " + str ( pos ) )
2020-10-30 19:31:18 +00:00
try :
mixer . music . play ( 0 , pos )
self . state . update ( " pos_offset " , pos )
2021-04-08 21:32:16 +00:00
except Exception :
2020-11-03 22:48:11 +00:00
self . logger . log . exception ( " Failed to play at pos: " + str ( pos ) )
2020-10-30 19:31:18 +00:00
return False
self . state . update ( " paused " , False )
2021-04-22 22:00:31 +00:00
self . _potentially_tracklist ( )
2021-04-07 19:14:12 +00:00
self . stopped_manually = False
2020-10-30 19:31:18 +00:00
return True
2020-10-24 20:31:52 +00:00
def pause ( self ) :
2020-10-30 19:31:18 +00:00
try :
2021-04-12 21:59:51 +00:00
mixer . music . stop ( )
2021-04-08 21:32:16 +00:00
except Exception :
2020-11-03 22:48:11 +00:00
self . logger . log . exception ( " Failed to pause. " )
2020-10-30 19:31:18 +00:00
return False
2021-04-07 19:14:12 +00:00
self . stopped_manually = True
2020-10-30 19:31:18 +00:00
self . state . update ( " paused " , True )
return True
2020-10-24 20:31:52 +00:00
def unpause ( self ) :
2020-10-29 22:25:17 +00:00
if not self . isPlaying :
2021-05-02 01:18:00 +00:00
state = self . state . get ( )
position : float = state [ " pos_true " ]
2020-10-29 22:25:17 +00:00
try :
2020-11-03 22:48:11 +00:00
self . play ( position )
2021-04-08 21:32:16 +00:00
except Exception :
2021-04-08 19:53:51 +00:00
self . logger . log . exception (
" Failed to unpause from pos: " + str ( position )
)
2020-10-29 22:25:17 +00:00
return False
2021-04-07 19:14:12 +00:00
2020-10-30 19:31:18 +00:00
self . state . update ( " paused " , False )
2021-05-02 01:18:00 +00:00
# Increment Played count
loaded_item = state [ " loaded_item " ]
if loaded_item :
loaded_item . play_count_increment ( )
self . state . update ( " loaded_item " , loaded_item )
2020-10-29 22:25:17 +00:00
return True
return False
2020-10-24 20:31:52 +00:00
2021-04-12 21:59:51 +00:00
def stop ( self , user_initiated : bool = False ) :
2020-10-30 19:31:18 +00:00
try :
mixer . music . stop ( )
2021-04-08 21:32:16 +00:00
except Exception :
2020-11-03 22:48:11 +00:00
self . logger . log . exception ( " Failed to stop playing. " )
2020-10-30 19:31:18 +00:00
return False
self . state . update ( " paused " , False )
2020-12-20 01:10:19 +00:00
2021-04-22 22:00:31 +00:00
if user_initiated :
self . _potentially_end_tracklist ( )
2021-04-07 19:14:12 +00:00
self . stopped_manually = True
2021-02-14 13:23:51 +00:00
2021-04-18 19:27:54 +00:00
if not self . state . get ( ) [ " loaded_item " ] :
2021-04-12 21:59:51 +00:00
self . logger . log . warning ( " Tried to stop without a loaded item. " )
return True
# This lets users toggle (using the stop button) between cue point and 0.
if user_initiated and not self . isCued :
# if there's a cue point ant we're not at it, go there.
2021-04-18 19:27:54 +00:00
self . seek ( self . state . get ( ) [ " loaded_item " ] . cue )
2021-04-12 21:59:51 +00:00
else :
# Otherwise, let's go to 0.
2021-04-24 18:32:43 +00:00
self . seek ( 0 )
2021-04-12 21:59:51 +00:00
2020-10-30 19:31:18 +00:00
return True
2020-10-24 20:31:52 +00:00
2020-12-19 14:57:37 +00:00
def seek ( self , pos : float ) - > bool :
2021-05-01 22:08:38 +00:00
self . logger . log . info ( " Seeking to pos: " + str ( pos ) )
2020-10-29 22:25:17 +00:00
if self . isPlaying :
try :
2020-10-30 19:31:18 +00:00
self . play ( pos )
2021-04-08 21:32:16 +00:00
except Exception :
2020-11-03 22:48:11 +00:00
self . logger . log . exception ( " Failed to seek to pos: " + str ( pos ) )
2020-10-29 22:25:17 +00:00
return False
return True
2020-10-30 19:31:18 +00:00
else :
2021-05-01 22:08:38 +00:00
self . logger . log . debug ( " Not playing during seek, setting pos state for next play. " )
2021-04-12 21:59:51 +00:00
self . stopped_manually = True # Don't trigger _ended() on seeking.
2021-04-24 18:32:43 +00:00
if pos > 0 :
self . state . update ( " paused " , True )
2020-10-30 19:31:18 +00:00
self . _updateState ( pos = pos )
return True
2020-10-24 20:31:52 +00:00
2021-02-14 17:53:28 +00:00
def set_auto_advance ( self , message : bool ) - > bool :
self . state . update ( " auto_advance " , message )
return True
2020-11-03 01:07:25 +00:00
def set_repeat ( self , message : str ) - > bool :
2021-02-14 17:53:28 +00:00
if message in [ " all " , " one " , " none " ] :
2020-11-03 01:07:25 +00:00
self . state . update ( " repeat " , message )
return True
else :
return False
2021-02-14 17:53:28 +00:00
def set_play_on_load ( self , message : bool ) - > bool :
self . state . update ( " play_on_load " , message )
return True
2020-11-03 01:07:25 +00:00
2020-11-15 19:34:13 +00:00
# Show Plan Related Methods
2021-02-14 00:29:47 +00:00
def get_plan ( self , message : int ) :
2021-04-18 02:14:14 +00:00
plan = sync ( self . api . get_showplan ( message ) )
2021-02-14 00:29:47 +00:00
self . clear_channel_plan ( )
2021-04-18 19:27:54 +00:00
channel = self . state . get ( ) [ " channel " ]
2021-04-25 16:23:41 +00:00
self . logger . log . debug ( plan )
if not isinstance ( plan , dict ) :
return False
if str ( channel ) in plan . keys ( ) :
2021-02-14 00:29:47 +00:00
for plan_item in plan [ str ( channel ) ] :
try :
2021-02-14 20:10:32 +00:00
self . add_to_plan ( plan_item )
except Exception as e :
2021-04-08 19:53:51 +00:00
self . logger . log . critical (
" Failed to add item to show plan: {} " . format ( e )
)
2021-02-14 00:29:47 +00:00
continue
return True
2021-04-24 20:33:36 +00:00
def _check_ghosts ( self , item : PlanItem ) :
if isinstance ( item . timeslotitemid , str ) and item . timeslotitemid . startswith ( " I " ) :
# Kinda a bodge for the moment, each "Ghost" (item which is not saved in the database showplan yet) needs to have a unique temporary item.
# To do this, we'll start with the channel number the item was originally added to (to stop items somehow simultaneously added to different channels from having the same id)
# And chuck in the unix epoch in ns for good measure.
item . timeslotitemid = " GHOST- {} - {} " . format ( self . state . get ( ) [ " channel " ] , time . time_ns ( ) )
return item
2021-05-01 23:45:03 +00:00
# TODO Allow just moving an item inside the channel instead of removing and adding.
2020-12-19 14:57:37 +00:00
def add_to_plan ( self , new_item : Dict [ str , Any ] ) - > bool :
2021-03-13 22:32:04 +00:00
new_item_obj = PlanItem ( new_item )
2021-04-24 20:33:36 +00:00
new_item_obj = self . _check_ghosts ( new_item_obj )
2021-04-18 19:27:54 +00:00
plan_copy : List [ PlanItem ] = copy . copy ( self . state . get ( ) [ " show_plan " ] )
2021-03-13 22:32:04 +00:00
# Shift any plan items after the new position down one to make space.
for item in plan_copy :
if item . weight > = new_item_obj . weight :
item . weight + = 1
2021-04-08 19:53:51 +00:00
plan_copy + = [ new_item_obj ] # Add the new item.
2021-03-13 22:32:04 +00:00
2021-05-19 23:31:44 +00:00
self . _fix_and_update_weights ( plan_copy )
2021-05-01 23:45:03 +00:00
loaded_item = self . state . get ( ) [ " loaded_item " ]
if loaded_item :
2021-05-19 23:31:44 +00:00
# Right. So this may be confusing.
# So... If the user has just moved the loaded item in the channel (by removing above and readding)
# Then we want to re-associate the loaded_item object reference with the new one.
# The loaded item object before this change is now an ophan, which was kept around while the loaded item was potentially moved to another channel.
2021-05-01 23:45:03 +00:00
if loaded_item . timeslotitemid == new_item_obj . timeslotitemid :
2021-05-19 23:31:44 +00:00
self . state . update ( " loaded_item " , new_item_obj )
# NOPE NOPE NOPE
# THIS IS AN EXAMPLE OF WHAT NOT TO DO!
# ONCE AGAIN, THE LOADED ITEM IS THE SAME OBJECT INSTANCE AS THE ONE IN THE SHOW PLAN (AS LONG AS IT HASN'T BEEN RE/MOVED)
## loaded_item.weight = new_item_obj.weight
2021-05-01 23:45:03 +00:00
# Bump the loaded_item's weight if we just added a new item above it.
2021-05-19 23:31:44 +00:00
##elif loaded_item.weight >= new_item_obj.weight:
## loaded_item.weight += 1
2021-05-01 23:45:03 +00:00
# Else, new weight stays the same.
2021-05-19 23:31:44 +00:00
##else:
## return True
2021-05-01 23:45:03 +00:00
2021-05-19 23:31:44 +00:00
##self.state.update("loaded_item", loaded_item)
2021-05-01 23:45:03 +00:00
2020-11-01 02:35:14 +00:00
return True
2021-02-14 20:10:32 +00:00
def remove_from_plan ( self , weight : int ) - > bool :
2021-04-18 19:27:54 +00:00
plan_copy : List [ PlanItem ] = copy . copy ( self . state . get ( ) [ " show_plan " ] )
2021-05-05 22:06:29 +00:00
found : Optional [ PlanItem ] = None
2021-05-17 21:03:38 +00:00
before = [ ]
for item in plan_copy :
before + = ( item . weight , item . name )
2021-05-19 23:31:44 +00:00
self . logger . log . debug ( " Weights before removing weight {} : \n {} " . format ( weight , before ) )
2021-05-17 21:03:38 +00:00
2020-12-19 14:57:37 +00:00
for i in plan_copy :
2021-02-14 20:10:32 +00:00
if i . weight == weight :
2021-05-05 22:06:29 +00:00
found = i
2020-11-02 23:06:45 +00:00
plan_copy . remove ( i )
2021-05-05 22:06:29 +00:00
2021-03-13 22:32:04 +00:00
if found :
2021-05-19 23:31:44 +00:00
self . _fix_and_update_weights ( plan_copy )
2021-05-01 23:45:03 +00:00
# If we removed the loaded item from this channel, update it's weight
# So we know how/not to autoadvance.
loaded_item = self . state . get ( ) [ " loaded_item " ]
2021-05-05 22:06:29 +00:00
if loaded_item == found :
# Loaded_item is actually the same PlanItem instance as in the show_plan.
# So if it's still in the show plan, we'll have corrected it's weight already.
# If it was removed above, fix_weights won't have done anything
# So we'll want to update the weight.
# We're removing the loaded item from the channel.
2021-05-05 22:36:24 +00:00
#if loaded_item.weight == weight:
2021-05-01 23:45:03 +00:00
loaded_item . weight = - 1
2021-05-05 22:06:29 +00:00
# If loaded_item wasn't the same instance, we'd want to do the below.
2021-05-01 23:45:03 +00:00
# We removed an item above it. Shift it up.
2021-05-05 22:06:29 +00:00
#elif loaded_item.weight > weight:
# loaded_item.weight -= 1
2021-05-01 23:45:03 +00:00
# Else, new weight stays the same.
2021-05-05 22:06:29 +00:00
#else:
# return True
2021-05-01 23:45:03 +00:00
2021-05-05 22:06:29 +00:00
self . state . update ( " loaded_item " , loaded_item )
2021-03-13 22:32:04 +00:00
return True
2020-11-02 23:06:45 +00:00
return False
def clear_channel_plan ( self ) - > bool :
self . state . update ( " show_plan " , [ ] )
return True
2021-02-14 20:10:32 +00:00
def load ( self , weight : int ) :
2020-10-29 22:25:17 +00:00
if not self . isPlaying :
2021-09-01 23:35:03 +00:00
loaded_state = self . state . get ( )
2020-10-30 19:31:18 +00:00
self . unload ( )
2020-10-29 22:25:17 +00:00
2021-09-01 23:35:03 +00:00
self . logger . log . info ( " Resetting output (in case of sound output gone silent somehow) to " + str ( loaded_state [ " output " ] ) )
self . output ( loaded_state [ " output " ] )
showplan = loaded_state [ " show_plan " ]
2020-12-08 19:41:11 +00:00
2020-12-19 14:57:37 +00:00
loaded_item : Optional [ PlanItem ] = None
2020-12-08 19:41:11 +00:00
for i in range ( len ( showplan ) ) :
2021-02-14 20:10:32 +00:00
if showplan [ i ] . weight == weight :
2020-12-08 19:41:11 +00:00
loaded_item = showplan [ i ]
2020-11-01 02:35:14 +00:00
break
2021-04-08 21:05:25 +00:00
if loaded_item is None :
2021-04-08 20:15:15 +00:00
self . logger . log . error (
" Failed to find weight: {} " . format ( weight ) )
2020-11-01 02:35:14 +00:00
return False
2021-04-07 21:52:33 +00:00
reload = False
2021-04-08 21:05:25 +00:00
if loaded_item . filename == " " or loaded_item . filename is None :
2021-04-08 20:15:15 +00:00
self . logger . log . info (
" Filename is not specified, loading from API. " )
2021-04-07 21:52:33 +00:00
reload = True
elif not os . path . exists ( loaded_item . filename ) :
2021-04-08 19:53:51 +00:00
self . logger . log . warn (
" Filename given doesn ' t exist. Re-loading from API. "
)
2021-04-07 21:52:33 +00:00
reload = True
if reload :
2021-04-18 02:14:14 +00:00
loaded_item . filename = sync ( self . api . get_filename ( item = loaded_item ) )
2020-12-08 19:41:11 +00:00
2020-12-19 14:57:37 +00:00
if not loaded_item . filename :
return False
2021-08-16 22:43:09 +00:00
# Swap with a normalised version if it's ready, else returns original.
loaded_item . filename = get_normalised_filename_if_available ( loaded_item . filename )
2020-12-08 19:41:11 +00:00
self . state . update ( " loaded_item " , loaded_item )
2020-10-29 22:25:17 +00:00
2020-12-08 19:41:11 +00:00
for i in range ( len ( showplan ) ) :
2021-02-14 20:10:32 +00:00
if showplan [ i ] . weight == weight :
2020-12-08 19:41:11 +00:00
self . state . update ( " show_plan " , index = i , value = loaded_item )
break
2021-04-12 21:59:51 +00:00
# TODO: Update the show plan filenames???
2020-10-29 22:25:17 +00:00
2021-05-02 14:41:14 +00:00
load_attempt = 0
2021-07-16 22:56:58 +00:00
if not isinstance ( loaded_item . filename , str ) :
return False
2021-05-02 14:41:14 +00:00
while load_attempt < 5 :
load_attempt + = 1
try :
self . logger . log . info ( " Loading file: " +
str ( loaded_item . filename ) )
mixer . music . load ( loaded_item . filename )
except Exception :
# We couldn't load that file.
self . logger . log . exception (
" Couldn ' t load file: " + str ( loaded_item . filename )
)
time . sleep ( 1 )
continue # Try loading again.
2020-10-29 22:25:17 +00:00
2021-05-02 14:41:14 +00:00
if not self . isLoaded :
self . logger . log . error ( " Pygame loaded file without error, but never actually loaded. " )
time . sleep ( 1 )
continue # Try loading again.
try :
2021-07-16 22:56:58 +00:00
if loaded_item . filename . endswith ( " .mp3 " ) :
2021-05-02 14:41:14 +00:00
song = MP3 ( loaded_item . filename )
self . state . update ( " length " , song . info . length )
else :
2021-07-16 22:56:58 +00:00
# WARNING! Pygame / SDL can't seek .wav files :/
2021-05-02 14:41:14 +00:00
self . state . update (
" length " , mixer . Sound (
loaded_item . filename ) . get_length ( ) / 1000
)
except Exception :
self . logger . log . exception (
" Failed to update the length of item. " )
time . sleep ( 1 )
continue # Try loading again.
# Everything worked, we made it!
if loaded_item . cue > 0 :
self . seek ( loaded_item . cue )
2020-10-29 22:25:17 +00:00
else :
2021-05-02 14:41:14 +00:00
self . seek ( 0 )
2021-02-14 13:23:51 +00:00
2021-05-02 14:41:14 +00:00
if self . state . get ( ) [ " play_on_load " ] :
self . unpause ( )
2021-04-12 21:59:51 +00:00
2021-05-02 14:41:14 +00:00
return True
2021-02-14 13:23:51 +00:00
2021-05-02 14:41:14 +00:00
self . logger . log . error ( " Failed to load track after numerous retries. " )
return False
return False
2020-10-29 22:25:17 +00:00
def unload ( self ) :
if not self . isPlaying :
try :
mixer . music . unload ( )
2020-10-30 19:31:18 +00:00
self . state . update ( " paused " , False )
2020-11-01 02:35:14 +00:00
self . state . update ( " loaded_item " , None )
2021-04-08 21:32:16 +00:00
except Exception :
2020-11-03 22:48:11 +00:00
self . logger . log . exception ( " Failed to unload channel. " )
2020-10-29 22:25:17 +00:00
return False
2021-04-22 22:00:31 +00:00
self . _potentially_end_tracklist ( )
# If we unloaded successfully, reset the tracklist_id, ready for the next item.
if not self . isLoaded :
self . state . update ( " tracklist_id " , None )
2020-10-29 22:25:17 +00:00
return not self . isLoaded
2020-10-24 20:31:52 +00:00
def quit ( self ) :
2020-11-03 22:48:11 +00:00
try :
mixer . quit ( )
self . state . update ( " paused " , False )
2021-04-04 21:34:46 +00:00
self . logger . log . info ( " Quit mixer. " )
2021-04-08 21:32:16 +00:00
except Exception :
2020-11-03 22:48:11 +00:00
self . logger . log . exception ( " Failed to quit mixer. " )
2020-10-24 20:31:52 +00:00
2020-12-19 14:57:37 +00:00
def output ( self , name : Optional [ str ] = None ) :
2021-04-18 19:27:54 +00:00
wasPlaying = self . state . get ( ) [ " playing " ]
2021-04-24 21:03:54 +00:00
oldPos = self . state . get ( ) [ " pos_true " ]
2021-04-07 19:14:39 +00:00
name = None if ( not name or name . lower ( ) == " none " ) else name
2020-11-05 18:59:28 +00:00
2020-10-24 20:31:52 +00:00
self . quit ( )
2020-10-30 00:32:34 +00:00
self . state . update ( " output " , name )
2020-10-24 20:31:52 +00:00
try :
if name :
2020-11-01 03:27:26 +00:00
mixer . init ( 44100 , - 16 , 2 , 1024 , devicename = name )
2020-10-24 20:31:52 +00:00
else :
2020-11-01 03:27:26 +00:00
mixer . init ( 44100 , - 16 , 2 , 1024 )
2021-04-08 21:32:16 +00:00
except Exception :
2021-04-08 19:53:51 +00:00
self . logger . log . exception (
" Failed to init mixer with device name: " + str ( name )
)
2020-10-30 00:32:34 +00:00
return False
2020-10-24 20:31:52 +00:00
2021-04-18 19:27:54 +00:00
loadedItem = self . state . get ( ) [ " loaded_item " ]
2021-04-08 19:53:51 +00:00
if loadedItem :
2021-05-01 22:08:38 +00:00
self . logger . log . info ( " Reloading after output change. " )
2021-02-14 20:10:32 +00:00
self . load ( loadedItem . weight )
2020-11-01 03:19:21 +00:00
if wasPlaying :
2021-05-01 22:08:38 +00:00
self . logger . log . info ( " Resuming playback after output change. " )
2021-04-24 21:03:54 +00:00
self . play ( oldPos )
2020-11-01 03:19:21 +00:00
2020-10-30 00:32:34 +00:00
return True
2020-10-24 20:31:52 +00:00
2021-04-17 17:27:36 +00:00
# Timeslotitemid can be a ghost (un-submitted item), so may be "IXXX"
def set_marker ( self , timeslotitemid : str , marker_str : str ) :
2021-04-10 21:56:53 +00:00
set_loaded = False
success = True
try :
marker = Marker ( marker_str )
except Exception as e :
self . logger . log . error ( " Failed to create Marker instance with {} {} : {} " . format ( timeslotitemid , marker_str , e ) )
return False
2021-04-17 17:27:36 +00:00
if timeslotitemid == " -1 " :
2021-04-10 21:56:53 +00:00
set_loaded = True
if not self . isLoaded :
return False
2021-04-18 19:27:54 +00:00
timeslotitemid = self . state . get ( ) [ " loaded_item " ] . timeslotitemid
2021-04-26 00:01:58 +00:00
elif self . isLoaded and self . state . get ( ) [ " loaded_item " ] . timeslotitemid == timeslotitemid :
set_loaded = True
2021-04-10 21:56:53 +00:00
2021-04-18 19:27:54 +00:00
plan_copy : List [ PlanItem ] = copy . copy ( self . state . get ( ) [ " show_plan " ] )
for i in range ( len ( self . state . get ( ) [ " show_plan " ] ) ) :
2021-04-12 21:59:51 +00:00
item = plan_copy [ i ]
2021-04-10 21:56:53 +00:00
2021-04-17 17:27:36 +00:00
if str ( item . timeslotitemid ) == str ( timeslotitemid ) :
2021-04-10 21:56:53 +00:00
try :
2021-04-12 21:59:51 +00:00
new_item = item . set_marker ( marker )
self . state . update ( " show_plan " , new_item , index = i )
2021-04-10 21:56:53 +00:00
except Exception as e :
self . logger . log . error (
" Failed to set marker on item {} : {} with marker \n {} " . format ( timeslotitemid , e , marker ) )
success = False
if set_loaded :
try :
2021-04-18 19:27:54 +00:00
self . state . update ( " loaded_item " , self . state . get ( ) [ " loaded_item " ] . set_marker ( marker ) )
2021-04-10 21:56:53 +00:00
except Exception as e :
self . logger . log . error (
" Failed to set marker on loaded_item {} : {} with marker \n {} " . format ( timeslotitemid , e , marker ) )
success = False
return success
2021-09-03 20:49:16 +00:00
def set_played ( self , weight : int , played : bool ) :
2021-05-02 01:18:00 +00:00
plan : List [ PlanItem ] = self . state . get ( ) [ " show_plan " ]
if weight == - 1 :
for item in plan :
2021-09-03 20:49:16 +00:00
item . play_count_increment ( ) if played else item . play_count_reset ( )
2021-05-02 01:18:00 +00:00
self . state . update ( " show_plan " , plan )
elif len ( plan ) > weight :
2021-09-03 20:49:16 +00:00
plan [ weight ] . play_count_increment ( ) if played else plan [ weight ] . play_count_reset ( )
2021-05-02 01:18:00 +00:00
self . state . update ( " show_plan " , plan [ weight ] , weight )
else :
return False
return True
2021-06-20 23:22:29 +00:00
# Tells the player that the fader is live on-air, so it can tell tracklisting from PFL
def set_live ( self , live : bool ) :
2021-06-22 17:20:18 +00:00
2021-06-20 23:22:29 +00:00
self . state . update ( " live " , live )
# If we're going to live (potentially from not live/PFL), potentially tracklist if it's playing.
if ( live ) :
self . _potentially_tracklist ( )
return True
2021-05-02 01:18:00 +00:00
2021-04-10 21:56:53 +00:00
# Helper functions
2021-04-22 22:00:31 +00:00
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
def _potentially_tracklist ( self ) :
2021-04-24 20:33:36 +00:00
mode = self . state . get ( ) [ " tracklist_mode " ]
2021-04-22 22:00:31 +00:00
time : int = - 1
2021-06-20 23:22:29 +00:00
if mode in [ " on " , " fader-live " ] :
2021-04-22 22:00:31 +00:00
time = 1 # Let's do it pretty quickly.
elif mode == " delayed " :
# Let's do it in a bit, once we're sure it's been playing. (Useful if we've got no idea if it's live or cueing.)
time = TRACKLISTING_DELAYED_S
if time > = 0 and not self . tracklist_start_timer :
self . logger . log . info ( " Setting timer for tracklisting in {} secs due to Mode: {} " . format ( time , mode ) )
self . tracklist_start_timer = Timer ( time , self . _tracklist_start )
self . tracklist_start_timer . start ( )
elif self . tracklist_start_timer :
self . logger . log . error ( " Failed to potentially tracklist, timer already busy. " )
# This essentially allows the tracklist end API call to happen in a separate thread, to avoid hanging playout/loading.
def _potentially_end_tracklist ( self ) :
2021-05-01 22:09:59 +00:00
if self . tracklist_start_timer :
self . logger . log . info ( " A tracklist start timer was running, cancelling. " )
self . tracklist_start_timer . cancel ( )
self . tracklist_start_timer = None
2021-05-02 01:18:00 +00:00
# Decrement Played count on track we didn't play much of.
state = self . state . get ( )
loaded_item = state [ " loaded_item " ]
if loaded_item and loaded_item . type == " central " :
loaded_item . play_count_decrement ( )
self . state . update ( " loaded_item " , loaded_item )
2021-04-22 22:00:31 +00:00
# Make a copy of the tracklist_id, it will get reset as we load the next item.
tracklist_id = self . state . get ( ) [ " tracklist_id " ]
2021-04-23 20:12:31 +00:00
if not tracklist_id :
self . logger . log . info ( " No tracklist to end. " )
return
2021-09-01 22:40:28 +00:00
self . logger . log . info ( " Setting timer for ending tracklist_id ' {} ' " . format ( tracklist_id ) )
2021-04-22 22:00:31 +00:00
if tracklist_id :
2021-09-01 22:40:28 +00:00
self . logger . log . info ( " Attempting to end tracklist_id ' {} ' " . format ( tracklist_id ) )
2021-04-22 22:00:31 +00:00
if self . tracklist_end_timer :
self . logger . log . error ( " Failed to potentially end tracklist, timer already busy. " )
return
2021-09-01 22:23:50 +00:00
self . state . update ( " tracklist_id " , None )
2021-04-22 22:00:31 +00:00
# This threads it, so it won't hang track loading if it fails.
self . tracklist_end_timer = Timer ( 1 , self . _tracklist_end , [ tracklist_id ] )
self . tracklist_end_timer . start ( )
else :
self . logger . log . warning ( " Failed to potentially end tracklist, no tracklist started. " )
def _tracklist_start ( self ) :
2021-06-20 23:22:29 +00:00
state = self . state . get ( )
loaded_item = state [ " loaded_item " ]
2021-04-22 22:00:31 +00:00
if not loaded_item :
self . logger . log . error ( " Tried to call _tracklist_start() with no loaded item! " )
2021-09-01 22:23:50 +00:00
elif not self . isPlaying :
2021-06-20 23:22:29 +00:00
self . logger . log . info ( " Not tracklisting since not playing. " )
2021-04-22 22:00:31 +00:00
else :
2021-09-01 22:23:50 +00:00
tracklist_id = state [ " tracklist_id " ]
if ( not tracklist_id ) :
if ( state [ " tracklist_mode " ] == " fader-live " and not state [ " live " ] ) :
self . logger . log . info ( " Not tracklisting since fader is not live. " )
else :
2021-09-01 22:40:28 +00:00
self . logger . log . info ( " Tracklisting item: ' {} ' " . format ( loaded_item . name ) )
2021-09-01 22:23:50 +00:00
tracklist_id = self . api . post_tracklist_start ( loaded_item )
if not tracklist_id :
2021-09-01 22:40:28 +00:00
self . logger . log . warning ( " Failed to tracklist ' {} ' " . format ( loaded_item . name ) )
2021-09-01 22:23:50 +00:00
else :
2021-09-01 22:40:28 +00:00
self . logger . log . info ( " Tracklist id: ' {} ' " . format ( tracklist_id ) )
2021-09-01 22:23:50 +00:00
self . state . update ( " tracklist_id " , tracklist_id )
else :
2021-09-01 22:40:28 +00:00
self . logger . log . info ( " Not tracklisting item ' {} ' , already got tracklistid: ' {} ' " . format (
2021-09-01 22:23:50 +00:00
loaded_item . name , tracklist_id ) )
# No matter what we end up doing, we need to kill this timer so future ones can run.
2021-04-22 22:00:31 +00:00
self . tracklist_start_timer = None
def _tracklist_end ( self , tracklist_id ) :
if tracklist_id :
2021-09-01 22:40:28 +00:00
self . logger . log . info ( " Attempting to end tracklist_id ' {} ' " . format ( tracklist_id ) )
2021-04-22 22:00:31 +00:00
self . api . post_tracklist_end ( tracklist_id )
else :
self . logger . log . error ( " Tracklist_id to _tracklist_end() missing. Failed to end tracklist. " )
self . tracklist_end_timer = None
2021-04-10 21:56:53 +00:00
def _ended ( self ) :
2021-04-22 22:00:31 +00:00
self . _potentially_end_tracklist ( )
2021-05-01 23:45:03 +00:00
state = self . state . get ( )
loaded_item = state [ " loaded_item " ]
2021-02-14 00:29:47 +00:00
2021-04-12 21:59:51 +00:00
if not loaded_item :
return
2021-02-14 00:29:47 +00:00
# Track has ended
2021-05-01 22:08:38 +00:00
self . logger . log . info ( " Playback ended of {} , weight {} : " . format ( loaded_item . name , loaded_item . weight ) )
2021-02-14 00:29:47 +00:00
# Repeat 1
2021-04-07 19:14:12 +00:00
# TODO ENUM
2021-05-01 23:45:03 +00:00
if state [ " repeat " ] == " one " :
2021-02-14 00:29:47 +00:00
self . play ( )
2021-04-07 19:14:12 +00:00
return
2021-02-14 00:29:47 +00:00
# Auto Advance
2021-05-01 23:45:03 +00:00
if state [ " auto_advance " ] :
# Check for loaded item in show plan.
# If it's been removed, weight will be -1.
# Just stop in this case.
if loaded_item . weight < 0 :
self . logger . log . debug ( " Loaded item is no longer in channel (weight {} ), not auto advancing. " . format ( loaded_item . weight ) )
else :
self . logger . log . debug ( " Found current loaded item in this channel show plan. Auto Advancing. " )
# If there's another item after this one, load that.
if len ( state [ " show_plan " ] ) > loaded_item . weight + 1 :
self . load ( loaded_item . weight + 1 )
return
# Repeat All (Jump to top again)
# TODO ENUM
elif state [ " repeat " ] == " all " :
self . load ( 0 ) # Jump to the top.
return
2021-02-14 13:23:51 +00:00
2021-04-07 19:14:12 +00:00
# No automations, just stop playing.
self . stop ( )
2021-05-01 23:45:03 +00:00
self . _retAll ( " STOPPED " ) # Tell clients that we've stopped playing.
2021-02-14 00:29:47 +00:00
2020-12-19 14:57:37 +00:00
def _updateState ( self , pos : Optional [ float ] = None ) :
2020-11-04 01:19:56 +00:00
2020-10-30 00:32:34 +00:00
self . state . update ( " initialised " , self . isInit )
if self . isInit :
2021-04-24 18:32:43 +00:00
if pos is not None :
2021-04-24 20:31:35 +00:00
# Seeking sets the position like this when not playing.
2021-04-24 18:32:43 +00:00
self . state . update ( " pos " , pos ) # Reset back to 0 if stopped.
self . state . update ( " pos_offset " , 0 )
2020-11-04 01:19:56 +00:00
elif self . isPlaying :
2021-04-24 18:32:43 +00:00
# This is the bit that makes the time actually progress during playback.
2020-10-30 19:31:18 +00:00
# Get one last update in, incase we're about to pause/stop it.
2021-04-08 19:53:51 +00:00
self . state . update ( " pos " , max ( 0 , mixer . music . get_pos ( ) / 1000 ) )
2021-04-07 19:14:12 +00:00
2021-04-22 22:00:31 +00:00
# If the state is changing from playing to not playing, and the user didn't stop it, the item must have ended.
2021-04-08 19:53:51 +00:00
if (
2021-04-18 19:27:54 +00:00
self . state . get ( ) [ " playing " ]
2021-04-08 19:53:51 +00:00
and not self . isPlaying
and not self . stopped_manually
) :
2021-04-10 21:56:53 +00:00
self . _ended ( )
2021-04-07 19:14:12 +00:00
2020-10-30 00:32:34 +00:00
self . state . update ( " playing " , self . isPlaying )
self . state . update ( " loaded " , self . isLoaded )
2020-10-30 19:31:18 +00:00
2021-04-08 19:53:51 +00:00
self . state . update (
" pos_true " ,
min (
2021-04-18 19:27:54 +00:00
self . state . get ( ) [ " length " ] ,
self . state . get ( ) [ " pos " ] + self . state . get ( ) [ " pos_offset " ] ,
2021-04-08 19:53:51 +00:00
) ,
)
2020-10-30 19:31:18 +00:00
2021-04-08 19:53:51 +00:00
self . state . update (
" remaining " ,
2021-04-18 19:27:54 +00:00
max ( 0 , ( self . state . get ( ) [ " length " ] -
self . state . get ( ) [ " pos_true " ] ) ) ,
2021-04-08 19:53:51 +00:00
)
2020-10-30 00:32:34 +00:00
2021-02-14 00:29:47 +00:00
def _ping_times ( self ) :
2021-03-21 13:06:09 +00:00
UPDATES_FREQ_SECS = 0.2
2021-04-08 19:53:51 +00:00
if (
2021-04-08 21:05:25 +00:00
self . last_time_update is None
2021-04-08 19:53:51 +00:00
or self . last_time_update + UPDATES_FREQ_SECS < time . time ( )
) :
2021-02-14 00:29:47 +00:00
self . last_time_update = time . time ( )
2021-04-18 19:27:54 +00:00
self . _retAll ( " POS: " + str ( self . state . get ( ) [ " pos_true " ] ) )
2020-11-03 01:07:25 +00:00
2021-03-22 00:33:14 +00:00
def _retAll ( self , msg ) :
2021-05-01 23:45:03 +00:00
if self . out_q :
self . out_q . put ( " ALL: " + msg )
2020-11-03 01:07:25 +00:00
2021-04-08 19:53:51 +00:00
def _retMsg (
self , msg : Any , okay_str : bool = False , custom_prefix : Optional [ str ] = None
) :
2021-03-22 00:33:14 +00:00
# Make sure to add the message source back, so that it can be sent to the correct destination in the main server.
if custom_prefix :
response = custom_prefix
else :
response = " {} : {} : " . format ( self . last_msg_source , self . last_msg )
2021-04-08 21:05:25 +00:00
if msg is True :
2020-10-30 00:32:34 +00:00
response + = " OKAY "
elif isinstance ( msg , str ) :
if okay_str :
response + = " OKAY: " + msg
else :
response + = " FAIL: " + msg
2020-10-24 20:31:52 +00:00
else :
2020-10-30 00:32:34 +00:00
response + = " FAIL "
2021-05-01 22:08:38 +00:00
2020-10-30 00:32:34 +00:00
if self . out_q :
2021-05-01 22:08:38 +00:00
if ( " STATUS: " not in response ) :
# Don't fill logs with status pushes, it's a mess.
self . logger . log . debug ( ( " Sending: {} " . format ( response ) ) )
2020-10-30 00:32:34 +00:00
self . out_q . put ( response )
2021-05-01 22:08:38 +00:00
else :
self . logger . log . exception ( " Message return Queue is missing!!!! Can ' t send message. " )
2020-10-24 20:31:52 +00:00
2021-02-14 13:57:07 +00:00
def _send_status ( self ) :
2021-03-22 00:33:14 +00:00
# TODO This is hacky
2021-04-08 20:15:15 +00:00
self . _retMsg ( str ( self . status ) , okay_str = True ,
custom_prefix = " ALL:STATUS: " )
2021-02-14 13:57:07 +00:00
2021-05-19 23:31:44 +00:00
def _fix_and_update_weights ( self , plan ) :
2021-04-24 20:33:36 +00:00
def _sort_weight ( e : PlanItem ) :
return e . weight
2021-05-05 22:06:29 +00:00
before = [ ]
for item in plan :
before + = ( item . weight , item . name )
self . logger . log . debug ( " Weights before fixing: \n {} " . format ( before ) )
2021-04-24 20:33:36 +00:00
plan . sort ( key = _sort_weight ) # Sort into weighted order.
2021-05-05 22:06:29 +00:00
sorted = [ ]
for item in plan :
sorted + = ( item . weight , item . name )
self . logger . log . debug ( " Weights after sorting: \n {} " . format ( sorted ) )
2021-04-24 20:33:36 +00:00
for i in range ( len ( plan ) ) :
plan [ i ] . weight = i # Recorrect the weights on the channel.
2021-05-05 22:06:29 +00:00
fixed = [ ]
for item in plan :
fixed + = ( item . weight , item . name )
self . logger . log . debug ( " Weights after sorting: \n {} " . format ( fixed ) )
2021-05-19 23:31:44 +00:00
self . state . update ( " show_plan " , plan )
2021-04-24 20:33:36 +00:00
2021-04-08 19:53:51 +00:00
def __init__ (
2021-04-22 22:00:31 +00:00
self , channel : int , in_q : multiprocessing . Queue , out_q : multiprocessing . Queue , server_state : StateManager
2021-04-08 19:53:51 +00:00
) :
2020-10-30 00:32:34 +00:00
2020-10-30 23:14:29 +00:00
process_title = " Player: Channel " + str ( channel )
setproctitle . setproctitle ( process_title )
multiprocessing . current_process ( ) . name = process_title
2020-10-24 20:31:52 +00:00
2020-10-30 22:06:03 +00:00
self . running = True
self . out_q = out_q
2020-10-24 20:31:52 +00:00
2021-05-26 23:23:49 +00:00
self . logger = LoggingManager ( " Player " + str ( channel ) , debug = package . build_beta )
2020-10-30 22:06:03 +00:00
2021-04-22 22:00:31 +00:00
self . api = MyRadioAPI ( self . logger , server_state )
2021-02-14 00:29:47 +00:00
2021-04-08 19:53:51 +00:00
self . state = StateManager (
" Player " + str ( channel ) ,
self . logger ,
self . __default_state ,
self . __rate_limited_params ,
)
2021-02-14 13:57:07 +00:00
self . state . add_callback ( self . _send_status )
2020-10-30 23:59:58 +00:00
self . state . update ( " channel " , channel )
2021-04-22 22:00:31 +00:00
self . state . update ( " tracklist_mode " , server_state . get ( ) [ " tracklist_mode " ] )
2021-06-20 23:22:29 +00:00
self . state . update ( " live " , True ) # Channel is live until controller says it isn't.
2020-10-30 23:59:58 +00:00
2021-04-24 20:33:36 +00:00
# Just in case there's any weights somehow messed up, let's fix them.
plan_copy : List [ PlanItem ] = copy . copy ( self . state . get ( ) [ " show_plan " ] )
2021-05-19 23:31:44 +00:00
self . _fix_and_update_weights ( plan_copy )
2021-04-24 20:33:36 +00:00
2020-10-24 20:31:52 +00:00
loaded_state = copy . copy ( self . state . state )
if loaded_state [ " output " ] :
2021-04-08 20:15:15 +00:00
self . logger . log . info ( " Setting output to: " +
str ( loaded_state [ " output " ] ) )
2020-10-24 20:31:52 +00:00
self . output ( loaded_state [ " output " ] )
else :
2020-10-30 22:06:03 +00:00
self . logger . log . info ( " Using default output device. " )
2020-10-24 20:31:52 +00:00
self . output ( )
2020-12-19 14:57:37 +00:00
loaded_item = loaded_state [ " loaded_item " ]
if loaded_item :
2021-05-01 22:09:13 +00:00
# No need to load on init, the output switch does this, as it would for regular output switching.
#self.load(loaded_item.weight)
2020-10-24 20:31:52 +00:00
2021-04-12 21:59:51 +00:00
# Load may jump to the cue point, as it would do on a regular load.
# If we were at a different state before, we have to override it now.
2020-10-30 19:31:18 +00:00
if loaded_state [ " pos_true " ] != 0 :
2021-04-08 19:53:51 +00:00
self . logger . log . info (
" Seeking to pos_true: " + str ( loaded_state [ " pos_true " ] )
)
2020-10-30 19:31:18 +00:00
self . seek ( loaded_state [ " pos_true " ] )
2020-10-24 20:31:52 +00:00
2021-04-08 21:05:25 +00:00
if loaded_state [ " playing " ] is True :
2021-05-01 22:08:38 +00:00
self . logger . log . info ( " Resuming playback on init. " )
2021-04-12 21:59:51 +00:00
self . unpause ( ) # Use un-pause as we don't want to jump to a new position.
2020-10-29 21:23:37 +00:00
else :
2021-05-01 22:08:38 +00:00
self . logger . log . info ( " No file was previously loaded to resume. " )
2020-10-24 20:31:52 +00:00
2021-04-04 21:34:46 +00:00
try :
while self . running :
2021-04-04 22:14:08 +00:00
time . sleep ( 0.02 )
2021-04-04 21:34:46 +00:00
self . _updateState ( )
self . _ping_times ( )
2020-10-29 21:23:37 +00:00
try :
2021-03-22 00:33:14 +00:00
message = in_q . get_nowait ( )
source = message . split ( " : " ) [ 0 ]
if source not in VALID_MESSAGE_SOURCES :
self . last_msg_source = " "
self . last_msg = " "
2021-04-08 19:53:51 +00:00
self . logger . log . warn (
2021-04-08 20:15:15 +00:00
" Message from unknown sender source: {} " . format (
source )
2021-04-08 19:53:51 +00:00
)
2021-03-22 00:33:14 +00:00
continue
self . last_msg_source = source
self . last_msg = message . split ( " : " , 1 ) [ 1 ]
2021-04-17 17:27:36 +00:00
self . logger . log . debug (
2021-04-08 19:53:51 +00:00
" Recieved message from source {} : {} " . format (
self . last_msg_source , self . last_msg
)
)
2020-10-29 21:23:37 +00:00
except Empty :
# The incomming message queue was empty,
# skip message processing
pass
else :
2020-10-30 00:32:34 +00:00
2020-10-29 21:23:37 +00:00
# We got a message.
2020-10-29 22:25:17 +00:00
2020-10-30 00:32:34 +00:00
# Output re-inits the mixer, so we can do this any time.
2021-04-08 19:53:51 +00:00
if self . last_msg . startswith ( " OUTPUT " ) :
2020-10-30 00:32:34 +00:00
split = self . last_msg . split ( " : " )
self . _retMsg ( self . output ( split [ 1 ] ) )
elif self . isInit :
2021-04-08 19:53:51 +00:00
message_types : Dict [
str , Callable [ . . . , Any ]
] = { # TODO Check Types
2021-02-14 00:29:47 +00:00
" STATUS " : lambda : self . _retMsg ( self . status , True ) ,
2020-11-03 01:07:25 +00:00
# Audio Playout
2021-04-12 21:59:51 +00:00
# Unpause, so we don't jump to 0, we play from the current pos.
" PLAY " : lambda : self . _retMsg ( self . unpause ( ) ) ,
2020-11-15 19:34:13 +00:00
" PAUSE " : lambda : self . _retMsg ( self . pause ( ) ) ,
2021-05-02 12:52:47 +00:00
" PLAYPAUSE " : lambda : self . _retMsg ( self . unpause ( ) if not self . isPlaying else self . pause ( ) ) , # For the hardware controller.
2020-11-15 19:34:13 +00:00
" UNPAUSE " : lambda : self . _retMsg ( self . unpause ( ) ) ,
2021-04-12 21:59:51 +00:00
" STOP " : lambda : self . _retMsg ( self . stop ( user_initiated = True ) ) ,
2021-04-08 19:53:51 +00:00
" SEEK " : lambda : self . _retMsg (
self . seek ( float ( self . last_msg . split ( " : " ) [ 1 ] ) )
) ,
" AUTOADVANCE " : lambda : self . _retMsg (
self . set_auto_advance (
( self . last_msg . split ( " : " ) [ 1 ] == " True " )
)
) ,
" REPEAT " : lambda : self . _retMsg (
self . set_repeat ( self . last_msg . split ( " : " ) [ 1 ] )
) ,
" PLAYONLOAD " : lambda : self . _retMsg (
self . set_play_on_load (
( self . last_msg . split ( " : " ) [ 1 ] == " True " )
)
) ,
2020-11-03 01:07:25 +00:00
# Show Plan Items
2021-09-03 20:49:16 +00:00
" GETPLAN " : lambda : self . _retMsg (
2021-04-08 19:53:51 +00:00
self . get_plan ( int ( self . last_msg . split ( " : " ) [ 1 ] ) )
) ,
" LOAD " : lambda : self . _retMsg (
self . load ( int ( self . last_msg . split ( " : " ) [ 1 ] ) )
) ,
2020-11-15 19:34:13 +00:00
" LOADED? " : lambda : self . _retMsg ( self . isLoaded ) ,
" UNLOAD " : lambda : self . _retMsg ( self . unload ( ) ) ,
2021-04-08 19:53:51 +00:00
" ADD " : lambda : self . _retMsg (
self . add_to_plan (
2021-04-08 20:15:15 +00:00
json . loads (
" : " . join ( self . last_msg . split ( " : " ) [ 1 : ] ) )
2021-04-08 19:53:51 +00:00
)
) ,
" REMOVE " : lambda : self . _retMsg (
2021-04-08 20:15:15 +00:00
self . remove_from_plan (
int ( self . last_msg . split ( " : " ) [ 1 ] ) )
2021-04-08 19:53:51 +00:00
) ,
" CLEAR " : lambda : self . _retMsg ( self . clear_channel_plan ( ) ) ,
2021-04-17 17:27:36 +00:00
" SETMARKER " : lambda : self . _retMsg ( self . set_marker ( self . last_msg . split ( " : " ) [ 1 ] , self . last_msg . split ( " : " , 2 ) [ 2 ] ) ) ,
2021-09-03 20:49:16 +00:00
" RESETPLAYED " : lambda : self . _retMsg ( self . set_played ( weight = int ( self . last_msg . split ( " : " ) [ 1 ] ) , played = False ) ) ,
" SETPLAYED " : lambda : self . _retMsg ( self . set_played ( weight = int ( self . last_msg . split ( " : " ) [ 1 ] ) , played = True ) ) ,
2021-06-22 17:20:18 +00:00
" SETLIVE " : lambda : self . _retMsg ( self . set_live ( self . last_msg . split ( " : " ) [ 1 ] == " True " ) ) ,
2020-11-01 02:37:56 +00:00
}
2020-11-03 00:32:43 +00:00
message_type : str = self . last_msg . split ( " : " ) [ 0 ]
if message_type in message_types . keys ( ) :
message_types [ message_type ] ( )
2020-10-30 00:32:34 +00:00
2021-04-08 19:53:51 +00:00
elif self . last_msg == " QUIT " :
2021-04-05 23:32:58 +00:00
self . _retMsg ( True )
2020-10-29 21:23:37 +00:00
self . running = False
2020-10-30 00:32:34 +00:00
continue
else :
self . _retMsg ( " Unknown Command " )
else :
2021-02-14 00:29:47 +00:00
2021-04-08 19:53:51 +00:00
if self . last_msg == " STATUS " :
2020-10-30 00:32:34 +00:00
self . _retMsg ( self . status )
else :
self . _retMsg ( False )
2020-10-29 21:23:37 +00:00
2021-04-04 21:34:46 +00:00
# Catch the player being killed externally.
except KeyboardInterrupt :
self . logger . log . info ( " Received KeyboardInterupt " )
except SystemExit :
self . logger . log . info ( " Received SystemExit " )
except Exception as e :
2021-04-08 20:15:15 +00:00
self . logger . log . exception (
2021-04-08 21:05:47 +00:00
" Received unexpected Exception: {} " . format ( e ) )
2020-10-24 20:31:52 +00:00
2021-04-04 21:34:46 +00:00
self . logger . log . info ( " Quiting player " + str ( channel ) )
2020-10-29 21:23:37 +00:00
self . quit ( )
2021-04-05 23:32:58 +00:00
self . _retAll ( " QUIT " )
2021-04-04 21:34:46 +00:00
del self . logger
os . _exit ( 0 )
2020-10-29 21:23:37 +00:00
if __name__ == " __main__ " :
2021-04-08 19:53:51 +00:00
raise Exception (
" This BAPSicle Player is a subcomponenet, it will not run individually. "
)