Files
rikku-home-assistant/custom_components/foobar/media_player.py
T
2025-04-21 21:03:30 -04:00

330 lines
12 KiB
Python

"""
Support for foobar2000 Music Player as media player
via pyfoobar2k https://gitlab.com/ed0zer-projects/pyfoobar2k
And foobar2000 component foo_httpcontrol by oblikoamorale https://bitbucket.org/oblikoamorale/foo_httpcontrol
"""
import logging
from datetime import timedelta
import voluptuous as vol
from homeassistant.helpers import script, config_validation as cv
import homeassistant.util.dt as dt_util
try:
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity
except ImportError:
from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice as MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC, SUPPORT_VOLUME_STEP, SUPPORT_TURN_ON, SUPPORT_TURN_OFF,
SUPPORT_STOP, SUPPORT_SELECT_SOURCE, SUPPORT_SEEK, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_MUTE,
SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_SHUFFLE_SET)
from homeassistant.const import (
CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME,
CONF_PASSWORD, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
CONF_TIMEOUT, STATE_UNKNOWN, STATE_IDLE)
REQUIREMENTS = ['pyfoobar2k==0.2.8']
SCAN_INTERVAL = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = 'Foobar2000'
DEFAULT_PORT = '8888'
DEFAULT_TIMEOUT = 3
DEFAULT_VOLUME_STEP = 5
CONF_VOLUME_STEP = 'volume_step'
CONF_TURN_ON_ACTION = 'turn_on_action'
CONF_TURN_OFF_ACTION = 'turn_off_action'
SUPPORT_FOOBAR_PLAYER = \
SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \
SUPPORT_PLAY | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_SHUFFLE_SET | \
SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): cv.positive_int,
vol.Optional(CONF_TURN_ON_ACTION, default=None): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_OFF_ACTION, default=None): cv.SCRIPT_SCHEMA})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Foobar Player platform."""
from pyfoobar2k import FoobarRemote
name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
username = config.get(CONF_USERNAME)
password = config.get(CONF_PASSWORD)
timeout = config.get(CONF_TIMEOUT)
volume_step = config.get(CONF_VOLUME_STEP)
turn_on_action = config.get(CONF_TURN_ON_ACTION)
turn_off_action = config.get(CONF_TURN_OFF_ACTION)
remote = FoobarRemote(host, port, username, password, timeout)
add_devices([FoobarDevice(hass, remote, name, volume_step, turn_on_action, turn_off_action)])
class FoobarDevice(MediaPlayerEntity):
def __init__(self, hass, remote, name, volume_step, turn_on_action=None, turn_off_action=None):
self._name = name
self._remote = remote
self.hass = hass
self._volume = 0.0
self._track_name = ''
self._track_artist = ''
self._track_album_name = ''
self._track_duration = 0
self._track_position = 0
self._track_position_updated_at = None
self._albumart = ''
self._current_playlist = ''
self._playlists = []
self._shuffle = 0
self._volume_step = volume_step
self._selected_source = None
self._state = STATE_UNKNOWN
self._base_url = self._remote.url
self._albumart_path = ''
# Script creation for the turn on/off config options
if turn_on_action is not None:
turn_on_action = script.Script(
self.hass, turn_on_action,
"{} turn ON script".format(self.name),
self.async_update_ha_state(True))
if turn_off_action is not None:
turn_off_action = script.Script(
self.hass, turn_off_action,
"{} turn OFF script".format(self.name),
self.async_update_ha_state(True))
self._turn_on_action = turn_on_action
self._turn_off_action = turn_off_action
def update(self):
try:
info = self._remote.state()
if info:
if info['isPlaying'] == '1':
self._state = STATE_PLAYING
elif info['isPaused'] == '1':
self._state = STATE_PAUSED
else:
self._state = STATE_IDLE
else:
self._state = STATE_OFF
self.schedule_update_ha_state()
if self._state in [STATE_PLAYING, STATE_PAUSED]:
self._track_name = info['title']
self._track_artist = info['artist']
self._track_album_name = info['album']
self._volume = int(info['volume']) / 100
self._shuffle = info['playbackorder']
self._track_duration = int(info['itemPlayingLen'])
self._albumart_path = info['albumArt']
self._track_position = int(info['itemPlayingPos'])
self._track_position_updated_at = dt_util.utcnow()
if self._state in [STATE_PLAYING, STATE_PAUSED, STATE_IDLE]:
sources_info = self._remote.playlist()
if sources_info:
current_playlist_position = int(sources_info['playlistActive'])
playlists_raw = sources_info['playlists']
self._current_playlist = playlists_raw[current_playlist_position]['name']
self._playlists = [item["name"] for item in playlists_raw]
else:
_LOGGER.warning("Updating %s sources failed:", self._name)
except Exception as e:
_LOGGER.error("Updating %s state failed: %s", self._name, e)
self._state = STATE_UNKNOWN
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def should_poll(self):
"""Return True if entity has to be polled for state."""
return True
@property
def volume_level(self):
"""Volume level of the media player (0 to 1)."""
return float(self._volume)
@property
def is_volume_muted(self):
"""Return True if volume is muted."""
return float(self._volume) == 0
@property
def media_content_type(self):
"""Content type of current playing media."""
return MEDIA_TYPE_MUSIC
@property
def media_title(self):
"""Title of current playing track."""
return self._track_name
@property
def media_artist(self):
"""Artist of current playing track."""
return self._track_artist
@property
def media_album_name(self):
"""Album name of current playing track."""
return self._track_album_name
@property
def supported_features(self):
"""Flag media player features that are supported."""
supported_features = SUPPORT_FOOBAR_PLAYER
if self._turn_on_action is not None:
supported_features |= SUPPORT_TURN_ON
if self._turn_off_action is not None:
supported_features |= SUPPORT_TURN_OFF
return supported_features
def turn_on(self):
"""Execute turn_on_action to turn on media player."""
if self._turn_on_action is not None:
self._turn_on_action.run(variables={"entity_id": self.entity_id})
else:
_LOGGER.warning("turn_on requested but turn_on_action is none")
def turn_off(self):
"""Execute turn_off_action to turn on media player."""
if self._turn_off_action is not None:
self._turn_off_action.run(variables={"entity_id": self.entity_id})
else:
_LOGGER.warning("turn_off requested but turn_off_action is none")
def media_play_pause(self):
"""Send the media player the command for play/pause."""
self._remote.cmd('PlayOrPause')
def media_pause(self):
"""Send the media player the command for play/pause if playing."""
if self._state == STATE_PLAYING:
self._remote.cmd('PlayOrPause')
def media_stop(self):
"""Send the media player the stop command."""
self._remote.cmd('Stop')
def media_play(self):
"""Send the media player the command to play at the current playlist."""
self._remote.cmd('Start')
def media_previous_track(self):
"""Send the media player the command for prev track."""
self._remote.cmd('StartPrevious')
def media_next_track(self):
"""Send the media player the command for next track."""
self._remote.cmd('StartNext')
def set_volume_level(self, volume):
"""Send the media player the command for setting the volume."""
self._remote.cmd('Volume', int(volume * 100))
def volume_up(self):
"""Send the media player the command for volume down."""
self._remote.cmd('VolumeDelta', self._volume_step)
def volume_down(self):
"""Send the media player the command for volume down."""
self._remote.cmd('VolumeDelta', -self._volume_step)
def mute_volume(self, mute):
"""Mute the volume."""
self._remote.cmd('VolumeMuteToggle')
@property
def media_position_updated_at(self):
"""When was the position of the current playing media valid.
Returns value from homeassistant.util.dt.utcnow().
"""
return self._track_position_updated_at
@property
def media_duration(self):
"""Duration of current playing media in seconds."""
return self._track_duration
@property
def media_position(self):
"""Position of current playing media in seconds."""
if self._state in [STATE_PLAYING, STATE_PAUSED]:
return self._track_position
def media_seek(self, position):
"""Send the media player the command to seek in current playing media."""
self._remote.cmd('SeekSecond', position)
@property
def media_image_url(self):
"""Image url of current playing media."""
if 'cover_not_available' not in self._albumart_path:
self._albumart = '{}/{}'.format(self._base_url, self._albumart_path)
return self._albumart
@property
def source(self):
"""Return current source name."""
return self._current_playlist
@property
def source_list(self):
"""List of available input sources."""
return self._playlists
def select_source(self, source):
"""Select input source."""
playlists = self._remote.playlist()['playlists']
source_position = [index for index, item in enumerate(playlists) if item['name'] == source][0]
"""ignoring first playlist in playlists index"""
if source_position == 0:
return None
if source_position is not None:
self._remote.cmd('SwitchPlaylist', source_position)
self._remote.cmd('Start', 0)
self._current_playlist = source
@property
def media_playlist(self):
"""Title of Playlist currently playing."""
return self._current_playlist
def set_shuffle(self, shuffle):
"""Send the media player the command to enable/disable shuffle mode."""
playback_order = 4 if shuffle else 0
self._remote.cmd('PlaybackOrder', playback_order)
@property
def shuffle(self):
"""Boolean if shuffle is enabled."""
return True if int(self._shuffle) == 4 else False