From 6a0e35e8c4de3c5fe7ca3d3910e3fad938eb6474 Mon Sep 17 00:00:00 2001 From: awiouy Date: Sat, 19 Jan 2019 21:59:39 +0100 Subject: [PATCH] librespot: update to 4e3576b (2019-06-01), only play with kodi --- .../addons/service/librespot/changelog.txt | 7 +- packages/addons/service/librespot/package.mk | 32 ++- .../patches/librespot-01_notify_kodi.patch | 151 ++++++++++++ .../patches/librespot-02_kodi_hooks.patch | 140 ----------- ...ibrespot-02_use_own_pulseaudio_sink.patch} | 4 +- .../addons/service/librespot/source/addon.py | 22 -- .../librespot/source/bin/librespot.env | 2 - .../librespot/source/bin/librespot.onevent | 2 - .../librespot/source/bin/librespot.start | 118 ++-------- .../service/librespot/source/default.py | 217 +++++++++++++++++- .../addons/service/librespot/source/ls_env.py | 5 + .../resources/language/English/strings.po | 76 +----- .../source/resources/lib/ls_librespot.py | 42 ---- .../librespot/source/resources/lib/ls_log.py | 12 - .../source/resources/lib/ls_monitor.py | 22 -- .../source/resources/lib/ls_player.py | 134 ----------- .../source/resources/lib/ls_spotify.py | 63 ----- .../librespot/source/resources/settings.xml | 13 +- .../librespot/source/settings-default.xml | 4 - .../source/system.d/service.librespot.service | 6 +- 20 files changed, 427 insertions(+), 645 deletions(-) create mode 100644 packages/addons/service/librespot/patches/librespot-01_notify_kodi.patch delete mode 100644 packages/addons/service/librespot/patches/librespot-02_kodi_hooks.patch rename packages/addons/service/librespot/patches/{librespot-03_pulseaudio_sink.patch => librespot-02_use_own_pulseaudio_sink.patch} (95%) delete mode 100644 packages/addons/service/librespot/source/addon.py delete mode 100644 packages/addons/service/librespot/source/bin/librespot.env delete mode 100755 packages/addons/service/librespot/source/bin/librespot.onevent create mode 100644 packages/addons/service/librespot/source/ls_env.py delete mode 100644 packages/addons/service/librespot/source/resources/lib/ls_librespot.py delete mode 100644 packages/addons/service/librespot/source/resources/lib/ls_log.py delete mode 100644 packages/addons/service/librespot/source/resources/lib/ls_monitor.py delete mode 100644 packages/addons/service/librespot/source/resources/lib/ls_player.py delete mode 100644 packages/addons/service/librespot/source/resources/lib/ls_spotify.py diff --git a/packages/addons/service/librespot/changelog.txt b/packages/addons/service/librespot/changelog.txt index 031b25c6ab..17d2f5dce2 100644 --- a/packages/addons/service/librespot/changelog.txt +++ b/packages/addons/service/librespot/changelog.txt @@ -1,5 +1,10 @@ +116 +- Update to 4e3576b (2019-06-01) +- Only use Kodi +- Rework + 115 -- Update to daeeeaa (22-02-2019) +- Update to daeeeaa (2019-02-22) 114 - Fix discovery mode setting diff --git a/packages/addons/service/librespot/package.mk b/packages/addons/service/librespot/package.mk index 37f62433cb..51e0b22adb 100644 --- a/packages/addons/service/librespot/package.mk +++ b/packages/addons/service/librespot/package.mk @@ -1,25 +1,25 @@ -# SPDX-License-Identifier: GPL-2.0-or-later +# SPDX-License-Identifier: GPL-2.0 # Copyright (C) 2017 Shane Meagher (shanemeagher) # Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) PKG_NAME="librespot" -PKG_VERSION="daeeeaa122fc2d71edf11e562e23038db4210b39" -PKG_SHA256="e9ebb8ca09c850598ae8c222bdab44e7d8321cb3c36017ba8e17a41db418c06e" -PKG_VERSION_DATE="2019-02-22" -PKG_REV="115" +PKG_VERSION="4e3576ba7c6146cf68e1953daeec929d619b26b1" +PKG_SHA256="c2fef0253bdbb6ff7085d4ec00e80da4727a1c5b1bf84dd2c14edc3de1cfd753" +PKG_VERSION_DATE="2019-06-01" +PKG_REV="116" PKG_ARCH="any" PKG_LICENSE="MIT" PKG_SITE="https://github.com/librespot-org/librespot/" PKG_URL="https://github.com/librespot-org/librespot/archive/$PKG_VERSION.zip" -PKG_DEPENDS_TARGET="toolchain avahi pulseaudio pyalsaaudio rust" +PKG_DEPENDS_TARGET="toolchain avahi pulseaudio rust" PKG_SECTION="service" -PKG_SHORTDESC="Librespot: play Spotify through LibreELEC using a Spotify app as a remote" -PKG_LONGDESC="Librespot ($PKG_VERSION_DATE) plays Spotify through LibreELEC using the open source librespot library using a Spotify app as a remote." +PKG_SHORTDESC="Librespot: play Spotify through Kodi using a Spotify app as a remote" +PKG_LONGDESC="Librespot ($PKG_VERSION_DATE) lets you play Spotify through Kodi using a Spotify app as a remote." PKG_TOOLCHAIN="manual" PKG_IS_ADDON="yes" PKG_ADDON_NAME="Librespot" -PKG_ADDON_TYPE="xbmc.service.library" +PKG_ADDON_TYPE="xbmc.service" PKG_MAINTAINER="Anton Voyl (awiouy)" configure_target() { @@ -29,21 +29,17 @@ configure_target() { make_target() { cd src - $CARGO_BUILD --no-default-features --features "alsa-backend pulseaudio-backend with-dns-sd" + $CARGO_BUILD --no-default-features --features "pulseaudio-backend with-dns-sd" cd "$PKG_BUILD/.$TARGET_NAME"/*/release $STRIP librespot } addon() { - mkdir -p "$ADDON_BUILD/$PKG_ADDON_ID" - cp "$(get_build_dir pyalsaaudio)/.install_pkg/usr/lib/$PKG_PYTHON_VERSION/site-packages/alsaaudio.so" \ - "$ADDON_BUILD/$PKG_ADDON_ID" - mkdir -p "$ADDON_BUILD/$PKG_ADDON_ID/bin" - cp "$PKG_BUILD/.$TARGET_NAME"/*/release/librespot \ - "$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" + cp "$(get_build_dir avahi)/avahi-compat-libdns_sd/.libs/libdns_sd.so.1" \ + "$ADDON_BUILD/$PKG_ADDON_ID/lib" } diff --git a/packages/addons/service/librespot/patches/librespot-01_notify_kodi.patch b/packages/addons/service/librespot/patches/librespot-01_notify_kodi.patch new file mode 100644 index 0000000000..e756a9c346 --- /dev/null +++ b/packages/addons/service/librespot/patches/librespot-01_notify_kodi.patch @@ -0,0 +1,151 @@ +diff --git a/playback/src/config.rs b/playback/src/config.rs +index 0f71110..931167d 100644 +--- a/playback/src/config.rs ++++ b/playback/src/config.rs +@@ -30,6 +30,7 @@ pub struct PlayerConfig { + pub bitrate: Bitrate, + pub normalisation: bool, + pub normalisation_pregain: f32, ++ pub notify_kodi: bool, + } + + impl Default for PlayerConfig { +@@ -38,6 +39,7 @@ impl Default for PlayerConfig { + bitrate: Bitrate::default(), + normalisation: false, + normalisation_pregain: 0.0, ++ notify_kodi: false, + } + } + } +diff --git a/playback/src/player.rs b/playback/src/player.rs +index ab1a8ab..19d6394 100644 +--- a/playback/src/player.rs ++++ b/playback/src/player.rs +@@ -4,7 +4,8 @@ use futures::sync::oneshot; + use futures::{future, Future}; + use std; + use std::borrow::Cow; +-use std::io::{Read, Result, Seek, SeekFrom}; ++use std::fs::OpenOptions; ++use std::io::{Read, Result, Seek, SeekFrom, Write}; + use std::mem; + use std::sync::mpsc::{RecvError, RecvTimeoutError, TryRecvError}; + use std::thread; +@@ -394,6 +395,14 @@ impl PlayerInternal { + } + } + ++ fn notify_kodi(&mut self, id: &str, track_id: &SpotifyId) { ++ // println!("fifo = {} {}", id, track_id.to_base62()); ++ if self.config.notify_kodi { ++ let mut file = OpenOptions::new().write(true).open("/tmp/librespot").unwrap(); ++ writeln!(&mut file, "{}\n{}", id, track_id.to_base62()).unwrap(); ++ } ++ } ++ + fn handle_command(&mut self, cmd: PlayerCommand) { + debug!("command={:?}", cmd); + match cmd { +@@ -413,11 +422,17 @@ impl PlayerInternal { + | PlayerState::EndOfTrack { + track_id: old_track_id, + .. +- } => self.send_event(PlayerEvent::Changed { +- old_track_id: old_track_id, +- new_track_id: track_id, +- }), +- _ => self.send_event(PlayerEvent::Started { track_id }), ++ } => { ++ self.send_event(PlayerEvent::Changed { ++ old_track_id: old_track_id, ++ new_track_id: track_id, ++ }); ++ self.notify_kodi("1", &track_id) ++ } ++ _ => { ++ self.send_event(PlayerEvent::Started { track_id }); ++ self.notify_kodi("2", &track_id) ++ } + } + + self.start_sink(); +@@ -443,13 +458,17 @@ impl PlayerInternal { + | PlayerState::EndOfTrack { + track_id: old_track_id, + .. +- } => self.send_event(PlayerEvent::Changed { +- old_track_id: old_track_id, +- new_track_id: track_id, +- }), ++ } => { ++ self.send_event(PlayerEvent::Changed { ++ old_track_id: old_track_id, ++ new_track_id: track_id, ++ }); ++ self.notify_kodi("3", &track_id) ++ } + _ => (), + } + self.send_event(PlayerEvent::Stopped { track_id }); ++ self.notify_kodi("4", &track_id) + } + } + +@@ -476,6 +495,7 @@ impl PlayerInternal { + + self.send_event(PlayerEvent::Started { track_id }); + self.start_sink(); ++ self.notify_kodi("5", &track_id) + } else { + warn!("Player::play called from invalid state"); + } +@@ -487,6 +507,7 @@ impl PlayerInternal { + + self.stop_sink_if_running(); + self.send_event(PlayerEvent::Stopped { track_id }); ++ self.notify_kodi("6", &track_id) + } else { + warn!("Player::pause called from invalid state"); + } +@@ -499,6 +520,7 @@ impl PlayerInternal { + self.stop_sink_if_running(); + self.send_event(PlayerEvent::Stopped { track_id }); + self.state = PlayerState::Stopped; ++ self.notify_kodi("7", &track_id) + } + PlayerState::Stopped => { + warn!("Player::stop called from invalid state"); +diff --git a/src/main.rs b/src/main.rs +index 36cd1b5..502cac8 100644 +--- a/src/main.rs ++++ b/src/main.rs +@@ -168,6 +168,11 @@ fn setup(args: &[String]) -> Setup { + "Pregain (dB) applied by volume normalisation", + "PREGAIN", + ) ++ .optflag( ++ "", ++ "notify-kodi", ++ "Notify Kodi", ++ ) + .optflag( + "", + "linear-volume", +@@ -248,6 +253,8 @@ fn setup(args: &[String]) -> Setup { + ) + }; + ++ let notify_kodi = matches.opt_present("notify-kodi"); ++ + let session_config = { + let device_id = device_id(&name); + +@@ -291,6 +298,7 @@ fn setup(args: &[String]) -> Setup { + .opt_str("normalisation-pregain") + .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) + .unwrap_or(PlayerConfig::default().normalisation_pregain), ++ notify_kodi: notify_kodi, + } + }; + diff --git a/packages/addons/service/librespot/patches/librespot-02_kodi_hooks.patch b/packages/addons/service/librespot/patches/librespot-02_kodi_hooks.patch deleted file mode 100644 index dc6a78028b..0000000000 --- a/packages/addons/service/librespot/patches/librespot-02_kodi_hooks.patch +++ /dev/null @@ -1,140 +0,0 @@ -From 109452968762175b255d3a46d2447bf103022a68 Mon Sep 17 00:00:00 2001 -From: awiouy -Date: Wed, 7 Nov 2018 07:49:31 +0100 -Subject: [PATCH] libreelec: kodi hooks - ---- - playback/src/player.rs | 44 ++++++++++++++++++++++++++----------- - src/player_event_handler.rs | 8 +++++-- - 2 files changed, 37 insertions(+), 15 deletions(-) - -diff --git a/playback/src/player.rs b/playback/src/player.rs -index ab1a8ab..0aa0630 100644 ---- a/playback/src/player.rs -+++ b/playback/src/player.rs -@@ -49,15 +49,18 @@ enum PlayerCommand { - pub enum PlayerEvent { - Started { - track_id: SpotifyId, -+ new_state: String, - }, - - Changed { - old_track_id: SpotifyId, - new_track_id: SpotifyId, -+ new_state: String, - }, - - Stopped { - track_id: SpotifyId, -+ new_state: String, - }, - } - -@@ -413,11 +416,18 @@ impl PlayerInternal { - | PlayerState::EndOfTrack { - track_id: old_track_id, - .. -- } => self.send_event(PlayerEvent::Changed { -- old_track_id: old_track_id, -- new_track_id: track_id, -- }), -- _ => self.send_event(PlayerEvent::Started { track_id }), -+ } => { -+ let new_state = "play".to_string(); -+ self.send_event(PlayerEvent::Changed { -+ old_track_id: old_track_id, -+ new_track_id: track_id, -+ new_state: new_state, -+ }); -+ }, -+ _ => { -+ let new_state = "play".to_string(); -+ self.send_event(PlayerEvent::Started { track_id, new_state }); -+ }, - } - - self.start_sink(); -@@ -443,13 +453,18 @@ impl PlayerInternal { - | PlayerState::EndOfTrack { - track_id: old_track_id, - .. -- } => self.send_event(PlayerEvent::Changed { -- old_track_id: old_track_id, -- new_track_id: track_id, -- }), -+ } => { -+ let new_state = "pause".to_string(); -+ self.send_event(PlayerEvent::Changed { -+ old_track_id: old_track_id, -+ new_track_id: track_id, -+ new_state: new_state, -+ }) -+ }, - _ => (), - } -- self.send_event(PlayerEvent::Stopped { track_id }); -+ let new_state = "pause".to_string(); -+ self.send_event(PlayerEvent::Stopped { track_id, new_state }); - } - } - -@@ -474,7 +489,8 @@ impl PlayerInternal { - if let PlayerState::Paused { track_id, .. } = self.state { - self.state.paused_to_playing(); - -- self.send_event(PlayerEvent::Started { track_id }); -+ let new_state = "play".to_string(); -+ self.send_event(PlayerEvent::Started { track_id, new_state }); - self.start_sink(); - } else { - warn!("Player::play called from invalid state"); -@@ -486,7 +502,8 @@ impl PlayerInternal { - self.state.playing_to_paused(); - - self.stop_sink_if_running(); -- self.send_event(PlayerEvent::Stopped { track_id }); -+ let new_state = "pause".to_string(); -+ self.send_event(PlayerEvent::Stopped { track_id, new_state }); - } else { - warn!("Player::pause called from invalid state"); - } -@@ -497,7 +514,8 @@ impl PlayerInternal { - | PlayerState::Paused { track_id, .. } - | PlayerState::EndOfTrack { track_id } => { - self.stop_sink_if_running(); -- self.send_event(PlayerEvent::Stopped { track_id }); -+ let new_state = "stop".to_string(); -+ self.send_event(PlayerEvent::Stopped { track_id, new_state }); - self.state = PlayerState::Stopped; - } - PlayerState::Stopped => { -diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs -index 1e682b9..3b478f1 100644 ---- a/src/player_event_handler.rs -+++ b/src/player_event_handler.rs -@@ -19,18 +19,22 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> io::Result { - env_vars.insert("PLAYER_EVENT", "change".to_string()); - env_vars.insert("OLD_TRACK_ID", old_track_id.to_base62()); - env_vars.insert("TRACK_ID", new_track_id.to_base62()); -+ env_vars.insert("STATE", new_state.to_string()); - } -- PlayerEvent::Started { track_id } => { -+ PlayerEvent::Started { track_id, new_state } => { - env_vars.insert("PLAYER_EVENT", "start".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); -+ env_vars.insert("STATE", new_state.to_string()); - } -- PlayerEvent::Stopped { track_id } => { -+ PlayerEvent::Stopped { track_id, new_state } => { - env_vars.insert("PLAYER_EVENT", "stop".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); -+ env_vars.insert("STATE", new_state.to_string()); - } - } - run_program(onevent, env_vars) diff --git a/packages/addons/service/librespot/patches/librespot-03_pulseaudio_sink.patch b/packages/addons/service/librespot/patches/librespot-02_use_own_pulseaudio_sink.patch similarity index 95% rename from packages/addons/service/librespot/patches/librespot-03_pulseaudio_sink.patch rename to packages/addons/service/librespot/patches/librespot-02_use_own_pulseaudio_sink.patch index f441a109e7..e6f4ce2405 100644 --- a/packages/addons/service/librespot/patches/librespot-03_pulseaudio_sink.patch +++ b/packages/addons/service/librespot/patches/librespot-02_use_own_pulseaudio_sink.patch @@ -12,7 +12,7 @@ index 88f6280..4e7186b 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -76,6 +76,7 @@ impl Open for PulseAudioSink { - + impl Sink for PulseAudioSink { fn start(&mut self) -> io::Result<()> { + let sink = CString::new("librespot_sink").unwrap(); @@ -24,7 +24,7 @@ index 88f6280..4e7186b 100644 self.name.as_ptr(), // Our application's name. PA_STREAM_PLAYBACK, - null(), // Use the default device. -+ sink.as_ptr(), // Our sink. ++ 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/addon.py b/packages/addons/service/librespot/source/addon.py deleted file mode 100644 index 5d60d74952..0000000000 --- a/packages/addons/service/librespot/source/addon.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) - -import alsaaudio -import xbmcaddon -import xbmcgui - - -dialog = xbmcgui.Dialog() -strings = xbmcaddon.Addon().getLocalizedString -while True: - pcms = alsaaudio.pcms()[1:] - if len(pcms) == 0: - dialog.ok(xbmcaddon.Addon().getAddonInfo('name'), strings(30210)) - break - pcmx = dialog.select(strings(30115), pcms) - if pcmx == -1: - break - pcm = pcms[pcmx] - xbmcaddon.Addon().setSetting('ls_o', pcm) - break -del dialog diff --git a/packages/addons/service/librespot/source/bin/librespot.env b/packages/addons/service/librespot/source/bin/librespot.env deleted file mode 100644 index 3ca5ee662a..0000000000 --- a/packages/addons/service/librespot/source/bin/librespot.env +++ /dev/null @@ -1,2 +0,0 @@ -LS_PORT="6666" -LS_SINK="librespot_sink" diff --git a/packages/addons/service/librespot/source/bin/librespot.onevent b/packages/addons/service/librespot/source/bin/librespot.onevent deleted file mode 100755 index d528c6f537..0000000000 --- a/packages/addons/service/librespot/source/bin/librespot.onevent +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -echo -e "$STATE\n$TRACK_ID" > "$LS_FIFO" diff --git a/packages/addons/service/librespot/source/bin/librespot.start b/packages/addons/service/librespot/source/bin/librespot.start index 7f321497db..2f981a3dfb 100755 --- a/packages/addons/service/librespot/source/bin/librespot.start +++ b/packages/addons/service/librespot/source/bin/librespot.start @@ -3,110 +3,34 @@ # SPDX-License-Identifier: GPL-2.0 # Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) -f="/storage/.kodi/userdata/addon_data/service.librespot/settings.xml" -[ -f "$f" ] && sed -i 's/ls_O/ls_m/g' "$f" - -activate_card() { - if [ -e "/proc/asound/$1" ]; then - return - fi - case "$LIBREELEC_ARCH" in - RPi*.arm) - if [ "$1" = "ALSA" ]; then - dtparam audio=on - sleep 1 - fi - ;; - *) - echo "Unable to activate card $1 on $LIBREELEC_ARCH" - exit - ;; - esac -} - -init_alsa() { - . /etc/os-release - - if [ ! "$(cat /proc/asound/pcm 2> /dev/null)" ]; then - case "$LIBREELEC_ARCH" in - RPi*.arm) - activate_card "ALSA" - ;; - *) - echo "Unable to activate an audio interface on $LIBREELEC_ARCH" - exit - ;; - esac - fi - - case "$ls_o" in - *:CARD=*) - card="${ls_o##*:CARD=}" - card="${card%%,*}" - activate_card "$card" - index="$(readlink /proc/asound/$card)" - index="${index##*card}" - ;; - hw:*,*) - echo "The hw:d,s specification is unreliable, use device:CARD=card instead" - index="${ls_o##hw:}" - index="${index%%,*}" - card="card$index" - activate_card "$card" - ;; - *) - if [ -n "$ls_o" ]; then - echo "Unknown playback device specification $ls_o" - fi - ;; - esac - - case "$LIBREELEC_ARCH" in - RPi*.arm) - [ "$(readlink /proc/asound/ALSA)" = "card$index" ] && [ "$pcm_3" ] && - amixer -c "$index" cset name="PCM Playback Route" "$pcm_3" - ;; - esac -} - - . /etc/profile oe_setup_addon service.librespot -LIBRESPOT="librespot --cache \"$ADDON_HOME/cache\" \ - --disable-audio-cache \ - --name \"Librespot@$HOSTNAME\" \ - --onevent librespot.onevent" +PORT="6666" +SINK_NAME="librespot_sink" -if [ -n "$ls_b" -a "$ls_b" != "-" ]; then - LIBRESPOT="$LIBRESPOT --bitrate $ls_b" +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 \ + --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\"" + LIBRESPOT="$LIBRESPOT \ + --disable-discovery \ + --password \"$ls_p\" \ + --username \"$ls_u\"" fi -if [ "$ls_m" = "Kodi" ]; then - LIBRESPOT="$LIBRESPOT --backend pulseaudio --device-type TV" -else - init_alsa - if [ -n "$ls_o" ]; then - LIBRESPOT="$LIBRESPOT --device \"$ls_o\"" - fi - LIBRESPOT="$LIBRESPOT --device-type Speaker" -fi - -if [ -z "$(pactl list short modules | grep sink_name=$LS_SINK)" ]; then - pactl load-module module-null-sink sink_name="$LS_SINK" > /dev/null -fi -pactl suspend-sink "$LS_SINK" 1 -if [ -z "$(pactl list short modules | grep source=$LS_SINK.monitor)" ]; then - pactl load-module module-rtp-send source="$LS_SINK.monitor" \ - destination_ip=127.0.0.1 port="$LS_PORT" source_ip=127.0.0.1 > /dev/null -fi - -export LS_FIFO="/var/run/librespot" - eval $LIBRESPOT diff --git a/packages/addons/service/librespot/source/default.py b/packages/addons/service/librespot/source/default.py index d9d6bc60c8..1185cf4bc7 100644 --- a/packages/addons/service/librespot/source/default.py +++ b/packages/addons/service/librespot/source/default.py @@ -1,13 +1,224 @@ # SPDX-License-Identifier: GPL-2.0 # Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) +import base64 +import json import os -import sys +import stat +import subprocess +import threading +import time +import urllib +import urllib2 +import xbmc +import xbmcaddon +import xbmcgui -sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'resources', 'lib')) +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" +SPOTIFY_ID = '169df5532dee47a59913f8528e83ae71' +SPOTIFY_SECRET = '1f3d8b507bbe4f68beb3a4472e8ad411' +STREAM_CODEC = 'pcm_s16be' +STREAM_PORT = '6666' +STREAM_URL = 'rtp://127.0.0.1:{}'.format(STREAM_PORT) -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.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') + 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, track_id): + track = self.spotify.getTrack(track_id) + self.listitem.setArt(track.getArt()) + self.listitem.setInfo('music', track.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 + os.mkfifo(FIFO) + while (os.path.exists(FIFO) and + stat.S_ISFIFO(os.stat(FIFO).st_mode)): + with open(FIFO, 'r') as fifo: + command = fifo.read().splitlines() + log('control pipe {}'.format(str(command))) + if len(command) == 0: + break + elif command[0] in ['3', '5', '6']: + self.pauseLibrespot() + elif command[0] in ['1', '2', '4']: + self.playLibrespot(command[1]) + elif command[0] in ['7']: + 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() + self.request = [ + 'https://accounts.spotify.com/api/token', + urllib.urlencode({'grant_type': 'client_credentials'}), + {'Authorization': 'Basic {}'.format(base64.b64encode( + '{}:{}'.format(SPOTIFY_ID, SPOTIFY_SECRET)))} + ] + + def getHeaders(self): + if time.time() > self.expiration: + log('token expired') + token = json.loads(urllib2.urlopen( + urllib2.Request(*self.request)).read()) + log('new token expires in {} seconds'.format(token['expires_in'])) + self.expiration = time.time() + float(token['expires_in']) - 60 + self.headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Bearer {}'.format(token['access_token']) + } + + def getTrack(self, track_id): + log('getting track') + try: + self.getHeaders() + track = json.loads(urllib2.urlopen(urllib2.Request( + 'https://api.spotify.com/v1/tracks/{}'.format(track_id), None, + self.headers)).read()) + except Exception as e: + log('failed to get track from Spotify: {}'.format(e)) + track = dict() + return Track(track) + + +class Track(): + + def __init__(self, track): + self.track = track + + def get(self, default, *indices): + tree = self.track + try: + for index in indices: + tree = tree[index] + except LookupError: + tree = default + return tree + + 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') diff --git a/packages/addons/service/librespot/source/ls_env.py b/packages/addons/service/librespot/source/ls_env.py new file mode 100644 index 0000000000..e86b58203a --- /dev/null +++ b/packages/addons/service/librespot/source/ls_env.py @@ -0,0 +1,5 @@ +# 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 1a72bdf8ee..9cac7a2e18 100644 --- a/packages/addons/service/librespot/source/resources/language/English/strings.po +++ b/packages/addons/service/librespot/source/resources/language/English/strings.po @@ -4,81 +4,17 @@ msgid "" msgstr "" msgctxt "#30100" -msgid "Librespot" +msgid "Configuration" msgstr "" -msgctxt "#30102" -msgid "Bit rate" -msgstr "" - -msgctxt "#30103" -msgid "-" -msgstr "" - -msgctxt "#30104" -msgid "96" -msgstr "" - -msgctxt "#30105" -msgid "160" -msgstr "" - -msgctxt "#30106" -msgid "320" -msgstr "" - -msgctxt "#30107" -msgid "Output mode" -msgstr "" - -msgctxt "#30108" -msgid "ALSA" -msgstr "" - -msgctxt "#30109" -msgid "Kodi" -msgstr "" - -msgctxt "#30110" -msgid "Username" -msgstr "" - -msgctxt "#30111" -msgid "Password" -msgstr "" - -msgctxt "#30112" +msgctxt "#30101" msgid "User mode" msgstr "" -msgctxt "#30113" -msgid "ALSA" +msgctxt "#30102" +msgid "Username" msgstr "" -msgctxt "#30114" -msgid "Configuration wizard" -msgstr "" - -msgctxt "#30115" -msgid "Playback device" -msgstr "" - -msgctxt "#30116" -msgid "Playback route" -msgstr "" - -msgctxt "#30117" -msgid "auto detect" -msgstr "" - -msgctxt "#30118" -msgid "headphone jack" -msgstr "" - -msgctxt "#30119" -msgid "HDMI" -msgstr "" - -msgctxt "#30210" -msgid "Could not find a playback device" +msgctxt "#30103" +msgid "Password" msgstr "" diff --git a/packages/addons/service/librespot/source/resources/lib/ls_librespot.py b/packages/addons/service/librespot/source/resources/lib/ls_librespot.py deleted file mode 100644 index 72ce6ce74f..0000000000 --- a/packages/addons/service/librespot/source/resources/lib/ls_librespot.py +++ /dev/null @@ -1,42 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) - -import subprocess -import xbmcaddon - -from ls_log import log as log - - -_SERVICE = xbmcaddon.Addon().getAddonInfo('id') -_SINK = 'librespot_sink' - - -def _pactl(bit): - log('pactl {}'.format(bit)) - subprocess.call(['pactl', 'suspend-sink', _SINK, bit]) - - -def _systemctl(command): - log('systemctl {}'.format(command)) - subprocess.call(['systemctl', command, _SERVICE]) - _pactl('1') - - -class Librespot(): - - def __init__(self): - self.state = True - - def restart(self): - log('restarting librespot') - _systemctl('restart') - self.state = True - - def stop(self): - if self.state: - log('stopping librespot') - _systemctl('stop') - self.state = False - - def unsuspend(self): - _pactl('0') diff --git a/packages/addons/service/librespot/source/resources/lib/ls_log.py b/packages/addons/service/librespot/source/resources/lib/ls_log.py deleted file mode 100644 index 1a95ebb6e2..0000000000 --- a/packages/addons/service/librespot/source/resources/lib/ls_log.py +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2018-present Team LibreELEC (https://libreelec.tv) - -import xbmc -import xbmcaddon - - -_MESSAGE = xbmcaddon.Addon().getAddonInfo('name') + ': {}' - - -def log(message): - xbmc.log(_MESSAGE.format(message), xbmc.LOGNOTICE) diff --git a/packages/addons/service/librespot/source/resources/lib/ls_monitor.py b/packages/addons/service/librespot/source/resources/lib/ls_monitor.py deleted file mode 100644 index 36cee28472..0000000000 --- a/packages/addons/service/librespot/source/resources/lib/ls_monitor.py +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) - -import xbmc - -from ls_log import log as log -from ls_player import Player as Player - - -class Monitor(xbmc.Monitor): - - def __init__(self): - log('monitor started') - self.player = Player() - - def onSettingsChanged(self): - self.player.onSettingsChanged() - - def waitForAbort(self): - super(Monitor, self).waitForAbort() - self.player.onAbortRequested() - log('monitor stopped') diff --git a/packages/addons/service/librespot/source/resources/lib/ls_player.py b/packages/addons/service/librespot/source/resources/lib/ls_player.py deleted file mode 100644 index 727d0bcc80..0000000000 --- a/packages/addons/service/librespot/source/resources/lib/ls_player.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) - -import os -import stat -import threading -import xbmc -import xbmcaddon -import xbmcgui - -from ls_librespot import Librespot as Librespot -from ls_log import log as log -from ls_spotify import Spotify as Spotify - -_CODEC = 'pcm_s16le' -_FIFO = '/var/run/librespot' -_STREAM = 'rtp://127.0.0.1:6666' - -_DEFAULT_ICON = xbmcaddon.Addon().getAddonInfo('icon') -_DEFAULT_TITLE = xbmcaddon.Addon().getAddonInfo('name') - - -class Player(threading.Thread, xbmc.Player): - - def __init__(self): - super(Player, self).__init__() - self.updateSettings() - self.dialog = xbmcgui.Dialog() - self.librespot = Librespot() - self.listitem = xbmcgui.ListItem() - self.listitem.addStreamInfo('audio', {'codec': _CODEC}) - self.listitem.setPath(_STREAM) - self.spotify = Spotify() - self.start() - if self.isPlaying(): - self.onPlayBackStarted() - - def onAbortRequested(self): - log('abort requested') - with open(_FIFO, 'w') as fifo: - fifo.close() - self.join() - - def onPlayBackEnded(self): - log('a playback ended') - self.librespot.restart() - - def onPlayBackStarted(self): - log('a playback started') - if self.getPlayingFile() != _STREAM: - self.librespot.stop() - - def onPlayBackStopped(self): - log('a playback stopped') - self.librespot.restart() - - def onSettingsChanged(self): - log('settings changed') - self.stop() - self.updateSettings() - - def pause(self): - if self.isPlaying() and self.getPlayingFile() == _STREAM: - log('pausing librespot playback') - super(Player, self).pause() - - def play(self, track_id): - track = self.spotify.getTrack(track_id) - if track['thumb'] == '': - track['thumb'] = _DEFAULT_ICON - if track['title'] == '': - track['title'] = _DEFAULT_TITLE - if self.kodi: - self.listitem.setArt({'thumb': track['thumb']}) - self.listitem.setInfo( - 'music', - { - 'album': track['album'], - 'artist': track['artist'], - 'title': track['title'] - } - ) - if self.isPlaying() and self.getPlayingFile() == _STREAM: - log('updating librespot playback') - self.updateInfoTag(self.listitem) - else: - self.librespot.unsuspend() - log('starting librespot playback') - super(Player, self).play(_STREAM, self.listitem) - else: - self.dialog.notification( - track['title'], - track['artist'], - icon=track['thumb'], - sound=False) - - def run(self): - log('named pipe started') - try: - os.unlink(_FIFO) - except OSError: - pass - os.mkfifo(_FIFO) - while (os.path.exists(_FIFO) and - stat.S_ISFIFO(os.stat(_FIFO).st_mode)): - with open(_FIFO, 'r') as fifo: - command = fifo.read().splitlines() - log('named pipe received {}'.format(str(command))) - if len(command) == 0: - break - elif command[0] == 'play': - self.play(command[1]) - elif command[0] == 'stop': - self.stop() - elif command[0] == 'pause': - self.pause() - try: - os.unlink(_FIFO) - except OSError: - pass - log('named pipe stopped') - - def stop(self): - if self.isPlaying(): - if self.getPlayingFile() == _STREAM: - log('stopping librespot playback') - super(Player, self).stop() - else: - self.librespot.stop() - else: - self.librespot.restart() - - def updateSettings(self): - self.kodi = (xbmcaddon.Addon().getSetting('ls_m') == 'Kodi') diff --git a/packages/addons/service/librespot/source/resources/lib/ls_spotify.py b/packages/addons/service/librespot/source/resources/lib/ls_spotify.py deleted file mode 100644 index 637368600a..0000000000 --- a/packages/addons/service/librespot/source/resources/lib/ls_spotify.py +++ /dev/null @@ -1,63 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0 -# Copyright (C) 2018-present Team LibreELEC (https://libreelec.tv) - -import base64 -import json -import time -import urllib -import urllib2 - -from ls_log import log as log - -_CLIENT_ID = '169df5532dee47a59913f8528e83ae71' -_CLIENT_SECRET = '1f3d8b507bbe4f68beb3a4472e8ad411' - - -def _get(default, tree, *indices): - try: - for index in indices: - tree = tree[index] - except LookupError: - tree = default - return tree - - -class Spotify(): - - def __init__(self): - self.headers = None - self.expiration = time.time() - self.request = [ - 'https://accounts.spotify.com/api/token', - urllib.urlencode({'grant_type': 'client_credentials'}), - {'Authorization': 'Basic {}'.format(base64.b64encode( - '{}:{}'.format(_CLIENT_ID, _CLIENT_SECRET)))} - ] - - def getTrack(self, track_id): - try: - if time.time() > self.expiration: - log('token expired') - token = json.loads(urllib2.urlopen( - urllib2.Request(*self.request)).read()) - log('token {} expires in {} seconds'.format( - token['access_token'], token['expires_in'])) - self.expiration = time.time() + float(token['expires_in']) - 60 - self.headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': 'Bearer {}'.format(token['access_token']) - } - track = json.loads(urllib2.urlopen(urllib2.Request( - 'https://api.spotify.com/v1/tracks/{}'.format(track_id), None, - self.headers)).read()) - except Exception as e: - log('failed to get track {} from Spotify: {}'.format(e)) - track = dict() - return { - 'album': _get('', track, 'album', 'name'), - 'artist': _get('', track, 'artists', 0, 'name'), - 'duration': _get(0, track, 'duration_ms') / 1000, - 'thumb': _get('', track, 'album', 'images', 0, 'url'), - 'title': _get('', track, 'name') - } diff --git a/packages/addons/service/librespot/source/resources/settings.xml b/packages/addons/service/librespot/source/resources/settings.xml index b2c0c6bc13..602afd1bb0 100644 --- a/packages/addons/service/librespot/source/resources/settings.xml +++ b/packages/addons/service/librespot/source/resources/settings.xml @@ -1,11 +1,8 @@ - - - - - - - - + + + + + diff --git a/packages/addons/service/librespot/source/settings-default.xml b/packages/addons/service/librespot/source/settings-default.xml index 8caeebcf80..58730ed143 100644 --- a/packages/addons/service/librespot/source/settings-default.xml +++ b/packages/addons/service/librespot/source/settings-default.xml @@ -1,9 +1,5 @@ false - 320 - ALSA - - 0 diff --git a/packages/addons/service/librespot/source/system.d/service.librespot.service b/packages/addons/service/librespot/source/system.d/service.librespot.service index 929da8af3c..4cf57aa695 100644 --- a/packages/addons/service/librespot/source/system.d/service.librespot.service +++ b/packages/addons/service/librespot/source/system.d/service.librespot.service @@ -4,10 +4,10 @@ Wants=kodi.service After=kodi.service [Service] -EnvironmentFile=/storage/.kodi/addons/service.librespot/bin/librespot.env +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 "$LS_SINK" 1 -Restart=on-failure +ExecStopPost=-/usr/bin/pactl suspend-sink librespot_sink 1 +Restart=always [Install] WantedBy=kodi.service