BeoGateway/BeoGateway.indigoPlugin/Contents/Server Plugin/Resources/ASBridge.py
2023-11-13 20:00:48 +00:00

175 lines
6.8 KiB
Python

#!/usr/bin/python
try:
import indigo
except ImportError:
pass
import logging
import os
import unicodedata
import threading
from Foundation import NSAppleScript
from ScriptingBridge import SBApplication
''' This module requires PyObjC to be installed in order to use the AppleScriptingBridge for Apple Music
Module defining a MusicController class for Apple Music, enables:
basic transport control of the player,
query of player status,
playing existing playlists,
running external appleScripts for more complex control, and
reporting messages in the notification centre'''
PLAYSTATE = dict([
(1800426320, "Play"),
(1800426352, "Pause"),
(1800426323, "Stop"),
(1800426310, "Wind"),
(1800426322, "Rewind")
])
class MusicController(object):
def __init__(self):
self.app = SBApplication.applicationWithBundleIdentifier_("com.apple.Music")
# ########################################################################################
# Player information
def get_current_track_info(self):
name = self.app.currentTrack().name()
album = self.app.currentTrack().album()
artist = self.app.currentTrack().artist()
number = self.app.currentTrack().trackNumber()
if name:
# Deal with tracks with non-ascii characters such as accents
name = unicodedata.normalize('NFD', name).encode('ascii', 'ignore')
album = unicodedata.normalize('NFD', album).encode('ascii', 'ignore')
artist = unicodedata.normalize('NFD', artist).encode('ascii', 'ignore')
return [name, album, artist, number]
def get_current_play_state(self):
return PLAYSTATE.get(self.app.playerState())
def get_current_track_position(self):
return self.app.playerPosition()
# ########################################################################################
# Transport Controls
def playpause(self):
self.app.playpause()
def play(self, playlist_name):
if PLAYSTATE.get(self.app.playerState()) in ['Wind', 'Rewind']:
self.app.resume()
elif PLAYSTATE.get(self.app.playerState()) == 'Pause':
self.app.playpause()
elif PLAYSTATE.get(self.app.playerState()) == 'Stop':
self. app.setValue_forKey_('true', 'shuffleEnabled')
# playlist = self.app.sources().objectWithName_("Library")
playlist = self.app.sources().objectWithName_("Library").playlists().objectWithName_(playlist_name)
playlist.playOnce_(None)
def pause(self):
if PLAYSTATE.get(self.app.playerState()) == 'Play':
self.app.pause()
def stop(self):
if PLAYSTATE.get(self.app.playerState()) != 'Stop':
self.app.stop()
def next_track(self):
self.app.nextTrack()
def previous_track(self):
self.app.previousTrack()
def wind(self, time):
# self.app.wind()
# Native wind function can be a bit annoying
# I provide an alternative below that skips a set number of seconds forwards
self.set_current_track_position(time)
def rewind(self, time):
# self.app.rewind()
# Native rewind function can be a bit annoying
# I provide an alternative below that skips a set number of seconds back
self.set_current_track_position(time)
# ########################################################################################
# More complex playback control functions
def shuffle(self):
if self.app.shuffleEnabled():
self.app.setValue_forKey_('false', 'shuffleEnabled')
else:
self.app.setValue_forKey_('true', 'shuffleEnabled')
def set_current_track_position(self, time, mode='Relative'):
if mode == 'Relative':
# Set playback position in seconds relative to current position
self.app.setPlayerPosition_(self.app.playerPosition() + time)
elif mode == 'Absolute':
# Set playback position in seconds from the start of the track
self.app.setPlayerPosition_(time)
def play_playlist(self, playlist):
self.app.stop()
self.app.setValue_forKey_('true', 'shuffleEnabled')
playlist = self.app.sources().objectWithName_("Library").playlists().objectWithName_(playlist)
playlist.playOnce_(None)
def get_playlist_names(self):
playlists = ["None"]
# Generate and return a list of playlists in Apple Music
for playlist in self.app.sources().objectWithName_("Library").playlists():
playlists.append(playlist.name())
return playlists
def set_rating(self, rate):
# Set the rating of the track in %
self.app.currentTrack().setValue_forKey_(str(rate), 'rating')
if int(rate) == 100:
# If rated 100% then set to loved and ensure the track is enabled
self.app.currentTrack().setValue_forKey_('true', 'loved')
self.app.currentTrack().setValue_forKey_('true', 'enabled')
elif int(rate) == 0:
# If rated 0% then set to disliked and disable the track, so it will not be played in shuffle
self.app.currentTrack().setValue_forKey_('true', 'disliked')
self.app.currentTrack().setValue_forKey_('false', 'enabled')
# It's obviously a stinker, so let's skip to the next track
self.app.nextTrack()
else:
# else remove disliked/loved flags and check the track is enabled for playback
self.app.currentTrack().setValue_forKey_('false', 'disliked')
self.app.currentTrack().setValue_forKey_('false', 'loved')
self.app.currentTrack().setValue_forKey_('true', 'enabled')
# ########################################################################################
# Accessory functions - threaded due to execution time
@staticmethod
def run_script(script, debug):
script = 'run script ("' + script + '" as POSIX file)'
if debug:
indigo.server.log(script, level=logging.DEBUG)
def applet(_script):
# Run an external applescript file
s = NSAppleScript.alloc().initWithSource_(_script)
s.executeAndReturnError_(None)
threading.Thread(target=applet, args=(script,)).start()
@staticmethod
def notify(message, subtitle):
def applet(body, title):
# Post message in notification center
path = os.path.dirname(os.path.abspath(__file__))
script = 'tell application "' + path + '/Notify.app" to notify("BeoGateway", "' + \
body + '", "' + title + '")'
s = NSAppleScript.alloc().initWithSource_(script)
s.executeAndReturnError_(None)
threading.Thread(target=applet, args=(message, subtitle,)).start()