2021-03-29 03:24:13 +05:30
|
|
|
# Released under the MIT License. See LICENSE for details.
|
|
|
|
|
#
|
|
|
|
|
"""Music playback functionality using the Mac Music (formerly iTunes) app."""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import threading
|
2023-01-30 23:35:08 +05:30
|
|
|
from collections import deque
|
2021-03-29 03:24:13 +05:30
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
|
|
import _ba
|
|
|
|
|
from ba._music import MusicPlayer
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
2022-06-30 00:31:52 +05:30
|
|
|
from typing import Callable, Any
|
2021-03-29 03:24:13 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
class MacMusicAppMusicPlayer(MusicPlayer):
|
|
|
|
|
"""A music-player that utilizes the macOS Music.app for playback.
|
|
|
|
|
|
|
|
|
|
Allows selecting playlists as entries.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self._thread = _MacMusicAppThread()
|
|
|
|
|
self._thread.start()
|
|
|
|
|
|
2022-11-06 01:04:52 +05:30
|
|
|
def on_select_entry(
|
|
|
|
|
self,
|
|
|
|
|
callback: Callable[[Any], None],
|
|
|
|
|
current_entry: Any,
|
|
|
|
|
selection_target_name: str,
|
|
|
|
|
) -> Any:
|
2021-03-29 03:24:13 +05:30
|
|
|
# pylint: disable=cyclic-import
|
|
|
|
|
from bastd.ui.soundtrack import entrytypeselect as etsel
|
2022-11-06 01:04:52 +05:30
|
|
|
|
|
|
|
|
return etsel.SoundtrackEntryTypeSelectWindow(
|
|
|
|
|
callback, current_entry, selection_target_name
|
|
|
|
|
)
|
2021-03-29 03:24:13 +05:30
|
|
|
|
|
|
|
|
def on_set_volume(self, volume: float) -> None:
|
|
|
|
|
self._thread.set_volume(volume)
|
|
|
|
|
|
|
|
|
|
def get_playlists(self, callback: Callable) -> None:
|
|
|
|
|
"""Asynchronously fetch the list of available iTunes playlists."""
|
|
|
|
|
self._thread.get_playlists(callback)
|
|
|
|
|
|
|
|
|
|
def on_play(self, entry: Any) -> None:
|
|
|
|
|
music = _ba.app.music
|
|
|
|
|
entry_type = music.get_soundtrack_entry_type(entry)
|
|
|
|
|
if entry_type == 'iTunesPlaylist':
|
|
|
|
|
self._thread.play_playlist(music.get_soundtrack_entry_name(entry))
|
|
|
|
|
else:
|
2022-11-06 01:04:52 +05:30
|
|
|
print(
|
|
|
|
|
'MacMusicAppMusicPlayer passed unrecognized entry type:',
|
|
|
|
|
entry_type,
|
|
|
|
|
)
|
2021-03-29 03:24:13 +05:30
|
|
|
|
|
|
|
|
def on_stop(self) -> None:
|
|
|
|
|
self._thread.play_playlist(None)
|
|
|
|
|
|
|
|
|
|
def on_app_shutdown(self) -> None:
|
|
|
|
|
self._thread.shutdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _MacMusicAppThread(threading.Thread):
|
|
|
|
|
"""Thread which wrangles Music.app playback"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
super().__init__()
|
|
|
|
|
self._commands_available = threading.Event()
|
2023-01-30 23:35:08 +05:30
|
|
|
self._commands = deque[list]()
|
2021-03-29 03:24:13 +05:30
|
|
|
self._volume = 1.0
|
2022-06-30 00:31:52 +05:30
|
|
|
self._current_playlist: str | None = None
|
|
|
|
|
self._orig_volume: int | None = None
|
2021-03-29 03:24:13 +05:30
|
|
|
|
|
|
|
|
def run(self) -> None:
|
|
|
|
|
"""Run the Music.app thread."""
|
|
|
|
|
from ba._general import Call
|
|
|
|
|
from ba._language import Lstr
|
2021-10-26 23:24:50 +05:30
|
|
|
from ba._generated.enums import TimeType
|
2022-11-06 01:04:52 +05:30
|
|
|
|
2021-03-29 03:24:13 +05:30
|
|
|
_ba.set_thread_name('BA_MacMusicAppThread')
|
|
|
|
|
_ba.mac_music_app_init()
|
|
|
|
|
|
|
|
|
|
# Let's mention to the user we're launching Music.app in case
|
|
|
|
|
# it causes any funny business (this used to background the app
|
|
|
|
|
# sometimes, though I think that is fixed now)
|
|
|
|
|
def do_print() -> None:
|
2022-11-06 01:04:52 +05:30
|
|
|
_ba.timer(
|
|
|
|
|
1.0,
|
|
|
|
|
Call(
|
|
|
|
|
_ba.screenmessage,
|
|
|
|
|
Lstr(resource='usingItunesText'),
|
|
|
|
|
(0, 1, 0),
|
|
|
|
|
),
|
|
|
|
|
timetype=TimeType.REAL,
|
|
|
|
|
)
|
2021-03-29 03:24:13 +05:30
|
|
|
|
|
|
|
|
_ba.pushcall(do_print, from_other_thread=True)
|
|
|
|
|
|
|
|
|
|
# Here we grab this to force the actual launch.
|
|
|
|
|
_ba.mac_music_app_get_volume()
|
|
|
|
|
_ba.mac_music_app_get_library_source()
|
|
|
|
|
done = False
|
|
|
|
|
while not done:
|
|
|
|
|
self._commands_available.wait()
|
|
|
|
|
self._commands_available.clear()
|
|
|
|
|
|
|
|
|
|
# We're not protecting this list with a mutex but we're
|
|
|
|
|
# just using it as a simple queue so it should be fine.
|
|
|
|
|
while self._commands:
|
2023-01-30 23:35:08 +05:30
|
|
|
cmd = self._commands.popleft()
|
2021-03-29 03:24:13 +05:30
|
|
|
if cmd[0] == 'DIE':
|
|
|
|
|
self._handle_die_command()
|
|
|
|
|
done = True
|
|
|
|
|
break
|
|
|
|
|
if cmd[0] == 'PLAY':
|
|
|
|
|
self._handle_play_command(target=cmd[1])
|
|
|
|
|
elif cmd[0] == 'GET_PLAYLISTS':
|
|
|
|
|
self._handle_get_playlists_command(target=cmd[1])
|
|
|
|
|
|
|
|
|
|
del cmd # Allows the command data/callback/etc to be freed.
|
|
|
|
|
|
|
|
|
|
def set_volume(self, volume: float) -> None:
|
|
|
|
|
"""Set volume to a value between 0 and 1."""
|
|
|
|
|
old_volume = self._volume
|
|
|
|
|
self._volume = volume
|
|
|
|
|
|
|
|
|
|
# If we've got nothing we're supposed to be playing,
|
|
|
|
|
# don't touch itunes/music.
|
|
|
|
|
if self._current_playlist is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# If volume is going to zero, stop actually playing
|
|
|
|
|
# but don't clear playlist.
|
|
|
|
|
if old_volume > 0.0 and volume == 0.0:
|
|
|
|
|
try:
|
|
|
|
|
assert self._orig_volume is not None
|
|
|
|
|
_ba.mac_music_app_stop()
|
|
|
|
|
_ba.mac_music_app_set_volume(self._orig_volume)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print('Error stopping iTunes music:', exc)
|
|
|
|
|
elif self._volume > 0:
|
|
|
|
|
|
|
|
|
|
# If volume was zero, store pre-playing volume and start
|
|
|
|
|
# playing.
|
|
|
|
|
if old_volume == 0.0:
|
|
|
|
|
self._orig_volume = _ba.mac_music_app_get_volume()
|
|
|
|
|
self._update_mac_music_app_volume()
|
|
|
|
|
if old_volume == 0.0:
|
|
|
|
|
self._play_current_playlist()
|
|
|
|
|
|
2022-06-30 00:31:52 +05:30
|
|
|
def play_playlist(self, musictype: str | None) -> None:
|
2021-03-29 03:24:13 +05:30
|
|
|
"""Play the given playlist."""
|
|
|
|
|
self._commands.append(['PLAY', musictype])
|
|
|
|
|
self._commands_available.set()
|
|
|
|
|
|
|
|
|
|
def shutdown(self) -> None:
|
|
|
|
|
"""Request that the player shuts down."""
|
|
|
|
|
self._commands.append(['DIE'])
|
|
|
|
|
self._commands_available.set()
|
|
|
|
|
self.join()
|
|
|
|
|
|
|
|
|
|
def get_playlists(self, callback: Callable[[Any], None]) -> None:
|
|
|
|
|
"""Request the list of playlists."""
|
|
|
|
|
self._commands.append(['GET_PLAYLISTS', callback])
|
|
|
|
|
self._commands_available.set()
|
|
|
|
|
|
|
|
|
|
def _handle_get_playlists_command(
|
2022-11-06 01:04:52 +05:30
|
|
|
self, target: Callable[[list[str]], None]
|
|
|
|
|
) -> None:
|
2021-03-29 03:24:13 +05:30
|
|
|
from ba._general import Call
|
2022-11-06 01:04:52 +05:30
|
|
|
|
2021-03-29 03:24:13 +05:30
|
|
|
try:
|
|
|
|
|
playlists = _ba.mac_music_app_get_playlists()
|
|
|
|
|
playlists = [
|
2022-11-06 01:04:52 +05:30
|
|
|
p
|
|
|
|
|
for p in playlists
|
|
|
|
|
if p
|
|
|
|
|
not in [
|
|
|
|
|
'Music',
|
|
|
|
|
'Movies',
|
|
|
|
|
'TV Shows',
|
|
|
|
|
'Podcasts',
|
|
|
|
|
'iTunes\xa0U',
|
|
|
|
|
'Books',
|
|
|
|
|
'Genius',
|
|
|
|
|
'iTunes DJ',
|
|
|
|
|
'Music Videos',
|
|
|
|
|
'Home Videos',
|
|
|
|
|
'Voice Memos',
|
|
|
|
|
'Audiobooks',
|
2021-03-29 03:24:13 +05:30
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
playlists.sort(key=lambda x: x.lower())
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print('Error getting iTunes playlists:', exc)
|
|
|
|
|
playlists = []
|
|
|
|
|
_ba.pushcall(Call(target, playlists), from_other_thread=True)
|
|
|
|
|
|
2022-06-30 00:31:52 +05:30
|
|
|
def _handle_play_command(self, target: str | None) -> None:
|
2021-03-29 03:24:13 +05:30
|
|
|
if target is None:
|
|
|
|
|
if self._current_playlist is not None and self._volume > 0:
|
|
|
|
|
try:
|
|
|
|
|
assert self._orig_volume is not None
|
|
|
|
|
_ba.mac_music_app_stop()
|
|
|
|
|
_ba.mac_music_app_set_volume(self._orig_volume)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print('Error stopping iTunes music:', exc)
|
|
|
|
|
self._current_playlist = None
|
|
|
|
|
else:
|
|
|
|
|
# If we've got something playing with positive
|
|
|
|
|
# volume, stop it.
|
|
|
|
|
if self._current_playlist is not None and self._volume > 0:
|
|
|
|
|
try:
|
|
|
|
|
assert self._orig_volume is not None
|
|
|
|
|
_ba.mac_music_app_stop()
|
|
|
|
|
_ba.mac_music_app_set_volume(self._orig_volume)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print('Error stopping iTunes music:', exc)
|
|
|
|
|
|
|
|
|
|
# Set our playlist and play it if our volume is up.
|
|
|
|
|
self._current_playlist = target
|
|
|
|
|
if self._volume > 0:
|
2022-11-06 01:04:52 +05:30
|
|
|
self._orig_volume = _ba.mac_music_app_get_volume()
|
2021-03-29 03:24:13 +05:30
|
|
|
self._update_mac_music_app_volume()
|
|
|
|
|
self._play_current_playlist()
|
|
|
|
|
|
|
|
|
|
def _handle_die_command(self) -> None:
|
|
|
|
|
|
|
|
|
|
# Only stop if we've actually played something
|
|
|
|
|
# (we don't want to kill music the user has playing).
|
|
|
|
|
if self._current_playlist is not None and self._volume > 0:
|
|
|
|
|
try:
|
|
|
|
|
assert self._orig_volume is not None
|
|
|
|
|
_ba.mac_music_app_stop()
|
|
|
|
|
_ba.mac_music_app_set_volume(self._orig_volume)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
print('Error stopping iTunes music:', exc)
|
|
|
|
|
|
|
|
|
|
def _play_current_playlist(self) -> None:
|
|
|
|
|
try:
|
|
|
|
|
from ba._general import Call
|
2022-11-06 01:04:52 +05:30
|
|
|
|
2021-03-29 03:24:13 +05:30
|
|
|
assert self._current_playlist is not None
|
|
|
|
|
if _ba.mac_music_app_play_playlist(self._current_playlist):
|
|
|
|
|
pass
|
|
|
|
|
else:
|
2022-11-06 01:04:52 +05:30
|
|
|
_ba.pushcall(
|
|
|
|
|
Call(
|
|
|
|
|
_ba.screenmessage,
|
|
|
|
|
_ba.app.lang.get_resource('playlistNotFoundText')
|
|
|
|
|
+ ': \''
|
|
|
|
|
+ self._current_playlist
|
|
|
|
|
+ '\'',
|
|
|
|
|
(1, 0, 0),
|
|
|
|
|
),
|
|
|
|
|
from_other_thread=True,
|
|
|
|
|
)
|
2021-03-29 03:24:13 +05:30
|
|
|
except Exception:
|
|
|
|
|
from ba import _error
|
2022-11-06 01:04:52 +05:30
|
|
|
|
2021-03-29 03:24:13 +05:30
|
|
|
_error.print_exception(
|
2022-11-06 01:04:52 +05:30
|
|
|
f'error playing playlist {self._current_playlist}'
|
|
|
|
|
)
|
2021-03-29 03:24:13 +05:30
|
|
|
|
|
|
|
|
def _update_mac_music_app_volume(self) -> None:
|
|
|
|
|
_ba.mac_music_app_set_volume(
|
2022-11-06 01:04:52 +05:30
|
|
|
max(0, min(100, int(100.0 * self._volume)))
|
|
|
|
|
)
|