Skip to content

Commit 67fd4f7

Browse files
authored
Merge pull request #4 from unparalleled-js/feat/custom-playe
feat: support for AFPlay
2 parents a5260a6 + 6ca8e0d commit 67fd4f7

19 files changed

Lines changed: 378 additions & 83 deletions

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ And if you really enjoy the track, you can download it by doing:
123123
audius tracks download G0wyE song.mp3
124124
```
125125

126+
By default, `audius-py` tries to find the best player.
127+
However, specify your player of choice using the `--player` flag:
128+
129+
```shell
130+
audius tracks play G0wyE --player vlc
131+
```
132+
126133
### Python SDK
127134

128135
Use the Python SDK directly:

audius/cli/options.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import click
2+
3+
from audius.types import PlayerType
4+
5+
6+
def player_option():
7+
return click.option(
8+
"--player",
9+
help="The player to use.",
10+
type=click.Choice([x.value for x in PlayerType.__members__.values()], case_sensitive=False),
11+
callback=lambda _, _2, val: PlayerType(val),
12+
)

audius/cli/tracks.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
import click
55

6+
from audius.cli.options import player_option
67
from audius.cli.utils import sdk
78

9+
DEFAULT_BUFFER_SIZE = 1024 * 1024
10+
811

912
def tracks(sdk_cls: Type):
1013
sdk.py = sdk_cls
@@ -55,23 +58,27 @@ def search(sdk, query):
5558
@cli.command()
5659
@sdk.audius()
5760
@click.argument("track_id")
58-
def play(sdk, track_id):
61+
@player_option()
62+
def play(sdk, track_id, player):
5963
"""
6064
Play a track.
6165
"""
6266

63-
sdk.tracks.play(track_id)
67+
sdk.tracks.play(track_id, player=player)
6468

6569
@cli.command()
6670
@sdk.audius()
6771
@click.argument("track_id")
6872
@click.argument("out_path", type=Path)
69-
def download(sdk, track_id, out_path):
73+
@click.option(
74+
"--buffer-size", help="The buffer size when downloading.", default=DEFAULT_BUFFER_SIZE
75+
)
76+
def download(sdk, track_id, out_path, buffer_size):
7077
"""
7178
Download a track.
7279
"""
7380

74-
sdk.tracks.download(track_id, out_path)
81+
sdk.tracks.download(track_id, out_path, chunk_size=buffer_size)
7582

7683
return cli
7784

audius/client.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,44 @@
11
from functools import cached_property
2+
from typing import TYPE_CHECKING
23

34
from requests import Response, Session
45

5-
from audius.config import Config
6+
if TYPE_CHECKING:
7+
from audius.config import Config
8+
from audius.playlists import Playlists
9+
from audius.sdk import Audius
10+
from audius.tips import Tips
11+
from audius.tracks import Tracks
12+
from audius.users import Users
613

714

815
class API:
9-
def __init__(self, client: "Client", config: Config):
10-
self.client = client
11-
self.config = config
16+
def __init__(self, sdk: "Audius"):
17+
self.sdk = sdk
18+
19+
@property
20+
def client(self) -> "Client":
21+
return self.sdk.client
22+
23+
@property
24+
def config(self) -> "Config":
25+
return self.sdk.config
26+
27+
@property
28+
def playlists(self) -> "Playlists":
29+
return self.sdk.playlists
30+
31+
@property
32+
def tips(self) -> "Tips":
33+
return self.sdk.tips
34+
35+
@property
36+
def tracks(self) -> "Tracks":
37+
return self.sdk.tracks
38+
39+
@property
40+
def users(self) -> "Users":
41+
return self.sdk.users
1242

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

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

63+
def get_redirect_url(self, uri: str):
64+
result = self.request("HEAD", uri, allow_redirects=True)
65+
return result.url
66+
3367
def request(self, method: str, url: str, **kwargs) -> Response:
34-
url = f"{self.host_address}/v1/{url}"
68+
prefix = f"{self.host_address}/v1/"
69+
uri = url.replace(prefix, "")
70+
if "https://" not in uri:
71+
url = f"{prefix}{uri}"
72+
3573
response = self.session.request(method, url, **kwargs)
3674
response.raise_for_status()
3775
return response

audius/config.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import os
2-
from typing import Dict, Optional
2+
from typing import Dict, Optional, Union
3+
4+
from audius.player import PlayerType
35

46
DEFAULT_APP_NAME = "audius-py"
57
AUDIUS_APP_NAME_ENV_VAR = "AUDIUS_APP_NAME"
68
AUDIUS_HOST_NAME_ENV_VAR = "AUDIUS_HOST_NAME"
9+
AUDIUS_PLAYER_ENV_VAR = "AUDIUS_PLAYER"
710

811

912
class Config:
@@ -16,13 +19,16 @@ def __init__(
1619
app_name: str = DEFAULT_APP_NAME,
1720
host: Optional[str] = None,
1821
aliases: Optional[Dict[str, str]] = None,
22+
player: Optional[Union[PlayerType, str]] = None,
1923
):
2024
self.app_name = app_name
2125
self.host = host # Uses random if not provided.
2226
self.aliases = aliases or {}
27+
self.player = PlayerType(player) if player is not None else None
2328

2429
@classmethod
2530
def from_env(cls) -> "Config":
2631
app_name = os.environ.get(AUDIUS_APP_NAME_ENV_VAR, DEFAULT_APP_NAME)
2732
host = os.environ.get(AUDIUS_HOST_NAME_ENV_VAR)
28-
return cls(app_name=app_name, host=host)
33+
player = os.environ.get(AUDIUS_PLAYER_ENV_VAR)
34+
return cls(app_name=app_name, host=host, player=player)

audius/data.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

audius/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class MissingPlayerError(AudiusException):
5050
"""
5151

5252
def __init__(self):
53-
super().__init__("Missing audio player. Ensure VLC music player is installed.")
53+
super().__init__("Missing audio player. Try installing VLC music player.")
5454

5555

5656
class OutputPathError(AudiusException):

audius/player.py

Lines changed: 0 additions & 34 deletions
This file was deleted.

audius/player/__init__.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from typing import TYPE_CHECKING, Dict, Optional, Type
2+
3+
from audius.client import API
4+
from audius.exceptions import MissingPlayerError
5+
from audius.player.af import AFPlayer
6+
from audius.player.base import BasePlayer
7+
from audius.player.vlc import VLCPlayer
8+
from audius.types import PlayerType
9+
10+
if TYPE_CHECKING:
11+
from audius.sdk import Audius
12+
13+
14+
class Player(API):
15+
def __init__(self, sdk: "Audius") -> None:
16+
super().__init__(sdk)
17+
self._player_classes: Dict[PlayerType, Type] = {
18+
PlayerType.AFPLAY: AFPlayer,
19+
PlayerType.VLC: VLCPlayer,
20+
}
21+
self._player_map: Dict[PlayerType, BasePlayer] = {}
22+
23+
def play(self, url: str, player_type: Optional[PlayerType] = None):
24+
player = self.get_player(player_type=player_type)
25+
player.play(url)
26+
27+
def display_now_playing(self, track: Dict, player_type: Optional[PlayerType] = None):
28+
player = self.get_player(player_type=player_type)
29+
player.display_now_playing(track)
30+
31+
def get_player(self, player_type: Optional[PlayerType] = None) -> BasePlayer:
32+
player_type = player_type or self.config.player
33+
if player_type is not None:
34+
if player_type not in self._player_classes:
35+
raise ValueError(f"Unknown player type '{player_type}'")
36+
37+
self._player_map[player_type] = self._player_classes[player_type](self.sdk)
38+
return self._player_map[player_type]
39+
40+
if self._player_map:
41+
# Use previously connected player.
42+
player_type = next(iter(self._player_map))
43+
return self._player_map[player_type]
44+
45+
# Find an available player.
46+
for player_cls in self._player_classes.values():
47+
player = player_cls(self.sdk)
48+
if player.is_available():
49+
self._player_map[player._type] = player
50+
return player
51+
52+
raise MissingPlayerError()

audius/player/af.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import os
2+
import subprocess
3+
import tempfile
4+
import threading
5+
import time
6+
from typing import TYPE_CHECKING
7+
8+
from audius.player.base import BasePlayer
9+
from audius.types import PlayerType
10+
11+
if TYPE_CHECKING:
12+
from audius.sdk import Audius
13+
14+
15+
class AFPlayer(BasePlayer):
16+
def __init__(self, sdk: "Audius"):
17+
super().__init__(PlayerType.AFPLAY, sdk)
18+
19+
def is_available(self):
20+
try:
21+
subprocess.run("afplay", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
22+
return True
23+
except FileNotFoundError:
24+
return False
25+
26+
def play(self, url: str):
27+
download_url = self.client.get_redirect_url(url)
28+
with tempfile.NamedTemporaryFile(mode="w+b", delete=False) as _file:
29+
fd3 = os.dup(_file.fileno())
30+
31+
def download():
32+
self.sdk.tracks.download(download_url, fd3, hide_output=True)
33+
34+
# Stream the song while playing it to prevent waiting
35+
# for entire track to finish download.
36+
thread = threading.Thread(target=download)
37+
thread.start()
38+
time.sleep(5) # Buffer
39+
subprocess.Popen(["afplay", _file.name])
40+
thread.join()
41+
time.sleep(1)

0 commit comments

Comments
 (0)