librespot: update to 4e3576b (2019-06-01), only play with kodi

This commit is contained in:
awiouy 2019-01-19 21:59:39 +01:00
parent 838b7587c4
commit 6a0e35e8c4
20 changed files with 427 additions and 645 deletions

View File

@ -1,5 +1,10 @@
116
- Update to 4e3576b (2019-06-01)
- Only use Kodi
- Rework
115 115
- Update to daeeeaa (22-02-2019) - Update to daeeeaa (2019-02-22)
114 114
- Fix discovery mode setting - Fix discovery mode setting

View File

@ -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 Shane Meagher (shanemeagher)
# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) # Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv)
PKG_NAME="librespot" PKG_NAME="librespot"
PKG_VERSION="daeeeaa122fc2d71edf11e562e23038db4210b39" PKG_VERSION="4e3576ba7c6146cf68e1953daeec929d619b26b1"
PKG_SHA256="e9ebb8ca09c850598ae8c222bdab44e7d8321cb3c36017ba8e17a41db418c06e" PKG_SHA256="c2fef0253bdbb6ff7085d4ec00e80da4727a1c5b1bf84dd2c14edc3de1cfd753"
PKG_VERSION_DATE="2019-02-22" PKG_VERSION_DATE="2019-06-01"
PKG_REV="115" PKG_REV="116"
PKG_ARCH="any" PKG_ARCH="any"
PKG_LICENSE="MIT" PKG_LICENSE="MIT"
PKG_SITE="https://github.com/librespot-org/librespot/" PKG_SITE="https://github.com/librespot-org/librespot/"
PKG_URL="https://github.com/librespot-org/librespot/archive/$PKG_VERSION.zip" 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_SECTION="service"
PKG_SHORTDESC="Librespot: play Spotify through LibreELEC 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) plays Spotify through LibreELEC using the open source librespot library 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_TOOLCHAIN="manual"
PKG_IS_ADDON="yes" PKG_IS_ADDON="yes"
PKG_ADDON_NAME="Librespot" PKG_ADDON_NAME="Librespot"
PKG_ADDON_TYPE="xbmc.service.library" PKG_ADDON_TYPE="xbmc.service"
PKG_MAINTAINER="Anton Voyl (awiouy)" PKG_MAINTAINER="Anton Voyl (awiouy)"
configure_target() { configure_target() {
@ -29,21 +29,17 @@ configure_target() {
make_target() { make_target() {
cd src 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 cd "$PKG_BUILD/.$TARGET_NAME"/*/release
$STRIP librespot $STRIP librespot
} }
addon() { 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" mkdir -p "$ADDON_BUILD/$PKG_ADDON_ID/bin"
cp "$PKG_BUILD/.$TARGET_NAME"/*/release/librespot \ cp "$PKG_BUILD/.$TARGET_NAME"/*/release/librespot \
"$ADDON_BUILD/$PKG_ADDON_ID/bin" "$ADDON_BUILD/$PKG_ADDON_ID/bin"
mkdir -p "$ADDON_BUILD/$PKG_ADDON_ID/lib" mkdir -p "$ADDON_BUILD/$PKG_ADDON_ID/lib"
cp "$(get_build_dir avahi)/avahi-compat-libdns_sd/.libs/libdns_sd.so.1" \ 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/lib"
} }

View File

@ -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::<f32>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain),
+ notify_kodi: notify_kodi,
}
};

View File

@ -1,140 +0,0 @@
From 109452968762175b255d3a46d2447bf103022a68 Mon Sep 17 00:00:00 2001
From: awiouy <awiouy@gmail.com>
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<Ch
PlayerEvent::Changed {
old_track_id,
new_track_id,
+ new_state,
} => {
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)

View File

@ -12,7 +12,7 @@ index 88f6280..4e7186b 100644
--- a/playback/src/audio_backend/pulseaudio.rs --- a/playback/src/audio_backend/pulseaudio.rs
+++ b/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs
@@ -76,6 +76,7 @@ impl Open for PulseAudioSink { @@ -76,6 +76,7 @@ impl Open for PulseAudioSink {
impl Sink for PulseAudioSink { impl Sink for PulseAudioSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> io::Result<()> {
+ let sink = CString::new("librespot_sink").unwrap(); + let sink = CString::new("librespot_sink").unwrap();
@ -24,7 +24,7 @@ index 88f6280..4e7186b 100644
self.name.as_ptr(), // Our application's name. self.name.as_ptr(), // Our application's name.
PA_STREAM_PLAYBACK, PA_STREAM_PLAYBACK,
- null(), // Use the default device. - null(), // Use the default device.
+ sink.as_ptr(), // Our sink. + sink.as_ptr(), // Our sink.
self.desc.as_ptr(), // desc of our stream. self.desc.as_ptr(), // desc of our stream.
&self.ss, // Our sample format. &self.ss, // Our sample format.
null(), // Use default channel map null(), // Use default channel map

View File

@ -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

View File

@ -1,2 +0,0 @@
LS_PORT="6666"
LS_SINK="librespot_sink"

View File

@ -1,2 +0,0 @@
#!/bin/sh
echo -e "$STATE\n$TRACK_ID" > "$LS_FIFO"

View File

@ -3,110 +3,34 @@
# SPDX-License-Identifier: GPL-2.0 # SPDX-License-Identifier: GPL-2.0
# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) # 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 . /etc/profile
oe_setup_addon service.librespot oe_setup_addon service.librespot
LIBRESPOT="librespot --cache \"$ADDON_HOME/cache\" \ PORT="6666"
--disable-audio-cache \ SINK_NAME="librespot_sink"
--name \"Librespot@$HOSTNAME\" \
--onevent librespot.onevent"
if [ -n "$ls_b" -a "$ls_b" != "-" ]; then if ! pactl list modules short | grep "sink_name=$SINK_NAME"; then
LIBRESPOT="$LIBRESPOT --bitrate $ls_b" pactl load-module module-null-sink sink_name="$SINK_NAME" > /dev/null
fi 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 if [ "$ls_a" = "true" -a -n "$ls_p" -a -n "$ls_u" ]; then
LIBRESPOT="$LIBRESPOT --disable-discovery \ LIBRESPOT="$LIBRESPOT \
--password \"$ls_p\" \ --disable-discovery \
--username \"$ls_u\"" --password \"$ls_p\" \
--username \"$ls_u\""
fi 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 eval $LIBRESPOT

View File

@ -1,13 +1,224 @@
# SPDX-License-Identifier: GPL-2.0 # SPDX-License-Identifier: GPL-2.0
# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) # Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv)
import base64
import json
import os 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__': if __name__ == '__main__':
log('service started')
Monitor().waitForAbort() Monitor().waitForAbort()
log('service stopped')

View File

@ -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"

View File

@ -4,81 +4,17 @@ msgid ""
msgstr "" msgstr ""
msgctxt "#30100" msgctxt "#30100"
msgid "Librespot" msgid "Configuration"
msgstr "" msgstr ""
msgctxt "#30102" msgctxt "#30101"
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"
msgid "User mode" msgid "User mode"
msgstr "" msgstr ""
msgctxt "#30113" msgctxt "#30102"
msgid "ALSA" msgid "Username"
msgstr "" msgstr ""
msgctxt "#30114" msgctxt "#30103"
msgid "Configuration wizard" msgid "Password"
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"
msgstr "" msgstr ""

View File

@ -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')

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -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')
}

View File

@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?> <?xml version="1.0" encoding="utf-8" standalone="yes"?>
<settings> <settings>
<setting id="ls_m" label="30107" type="labelenum" lvalues="30108|30109" /> <category label="30100">
<setting label="30114" type="action" subsetting="true" visible="eq(-1,0)" action="RunAddon(service.librespot)" /> <setting label="30101" id="ls_a" type="bool" default="false" />
<setting id="ls_o" label="30115" type="text" subsetting="true" visible="eq(-2,0)" default="" /> <setting label="30102" id="ls_u" type="text" default="" subsetting="true" visible="eq(-1,true)" />
<setting id="pcm_3" label="30116" type="enum" subsetting="true" visible="eq(-3,0)" lvalues="30117|30118|30119" enable="eq(-1,default:CARD=ALSA)|eq(-1,sysdefault:CARD=ALSA)" /> <setting label="30103" id="ls_p" type="text" default="" subsetting="true" visible="eq(-2,true)" option="hidden" />
<setting id="ls_b" label="30102" type="labelenum" lvalues="30103|30104|30105|30106" /> </category>
<setting id="ls_a" label="30112" type="bool" default="false" />
<setting id="ls_u" label="30110" type="text" subsetting="true" visible="eq(-1,true)" default="" />
<setting id="ls_p" label="30111" type="text" subsetting="true" visible="eq(-2,true)" default="" option="hidden" />
</settings> </settings>

View File

@ -1,9 +1,5 @@
<settings version="2"> <settings version="2">
<setting id="ls_a" default="true">false</setting> <setting id="ls_a" default="true">false</setting>
<setting id="ls_b">320</setting>
<setting id="ls_m">ALSA</setting>
<setting id="ls_o" default="true"></setting>
<setting id="ls_p" default="true"></setting> <setting id="ls_p" default="true"></setting>
<setting id="ls_u" default="true"></setting> <setting id="ls_u" default="true"></setting>
<setting id="pcm_3" default="true">0</setting>
</settings> </settings>

View File

@ -4,10 +4,10 @@ Wants=kodi.service
After=kodi.service After=kodi.service
[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 ExecStart=/bin/sh /storage/.kodi/addons/service.librespot/bin/librespot.start
ExecStopPost=/usr/bin/pactl suspend-sink "$LS_SINK" 1 ExecStopPost=-/usr/bin/pactl suspend-sink librespot_sink 1
Restart=on-failure Restart=always
[Install] [Install]
WantedBy=kodi.service WantedBy=kodi.service