castero/player.py
from abc import abstractmethod
import castero
from castero.config import Config
from castero.episode import Episode
class PlayerError(Exception):
"""An ambiguous error while handling the player."""
class PlayerDependencyError(PlayerError):
"""A dependency for playing the player was not met."""
class PlayerCreateError(PlayerError):
"""An error occurred while creating the player."""
class Player:
"""Extendable class for media players.
This class is extended by players -- classes which offer methods to handle
media operations for a specific external player (i.e. VLC, mpv).
"""
def __init__(self, title, path, episode) -> None:
"""
:param title the title of the media (usually an episode title)
:param path a URL or file-path of a media file (usually an audio file)
:param episode the Episode which this is a player for
"""
assert isinstance(title, str) and title != ""
assert isinstance(path, str) and path != ""
assert isinstance(episode, Episode) and episode is not None
self._title = title
self._path = path
self._episode = episode
self._media = None
self._player = None
self._duration = -1 # in milliseconds
self._state = 0 # 0=stopped, 1=playing, 2=paused
def __del__(self) -> None:
if self._player is not None:
self.stop()
def __str__(self) -> str:
"""Represent this object as a string.
:returns string: the name of the feed and the title of the player
"""
return "[%s] %s" % (self._episode.feed_str, self._title)
@staticmethod
def create_instance(available_players, title, path, episode):
"""Create an instance of an appropriate Player subclass.
This method attempts to create a player based on the user's config
option. If their option is not a key in available_players or
check_dependencies() fails on the instance, we instead try to
initialize the first working player using the order defined in
available_players.
:param available_players a list of implemented Player subclasses
:param title the title of the media (usually an episode title)
:param path a URL or file-path of a media file (usually an audio file)
:param episode the Episode which this is a player for
:raises PlayerDependencyError: at least one dependency per player for all
players was not met
"""
if Config["player"] in available_players:
try:
available_players[Config["player"]].check_dependencies()
inst = available_players[Config["player"]](title, path, episode)
return inst
except PlayerDependencyError:
pass
# Config had a bad/unsupported value; we'll instead try all implemented
# options in order
for av_player in sorted(available_players):
try:
available_players[av_player].check_dependencies()
inst = available_players[av_player](title, path, episode)
return inst
except PlayerDependencyError:
pass
raise PlayerDependencyError(
"Sufficient dependencies were not met for"
" any players. If you recently downloaded"
" a player, you may need to reinstall %s" % castero.__title__
)
@staticmethod
@abstractmethod
def check_dependencies():
"""Checks whether dependencies are met for playing a player.
:raises PlayerDependencyError: a dependency was not met
"""
@abstractmethod
def _create_player(self) -> None:
"""Creates the player object while making sure it is a valid file.
Checks some basic properties of the file to ensure it will play
properly, including:
- the media object could be parsed
- it has a duration > 0
:raises PlayerCreateError: the player object could not be created
"""
@abstractmethod
def play(self) -> None:
"""Plays the media."""
@abstractmethod
def stop(self) -> None:
"""Stops the media."""
@abstractmethod
def pause(self) -> None:
"""Pauses the media."""
@abstractmethod
def seek(self, direction, amount) -> None:
"""Seek forward or backward in the media.
:param direction 1 to seek forward, -1 to seek backward
:param amount the amount of seconds to seek
"""
@abstractmethod
def play_from(self, seconds) -> None:
"""play media from point.
:param amount the seconds to start from
"""
@abstractmethod
def change_rate(self, direction, display=None) -> None:
"""Increase or decrease the playback speed.
:param direction 1 to increase, -1 to decrease
:param display (optional) the display to write status updates to
"""
@abstractmethod
def set_rate(self, rate) -> None:
"""Set the playback speed.
:param rate the desired playback speed
"""
@abstractmethod
def set_volume(self, volume) -> int:
"""Set the player volume.
:param volume the desired volume
"""
@property
def state(self) -> int:
"""int: the state of the player"""
return self._state
@property
def title(self) -> str:
"""str: the title of the player"""
return self._title
@property
def episode(self) -> Episode:
"""Episode: the Episode which this player has the media for"""
return self._episode
@property
@abstractmethod
def duration(self) -> int:
"""int: the duration of the player, in ms"""
@property
@abstractmethod
def volume(self) -> int:
"""int: the volume of the player"""
@property
@abstractmethod
def time(self) -> int:
"""int: the current time of the player, in ms"""
@property
@abstractmethod
def time_str(self) -> str:
"""str: the formatted time and duration of the player"""