mirror of
https://github.com/kennethreitz/replit-py.git
synced 2026-06-05 23:10:18 +00:00
416 lines
13 KiB
Python
416 lines
13 KiB
Python
"""A library to play audio in a repl."""
|
|
from datetime import datetime, timedelta
|
|
import json
|
|
from os import path
|
|
import time
|
|
from typing import Any, List, Optional
|
|
|
|
from .types import (
|
|
AudioStatus,
|
|
file_types,
|
|
ReaderType,
|
|
RequestArgs,
|
|
RequestData,
|
|
SourceData,
|
|
WaveType,
|
|
)
|
|
|
|
|
|
class InvalidFileType(Exception):
|
|
"""Exception for when a requested file's type isnt valid."""
|
|
|
|
pass
|
|
|
|
|
|
class NoSuchSourceException(Exception):
|
|
"""Exception used when a source doesn't exist."""
|
|
|
|
pass
|
|
|
|
|
|
class Source:
|
|
"""A Source is used to get audio that is sent to the user."""
|
|
|
|
__payload: SourceData
|
|
_loops: bool
|
|
_name: str
|
|
|
|
def __init__(self, payload: SourceData, loops: bool) -> None:
|
|
"""Initialize the class.
|
|
|
|
Args:
|
|
payload (SourceData): The payload for the source.
|
|
loops (bool): How many times the source should loop.
|
|
"""
|
|
self.__payload = payload
|
|
self._loops = loops
|
|
self._name = payload["Name"]
|
|
|
|
def __get_source(self) -> SourceData or None:
|
|
source = None
|
|
with open("/tmp/audioStatus.json", "r") as f:
|
|
data = json.loads(f.read())
|
|
for s in data["Sources"]:
|
|
if s["ID"] == self.id:
|
|
source = s
|
|
break
|
|
if source:
|
|
self.__payload = source
|
|
return source
|
|
|
|
def __update_source(self, **changes: Any) -> None:
|
|
s = self.__get_source()
|
|
if not s:
|
|
raise NoSuchSourceException(
|
|
f'No player with id "{id}" found! It might be done playing.'
|
|
)
|
|
s.update({key.title(): changes[key] for key in changes})
|
|
with open("/tmp/audio", "w") as f:
|
|
f.write(json.dumps(s))
|
|
self.__get_source()
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""The name of the source."""
|
|
return self._name
|
|
|
|
def get_start_time(self) -> datetime:
|
|
"""When the source started plaing."""
|
|
timestamp_str = self.__payload["StartTime"]
|
|
timestamp = datetime.strptime(timestamp_str[:-4], "%Y-%m-%dT%H:%M:%S.%f")
|
|
return timestamp
|
|
|
|
start_time: datetime = property(get_start_time)
|
|
"Property wrapper for :py:meth:`~replit.Source.get_start_time`"
|
|
|
|
@property
|
|
def path(self) -> str or None:
|
|
"""The path to the source, if available."""
|
|
data = self.__payload
|
|
if ReaderType(data["Type"]) in file_types:
|
|
return self.__payload["Request"]["Args"]["Path"]
|
|
|
|
@property
|
|
def id(self) -> int:
|
|
"""The ID of the source."""
|
|
return self.__payload["ID"]
|
|
|
|
def get_remaining(self) -> timedelta:
|
|
"""The estimated time remaining in the source's current loop."""
|
|
data = self.__get_source()
|
|
if not data:
|
|
return timedelta(milliseconds=0)
|
|
|
|
return timedelta(milliseconds=data["Remaining"])
|
|
|
|
remaining: int = property(get_remaining)
|
|
"Property wrapper for :py:meth:`~replit.Source.get_remaining`"
|
|
|
|
def get_end_time(self) -> Optional[datetime]:
|
|
"""The estimated time when the source will be done playing.
|
|
|
|
Returns:
|
|
Optional[datetime]: The estimated time when the source will be done playing
|
|
or None if it is already finished.
|
|
"""
|
|
s = self.__get_source()
|
|
if not s:
|
|
return None
|
|
|
|
timestamp_str = s["EndTime"]
|
|
timestamp = datetime.strptime(timestamp_str[:-4], "%Y-%m-%dT%H:%M:%S.%f")
|
|
return timestamp
|
|
|
|
end_time: datetime or None = property(get_end_time)
|
|
"Property wrapper for :py:meth:`~replit.Source.get_end_time`"
|
|
|
|
@property
|
|
def does_loop(self) -> bool:
|
|
"""Whether the source repeats itself or not."""
|
|
return self._loops
|
|
|
|
@property
|
|
def duration(self) -> timedelta:
|
|
"""The duration of the source."""
|
|
return timedelta(millaseconds=self.__payload["Duration"])
|
|
|
|
def get_volume(self) -> float:
|
|
"""The volume the source is set to."""
|
|
self.__get_source()
|
|
return self.__payload["Volume"]
|
|
|
|
def set_volume(self, volume: float) -> None:
|
|
"""Set the volume.
|
|
|
|
Args:
|
|
volume (float): The volume the source should be set to.
|
|
"""
|
|
self.__update_source(volume=volume)
|
|
|
|
volume: float = property(get_volume, set_volume)
|
|
"Property wrapper for `replit.Source.get_volume` and `replit.Source.set_volume`"
|
|
|
|
def get_paused(self) -> bool:
|
|
"""Whether the source is paused."""
|
|
self.__get_source()
|
|
return self.__payload["Paused"]
|
|
|
|
def set_paused(self, paused: bool) -> None:
|
|
"""Change if the source is paused.
|
|
|
|
Args:
|
|
paused (bool): Whether the source should be paused.
|
|
"""
|
|
self.__update_source(paused=paused)
|
|
|
|
paused = property(get_paused, set_paused)
|
|
"Property wrapper for `replit.Source.get_paused` and `replit.Source.set_paused`"
|
|
|
|
def get_loops_remaining(self) -> Optional[int]:
|
|
"""The remaining amount of times the file will restart.
|
|
|
|
Returns:
|
|
Optional[int]: The number of loops remaining or None if the source can't be
|
|
found, either because it has finished playing or an error occured.
|
|
"""
|
|
if not self._loops:
|
|
return 0
|
|
|
|
s = self.__get_source()
|
|
if not s:
|
|
return None
|
|
|
|
if s["ID"] == self.id:
|
|
loops = s["Loop"]
|
|
|
|
return loops
|
|
|
|
def set_loop(self, loop_count: int) -> None:
|
|
"""Set the remaining amount of loops for the source.
|
|
|
|
Args:
|
|
loop_count (int): How many times the source should repeat itself. Set to a
|
|
negative value for infinite.
|
|
"""
|
|
does_loop = loop_count != 0
|
|
self._loops = does_loop
|
|
self.__update_source(doesLoop=does_loop, loopCount=loop_count)
|
|
|
|
loops_remaining: int or None = property(get_loops_remaining)
|
|
"Property wrapper for :py:meth:`~replit.Source.get_loops_remaining`"
|
|
|
|
def toggle_playing(self) -> None:
|
|
"""Play/pause the source."""
|
|
self.set_paused(not self.paused)
|
|
|
|
|
|
class Audio:
|
|
"""The basic audio manager.
|
|
|
|
Notes
|
|
-----
|
|
This is not intended to be called directly, instead use :py:const:`audio`.
|
|
|
|
Using this in addition to `audio` can cause **major** issues.
|
|
"""
|
|
|
|
__known_ids = []
|
|
__names_created = 0
|
|
|
|
def __gen_name(self) -> str:
|
|
return f"Source {time.time()}"
|
|
|
|
def __get_new_source(self, name: str, does_loop: bool) -> Source:
|
|
new_source = None
|
|
timeOut = datetime.now() + timedelta(seconds=2)
|
|
|
|
while not new_source and datetime.now() < timeOut:
|
|
try:
|
|
sources = AudioStatus(self.read_status())["Sources"]
|
|
new_source = SourceData([s for s in sources if s["Name"] == name][0])
|
|
except IndexError:
|
|
pass
|
|
except json.JSONDecodeError:
|
|
pass
|
|
|
|
if not new_source:
|
|
raise TimeoutError("Source was not created within 2 seconds.")
|
|
|
|
return Source(new_source, does_loop)
|
|
|
|
def play_file(
|
|
self,
|
|
file_path: str,
|
|
volume: float = 1,
|
|
does_loop: bool = False,
|
|
loop_count: int = 0,
|
|
name: Optional[str] = None,
|
|
) -> Source:
|
|
"""Sends a request to play a file, assuming the file is valid.
|
|
|
|
Args:
|
|
file_path (str): The path to the file that should be played. Can be
|
|
absolute or relative.
|
|
volume (float): The volume the source should be played at. (1 being
|
|
100%)
|
|
does_loop (bool): Wether the source should repeat itself or not. Note, if
|
|
you set this you should also set loop_count.
|
|
loop_count (int): How many times the source should repeat itself. Set to 0
|
|
to have the source play only once, or set to a negative value for the
|
|
source to repeat forever.
|
|
name (str): The name of the file. Default value is a unique name for the
|
|
source.
|
|
|
|
Returns:
|
|
Source: The source created with the provided data.
|
|
|
|
Raises:
|
|
FileNotFoundError: If the file is not found.
|
|
InvalidFileType: If the file type is not valid.
|
|
"""
|
|
name = name or self.__gen_name()
|
|
|
|
if not path.exists(file_path):
|
|
raise FileNotFoundError(f'File "{file_path}" not found.')
|
|
|
|
file_type = file_path.split(".")[-1]
|
|
|
|
if ReaderType(file_type) not in file_types:
|
|
raise InvalidFileType(f"Type {file_type} is not supported.")
|
|
|
|
data = RequestData(
|
|
Type=file_type,
|
|
Volume=volume,
|
|
DoesLoop=does_loop,
|
|
LoopCount=loop_count,
|
|
Name=name,
|
|
Args=RequestArgs(Path=file_path),
|
|
)
|
|
|
|
with open("/tmp/audio", "w") as p:
|
|
p.write(json.dumps(dict(data)))
|
|
|
|
return self.__get_new_source(name, does_loop)
|
|
|
|
def play_tone(
|
|
self,
|
|
duration: float,
|
|
pitch: int,
|
|
wave_type: WaveType,
|
|
does_loop: bool = False,
|
|
loop_count: int = 0,
|
|
volume: float = 1,
|
|
name: Optional[str] = None,
|
|
) -> Source:
|
|
"""Play a tone from a frequency and wave type.
|
|
|
|
Args:
|
|
duration (float): How long the tone should be played (in seconds).
|
|
pitch (int): The frequency the tone should be played at.
|
|
wave_type (WaveType): The wave shape used to generate the tone.
|
|
does_loop (bool): Wether the source should repeat itself or not. Note, if
|
|
you set this you should also set loop_count.
|
|
loop_count (int): How many times the source should repeat itself. Set to 0
|
|
to have the source play only once, or set to a negative value for the
|
|
source to repeat forever.
|
|
volume (float): The volume the tone should be played at (1 being 100%).
|
|
name (str): The name of the file. Default value is a unique name for the
|
|
source.
|
|
|
|
Returns:
|
|
Source: The source for the tone.
|
|
"""
|
|
name = name or self.__gen_name()
|
|
|
|
# ensure the wave type is valid. This will throw an error if it isn't.
|
|
WaveType(wave_type)
|
|
|
|
data = RequestData(
|
|
Name=name,
|
|
DoesLoop=does_loop,
|
|
LoopCount=loop_count,
|
|
Volume=volume,
|
|
Type=str(ReaderType.tone),
|
|
Args=RequestArgs(
|
|
WaveType=wave_type,
|
|
Pitch=pitch,
|
|
Seconds=duration,
|
|
),
|
|
)
|
|
|
|
with open("/tmp/audio", "w") as f:
|
|
f.write(json.dumps(data))
|
|
|
|
return self.__get_new_source(name, does_loop)
|
|
|
|
def get_source(self, source_id: int) -> Source or None:
|
|
"""Get a source by it's ID.
|
|
|
|
Args:
|
|
source_id (int): The ID for the source that should be found.
|
|
|
|
Raises:
|
|
NoSuchSourceException: If the source isnt found or there isn't any sources
|
|
known to the audio manager.
|
|
|
|
Returns:
|
|
Source: The source with the ID provided.
|
|
"""
|
|
source = None
|
|
with open("/tmp/audioStatus.json", "r") as f:
|
|
data = AudioStatus(json.loads(f.read()))
|
|
if not data["Sources"]:
|
|
raise NoSuchSourceException("No sources exist yet.")
|
|
for s in data["Sources"]:
|
|
|
|
if s["ID"] == int(source_id):
|
|
source = s
|
|
break
|
|
if not source:
|
|
raise NoSuchSourceException(f'Could not find source with ID "{source_id}"')
|
|
return Source(source, source["Loop"])
|
|
|
|
def read_status(self) -> AudioStatus:
|
|
"""Get the raw data for what's playing.
|
|
|
|
This is an api call, and shouldn't be needed for general usage.
|
|
|
|
Returns:
|
|
AudioStatus: The contents of /tmp/audioStatus.json
|
|
"""
|
|
with open("/tmp/audioStatus.json", "r") as f:
|
|
data = AudioStatus(json.loads(f.read()))
|
|
if data["Sources"] is None:
|
|
data["Sources"]: List[SourceData] = []
|
|
return data
|
|
|
|
def get_playing(self) -> List[Source]:
|
|
"""Get a list of playing sources.
|
|
|
|
Returns:
|
|
List[Source]: A list of sources that aren't paused.
|
|
"""
|
|
data = self.read_status()
|
|
sources = data["Sources"]
|
|
return [Source(s, s["Loop"]) for s in sources if not s["Paused"]]
|
|
|
|
def get_paused(self) -> List[Source]:
|
|
"""Get a list of paused sources.
|
|
|
|
Returns:
|
|
List[Source]: A list of sources that are paused.
|
|
"""
|
|
data = self.read_status()
|
|
sources = data["Sources"]
|
|
return [Source(s, s["Loop"]) for s in sources if s["Paused"]]
|
|
|
|
def get_sources(self) -> List[Source]:
|
|
"""Gets all sources.
|
|
|
|
Returns:
|
|
List[Source]: Every source known to the audio manager, paused or playing.
|
|
"""
|
|
data = self.read_status()
|
|
sources = data["Sources"]
|
|
return [Source(s, s["Loop"]) for s in sources]
|