diff --git a/packages/addons/service/librespot/changelog.txt b/packages/addons/service/librespot/changelog.txt index c700ceeac6..ccac305b82 100644 --- a/packages/addons/service/librespot/changelog.txt +++ b/packages/addons/service/librespot/changelog.txt @@ -1,3 +1,8 @@ +122 +- Improve track information: fanart, podcast +- Improve settings: autoplay, name, RTP port +- Drop system.d + 121 - Update to 66f8a98 (2020-02-26) - Gapless playback diff --git a/packages/addons/service/librespot/package.mk b/packages/addons/service/librespot/package.mk index a236cfc85d..b4b535d345 100644 --- a/packages/addons/service/librespot/package.mk +++ b/packages/addons/service/librespot/package.mk @@ -5,8 +5,8 @@ PKG_NAME="librespot" PKG_VERSION="66f8a98ad2f5bf35be4daecd788dad6f0d87fb7c" PKG_SHA256="b027c983341aa53d940412d5624cfe91392958ea9836ba597289680a4430b253" -PKG_VERSION_DATE="2020-02-26" -PKG_REV="121" +PKG_VERSION_DATE="2020-02-27" +PKG_REV="122" PKG_ARCH="any" PKG_LICENSE="MIT" PKG_SITE="https://github.com/librespot-org/librespot/" @@ -24,20 +24,16 @@ PKG_ADDON_REQUIRES="script.module.requests:0.0.0" PKG_MAINTAINER="Anton Voyl (awiouy)" make_target() { - . "$(get_build_dir rust)/cargo/env" + . $(get_build_dir rust)/cargo/env cargo build \ --release \ --no-default-features \ - --features "alsa-backend pulseaudio-backend with-dns-sd with-vorbis" - "$STRIP" $PKG_BUILD/.$TARGET_NAME/*/release/librespot + --features "alsa-backend pulseaudio-backend with-vorbis" + $STRIP $PKG_BUILD/.$TARGET_NAME/*/release/librespot } addon() { - mkdir -p "$ADDON_BUILD/$PKG_ADDON_ID/bin" + mkdir -p $ADDON_BUILD/$PKG_ADDON_ID/bin cp $PKG_BUILD/.$TARGET_NAME/*/release/librespot \ - "$ADDON_BUILD/$PKG_ADDON_ID/bin" - - mkdir -p "$ADDON_BUILD/$PKG_ADDON_ID/lib" - cp "$(get_build_dir avahi)/avahi-compat-libdns_sd/.libs/libdns_sd.so.1" \ - "$ADDON_BUILD/$PKG_ADDON_ID/lib" + $ADDON_BUILD/$PKG_ADDON_ID/bin } diff --git a/packages/addons/service/librespot/patches/librespot-01_notify_kodi.patch b/packages/addons/service/librespot/patches/librespot-01_notify_kodi.patch index c310c058db..fb6ad6740e 100644 --- a/packages/addons/service/librespot/patches/librespot-01_notify_kodi.patch +++ b/packages/addons/service/librespot/patches/librespot-01_notify_kodi.patch @@ -1,4 +1,4 @@ -commit 0623a601115f5c62d2b30645a85d9496546d5cba +commit 93ad8ce6f94e51bb14547ddf9e7a234c41adc266 Author: awiouy Date: Tue Dec 3 23:21:35 2019 +0100 @@ -42,40 +42,27 @@ index 0f71110..931167d 100644 } } diff --git a/playback/src/player.rs b/playback/src/player.rs -index ef7484c..5b56782 100644 +index ef7484c..22ecb2c 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs -@@ -4,7 +4,8 @@ use futures::{future, Async, Future, Poll, Stream}; - use std; - use std::borrow::Cow; - use std::cmp::max; --use std::io::{Read, Result, Seek, SeekFrom}; -+use std::fs::OpenOptions; -+use std::io::{Read, Result, Seek, SeekFrom, Write}; - use std::mem; - use std::thread; - use std::time::{Duration, Instant}; -@@ -1415,6 +1416,12 @@ impl PlayerInternal { +@@ -1415,6 +1415,10 @@ impl PlayerInternal { } } + fn notify_kodi(&mut self, event: String) { -+ // println!("Librespot fifo = {}", event); -+ let mut file = OpenOptions::new().write(true).open("/tmp/librespot").unwrap(); -+ writeln!(&mut file, "{}", event).unwrap(); ++ eprintln!("@{}", event); + } + fn send_event(&mut self, event: PlayerEvent) { let mut index = 0; while index < self.event_senders.len() { -@@ -1425,6 +1432,17 @@ impl PlayerInternal { +@@ -1425,6 +1429,16 @@ impl PlayerInternal { } } } + if self.config.notify_kodi { + use PlayerEvent::*; + match event { -+ Paused { .. } => self.notify_kodi("Paused".to_string()), + Playing {track_id, .. } => self.notify_kodi(["Playing", + &track_id.audio_type.to_string(), + &track_id.to_base62()].join(" ")), @@ -87,7 +74,7 @@ index ef7484c..5b56782 100644 fn load_track( diff --git a/src/main.rs b/src/main.rs -index 70a2dff..3e63308 100644 +index f749a52..5a9f75d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -171,6 +171,11 @@ fn setup(args: &[String]) -> Setup { diff --git a/packages/addons/service/librespot/patches/librespot-02_enable_pulseaudio_device_names.patch b/packages/addons/service/librespot/patches/librespot-02_enable_pulseaudio_device_names.patch new file mode 100644 index 0000000000..4e79567a91 --- /dev/null +++ b/packages/addons/service/librespot/patches/librespot-02_enable_pulseaudio_device_names.patch @@ -0,0 +1,59 @@ +From 389276e5d361da5f2a2116d11724c815ce4b6ec8 Mon Sep 17 00:00:00 2001 +From: Konstantin Seiler +Date: Wed, 11 Mar 2020 13:49:52 +1100 +Subject: [PATCH] Enable pulseaudio device names + +--- + playback/src/audio_backend/pulseaudio.rs | 12 +++++++----- + 1 file changed, 7 insertions(+), 5 deletions(-) + +diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs +index e844b0d6..8a833d65 100644 +--- a/playback/src/audio_backend/pulseaudio.rs ++++ b/playback/src/audio_backend/pulseaudio.rs +@@ -12,6 +12,7 @@ pub struct PulseAudioSink { + ss: pa_sample_spec, + name: CString, + desc: CString, ++ device: Option, + } + + fn call_pulseaudio( +@@ -56,10 +57,6 @@ impl Open for PulseAudioSink { + fn open(device: Option) -> PulseAudioSink { + debug!("Using PulseAudio sink"); + +- if device.is_some() { +- panic!("pulseaudio sink does not support specifying a device name"); +- } +- + let ss = pa_sample_spec { + format: PA_SAMPLE_S16LE, + channels: 2, // stereo +@@ -74,6 +71,7 @@ impl Open for PulseAudioSink { + ss: ss, + name: name, + desc: description, ++ device: device.and_then(|s| CString::new(s).ok()), + } + } + } +@@ -81,13 +79,17 @@ impl Open for PulseAudioSink { + impl Sink for PulseAudioSink { + fn start(&mut self) -> io::Result<()> { + if self.s == null_mut() { ++ let device = match &self.device { ++ None => null(), ++ Some(device) => device.as_ptr(), ++ }; + self.s = call_pulseaudio( + |err| unsafe { + pa_simple_new( + null(), // Use the default server. + self.name.as_ptr(), // Our application's name. + PA_STREAM_PLAYBACK, +- null(), // Use the default device. ++ device, + self.desc.as_ptr(), // desc of our stream. + &self.ss, // Our sample format. + null(), // Use default channel map diff --git a/packages/addons/service/librespot/patches/librespot-02_use_own_pulseaudio_sink.patch b/packages/addons/service/librespot/patches/librespot-02_use_own_pulseaudio_sink.patch deleted file mode 100644 index 5614ef7c69..0000000000 --- a/packages/addons/service/librespot/patches/librespot-02_use_own_pulseaudio_sink.patch +++ /dev/null @@ -1,27 +0,0 @@ -commit 88ba7c3ad879e784ce55714c2c834911b2573ce1 -Author: awiouy -Date: Tue Dec 3 23:22:55 2019 +0100 - - Use own pulseaudio sink - -diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs -index e844b0d..0320f65 100644 ---- a/playback/src/audio_backend/pulseaudio.rs -+++ b/playback/src/audio_backend/pulseaudio.rs -@@ -80,6 +80,7 @@ impl Open for PulseAudioSink { - - impl Sink for PulseAudioSink { - fn start(&mut self) -> io::Result<()> { -+ let sink = CString::new("librespot_sink").unwrap(); - if self.s == null_mut() { - self.s = call_pulseaudio( - |err| unsafe { -@@ -87,7 +88,7 @@ impl Sink for PulseAudioSink { - null(), // Use the default server. - self.name.as_ptr(), // Our application's name. - PA_STREAM_PLAYBACK, -- null(), // Use the default device. -+ sink.as_ptr(), // Our sink. - self.desc.as_ptr(), // desc of our stream. - &self.ss, // Our sample format. - null(), // Use default channel map diff --git a/packages/addons/service/librespot/source/bin/librespot.start b/packages/addons/service/librespot/source/bin/librespot.start deleted file mode 100755 index e6902cb0b7..0000000000 --- a/packages/addons/service/librespot/source/bin/librespot.start +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh - -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) - -. /etc/profile -oe_setup_addon service.librespot - -PORT="6666" -SINK_NAME="librespot_sink" - -if ! pactl list modules short | grep "sink_name=$SINK_NAME"; then - pactl load-module module-null-sink sink_name="$SINK_NAME" > /dev/null -fi -if ! pactl list modules short | grep "source=$SINK_NAME"; then - pactl load-module module-rtp-send source="$SINK_NAME.monitor" \ - destination_ip=127.0.0.1 port="$PORT" source_ip=127.0.0.1 > /dev/null -fi -pactl suspend-sink "$SINK_NAME" 1 - -LIBRESPOT="librespot \ - --backend pulseaudio \ - --bitrate 320 \ - --cache \"$ADDON_HOME/cache\" \ - --device-type TV \ - --disable-audio-cache \ - --name \"Kodi ($HOSTNAME)\" \ - --notify-kodi" - -if [ "$ls_a" = "true" -a -n "$ls_p" -a -n "$ls_u" ]; then - LIBRESPOT="$LIBRESPOT \ - --disable-discovery \ - --password \"$ls_p\" \ - --username \"$ls_u\"" -fi - -eval $LIBRESPOT diff --git a/packages/addons/service/librespot/source/default.py b/packages/addons/service/librespot/source/default.py index dfc9e3615d..acf8371f3f 100644 --- a/packages/addons/service/librespot/source/default.py +++ b/packages/addons/service/librespot/source/default.py @@ -1,240 +1,8 @@ -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) - -import base64 import os -import stat -import subprocess -import threading -import time -import requests -import xbmc -import xbmcaddon -import xbmcgui +import sys -ADDON = xbmcaddon.Addon() -ADDON_ID = ADDON.getAddonInfo('id') -ADDON_NAME = ADDON.getAddonInfo('name') -FIFO = '/tmp/librespot' -LOG_LEVEL = xbmc.LOGNOTICE -LOG_MESSAGE = ADDON.getAddonInfo('name') + ': {}' -SINK_NAME = "librespot_sink" -STREAM_CODEC = 'pcm_s16be' -STREAM_PORT = '6666' -STREAM_URL = 'rtp://127.0.0.1:{}'.format(STREAM_PORT) +sys.path.append(os.path.join(os.path.dirname(__file__), 'resources', 'lib')) +from ls_monitor import Monitor as Monitor -def log(message): - xbmc.log(LOG_MESSAGE.format(message), LOG_LEVEL) - - -class Monitor(xbmc.Monitor): - - def __init__(self): - log('monitor started') - self.player = Player() - - def onSettingsChanged(self): - log('settings changed') - self.player.update() - - def waitForAbort(self): - super(Monitor, self).waitForAbort() - log('abort requested') - self.player.abort() - - -class Player(threading.Thread, xbmc.Player): - - def __init__(self): - log('player started') - super(Player, self).__init__() - self.aborted = False - self.isLibrespotStarted = True - self.listitem = xbmcgui.ListItem() - self.listitem.addStreamInfo('audio', {'codec': STREAM_CODEC}) - self.listitem.setPath(STREAM_URL) - self.spotify = Spotify() - if self.isPlaying(): - self.onAVStarted() - else: - self.playingFile = '' - self.onPlayBackStopped() - self.start() - - def abort(self): - log('aborting player') - self.aborted = True - with open(FIFO, 'w') as fifo: - fifo.close() - self.join() - - def onAVChange(self): - self.onAVStarted() - - def onAVStarted(self): - log('playback started') - self.playingFile = self.getPlayingFile() - if self.isLibrespotStarted and (self.playingFile != STREAM_URL): - self.isLibrespotStarted = False - self.systemctl('stop') - - def onPlayBackEnded(self): - self.onPlayBackStopped() - - def onPlayBackError(self): - self.onPlayBackStopped() - - def onPlayBackStopped(self): - log('playback stopped') - if self.playingFile == STREAM_URL: - self.systemctl('restart') - elif not self.isLibrespotStarted: - self.systemctl('start') - self.isLibrespotStarted = True - - def pauseLibrespot(self): - if self.isPlaying() and (self.getPlayingFile() == STREAM_URL): - log('pausing librespot playback') - self.pause() - - def playLibrespot(self, type, id): - info = self.spotify.getInfo(type, id) - self.listitem.setArt(info.getArt()) - self.listitem.setInfo('music', info.getInfo()) - if not self.isPlaying(): - subprocess.call(['pactl', 'suspend-sink', SINK_NAME, '0']) - log('starting librespot playback') - self.play(STREAM_URL, self.listitem) - elif self.getPlayingFile() == STREAM_URL: - log('updating librespot playback') - self.updateInfoTag(self.listitem) - - def run(self): - log('control pipe started') - try: - os.unlink(FIFO) - except OSError: - pass - while not self.aborted: - if not os.path.exists(FIFO): - os.mkfifo(FIFO) - with open(FIFO, 'r') as fifo: - command = fifo.readline() - log('control pipe {}'.format(command)) - if command == '': - continue - command = command.split() - if command[0] in ['Paused']: - self.pauseLibrespot() - elif command[0] in ['Playing']: - self.playLibrespot(command[1], command[2]) - elif command[0] in ['Stopped']: - self.stopLibrespot() - try: - os.unlink(FIFO) - except OSError: - pass - log('control pipe stopped') - - def stopLibrespot(self): - if self.isPlaying() and (self.getPlayingFile() == STREAM_URL): - log('stopping librespot playback') - self.stop() - - def systemctl(self, command): - log('{} librespot'.format(command)) - subprocess.call(['systemctl', command, ADDON_ID]) - - def update(self): - log('updating player') - if self.isLibrespotStarted: - self.systemctl('restart') - - -class Spotify(): - - def __init__(self): - self.headers = None - self.expiration = time.time() - - def getHeaders(self): - if time.time() > self.expiration: - log('token expired') - token = requests.post( - url='https://accounts.spotify.com/api/token', - data={'grant_type': 'client_credentials'}, - headers={ - 'Authorization': 'Basic MTY5ZGY1NTMyZGVlNDdhNTk5MTNmODUyOGU4M2FlNzE6MWYzZDhiNTA3YmJlNGY2OGJlYjNhNDQ3MmU4YWQ0MTE='} - ).json() - print(token) - log('new token expires in {} seconds'.format(token['expires_in'])) - self.expiration = time.time() + float(token['expires_in']) - 15 - self.headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer {}'.format(token['access_token']) - } - - def getEndpoint(self, url): - try: - self.getHeaders() - return requests.get( - url=url, - headers=self.headers - ).json() - except Exception as e: - log('failed to get endpoint from Spotify {}'.format(e)) - return {} - - def getInfo(self, type, id): - if type == 'Track': - return TrackInfo(self.getEndpoint('https://api.spotify.com/v1/tracks/{}'.format(id))) - else: - return UnknownInfo(type, id) - - -class UnknownInfo: - - def __init__(self, type, id): - self.id = id - self.type = type - - def get(self, default, *indices): - tree = self.info - try: - for index in indices: - tree = tree[index] - except LookupError: - tree = default - return tree - - def getArt(self): - return {'thumb': ''} - - def getInfo(self): - return {'album': '', 'artist': self.type, 'title': self.id} - - -class TrackInfo(UnknownInfo): - - def __init__(self, info): - self.info = info - - def getArt(self): - return { - 'thumb': self.get('', 'album', 'images', 0, 'url') - } - - def getInfo(self): - return { - 'album': self.get('', 'album', 'name'), - 'artist': self.get('', 'artists', 0, 'name'), - 'title': self.get('', 'name') - } - - -if __name__ == '__main__': - log('service started') - Monitor().waitForAbort() - log('service stopped') +Monitor().run() diff --git a/packages/addons/service/librespot/source/ls_env.py b/packages/addons/service/librespot/source/ls_env.py deleted file mode 100644 index e86b58203a..0000000000 --- a/packages/addons/service/librespot/source/ls_env.py +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) - -PORT="6666" -SINK_NAME="librespot_sink" diff --git a/packages/addons/service/librespot/source/resources/language/English/strings.po b/packages/addons/service/librespot/source/resources/language/English/strings.po index 9cac7a2e18..0cc500223c 100644 --- a/packages/addons/service/librespot/source/resources/language/English/strings.po +++ b/packages/addons/service/librespot/source/resources/language/English/strings.po @@ -8,13 +8,25 @@ msgid "Configuration" msgstr "" msgctxt "#30101" -msgid "User mode" +msgid "Autoplay" msgstr "" msgctxt "#30102" -msgid "Username" +msgid "Discovery" msgstr "" msgctxt "#30103" +msgid "Username" +msgstr "" + +msgctxt "#30104" msgid "Password" msgstr "" + +msgctxt "#30105" +msgid "Name" +msgstr "" + +msgctxt "#30106" +msgid "RTP Port" +msgstr "" diff --git a/packages/addons/service/librespot/source/resources/lib/ls_addon.py b/packages/addons/service/librespot/source/resources/lib/ls_addon.py new file mode 100644 index 0000000000..8fab49300f --- /dev/null +++ b/packages/addons/service/librespot/source/resources/lib/ls_addon.py @@ -0,0 +1,45 @@ +import os +import socket +import xbmc +import xbmcaddon +import xbmcgui + + +DEFAULTS = dict( + autoplay='true', + bitrate='320', + device='librespot', + discovery='true', + name='Librespot@{}', + password='', + rtp_dest='127.0.0.1', + rtp_port='24642', + username='', +) + +ADDON = xbmcaddon.Addon() +ADDON_HOME = xbmc.translatePath(ADDON.getAddonInfo('profile')) +ADDON_ICON = ADDON.getAddonInfo('icon') +ADDON_NAME = ADDON.getAddonInfo('name') +ADDON_PATH = ADDON.getAddonInfo('path') +ADDON_ENVT = dict(PATH=os.path.join(ADDON_PATH, 'bin')) +DIALOG = xbmcgui.Dialog() + + +def get_settings(): + if not os.path.exists(ADDON_HOME): + os.makedirs(ADDON_HOME) + settings = dict() + for id in DEFAULTS.keys(): + value = ADDON.getSetting(id) + settings[id] = DEFAULTS[id] if value == '' else value + settings['name'] = settings['name'].format(socket.gethostname()) + return settings + + +def log(message): + xbmc.log('{}: {}'.format(ADDON_NAME, message), xbmc.LOGNOTICE) + + +def notification(message): + DIALOG.notification(ADDON_NAME, message, ADDON_ICON) diff --git a/packages/addons/service/librespot/source/resources/lib/ls_librespot.py b/packages/addons/service/librespot/source/resources/lib/ls_librespot.py new file mode 100644 index 0000000000..a85cc4a204 --- /dev/null +++ b/packages/addons/service/librespot/source/resources/lib/ls_librespot.py @@ -0,0 +1,164 @@ +import pipes +import shlex +import subprocess +import threading +import xbmc +import xbmcgui + +from ls_addon import ADDON_ENVT as ADDON_ENVT +from ls_addon import ADDON_HOME as ADDON_HOME +from ls_addon import get_settings as get_settings +from ls_addon import log as log +from ls_pulseaudio import Pulseaudio as Pulseaudio +from ls_spotify import SPOTIFY as SPOTIFY + + +LIBRESPOT = 'librespot' \ + ' --backend pulseaudio' \ + ' --bitrate {bitrate}' \ + ' --cache cache' \ + ' --device {device}' \ + ' --device-type TV' \ + ' --disable-audio-cache' \ + ' --name {name}' \ + ' --notify-kodi' +LIBRESPOT_AUTOPLAY = ' --autoplay' +LIBRESPOT_AUTHENTICATE = ' --disable-discovery' \ + ' --password {password}' \ + ' --username {username}' + +CODEC = 'pcm_s16be' +MAX_PANICS = 3 + + +class Librespot(xbmc.Player): + + def __init__(self): + super(Librespot, self).__init__() + settings = get_settings() + quoted = {k: pipes.quote(v) for (k, v) in settings.items()} + command = LIBRESPOT + if settings['autoplay'] == 'true': + command += LIBRESPOT_AUTOPLAY + if (settings['discovery'] == 'false' and + settings['password'] != '' and + settings['username'] != ''): + command += LIBRESPOT_AUTHENTICATE + self.command = shlex.split(command.format(**quoted)) + log(shlex.split(command.format(**dict(quoted, password='*obfuscated*')))) + self.is_aborted = False + self.is_dead = False + self.pulseaudio = Pulseaudio(settings) + self.listitem = xbmcgui.ListItem() + self.listitem.addStreamInfo('audio', {'codec': CODEC}) + self.listitem.setPath(path=self.pulseaudio.url) + + def __enter__(self): + self.pulseaudio.load_modules() + self.panics = 0 + self.librespot = None + self.is_playing_librespot = False + if not self.isPlaying(): + self.start_librespot() + + def __exit__(self, *args): + self.stop_librespot() + self.pulseaudio.unload_modules() + + def on_event_panic(self): + self.pulseaudio.suspend_sink(1) + self.panics += 1 + log('event panic {}/{}'.format(self.panics, MAX_PANICS)) + self.is_dead = self.panics >= MAX_PANICS + self.stop_librespot(True) + + def on_event_playing(self, type, id): + log('event playing') + SPOTIFY.update_listitem(self.listitem, type, id, self.country) + if not self.isPlaying(): + log('starting librespot playback') + self.pulseaudio.suspend_sink(0) + self.play(self.pulseaudio.url, self.listitem) + elif self.is_playing_librespot: + log('updating librespot playback') + self.updateInfoTag(self.listitem) + + def on_event_stopped(self): + self.pulseaudio.suspend_sink(1) + log('event stopped') + self.panics = 0 + self.stop() + + def onPlayBackEnded(self): + self.onPlayBackStopped() + + def onPlayBackError(self): + self.onPlayBackStopped() + + def onPlayBackStarted(self): + log('Kodi playback started') + self.is_playing_librespot = self.getPlayingFile() == self.pulseaudio.url + if not self.is_playing_librespot: + self.stop_librespot() + + def onPlayBackStopped(self): + if self.is_playing_librespot: + log('librespot playback stopped') + self.is_playing_librespot = False + self.stop_librespot(True) + else: + log('Kodi playback stopped') + self.start_librespot() + + def run_librespot(self): + log('librespot thread started') + self.restart = True + while self.restart and not self.is_dead: + self.librespot = subprocess.Popen( + self.command, + cwd=ADDON_HOME, + env=ADDON_ENVT, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE) + log('librespot started') + with self.librespot.stdout: + while True: + line = self.librespot.stdout.readline() + if line == '': + break + words = line.split() + if words[0] == '@Playing': + self.on_event_playing(words[1], words[2]) + elif words[0] == '@Stopped': + self.on_event_stopped() + elif words[0] == 'stack': + self.on_event_panic() + else: + log(line.rstrip()) + if 'Country:' in line: + self.country = words[-1].strip('"') + log('country={}'.format(self.country)) + self.pulseaudio.suspend_sink(1) + self.stop() + self.librespot.wait() + log('librespot stopped') + self.librespot = None + log('librespot thread stopped') + + def start_librespot(self): + if self.librespot is None: + self.thread = threading.Thread(target=self.run_librespot) + self.thread.start() + + def stop(self): + if self.is_playing_librespot and not self.is_aborted: + log('stopping librespot playback') + self.is_playing_librespot = False + super(Librespot, self).stop() + + def stop_librespot(self, restart=False): + self.restart = restart + if self.librespot is not None: + self.librespot.terminate() + if not restart: + self.thread.join() diff --git a/packages/addons/service/librespot/source/resources/lib/ls_monitor.py b/packages/addons/service/librespot/source/resources/lib/ls_monitor.py new file mode 100644 index 0000000000..fb56eb7cfe --- /dev/null +++ b/packages/addons/service/librespot/source/resources/lib/ls_monitor.py @@ -0,0 +1,35 @@ +import xbmc + +from ls_addon import log as log +from ls_addon import notification as notification +from ls_librespot import Librespot as Librespot + + +class Monitor(xbmc.Monitor): + + def onSettingsChanged(self): + self.is_changed = True + + def run(self): + log('monitor started') + is_aborted = False + is_dead = False + while not (is_aborted or is_dead): + self.is_changed = False + librespot = Librespot() + with librespot: + while True: + is_aborted = self.waitForAbort(1) + if is_aborted: + log('monitor aborted') + librespot.is_aborted = True + break + is_dead = librespot.is_dead + if is_dead: + log('librespot died') + notification('Too many errors') + break + if self.is_changed: + log('settings changed') + break + log('monitor stopped') diff --git a/packages/addons/service/librespot/source/resources/lib/ls_pulseaudio.py b/packages/addons/service/librespot/source/resources/lib/ls_pulseaudio.py new file mode 100644 index 0000000000..3ab0afbc29 --- /dev/null +++ b/packages/addons/service/librespot/source/resources/lib/ls_pulseaudio.py @@ -0,0 +1,46 @@ +import subprocess + +from ls_addon import log as log + + +def run(command): + return subprocess.check_output(command.split()) + + +class Pulseaudio: + + def __init__(self, settings): + self.null_sink = dict( + module='module-null-sink', + args='sink_name={device}'.format(**settings) + ) + self.rtp_send = dict( + module='module-rtp-send', + args='destination_ip={rtp_dest} port={rtp_port}' + ' source={device}.monitor'.format(**settings) + ) + self.suspend = 'pactl suspend-sink {device} {{}}'.format(**settings) + self.url = 'rtp://{rtp_dest}:{rtp_port}'.format(**settings) + + def list_modules(self): + return [module.split('\t') + for module in run('pactl list modules short').splitlines()[::-1]] + + def load_modules(self): + args = [module[2] for module in self.list_modules()] + for module in [self.null_sink, self.rtp_send]: + if module['args'] not in args: + run('pactl load-module {} {}'.format( + module['module'], module['args'])) + log('loaded {} {}'.format(module['module'], module['args'])) + self.suspend_sink(1) + + def suspend_sink(self, bit): + run(self.suspend.format(bit)) + log('suspended sink {}'.format(bit)) + + def unload_modules(self): + for module in self.list_modules(): + if module[2] in [self.null_sink['args'], self.rtp_send['args']]: + run('pactl unload-module {}'.format(module[0])) + log('unloaded {} {}'.format(module[1], module[2])) diff --git a/packages/addons/service/librespot/source/resources/lib/ls_spotify.py b/packages/addons/service/librespot/source/resources/lib/ls_spotify.py new file mode 100644 index 0000000000..8484658e9b --- /dev/null +++ b/packages/addons/service/librespot/source/resources/lib/ls_spotify.py @@ -0,0 +1,80 @@ +import requests +import time + + +from ls_addon import ADDON_ICON as ADDON_ICON +from ls_addon import log as log + + +SPOTIFY_ENDPOINT_EPISODES = 'https://api.spotify.com/v1/episodes/' +SPOTIFY_ENDPOINT_TRACKS = 'https://api.spotify.com/v1/tracks/' +SPOTIFY_HEADERS = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', +} +SPOTIFY_REQUEST_TOKEN = { + 'url': 'https://accounts.spotify.com/api/token', + 'data': {'grant_type': 'client_credentials'}, + 'headers': {'Authorization': 'Basic MTY5ZGY1NTMyZGVlNDdhNTk5MTNmODUyOGU4M2FlNzE6MWYzZDhiNTA3YmJlNGY2OGJlYjNhNDQ3MmU4YWQ0MTE='} +} + + +def get(info, indices, default): + try: + for index in indices: + info = info[index] + return info + except LookupError: + return default + + +class Spotify: + + def __init__(self): + self.headers = SPOTIFY_HEADERS + self.expiration = time.time() + + def get_headers(self): + if time.time() > self.expiration: + log('token expired') + token = requests.post(**SPOTIFY_REQUEST_TOKEN).json() + log(token) + self.expiration = time.time() + float(token['expires_in']) - 5 + self.headers['Authorization'] = 'Bearer {}'.format( + token['access_token']) + + def get_endpoint(self, endpoint, id, market): + try: + self.get_headers() + return requests.get(url=endpoint + id, + headers=self.headers, + params=dict(market=market)).json() + except Exception as e: + log('failed to get {} from Spotify {}'.format(endpoint, e)) + return {} + + def update_listitem(self, listitem, type, id, market='SE'): + if type == 'Podcast': + info = self.get_endpoint(SPOTIFY_ENDPOINT_EPISODES, id, market) + album = get(info, ['show', 'name'], 'unknown show',) + artist = get(info, ['show', 'publisher'], 'unknown publisher') + thumb = get(info, ['images', 0, 'url'], ADDON_ICON) + title = get(info, ['name'], 'unknown episode') + elif type == 'Track': + info = self.get_endpoint(SPOTIFY_ENDPOINT_TRACKS, id, market) + album = get(info, ['album', 'name'], 'unknown album') + artist = get(info, ['artists', 0, 'name'], 'unknown artist') + thumb = get(info, ['album', 'images', 0, 'url'], ADDON_ICON) + title = get(info, ['name'], 'unknown title') + else: + album = '' + artist = 'Unknown Media Type' + thumb = ADDON_ICON + title = '' + listitem.setArt(dict(fanart=thumb, thumb=thumb)) + listitem.setInfo('music', dict( + album=album, artist=artist, title=title)) + log('{}#{}#{}#{}'.format(title, artist, album, thumb)) + + +SPOTIFY = Spotify() diff --git a/packages/addons/service/librespot/source/resources/settings.xml b/packages/addons/service/librespot/source/resources/settings.xml index 602afd1bb0..3801543450 100644 --- a/packages/addons/service/librespot/source/resources/settings.xml +++ b/packages/addons/service/librespot/source/resources/settings.xml @@ -1,8 +1,11 @@ - - - + + + + + + diff --git a/packages/addons/service/librespot/source/settings-default.xml b/packages/addons/service/librespot/source/settings-default.xml deleted file mode 100644 index 58730ed143..0000000000 --- a/packages/addons/service/librespot/source/settings-default.xml +++ /dev/null @@ -1,5 +0,0 @@ - - false - - - diff --git a/packages/addons/service/librespot/source/system.d/service.librespot.service b/packages/addons/service/librespot/source/system.d/service.librespot.service deleted file mode 100644 index 4cf57aa695..0000000000 --- a/packages/addons/service/librespot/source/system.d/service.librespot.service +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Description=librespot -Wants=kodi.service -After=kodi.service - -[Service] -EnvironmentFile=/storage/.kodi/addons/service.librespot/ls_env.py -ExecStart=/bin/sh /storage/.kodi/addons/service.librespot/bin/librespot.start -ExecStopPost=-/usr/bin/pactl suspend-sink librespot_sink 1 -Restart=always - -[Install] -WantedBy=kodi.service