librespot: adapt to Python3

This commit is contained in:
awiouy 2019-12-05 13:52:10 +01:00
parent 8d5111c208
commit 1392e81d00
6 changed files with 145 additions and 70 deletions

View File

@ -1,3 +1,10 @@
119
- Update to f610436 (2020-01-02)
- Hook audiotype
- Use vorbis, which is more responsive than lewton
- Build alsa backend and provide sample alsa service
- Python 3 compliance
118 118
- Update to 0.1.0 - Update to 0.1.0

View File

@ -3,14 +3,15 @@
# 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="0.1.0" PKG_VERSION="f610436641e957750fad35e7da4933b8308ddd6d"
PKG_SHA256="4e03c69d1893ed14414d5a76ecdb5ea139ddfcce47fd57cc4e77b696001badb7" PKG_SHA256="8c15c33a66602715b8bfc8e84d9457e70135d37079eda0df153a2ed57b4f1a33"
PKG_REV="118" PKG_VERSION_DATE="2020-01-02"
PKG_REV="119"
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/v$PKG_VERSION.zip" PKG_URL="https://github.com/librespot-org/librespot/archive/$PKG_VERSION.zip"
PKG_DEPENDS_TARGET="toolchain pulseaudio rust" PKG_DEPENDS_TARGET="toolchain alsa-lib libvorbis pulseaudio rust"
PKG_SECTION="service" PKG_SECTION="service"
PKG_SHORTDESC="Librespot: play Spotify through Kodi 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_LONGDESC="Librespot ($PKG_VERSION_DATE) lets you play Spotify through Kodi using a Spotify app as a remote."
@ -19,12 +20,15 @@ PKG_TOOLCHAIN="manual"
PKG_IS_ADDON="yes" PKG_IS_ADDON="yes"
PKG_ADDON_NAME="Librespot" PKG_ADDON_NAME="Librespot"
PKG_ADDON_TYPE="xbmc.service" PKG_ADDON_TYPE="xbmc.service"
PKG_ADDON_REQUIRES="script.module.requests:0.0.0"
PKG_MAINTAINER="Anton Voyl (awiouy)" PKG_MAINTAINER="Anton Voyl (awiouy)"
make_target() { make_target() {
cd src
. "$(get_build_dir rust)/cargo/env" . "$(get_build_dir rust)/cargo/env"
$CARGO_BUILD --no-default-features --features "pulseaudio-backend with-dns-sd" cargo build \
--release \
--no-default-features \
--features "alsa-backend pulseaudio-backend with-dns-sd with-vorbis"
"$STRIP" $PKG_BUILD/.$TARGET_NAME/*/release/librespot "$STRIP" $PKG_BUILD/.$TARGET_NAME/*/release/librespot
} }

View File

@ -1,3 +1,26 @@
commit 6a9b509f4f85427f6392c08fc642afb9fc7bb8f2
Author: awiouy <awiouy@gmail.com>
Date: Tue Dec 3 23:21:35 2019 +0100
Notify Kodi
diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs
index e6f0cdd..03f8912 100644
--- a/core/src/spotify_id.rs
+++ b/core/src/spotify_id.rs
@@ -8,6 +8,12 @@ pub enum SpotifyAudioType {
NonPlayable,
}
+impl fmt::Display for SpotifyAudioType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SpotifyId {
pub id: u128,
diff --git a/playback/src/config.rs b/playback/src/config.rs diff --git a/playback/src/config.rs b/playback/src/config.rs
index 0f71110..931167d 100644 index 0f71110..931167d 100644
--- a/playback/src/config.rs --- a/playback/src/config.rs
@ -8,7 +31,7 @@ index 0f71110..931167d 100644
pub normalisation_pregain: f32, pub normalisation_pregain: f32,
+ pub notify_kodi: bool, + pub notify_kodi: bool,
} }
impl Default for PlayerConfig { impl Default for PlayerConfig {
@@ -38,6 +39,7 @@ impl Default for PlayerConfig { @@ -38,6 +39,7 @@ impl Default for PlayerConfig {
bitrate: Bitrate::default(), bitrate: Bitrate::default(),
@ -19,35 +42,35 @@ index 0f71110..931167d 100644
} }
} }
diff --git a/playback/src/player.rs b/playback/src/player.rs diff --git a/playback/src/player.rs b/playback/src/player.rs
index ab1a8ab..19d6394 100644 index a54a577..4e006ab 100644
--- a/playback/src/player.rs --- a/playback/src/player.rs
+++ b/playback/src/player.rs +++ b/playback/src/player.rs
@@ -4,7 +4,8 @@ use futures::sync::oneshot; @@ -5,7 +5,8 @@ use futures::{future, Future};
use futures::{future, Future};
use std; use std;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::max;
-use std::io::{Read, Result, Seek, SeekFrom}; -use std::io::{Read, Result, Seek, SeekFrom};
+use std::fs::OpenOptions; +use std::fs::OpenOptions;
+use std::io::{Read, Result, Seek, SeekFrom, Write}; +use std::io::{Read, Result, Seek, SeekFrom, Write};
use std::mem; use std::mem;
use std::sync::mpsc::{RecvError, RecvTimeoutError, TryRecvError}; use std::sync::mpsc::{RecvError, RecvTimeoutError, TryRecvError};
use std::thread; use std::thread;
@@ -394,6 +395,14 @@ impl PlayerInternal { @@ -427,6 +428,14 @@ impl PlayerInternal {
} }
} }
+ fn notify_kodi(&mut self, id: &str, track_id: &SpotifyId) { + fn notify_kodi(&mut self, id: &str, track_id: &SpotifyId) {
+ // println!("fifo = {} {}", id, track_id.to_base62()); + // println!("fifo = {} {}", id, track_id.to_base62());
+ if self.config.notify_kodi { + if self.config.notify_kodi {
+ let mut file = OpenOptions::new().write(true).open("/tmp/librespot").unwrap(); + let mut file = OpenOptions::new().write(true).open("/tmp/librespot").unwrap();
+ writeln!(&mut file, "{}\n{}", id, track_id.to_base62()).unwrap(); + writeln!(&mut file, "{}\n{} {}", id, track_id.audio_type.to_string(), track_id.to_base62()).unwrap();
+ } + }
+ } + }
+ +
fn handle_command(&mut self, cmd: PlayerCommand) { fn handle_command(&mut self, cmd: PlayerCommand) {
debug!("command={:?}", cmd); debug!("command={:?}", cmd);
match cmd { match cmd {
@@ -413,11 +422,17 @@ impl PlayerInternal { @@ -451,11 +460,17 @@ impl PlayerInternal {
| PlayerState::EndOfTrack { | PlayerState::EndOfTrack {
track_id: old_track_id, track_id: old_track_id,
.. ..
@ -68,9 +91,9 @@ index ab1a8ab..19d6394 100644
+ self.notify_kodi("2", &track_id) + self.notify_kodi("2", &track_id)
+ } + }
} }
self.start_sink(); self.start_sink();
@@ -443,13 +458,17 @@ impl PlayerInternal { @@ -485,13 +500,17 @@ impl PlayerInternal {
| PlayerState::EndOfTrack { | PlayerState::EndOfTrack {
track_id: old_track_id, track_id: old_track_id,
.. ..
@ -91,24 +114,24 @@ index ab1a8ab..19d6394 100644
+ self.notify_kodi("4", &track_id) + self.notify_kodi("4", &track_id)
} }
} }
@@ -476,6 +495,7 @@ impl PlayerInternal { @@ -547,6 +566,7 @@ impl PlayerInternal {
self.send_event(PlayerEvent::Started { track_id }); self.send_event(PlayerEvent::Started { track_id });
self.start_sink(); self.start_sink();
+ self.notify_kodi("5", &track_id) + self.notify_kodi("5", &track_id)
} else { } else {
warn!("Player::play called from invalid state"); warn!("Player::play called from invalid state");
} }
@@ -487,6 +507,7 @@ impl PlayerInternal { @@ -558,6 +578,7 @@ impl PlayerInternal {
self.stop_sink_if_running(); self.stop_sink_if_running();
self.send_event(PlayerEvent::Stopped { track_id }); self.send_event(PlayerEvent::Stopped { track_id });
+ self.notify_kodi("6", &track_id) + self.notify_kodi("6", &track_id)
} else { } else {
warn!("Player::pause called from invalid state"); warn!("Player::pause called from invalid state");
} }
@@ -499,6 +520,7 @@ impl PlayerInternal { @@ -570,6 +591,7 @@ impl PlayerInternal {
self.stop_sink_if_running(); self.stop_sink_if_running();
self.send_event(PlayerEvent::Stopped { track_id }); self.send_event(PlayerEvent::Stopped { track_id });
self.state = PlayerState::Stopped; self.state = PlayerState::Stopped;
@ -117,10 +140,10 @@ index ab1a8ab..19d6394 100644
PlayerState::Stopped => { PlayerState::Stopped => {
warn!("Player::stop called from invalid state"); warn!("Player::stop called from invalid state");
diff --git a/src/main.rs b/src/main.rs diff --git a/src/main.rs b/src/main.rs
index 36cd1b5..502cac8 100644 index e3718fb..a480480 100644
--- a/src/main.rs --- a/src/main.rs
+++ b/src/main.rs +++ b/src/main.rs
@@ -168,6 +168,11 @@ fn setup(args: &[String]) -> Setup { @@ -184,6 +184,11 @@ fn setup(args: &[String]) -> Setup {
"Pregain (dB) applied by volume normalisation", "Pregain (dB) applied by volume normalisation",
"PREGAIN", "PREGAIN",
) )
@ -132,20 +155,20 @@ index 36cd1b5..502cac8 100644
.optflag( .optflag(
"", "",
"linear-volume", "linear-volume",
@@ -248,6 +253,8 @@ fn setup(args: &[String]) -> Setup { @@ -276,6 +281,8 @@ fn setup(args: &[String]) -> Setup {
) )
}; };
+ let notify_kodi = matches.opt_present("notify-kodi"); + let notify_kodi = matches.opt_present("notify-kodi");
+ +
let session_config = { let session_config = {
let device_id = device_id(&name); let device_id = device_id(&name);
@@ -291,6 +298,7 @@ fn setup(args: &[String]) -> Setup { @@ -319,6 +326,7 @@ fn setup(args: &[String]) -> Setup {
.opt_str("normalisation-pregain") .opt_str("normalisation-pregain")
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value")) .map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain), .unwrap_or(PlayerConfig::default().normalisation_pregain),
+ notify_kodi: notify_kodi, + notify_kodi: notify_kodi,
} }
}; };

View File

@ -1,18 +1,15 @@
From b87d18c6513cebc31118ffb447e2b7ae9255e6bd Mon Sep 17 00:00:00 2001 commit 0f3141a0874566ce867139aeadbefdb076706baf
From: awiouy <awiouy@gmail.com> Author: awiouy <awiouy@gmail.com>
Date: Wed, 7 Nov 2018 07:51:46 +0100 Date: Tue Dec 3 23:22:55 2019 +0100
Subject: [PATCH] libreelec: pulseaudio sink
--- Use own pulseaudio sink
playback/src/audio_backend/pulseaudio.rs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs
index 88f6280..4e7186b 100644 index 88f6280..702be66 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();

View File

@ -2,14 +2,12 @@
# Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv) # Copyright (C) 2017-present Team LibreELEC (https://libreelec.tv)
import base64 import base64
import json
import os import os
import stat import stat
import subprocess import subprocess
import threading import threading
import time import time
import urllib import requests
import urllib2
import xbmc import xbmc
import xbmcaddon import xbmcaddon
import xbmcgui import xbmcgui
@ -21,8 +19,6 @@ FIFO = '/tmp/librespot'
LOG_LEVEL = xbmc.LOGNOTICE LOG_LEVEL = xbmc.LOGNOTICE
LOG_MESSAGE = ADDON.getAddonInfo('name') + ': {}' LOG_MESSAGE = ADDON.getAddonInfo('name') + ': {}'
SINK_NAME = "librespot_sink" SINK_NAME = "librespot_sink"
SPOTIFY_ID = '169df5532dee47a59913f8528e83ae71'
SPOTIFY_SECRET = '1f3d8b507bbe4f68beb3a4472e8ad411'
STREAM_CODEC = 'pcm_s16be' STREAM_CODEC = 'pcm_s16be'
STREAM_PORT = '6666' STREAM_PORT = '6666'
STREAM_URL = 'rtp://127.0.0.1:{}'.format(STREAM_PORT) STREAM_URL = 'rtp://127.0.0.1:{}'.format(STREAM_PORT)
@ -100,10 +96,11 @@ class Player(threading.Thread, xbmc.Player):
log('pausing librespot playback') log('pausing librespot playback')
self.pause() self.pause()
def playLibrespot(self, track_id): def playLibrespot(self, spotify_id):
track = self.spotify.getTrack(track_id) type, id = spotify_id.split()
self.listitem.setArt(track.getArt()) info = self.spotify.getInfo(type, id)
self.listitem.setInfo('music', track.getInfo()) self.listitem.setArt(info.getArt())
self.listitem.setInfo('music', info.getInfo())
if not self.isPlaying(): if not self.isPlaying():
subprocess.call(['pactl', 'suspend-sink', SINK_NAME, '0']) subprocess.call(['pactl', 'suspend-sink', SINK_NAME, '0'])
log('starting librespot playback') log('starting librespot playback')
@ -158,46 +155,51 @@ class Spotify():
def __init__(self): def __init__(self):
self.headers = None self.headers = None
self.expiration = time.time() 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): def getHeaders(self):
if time.time() > self.expiration: if time.time() > self.expiration:
log('token expired') log('token expired')
token = json.loads(urllib2.urlopen( token = requests.post(
urllib2.Request(*self.request)).read()) 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'])) log('new token expires in {} seconds'.format(token['expires_in']))
self.expiration = time.time() + float(token['expires_in']) - 60 self.expiration = time.time() + float(token['expires_in']) - 15
self.headers = { self.headers = {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer {}'.format(token['access_token']) 'Authorization': 'Bearer {}'.format(token['access_token'])
} }
def getTrack(self, track_id): def getEndpoint(self, url):
log('getting track')
try: try:
self.getHeaders() self.getHeaders()
track = json.loads(urllib2.urlopen(urllib2.Request( return requests.get(
'https://api.spotify.com/v1/tracks/{}'.format(track_id), None, url=url,
self.headers)).read()) headers=self.headers
).json()
except Exception as e: except Exception as e:
log('failed to get track from Spotify: {}'.format(e)) log('failed to get endpoint from Spotify {}'.format(e))
track = dict() return {}
return Track(track)
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 Track(): class UnknownInfo:
def __init__(self, track): def __init__(self, type, id):
self.track = track self.id = id
self.type = type
def get(self, default, *indices): def get(self, default, *indices):
tree = self.track tree = self.info
try: try:
for index in indices: for index in indices:
tree = tree[index] tree = tree[index]
@ -205,6 +207,18 @@ class Track():
tree = default tree = default
return tree 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): def getArt(self):
return { return {
'thumb': self.get('', 'album', 'images', 0, 'url') 'thumb': self.get('', 'album', 'images', 0, 'url')
@ -214,7 +228,7 @@ class Track():
return { return {
'album': self.get('', 'album', 'name'), 'album': self.get('', 'album', 'name'),
'artist': self.get('', 'artists', 0, 'name'), 'artist': self.get('', 'artists', 0, 'name'),
'title': self.get('', 'name'), 'title': self.get('', 'name')
} }

View File

@ -0,0 +1,30 @@
# Librespot for ALSA
# Copy this file to '/storage/.config/system.d/service.librespot-alsa.service' and adapt it to your needs
# Enable the service with 'systemctl enable /storage/.config/system.d/service.librespot-alsa.service'
# Start the service with 'systemctl start service.librespot-alsa.service'
# If you update the file, reload units with 'systemctl daemon-reload' and restart the service
[Unit]
Description=librespot alsa backend
After=network-online.target
Wants=network-online.target
[Service]
Environment=LD_LIBRARY_PATH=/storage/.kodi/addons/service.librespot/lib
#Enable Raspberry Pi onboard audio
#ExecStartPre=-dtparam audio=on
#Set Raspberry Pi playback route
#ExecStartPre=-amixer -c 0 cset name="PCM Playback Route" 1
ExecStart=/storage/.kodi/addons/service.librespot/bin/librespot \
--backend alsa \
--bitrate 320 \
--cache "/storage/.config/lsa_cache" \
# Use 'aplay -L' to list available devices
# --device "default:CARD=ALSA" \
--device-type computer \
--disable-audio-cache \
--name "Librespot ALSA" \
Restart=always
[Install]
WantedBy=network-online.target