Initial commit of audio service
Three backends have been added, mopidy, vlc and mpg123. Depending on uri type an apporpriate service is selected when media playback is requested. (for example mopidy service can handle spotify://... and local://...) A playback Control skill can pause, resume, stop change track, etc any started media. So for example if the NPR news skill used this after starting playing the news the user can say "Hey Mycroft, pause" and the playback will pause. The playback control also handles stuff like lowering the volume of the playback if mycroft is asked another question. Currently the different backend runs under the playbeck control, this was made most for convenience and the services should be moved in the future. Usage: The user needs to import the audioservice interface `from mycroft.skills.audioservice import AudioService` and initialize an instance in the skill `initialize` method `self.audio_service = AudioService(self.emitter)` Then playing an uri is as simple as `self.audio_service.play(uri)` TODO: * Configuration (Alias for the different backends, service specific config, active services, etc.) * Manual selection of backend (This is prepared in the audioservice interface biut not implemented) * More feature complete audio service interface (playback control, get trackname etc) * Separate audio services from the playback control * Probably lots morepull/433/head
parent
edbd8edeb2
commit
89adf6e8ac
|
@ -0,0 +1,12 @@
|
|||
from mycroft.messagebus.message import Message
|
||||
|
||||
|
||||
class AudioService():
|
||||
def __init__(self, emitter):
|
||||
self.emitter = emitter
|
||||
|
||||
def play(self, tracks=[], utterance=''):
|
||||
self.emitter.emit(Message('MycroftAudioServicePlay',
|
||||
metadata={'tracks': tracks,
|
||||
'utterance': utterance}))
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import sys
|
||||
from os.path import dirname, abspath, basename
|
||||
|
||||
from mycroft.skills.media import MediaSkill
|
||||
from adapt.intent import IntentBuilder
|
||||
from mycroft.messagebus.message import Message
|
||||
from mycroft.configuration import ConfigurationManager
|
||||
import subprocess
|
||||
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from os.path import dirname
|
||||
|
||||
from mycroft.util.log import getLogger
|
||||
|
||||
logger = getLogger(abspath(__file__).split('/')[-2])
|
||||
__author__ = 'forslund'
|
||||
|
||||
sys.path.append(abspath(dirname(__file__)))
|
||||
MopidyService = __import__('mopidy_service').MopidyService
|
||||
VlcService = __import__('vlc_service').VlcService
|
||||
|
||||
|
||||
class Mpg123Service():
|
||||
def __init__(self, emitter):
|
||||
self.process = None
|
||||
self.emitter = emitter
|
||||
self.emitter.on('Mpg123ServicePlay', self._play)
|
||||
|
||||
def supported_uris(self):
|
||||
return ['file', 'http']
|
||||
|
||||
def clear_list(self):
|
||||
self.tracks = []
|
||||
|
||||
def add_list(self, tracks):
|
||||
self.tracks = tracks
|
||||
logger.info("Track list is " + str(tracks))
|
||||
|
||||
def _play(self, message):
|
||||
logger.info('Mpg123Service._play')
|
||||
track = self.tracks[self.index]
|
||||
self.process = subprocess.Popen(['mpg123', track])
|
||||
self.process.communicate()
|
||||
self.process = None
|
||||
self.index += 1
|
||||
if self.index >= len(self.tracks):
|
||||
self.emitter.emit(Message('Mpg123ServicePlay'))
|
||||
|
||||
def play(self):
|
||||
logger.info('Call Mpg123ServicePlay')
|
||||
self.index = 0
|
||||
self.emitter.emit(Message('Mpg123ServicePlay'))
|
||||
|
||||
def stop(self):
|
||||
logger.info('Mpg123ServiceStop')
|
||||
self.clear_list()
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
self.process = None
|
||||
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
def resume(self):
|
||||
pass
|
||||
|
||||
def next(self):
|
||||
self.process.terminate()
|
||||
|
||||
def previous(self):
|
||||
pass
|
||||
|
||||
def lower_volume(self):
|
||||
pass
|
||||
|
||||
def restore_volume(self):
|
||||
pass
|
||||
|
||||
def track_info(self):
|
||||
pass
|
||||
|
||||
|
||||
class PlaybackControlSkill(MediaSkill):
|
||||
def __init__(self):
|
||||
super(PlaybackControlSkill, self).__init__('Playback Control Skill')
|
||||
self.volume_is_low = False
|
||||
self.current = None
|
||||
logger.info('Playback Control Inited')
|
||||
self.service = []
|
||||
|
||||
def initialize(self):
|
||||
logger.info('initializing Playback Control Skill')
|
||||
super(PlaybackControlSkill, self).initialize()
|
||||
self.load_data_files(dirname(__file__))
|
||||
|
||||
logger.info('starting VLC service')
|
||||
self.service.append(VlcService(self.emitter))
|
||||
logger.info('starting Mopidy service')
|
||||
self.service.append(MopidyService(self.emitter))
|
||||
logger.info('starting Mpg123 service')
|
||||
self.service.append(Mpg123Service(self.emitter))
|
||||
self.emitter.on('MycroftAudioServicePlay', self._play)
|
||||
|
||||
def play(self, tracks):
|
||||
logger.info('play')
|
||||
self.stop()
|
||||
uri_type = tracks[0].split(':')[0]
|
||||
logger.info('uri_type: ' + uri_type)
|
||||
for s in self.service:
|
||||
logger.info(str(s))
|
||||
if uri_type in s.supported_uris():
|
||||
service = s
|
||||
break
|
||||
else:
|
||||
return
|
||||
logger.info('Clear list')
|
||||
service.clear_list()
|
||||
logger.info('Add tracks' + str(tracks))
|
||||
service.add_list(tracks)
|
||||
logger.info('Playing')
|
||||
service.play()
|
||||
self.current = service
|
||||
|
||||
def _play(self, message):
|
||||
logger.info('MycroftAudioServicePlay')
|
||||
logger.info(message.metadata['tracks'])
|
||||
|
||||
tracks = message.metadata['tracks']
|
||||
self.play(tracks)
|
||||
|
||||
def stop(self, message=None):
|
||||
logger.info('stopping all playing services')
|
||||
if self.current:
|
||||
self.current.stop()
|
||||
self.current = None
|
||||
|
||||
def handle_next(self, message):
|
||||
if self.current:
|
||||
self.current.next()
|
||||
|
||||
def handle_prev(self, message):
|
||||
if self.current:
|
||||
self.current.previous()
|
||||
|
||||
def handle_pause(self, message):
|
||||
if self.current:
|
||||
self.current.pause()
|
||||
|
||||
def handle_play(self, message):
|
||||
"""Resume playback if paused"""
|
||||
if self.current:
|
||||
self.current.resume()
|
||||
|
||||
def lower_volume(self, message):
|
||||
logger.info('lowering volume')
|
||||
if self.current:
|
||||
self.current.lower_volume()
|
||||
self.volume_is_low = True
|
||||
|
||||
def restore_volume(self, message):
|
||||
logger.info('maybe restoring volume')
|
||||
if self.current:
|
||||
self.volume_is_low = False
|
||||
time.sleep(2)
|
||||
if not self.volume_is_low:
|
||||
logger.info('restoring volume')
|
||||
self.current.restore_volume()
|
||||
|
||||
def handle_currently_playing(self, message):
|
||||
if self.current:
|
||||
track_info = self.current.track_info()
|
||||
if track_info is not None:
|
||||
data = {'current_track': track_info['name'],
|
||||
'artist': track_info['album']}
|
||||
self.speak_dialog('currently_playing', data)
|
||||
time.sleep(6)
|
||||
|
||||
|
||||
def create_skill():
|
||||
return PlaybackControlSkill()
|
|
@ -0,0 +1 @@
|
|||
We're listening to {{current_track}} by {{artist}}.
|
|
@ -0,0 +1 @@
|
|||
Listening to {{tracks}}.
|
|
@ -0,0 +1,80 @@
|
|||
from mycroft.messagebus.message import Message
|
||||
from mycroft.util.log import getLogger
|
||||
from os.path import dirname, abspath, basename
|
||||
import sys
|
||||
import time
|
||||
|
||||
logger = getLogger(abspath(__file__).split('/')[-2])
|
||||
__author__ = 'forslund'
|
||||
|
||||
sys.path.append(abspath(dirname(__file__)))
|
||||
Mopidy = __import__('mopidypost').Mopidy
|
||||
|
||||
class MopidyService():
|
||||
def _connect(self, message):
|
||||
logger.debug('Could not connect to server, will retry quietly')
|
||||
url = 'http://localhost:6680'
|
||||
try:
|
||||
self.mopidy = Mopidy(url)
|
||||
except:
|
||||
if self.connection_attempts < 1:
|
||||
logger.debug('Could not connect to server, will retry quietly')
|
||||
self.connection_attempts += 1
|
||||
time.sleep(10)
|
||||
self.emitter.emit(Message('MopidyServiceConnect'))
|
||||
return
|
||||
|
||||
logger.info('Connected to mopidy server')
|
||||
|
||||
def __init__(self, emitter):
|
||||
self.connection_attempts = 0
|
||||
self.emitter = emitter
|
||||
self.emitter.on('MopidyServiceConnect', self._connect)
|
||||
self._connect(None)
|
||||
|
||||
def supported_uris(self):
|
||||
return ['file', 'http', 'https', 'local', 'spotify', 'gmusic']
|
||||
|
||||
def clear_list(self):
|
||||
self.mopidy.clear_list()
|
||||
|
||||
def add_list(self, tracks):
|
||||
self.mopidy.add_list(tracks)
|
||||
|
||||
def play(self):
|
||||
self.mopidy.play()
|
||||
|
||||
def stop(self):
|
||||
self.mopidy.clear_list()
|
||||
self.mopidy.stop()
|
||||
|
||||
def pause(self):
|
||||
self.mopidy.pause()
|
||||
|
||||
def resume(self):
|
||||
self.mopidy.resume()
|
||||
|
||||
def next(self):
|
||||
self.mopidy.next()
|
||||
|
||||
def previous(self):
|
||||
self.mopidy.previous()
|
||||
|
||||
def lower_volume(self):
|
||||
self.mopidy.lower_volume()
|
||||
|
||||
def restore_volume(self):
|
||||
self.mopidy.restore_volume()
|
||||
|
||||
def track_info(self):
|
||||
info = self.mopidy.currently_playing()
|
||||
ret = {}
|
||||
ret['name'] = info.get('name', '')
|
||||
if 'album' in info:
|
||||
ret['artist'] = info['album']['artists'][0]['name']
|
||||
ret['album'] = info['album'].get('name', '')
|
||||
else:
|
||||
ret['artist'] = ''
|
||||
ret['album'] = ''
|
||||
return ret
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
import requests
|
||||
from copy import copy
|
||||
import json
|
||||
|
||||
MOPIDY_API = '/mopidy/rpc'
|
||||
|
||||
_base_dict = {'jsonrpc': '2.0', 'id': 1, 'params': {}}
|
||||
|
||||
|
||||
class Mopidy(object):
|
||||
def __init__(self, url):
|
||||
print "MOPIDY URL: " + url
|
||||
self.is_playing = False
|
||||
self.url = url + MOPIDY_API
|
||||
self.volume = None
|
||||
self.clear_list(force=True)
|
||||
self.volume_low = 3
|
||||
self.volume_high = 100
|
||||
|
||||
def find_artist(self, artist):
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.library.search'
|
||||
d['params'] = {'artist': [artist]}
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
return r.json()['result'][1]['artists']
|
||||
|
||||
def get_playlists(self, filter=None):
|
||||
print "GETTING PLAYLISTS"
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playlists.as_list'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
if filter is None:
|
||||
return r.json()['result']
|
||||
else:
|
||||
return [l for l in r.json()['result'] if filter + ':' in l['uri']]
|
||||
|
||||
def find_album(self, album, filter=None):
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.library.search'
|
||||
d['params'] = {'album': [album]}
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
l = [res['albums'] for res in r.json()['result'] if 'albums' in res]
|
||||
if filter is None:
|
||||
return l
|
||||
else:
|
||||
return [i for sl in l for i in sl if filter + ':' in i['uri']]
|
||||
|
||||
def find_exact(self, uris='null'):
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.library.find_exact'
|
||||
d['params'] = {'uris': uris}
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
return r.json()
|
||||
|
||||
def browse(self, uri):
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.library.browse'
|
||||
d['params'] = {'uri': uri}
|
||||
print "BROWSE"
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
if 'result' in r.json():
|
||||
return r.json()['result']
|
||||
else:
|
||||
return None
|
||||
|
||||
def clear_list(self, force=False):
|
||||
if self.is_playing or force:
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.tracklist.clear'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
return r
|
||||
|
||||
def add_list(self, uri):
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.tracklist.add'
|
||||
if type(uri) == str or type(uri) == unicode:
|
||||
d['params'] = {'uri': uri}
|
||||
elif type(uri) == list:
|
||||
d['params'] = {'uris': uri}
|
||||
else:
|
||||
return None
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
return r
|
||||
|
||||
def play(self):
|
||||
self.is_playing = True
|
||||
self.restore_volume()
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playback.play'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
|
||||
def next(self):
|
||||
if self.is_playing:
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playback.next'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
|
||||
def previous(self):
|
||||
if self.is_playing:
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playback.previous'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
|
||||
def stop(self):
|
||||
print self.is_playing
|
||||
if self.is_playing:
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playback.stop'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
self.is_playing = False
|
||||
|
||||
def currently_playing(self):
|
||||
if self.is_playing:
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playback.get_current_track'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
return r.json()['result']
|
||||
else:
|
||||
return None
|
||||
|
||||
def set_volume(self, percent):
|
||||
if self.is_playing:
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.mixer.set_volume'
|
||||
d['params'] = {'volume': percent}
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
|
||||
def lower_volume(self):
|
||||
self.set_volume(self.volume_low)
|
||||
|
||||
def restore_volume(self):
|
||||
self.set_volume(self.volume_high)
|
||||
|
||||
def pause(self):
|
||||
if self.is_playing:
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playback.pause'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
|
||||
def resume(self):
|
||||
if self.is_playing:
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playback.resume'
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
|
||||
def get_items(self, uri):
|
||||
d = copy(_base_dict)
|
||||
d['method'] = 'core.playlists.get_items'
|
||||
d['params'] = {'uri': uri}
|
||||
r = requests.post(self.url, data=json.dumps(d))
|
||||
if 'result' in r.json():
|
||||
print r.json()
|
||||
return [e['uri'] for e in r.json()['result']]
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_tracks(self, uri):
|
||||
tracks = self.browse(uri)
|
||||
ret = [t['uri'] for t in tracks if t['type'] == 'track']
|
||||
|
||||
sub_tracks = [t['uri'] for t in tracks if t['type'] != 'track']
|
||||
for t in sub_tracks:
|
||||
ret = ret + self.get_tracks(t)
|
||||
return ret
|
||||
|
||||
def get_local_albums(self):
|
||||
p = self.browse('local:directory?type=album')
|
||||
return {e['name']: e for e in p if e['type'] == 'album'}
|
||||
|
||||
def get_local_artists(self):
|
||||
p = self.browse('local:directory?type=artist')
|
||||
return {e['name']: e for e in p if e['type'] == 'artist'}
|
||||
|
||||
def get_local_genres(self):
|
||||
p = self.browse('local:directory?type=genre')
|
||||
return {e['name']: e for e in p if e['type'] == 'directory'}
|
||||
|
||||
def get_local_playlists(self):
|
||||
print "GETTING PLAYLISTS"
|
||||
p = self.get_playlists('m3u')
|
||||
print "RETURNING PLAYLISTS"
|
||||
return {e['name']: e for e in p}
|
||||
|
||||
def get_spotify_playlists(self):
|
||||
p = self.get_playlists('spotify')
|
||||
return {e['name'].split('(by')[0].strip().lower(): e for e in p}
|
||||
|
||||
def get_gmusic_albums(self):
|
||||
p = self.browse('gmusic:album')
|
||||
print p
|
||||
p = {e['name']: e for e in p if e['type'] == 'directory'}
|
||||
print p
|
||||
return {e.split(' - ')[1]: p[e] for e in p}
|
||||
|
||||
def get_gmusic_artists(self):
|
||||
p = self.browse('gmusic:artist')
|
||||
return {e['name']: e for e in p if e['type'] == 'directory'}
|
||||
|
||||
def get_gmusic_radio(self):
|
||||
p = self.browse('gmusic:radio')
|
||||
return {e['name']: e for e in p if e['type'] == 'directory'}
|
|
@ -0,0 +1,67 @@
|
|||
from os.path import dirname, abspath, basename
|
||||
from mycroft.util.log import getLogger
|
||||
|
||||
logger = getLogger(abspath(__file__).split('/')[-2])
|
||||
|
||||
import vlc
|
||||
|
||||
class VlcService():
|
||||
def __init__(self, emitter):
|
||||
self.instance = vlc.Instance()
|
||||
self.list_player = self.instance.media_list_player_new()
|
||||
self.player = self.instance.media_player_new()
|
||||
self.list_player.set_media_player(self.player)
|
||||
|
||||
self.emitter = emitter
|
||||
|
||||
def supported_uris(self):
|
||||
return ['file', 'http']
|
||||
|
||||
def clear_list(self):
|
||||
empty = self.instance.media_list_new()
|
||||
self.list_player.set_media_list(empty)
|
||||
|
||||
def add_list(self, tracks):
|
||||
logger.info("Track list is " + str(tracks))
|
||||
vlc_tracks = self.instance.media_list_new()
|
||||
for t in tracks:
|
||||
vlc_tracks.add_media(self.instance.media_new(t))
|
||||
self.list_player.set_media_list(vlc_tracks)
|
||||
|
||||
def play(self):
|
||||
logger.info('VLCService Play')
|
||||
self.list_player.play()
|
||||
|
||||
def stop(self):
|
||||
logger.info('VLCService Stop')
|
||||
self.clear_list()
|
||||
self.list_player.stop()
|
||||
|
||||
def pause(self):
|
||||
self.player.set_pause(1)
|
||||
|
||||
def resume(self):
|
||||
self.player.set_pause(0)
|
||||
|
||||
def next(self):
|
||||
self.list_player.next()
|
||||
|
||||
def previous(self):
|
||||
self.list_player.previous()
|
||||
|
||||
def lower_volume(self):
|
||||
self.player.audio_set_volume(30)
|
||||
|
||||
def restore_volume(self):
|
||||
self.player.audio_set_volume(100)
|
||||
|
||||
def track_info(self):
|
||||
ret = {}
|
||||
meta = vlc.Meta
|
||||
t = self.player.get_media()
|
||||
ret['album'] = t.get_meta(meta.Album)
|
||||
ret['artists'] = [t.get_meta(meta.Artist)]
|
||||
ret['name'] = t.get_meta(meta.Title)
|
||||
return ret
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
Search
|
|
@ -0,0 +1 @@
|
|||
Spotify
|
Loading…
Reference in New Issue