Skip to content

Commit

Permalink
Merge pull request #4 from unparalleled-js/feat/custom-playe
Browse files Browse the repository at this point in the history
feat: support for AFPlay
  • Loading branch information
antazoey authored Apr 2, 2023
2 parents a5260a6 + 6ca8e0d commit 67fd4f7
Show file tree
Hide file tree
Showing 19 changed files with 378 additions and 83 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,13 @@ And if you really enjoy the track, you can download it by doing:
audius tracks download G0wyE song.mp3
```

By default, `audius-py` tries to find the best player.
However, specify your player of choice using the `--player` flag:

```shell
audius tracks play G0wyE --player vlc
```

### Python SDK

Use the Python SDK directly:
Expand Down
12 changes: 12 additions & 0 deletions audius/cli/options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import click

from audius.types import PlayerType


def player_option():
return click.option(
"--player",
help="The player to use.",
type=click.Choice([x.value for x in PlayerType.__members__.values()], case_sensitive=False),
callback=lambda _, _2, val: PlayerType(val),
)
15 changes: 11 additions & 4 deletions audius/cli/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

import click

from audius.cli.options import player_option
from audius.cli.utils import sdk

DEFAULT_BUFFER_SIZE = 1024 * 1024


def tracks(sdk_cls: Type):
sdk.py = sdk_cls
Expand Down Expand Up @@ -55,23 +58,27 @@ def search(sdk, query):
@cli.command()
@sdk.audius()
@click.argument("track_id")
def play(sdk, track_id):
@player_option()
def play(sdk, track_id, player):
"""
Play a track.
"""

sdk.tracks.play(track_id)
sdk.tracks.play(track_id, player=player)

@cli.command()
@sdk.audius()
@click.argument("track_id")
@click.argument("out_path", type=Path)
def download(sdk, track_id, out_path):
@click.option(
"--buffer-size", help="The buffer size when downloading.", default=DEFAULT_BUFFER_SIZE
)
def download(sdk, track_id, out_path, buffer_size):
"""
Download a track.
"""

sdk.tracks.download(track_id, out_path)
sdk.tracks.download(track_id, out_path, chunk_size=buffer_size)

return cli

Expand Down
48 changes: 43 additions & 5 deletions audius/client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
from functools import cached_property
from typing import TYPE_CHECKING

from requests import Response, Session

from audius.config import Config
if TYPE_CHECKING:
from audius.config import Config
from audius.playlists import Playlists
from audius.sdk import Audius
from audius.tips import Tips
from audius.tracks import Tracks
from audius.users import Users


class API:
def __init__(self, client: "Client", config: Config):
self.client = client
self.config = config
def __init__(self, sdk: "Audius"):
self.sdk = sdk

@property
def client(self) -> "Client":
return self.sdk.client

@property
def config(self) -> "Config":
return self.sdk.config

@property
def playlists(self) -> "Playlists":
return self.sdk.playlists

@property
def tips(self) -> "Tips":
return self.sdk.tips

@property
def tracks(self) -> "Tracks":
return self.sdk.tracks

@property
def users(self) -> "Users":
return self.sdk.users

def _handle_id(self, _id: str) -> str:
return self.config.aliases[_id] if _id in self.config.aliases else _id
Expand All @@ -30,8 +60,16 @@ def get(self, url: str, **kwargs) -> dict:

return self.request("GET", url, **kwargs).json()

def get_redirect_url(self, uri: str):
result = self.request("HEAD", uri, allow_redirects=True)
return result.url

def request(self, method: str, url: str, **kwargs) -> Response:
url = f"{self.host_address}/v1/{url}"
prefix = f"{self.host_address}/v1/"
uri = url.replace(prefix, "")
if "https://" not in uri:
url = f"{prefix}{uri}"

response = self.session.request(method, url, **kwargs)
response.raise_for_status()
return response
10 changes: 8 additions & 2 deletions audius/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import os
from typing import Dict, Optional
from typing import Dict, Optional, Union

from audius.player import PlayerType

DEFAULT_APP_NAME = "audius-py"
AUDIUS_APP_NAME_ENV_VAR = "AUDIUS_APP_NAME"
AUDIUS_HOST_NAME_ENV_VAR = "AUDIUS_HOST_NAME"
AUDIUS_PLAYER_ENV_VAR = "AUDIUS_PLAYER"


class Config:
Expand All @@ -16,13 +19,16 @@ def __init__(
app_name: str = DEFAULT_APP_NAME,
host: Optional[str] = None,
aliases: Optional[Dict[str, str]] = None,
player: Optional[Union[PlayerType, str]] = None,
):
self.app_name = app_name
self.host = host # Uses random if not provided.
self.aliases = aliases or {}
self.player = PlayerType(player) if player is not None else None

@classmethod
def from_env(cls) -> "Config":
app_name = os.environ.get(AUDIUS_APP_NAME_ENV_VAR, DEFAULT_APP_NAME)
host = os.environ.get(AUDIUS_HOST_NAME_ENV_VAR)
return cls(app_name=app_name, host=host)
player = os.environ.get(AUDIUS_PLAYER_ENV_VAR)
return cls(app_name=app_name, host=host, player=player)
5 changes: 0 additions & 5 deletions audius/data.py

This file was deleted.

2 changes: 1 addition & 1 deletion audius/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class MissingPlayerError(AudiusException):
"""

def __init__(self):
super().__init__("Missing audio player. Ensure VLC music player is installed.")
super().__init__("Missing audio player. Try installing VLC music player.")


class OutputPathError(AudiusException):
Expand Down
34 changes: 0 additions & 34 deletions audius/player.py

This file was deleted.

52 changes: 52 additions & 0 deletions audius/player/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import TYPE_CHECKING, Dict, Optional, Type

from audius.client import API
from audius.exceptions import MissingPlayerError
from audius.player.af import AFPlayer
from audius.player.base import BasePlayer
from audius.player.vlc import VLCPlayer
from audius.types import PlayerType

if TYPE_CHECKING:
from audius.sdk import Audius


class Player(API):
def __init__(self, sdk: "Audius") -> None:
super().__init__(sdk)
self._player_classes: Dict[PlayerType, Type] = {
PlayerType.AFPLAY: AFPlayer,
PlayerType.VLC: VLCPlayer,
}
self._player_map: Dict[PlayerType, BasePlayer] = {}

def play(self, url: str, player_type: Optional[PlayerType] = None):
player = self.get_player(player_type=player_type)
player.play(url)

def display_now_playing(self, track: Dict, player_type: Optional[PlayerType] = None):
player = self.get_player(player_type=player_type)
player.display_now_playing(track)

def get_player(self, player_type: Optional[PlayerType] = None) -> BasePlayer:
player_type = player_type or self.config.player
if player_type is not None:
if player_type not in self._player_classes:
raise ValueError(f"Unknown player type '{player_type}'")

self._player_map[player_type] = self._player_classes[player_type](self.sdk)
return self._player_map[player_type]

if self._player_map:
# Use previously connected player.
player_type = next(iter(self._player_map))
return self._player_map[player_type]

# Find an available player.
for player_cls in self._player_classes.values():
player = player_cls(self.sdk)
if player.is_available():
self._player_map[player._type] = player
return player

raise MissingPlayerError()
41 changes: 41 additions & 0 deletions audius/player/af.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import os
import subprocess
import tempfile
import threading
import time
from typing import TYPE_CHECKING

from audius.player.base import BasePlayer
from audius.types import PlayerType

if TYPE_CHECKING:
from audius.sdk import Audius


class AFPlayer(BasePlayer):
def __init__(self, sdk: "Audius"):
super().__init__(PlayerType.AFPLAY, sdk)

def is_available(self):
try:
subprocess.run("afplay", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return True
except FileNotFoundError:
return False

def play(self, url: str):
download_url = self.client.get_redirect_url(url)
with tempfile.NamedTemporaryFile(mode="w+b", delete=False) as _file:
fd3 = os.dup(_file.fileno())

def download():
self.sdk.tracks.download(download_url, fd3, hide_output=True)

# Stream the song while playing it to prevent waiting
# for entire track to finish download.
thread = threading.Thread(target=download)
thread.start()
time.sleep(5) # Buffer
subprocess.Popen(["afplay", _file.name])
thread.join()
time.sleep(1)
35 changes: 35 additions & 0 deletions audius/player/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from abc import abstractmethod
from typing import TYPE_CHECKING, Dict

import click

from audius.client import API
from audius.types import PlayerType

if TYPE_CHECKING:
from audius.sdk import Audius


class BasePlayer(API):
def __init__(self, player_type: PlayerType, sdk: "Audius"):
self._type = player_type
super().__init__(sdk)

@abstractmethod
def is_available(self) -> bool:
"""
Returns True if this play is working.
"""

@abstractmethod
def play(self, url: str):
"""
Stream and play track from Audius.
Player-subclasses must implement this method.
"""

def display_now_playing(self, track: Dict):
click.echo(
f"({self._type.value.lower().capitalize()}) "
f"Now playing '{track['title']}' by {track['user']['name']}"
)
45 changes: 45 additions & 0 deletions audius/player/vlc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import time
from functools import cached_property
from typing import TYPE_CHECKING

from audius.exceptions import MissingPlayerError
from audius.player.base import BasePlayer
from audius.types import PlayerType

if TYPE_CHECKING:
from audius.sdk import Audius


class VLCPlayer(BasePlayer):
def __init__(self, sdk: "Audius"):
super().__init__(PlayerType.VLC, sdk)

@cached_property
def vlc(self):
# Lazy load to allow SDK to work when VLC not installed.
try:
import vlc # type: ignore

except Exception:
raise MissingPlayerError()

return vlc

@cached_property
def _player(self):
return self.vlc.MediaPlayer()

def is_available(self) -> bool:
try:
_ = self.vlc
return True
except MissingPlayerError:
return False

def play(self, url: str):
media = self.vlc.Media(url)
self._player.set_media(media)
self._player.play()
time.sleep(5) # Wait 5 seconds for it to start.
while self._player.is_playing():
time.sleep(1)
Loading

0 comments on commit 67fd4f7

Please sign in to comment.