Files
replit-py/src/replit/audio/__init__.py
T
2021-01-01 13:11:09 -05:00

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]