mirror of
https://github.com/LibreELEC/LibreELEC.tv.git
synced 2025-07-28 13:16:41 +00:00
librespot: update to 03b547d
This commit is contained in:
parent
f1977b90c5
commit
84cc997dc0
@ -1 +1,2 @@
|
|||||||
initial release
|
1
|
||||||
|
- update librespot to githash 03b547d (2023-04-16)
|
||||||
|
@ -3,13 +3,14 @@
|
|||||||
# 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.3.1"
|
PKG_VERSION="03b547d3b3b434811225ebc153c651e5f1917b95"
|
||||||
PKG_SHA256="d360eaf61ad4216ee2c4a4d583d61c8ec7367b5efbe512011d049f73e4f24952"
|
PKG_VERSION_DATE="2023-04-16"
|
||||||
PKG_REV="0"
|
PKG_SHA256="fd6ffed75f5d9a0941c4ca2a86192c17217b60d1d301e47ff0d8aeec3b3e0e97"
|
||||||
|
PKG_REV="1"
|
||||||
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}.tar.gz"
|
PKG_URL="https://github.com/librespot-org/librespot/archive/${PKG_VERSION}.tar.gz"
|
||||||
PKG_DEPENDS_TARGET="toolchain alsa-lib avahi pulseaudio cargo:host"
|
PKG_DEPENDS_TARGET="toolchain alsa-lib avahi pulseaudio cargo:host"
|
||||||
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"
|
||||||
@ -19,10 +20,14 @@ 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() {
|
||||||
|
unset CFLAGS
|
||||||
|
unset CXXFLAGS
|
||||||
|
unset CPPFLAGS
|
||||||
|
unset LDFLAGS
|
||||||
|
export RUSTC_LINKER=${CC}
|
||||||
cargo build \
|
cargo build \
|
||||||
--target ${TARGET_NAME} \
|
--target ${TARGET_NAME} \
|
||||||
--release \
|
--release \
|
||||||
|
@ -1,108 +0,0 @@
|
|||||||
commit 10489ef0b9de4241eb8e007596f3d62616120545
|
|
||||||
Author: awiouy <awiouy@gmail.com>
|
|
||||||
Date: Fri May 29 07:40:19 2020 +0200
|
|
||||||
|
|
||||||
Notify Kodi
|
|
||||||
|
|
||||||
diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs
|
|
||||||
index 1a5fcd2..c670977 100644
|
|
||||||
--- a/core/src/spotify_id.rs
|
|
||||||
+++ b/core/src/spotify_id.rs
|
|
||||||
@@ -9,6 +9,12 @@
|
|
||||||
Podcast,
|
|
||||||
NonPlayable,
|
|
||||||
}
|
|
||||||
+
|
|
||||||
+impl fmt::Display for SpotifyAudioType {
|
|
||||||
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
+ write!(f, "{:?}", self)
|
|
||||||
+ }
|
|
||||||
+}
|
|
||||||
|
|
||||||
impl From<&str> for SpotifyAudioType {
|
|
||||||
fn from(v: &str) -> Self {
|
|
||||||
diff --git a/playback/src/config.rs b/playback/src/config.rs
|
|
||||||
index 9d65042..6d098db 100644
|
|
||||||
--- a/playback/src/config.rs
|
|
||||||
+++ b/playback/src/config.rs
|
|
||||||
@@ -142,6 +142,7 @@
|
|
||||||
// pass function pointers so they can be lazily instantiated *after* spawning a thread
|
|
||||||
// (thereby circumventing Send bounds that they might not satisfy)
|
|
||||||
pub ditherer: Option<DithererBuilder>,
|
|
||||||
+ pub notify_kodi: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for PlayerConfig {
|
|
||||||
@@ -159,6 +160,7 @@
|
|
||||||
normalisation_knee: 1.0,
|
|
||||||
passthrough: false,
|
|
||||||
ditherer: Some(mk_ditherer::<TriangularDitherer>),
|
|
||||||
+ notify_kodi: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diff --git a/playback/src/player.rs b/playback/src/player.rs
|
|
||||||
index 2dd8f3b..67b3b28 100644
|
|
||||||
--- a/playback/src/player.rs
|
|
||||||
+++ b/playback/src/player.rs
|
|
||||||
@@ -1868,6 +1868,10 @@ impl PlayerInternal {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
+ fn notify_kodi(&mut self, event: String) {
|
|
||||||
+ eprintln!("@{}", event);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
fn send_event(&mut self, event: PlayerEvent) {
|
|
||||||
let mut index = 0;
|
|
||||||
while index < self.event_senders.len() {
|
|
||||||
@@ -1878,6 +1882,16 @@ impl PlayerInternal {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+ if self.config.notify_kodi {
|
|
||||||
+ use PlayerEvent::*;
|
|
||||||
+ match event {
|
|
||||||
+ Playing {track_id, .. } => self.notify_kodi(["Playing",
|
|
||||||
+ &track_id.audio_type.to_string(),
|
|
||||||
+ &track_id.to_base62()].join(" ")),
|
|
||||||
+ Stopped { .. } => self.notify_kodi("Stopped".to_string()),
|
|
||||||
+ _ => ()
|
|
||||||
+ }
|
|
||||||
+ }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_track(
|
|
||||||
diff --git a/src/main.rs b/src/main.rs
|
|
||||||
index 2efd62b..ecee2ff 100644
|
|
||||||
--- a/src/main.rs
|
|
||||||
+++ b/src/main.rs
|
|
||||||
@@ -424,6 +424,11 @@
|
|
||||||
"",
|
|
||||||
PASSTHROUGH,
|
|
||||||
"Pass a raw stream to the output. Only works with the pipe and subprocess backends.",
|
|
||||||
+ )
|
|
||||||
+ .optflag(
|
|
||||||
+ "",
|
|
||||||
+ "notify-kodi",
|
|
||||||
+ "Notify Kodi",
|
|
||||||
);
|
|
||||||
|
|
||||||
let matches = match opts.parse(&args[1..]) {
|
|
||||||
@@ -644,6 +649,8 @@ fn setup(args: &[String]) -> Setup {
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
+ let notify_kodi = matches.opt_present("notify-kodi");
|
|
||||||
+
|
|
||||||
let session_config = {
|
|
||||||
let device_id = device_id(&name);
|
|
||||||
|
|
||||||
@@ -763,6 +763,7 @@
|
|
||||||
normalisation_release,
|
|
||||||
normalisation_knee,
|
|
||||||
ditherer,
|
|
||||||
+ notify_kodi: notify_kodi,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
66
packages/addons/service/librespot/source/bin/onevent.py
Normal file
66
packages/addons/service/librespot/source/bin/onevent.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
|
||||||
|
ADDRESS = ('127.0.0.1', 36963)
|
||||||
|
BUFFER_SIZE = 1024
|
||||||
|
|
||||||
|
|
||||||
|
def send_event(event):
|
||||||
|
data = json.dumps(event).encode()
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||||
|
sock.sendto(data, ADDRESS)
|
||||||
|
|
||||||
|
|
||||||
|
def receive_event():
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||||
|
sock.settimeout(None)
|
||||||
|
sock.bind(ADDRESS)
|
||||||
|
while True:
|
||||||
|
data, addr = sock.recvfrom(BUFFER_SIZE)
|
||||||
|
event = json.loads(data.decode())
|
||||||
|
if not event:
|
||||||
|
break
|
||||||
|
yield event
|
||||||
|
|
||||||
|
|
||||||
|
ARG_ALBUM = 'album'
|
||||||
|
ARG_ARTIST = 'artist'
|
||||||
|
ARG_ART = 'art'
|
||||||
|
ARG_TITLE = 'title'
|
||||||
|
|
||||||
|
KEY_ALBUM = 'ALBUM'
|
||||||
|
KEY_ARTISTS = 'ARTISTS'
|
||||||
|
KEY_COVERS = 'COVERS'
|
||||||
|
KEY_ITEM_TYPE = 'ITEM_TYPE'
|
||||||
|
KEY_NAME = 'NAME'
|
||||||
|
KEY_PLAYER_EVENT = 'PLAYER_EVENT'
|
||||||
|
KEY_SHOW_NAME = 'SHOW_NAME'
|
||||||
|
|
||||||
|
PLAYER_EVENT_STOPPED = 'stopped'
|
||||||
|
PLAYER_EVENT_TRACK_CHANGED = 'track_changed'
|
||||||
|
|
||||||
|
ITEM_TYPE_EPISODE = 'Episode'
|
||||||
|
ITEM_TYPE_TRACK = 'Track'
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_value(key):
|
||||||
|
return os.environ.get(key, '').partition('\n')[0]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
player_event = get_env_value(KEY_PLAYER_EVENT)
|
||||||
|
event = {KEY_PLAYER_EVENT: player_event}
|
||||||
|
if player_event == PLAYER_EVENT_STOPPED:
|
||||||
|
send_event(event)
|
||||||
|
elif player_event == PLAYER_EVENT_TRACK_CHANGED:
|
||||||
|
event[ARG_ART] = get_env_value(KEY_COVERS)
|
||||||
|
event[ARG_TITLE] = get_env_value(KEY_NAME)
|
||||||
|
item_type = get_env_value(KEY_ITEM_TYPE)
|
||||||
|
if item_type == ITEM_TYPE_EPISODE:
|
||||||
|
event[ARG_ALBUM] = get_env_value(KEY_SHOW_NAME)
|
||||||
|
elif item_type == ITEM_TYPE_TRACK:
|
||||||
|
event[ARG_ALBUM] = get_env_value(KEY_ALBUM)
|
||||||
|
event[ARG_ARTIST] = get_env_value(KEY_ARTISTS)
|
||||||
|
send_event(event)
|
@ -1,8 +1,25 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import xbmcaddon
|
||||||
|
import xbmcvfs
|
||||||
|
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), 'resources', 'lib'))
|
|
||||||
|
|
||||||
from ls_monitor import Monitor as Monitor
|
def _set_home():
|
||||||
|
home = xbmcvfs.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||||
|
os.makedirs(home, exist_ok=True)
|
||||||
|
os.chdir(home)
|
||||||
|
|
||||||
Monitor().run()
|
|
||||||
|
def _set_paths():
|
||||||
|
path = xbmcaddon.Addon().getAddonInfo('path')
|
||||||
|
os.environ['PATH'] += os.pathsep + os.path.join(path, 'bin')
|
||||||
|
os.environ['LD_LIBRARY_PATH'] += os.pathsep + os.path.join(path, 'lib')
|
||||||
|
sys.path.append(os.path.join(path, 'bin'))
|
||||||
|
sys.path.append(os.path.join(path, 'resources', 'lib'))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
_set_home()
|
||||||
|
_set_paths()
|
||||||
|
import service
|
||||||
|
service.Monitor().run()
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
# 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
|
|
@ -8,29 +8,21 @@ msgid "Configuration"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30101"
|
msgctxt "#30101"
|
||||||
msgid "Autoplay"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#30102"
|
|
||||||
msgid "Discovery"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#30103"
|
|
||||||
msgid "Username"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#30104"
|
|
||||||
msgid "Password"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgctxt "#30105"
|
|
||||||
msgid "Name"
|
msgid "Name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30106"
|
msgctxt "#30102"
|
||||||
msgid "RTP Port"
|
msgid "Do not disturb Kodi"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgctxt "#30107"
|
msgctxt "#30103"
|
||||||
msgid "Connect Port"
|
msgid "User options"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30104"
|
||||||
|
msgid "Backend"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgctxt "#30105"
|
||||||
|
msgid "ALSA device"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
import player
|
||||||
|
import service
|
||||||
|
|
||||||
|
|
||||||
|
class Player(player.Player):
|
||||||
|
|
||||||
|
def onLibrespotTrackChanged(self, art, artist, title, **kwargs):
|
||||||
|
service.notification(heading=title, message=artist, icon=art)
|
@ -0,0 +1,34 @@
|
|||||||
|
import xbmc
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
|
import player
|
||||||
|
import service
|
||||||
|
|
||||||
|
|
||||||
|
class Player(player.Player):
|
||||||
|
|
||||||
|
def __init__(self, codec='pcm_sb16be', max_fanarts='10', **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self._max_fanarts = int(max_fanarts)
|
||||||
|
self._list_item = xbmcgui.ListItem(path=self.librespot.file)
|
||||||
|
self._list_item.getVideoInfoTag().addAudioStream(xbmc.AudioStreamDetail(2, codec))
|
||||||
|
self._music_info_tag = self._list_item.getMusicInfoTag()
|
||||||
|
|
||||||
|
def onLibrespotTrackChanged(self, album='', art='', artist='', title=''):
|
||||||
|
fanart = service.get_fanart(art, self._max_fanarts) if art else art
|
||||||
|
self._list_item.setArt({'fanart': fanart, 'thumb': art})
|
||||||
|
self._music_info_tag.setAlbum(album)
|
||||||
|
self._music_info_tag.setArtist(artist)
|
||||||
|
self._music_info_tag.setTitle(title)
|
||||||
|
if self.isPlaying() and self.getPlayingFile() == self.librespot.file:
|
||||||
|
self.updateInfoTag(self._list_item)
|
||||||
|
else:
|
||||||
|
self.stop() # fixes unepxected behaviour of Player.play()
|
||||||
|
self.librespot.start_sink()
|
||||||
|
self.play(self.librespot.file, listitem=self._list_item)
|
||||||
|
|
||||||
|
def onLibrespotStopped(self):
|
||||||
|
self.librespot.stop_sink()
|
||||||
|
if self.isPlaying() and self.getPlayingFile() == self.librespot.file:
|
||||||
|
self.last_file = None
|
||||||
|
self.stop()
|
@ -0,0 +1,93 @@
|
|||||||
|
import shlex
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import external_player
|
||||||
|
import internal_player
|
||||||
|
import service
|
||||||
|
|
||||||
|
|
||||||
|
class Librespot:
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
bitrate='320',
|
||||||
|
device_type='tv',
|
||||||
|
max_retries='5',
|
||||||
|
name='Librespot@{}',
|
||||||
|
options='',
|
||||||
|
**kwargs):
|
||||||
|
name = name.format(socket.gethostname())
|
||||||
|
self.command = [
|
||||||
|
'librespot',
|
||||||
|
'--bitrate', f'{bitrate}',
|
||||||
|
'--device-type', f'{device_type}',
|
||||||
|
'--disable-audio-cache',
|
||||||
|
'--disable-credential-cache',
|
||||||
|
'--name', f'{name}',
|
||||||
|
'--onevent', 'onevent.py',
|
||||||
|
'--quiet',
|
||||||
|
] + shlex.split(options)
|
||||||
|
service.log(self.command)
|
||||||
|
self.file = ''
|
||||||
|
self._is_started = threading.Event()
|
||||||
|
self._is_stopped = threading.Event()
|
||||||
|
self._librespot = None
|
||||||
|
self._max_retries = int(max_retries)
|
||||||
|
self._retries = 0
|
||||||
|
self._thread = threading.Thread()
|
||||||
|
|
||||||
|
def get_player(self, **kwargs):
|
||||||
|
return (internal_player if self.file else external_player).Player(**kwargs)
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
if self._thread.is_alive():
|
||||||
|
self._librespot.terminate()
|
||||||
|
else:
|
||||||
|
self.start()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if not self._thread.is_alive() and self._retries < self._max_retries:
|
||||||
|
self._thread = threading.Thread(daemon=True, target=self._run)
|
||||||
|
self._thread.start()
|
||||||
|
self._is_started.wait(1)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._thread.is_alive():
|
||||||
|
self._is_stopped.set()
|
||||||
|
self._librespot.terminate()
|
||||||
|
self._thread.join()
|
||||||
|
|
||||||
|
def start_sink(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def stop_sink(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
service.log('librespot thread started')
|
||||||
|
self._is_started.clear()
|
||||||
|
self._is_stopped.clear()
|
||||||
|
while not self._is_stopped.is_set():
|
||||||
|
with subprocess.Popen(self.command, stderr=subprocess.PIPE, text=True) as self._librespot:
|
||||||
|
self._is_started.set()
|
||||||
|
for line in self._librespot.stderr:
|
||||||
|
service.log(line.rstrip())
|
||||||
|
self.stop_sink()
|
||||||
|
if self._librespot.returncode <= 0:
|
||||||
|
self._retries = 0
|
||||||
|
else:
|
||||||
|
self._retries += 1
|
||||||
|
if self._retries < self._max_retries:
|
||||||
|
service.notification(
|
||||||
|
f'librespot failed {self._retries}/{self._max_retries}')
|
||||||
|
else:
|
||||||
|
service.notification('librespot failed too many times')
|
||||||
|
break
|
||||||
|
service.log('librespot thread stopped')
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
self.stop()
|
@ -0,0 +1,11 @@
|
|||||||
|
import librespot
|
||||||
|
|
||||||
|
|
||||||
|
class Librespot(librespot.Librespot):
|
||||||
|
|
||||||
|
def __init__(self, alsa_device='hw:2,0', **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.command += [
|
||||||
|
'--backend', 'alsa',
|
||||||
|
'--device', f'{alsa_device}',
|
||||||
|
]
|
@ -0,0 +1,73 @@
|
|||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import librespot
|
||||||
|
import service
|
||||||
|
|
||||||
|
|
||||||
|
class Librespot(librespot.Librespot):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
codec='pcm_sb16be',
|
||||||
|
pa_rtp_address='127.0.0.1',
|
||||||
|
pa_rtp_device='librespot',
|
||||||
|
pa_rtp_port='24642',
|
||||||
|
**kwargs):
|
||||||
|
service.log('pulseaudio backend started')
|
||||||
|
sap_cmd = f'nc -l -u -s {pa_rtp_address} -p 9875'.split()
|
||||||
|
self._sap_server = subprocess.Popen(sap_cmd,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
service.log(f'sap server started')
|
||||||
|
if not pa_rtp_port:
|
||||||
|
with socket.socket() as s:
|
||||||
|
s.bind((pa_rtp_address, 0))
|
||||||
|
pa_rtp_port = s.getsockname()[1]
|
||||||
|
modules = [
|
||||||
|
[
|
||||||
|
f'module-null-sink',
|
||||||
|
f'sink_name={pa_rtp_device}',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
f'module-rtp-send',
|
||||||
|
f'destination_ip={pa_rtp_address}',
|
||||||
|
f'inhibit_auto_suspend=always',
|
||||||
|
f'port={pa_rtp_port}',
|
||||||
|
f'source={pa_rtp_device}.monitor',
|
||||||
|
],
|
||||||
|
]
|
||||||
|
self._modules = [self._pactl('load-module', *m) for m in modules]
|
||||||
|
self._sink_name = f'{pa_rtp_device}'
|
||||||
|
self.stop_sink()
|
||||||
|
service.log(f'pulseaudio modules loaded: {self._modules}')
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.command += [
|
||||||
|
'--backend', 'pulseaudio',
|
||||||
|
'--device', f'{pa_rtp_device}',
|
||||||
|
]
|
||||||
|
self.file = f'rtp://{pa_rtp_address}:{pa_rtp_port}'
|
||||||
|
|
||||||
|
def start_sink(self):
|
||||||
|
self._pactl('suspend-sink', self._sink_name, '0')
|
||||||
|
|
||||||
|
def stop_sink(self):
|
||||||
|
self._pactl('suspend-sink', self._sink_name, '1')
|
||||||
|
|
||||||
|
def _pactl(self, command, *args):
|
||||||
|
out = subprocess.run(['pactl', command, *args],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
).stdout.rstrip()
|
||||||
|
service.log(f'pactl {command} {args}: {out}')
|
||||||
|
return out
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
super().__exit__(*args)
|
||||||
|
for module in reversed(self._modules):
|
||||||
|
if module:
|
||||||
|
self._pactl('unload-module', module)
|
||||||
|
service.log('pulseaudio backend stopped')
|
||||||
|
if self._sap_server.poll() is None:
|
||||||
|
self._sap_server.terminate()
|
||||||
|
self._sap_server.wait()
|
||||||
|
service.log('sap server stopped')
|
@ -1,49 +0,0 @@
|
|||||||
import os
|
|
||||||
import socket
|
|
||||||
import xbmc
|
|
||||||
import xbmcvfs
|
|
||||||
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',
|
|
||||||
connect_port='0',
|
|
||||||
username='',
|
|
||||||
)
|
|
||||||
|
|
||||||
ADDON = xbmcaddon.Addon()
|
|
||||||
ADDON_HOME = xbmcvfs.translatePath(ADDON.getAddonInfo('profile'))
|
|
||||||
ADDON_ICON = ADDON.getAddonInfo('icon')
|
|
||||||
ADDON_NAME = ADDON.getAddonInfo('name')
|
|
||||||
ADDON_PATH = ADDON.getAddonInfo('path')
|
|
||||||
ADDON_ENVT = dict(
|
|
||||||
LD_LIBRARY_PATH=os.path.join(ADDON_PATH, 'lib'),
|
|
||||||
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.LOGINFO)
|
|
||||||
|
|
||||||
|
|
||||||
def notification(message):
|
|
||||||
DIALOG.notification(ADDON_NAME, message, ADDON_ICON)
|
|
@ -1,164 +0,0 @@
|
|||||||
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().__init__()
|
|
||||||
settings = get_settings()
|
|
||||||
quoted = {k: shlex.quote(v) for (k, v) in settings.items()}
|
|
||||||
command = LIBRESPOT
|
|
||||||
if settings['connect_port'] != "0":
|
|
||||||
command += ' --zeroconf-port %s ' % settings['connect_port']
|
|
||||||
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,
|
|
||||||
text=True,
|
|
||||||
encoding='utf-8')
|
|
||||||
log('librespot started')
|
|
||||||
with self.librespot.stdout:
|
|
||||||
for line in self.librespot.stdout:
|
|
||||||
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().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()
|
|
@ -1,35 +0,0 @@
|
|||||||
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')
|
|
@ -1,46 +0,0 @@
|
|||||||
import subprocess
|
|
||||||
|
|
||||||
from ls_addon import log as log
|
|
||||||
|
|
||||||
|
|
||||||
def run(command):
|
|
||||||
return subprocess.check_output(command.split(), text=True)
|
|
||||||
|
|
||||||
|
|
||||||
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]))
|
|
@ -1,80 +0,0 @@
|
|||||||
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.encode('utf-8')
|
|
||||||
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()
|
|
@ -0,0 +1,71 @@
|
|||||||
|
import threading
|
||||||
|
import xbmc
|
||||||
|
|
||||||
|
import onevent
|
||||||
|
import service
|
||||||
|
|
||||||
|
|
||||||
|
class Player(xbmc.Player):
|
||||||
|
|
||||||
|
def __init__(self, dnd_kodi='false', librespot=None, **kwargs):
|
||||||
|
super().__init__()
|
||||||
|
self._dnd_kodi = (dnd_kodi == 'true')
|
||||||
|
self._thread = threading.Thread(daemon=True, target=self._run)
|
||||||
|
self._thread.start()
|
||||||
|
self.last_file = None
|
||||||
|
self.librespot = librespot
|
||||||
|
if not (self._dnd_kodi and self.isPlaying()):
|
||||||
|
self.librespot.start()
|
||||||
|
|
||||||
|
def onAVStarted(self):
|
||||||
|
file = self.getPlayingFile()
|
||||||
|
if file != self.librespot.file:
|
||||||
|
if self._dnd_kodi:
|
||||||
|
self.librespot.stop()
|
||||||
|
elif self.last_file == self.librespot.file:
|
||||||
|
self.librespot.restart()
|
||||||
|
self.last_file = file
|
||||||
|
|
||||||
|
def onLibrespotStopped(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def onLibrespotTrackChanged(self, album='', art='', artist='', title=''):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def onPlayBackEnded(self):
|
||||||
|
if self.last_file == self.librespot.file:
|
||||||
|
self.librespot.restart()
|
||||||
|
else:
|
||||||
|
self.librespot.start()
|
||||||
|
self.last_file = None
|
||||||
|
|
||||||
|
def onPlayBackError(self):
|
||||||
|
self.onPlayBackEnded()
|
||||||
|
|
||||||
|
def onPlayBackStopped(self):
|
||||||
|
self.onPlayBackEnded()
|
||||||
|
|
||||||
|
# fixes unexpected behaviour of Player.stop()
|
||||||
|
def stop(self):
|
||||||
|
xbmc.executebuiltin('PlayerControl(Stop)')
|
||||||
|
|
||||||
|
def _run(self):
|
||||||
|
service.log('onevent dispatcher started')
|
||||||
|
for event in onevent.receive_event():
|
||||||
|
try:
|
||||||
|
player_event = event.pop(onevent.KEY_PLAYER_EVENT)
|
||||||
|
if player_event == onevent.PLAYER_EVENT_STOPPED:
|
||||||
|
self.onLibrespotStopped()
|
||||||
|
elif player_event == onevent.PLAYER_EVENT_TRACK_CHANGED:
|
||||||
|
self.onLibrespotTrackChanged(**event)
|
||||||
|
except Exception as e:
|
||||||
|
service.log(e, True)
|
||||||
|
service.log('onevent dispatcher stopped')
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
onevent.send_event({})
|
||||||
|
self._thread.join()
|
||||||
|
self.onLibrespotStopped()
|
@ -0,0 +1,88 @@
|
|||||||
|
import PIL.Image
|
||||||
|
import urllib.request
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import xbmc
|
||||||
|
import xbmcaddon
|
||||||
|
import xbmcgui
|
||||||
|
|
||||||
|
_ADDON = xbmcaddon.Addon()
|
||||||
|
_ICON = _ADDON.getAddonInfo('icon')
|
||||||
|
_NAME = _ADDON.getAddonInfo('name')
|
||||||
|
_DIALOG = xbmcgui.Dialog()
|
||||||
|
|
||||||
|
|
||||||
|
def log(message, show=False):
|
||||||
|
xbmc.log(f'{_NAME}: {message}', xbmc.LOGINFO if show else xbmc.LOGDEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
def notification(message='', sound=False, heading=_NAME, icon=_ICON, time=5000):
|
||||||
|
_DIALOG.notification(heading, message, icon, time, sound)
|
||||||
|
|
||||||
|
|
||||||
|
_FANART_DIR = os.path.join(tempfile.gettempdir(), 'librespot.fanart')
|
||||||
|
|
||||||
|
|
||||||
|
def get_fanart(url, max_fanarts):
|
||||||
|
name = os.path.basename(url)
|
||||||
|
target = os.path.join(_FANART_DIR, f'{name}_16x9')
|
||||||
|
if not os.path.exists(target):
|
||||||
|
if not os.path.exists(_FANART_DIR):
|
||||||
|
os.makedirs(_FANART_DIR)
|
||||||
|
files = os.listdir(_FANART_DIR)
|
||||||
|
files = [os.path.join(_FANART_DIR, file) for file in files if os.path.isfile(
|
||||||
|
os.path.join(_FANART_DIR, file))]
|
||||||
|
files.sort(key=os.path.getmtime)
|
||||||
|
for file in files[:-max_fanarts]:
|
||||||
|
os.remove(file)
|
||||||
|
source = os.path.join(_FANART_DIR, f'{name}_9x9')
|
||||||
|
urllib.request.urlretrieve(url, source)
|
||||||
|
image = PIL.Image.open(source)
|
||||||
|
width, height = image.size
|
||||||
|
new_width = int(height * 16 / 9)
|
||||||
|
delta_w = new_width - width
|
||||||
|
new_image = PIL.Image.new('RGB', (new_width, height), (0, 0, 0))
|
||||||
|
new_image.paste(image, (delta_w // 2, 0))
|
||||||
|
new_image.save(target, 'JPEG', optimize=True)
|
||||||
|
os.remove(source)
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
_SETTINGS = {
|
||||||
|
'alsa_device': 'hw:2,0',
|
||||||
|
'backend': 'pulseaudio_rtp',
|
||||||
|
'dnd_kodi': 'false',
|
||||||
|
'name': f'{_NAME}@{{}}',
|
||||||
|
'options': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_setting(setting, default):
|
||||||
|
value = _ADDON.getSetting(setting)
|
||||||
|
return value if value else default
|
||||||
|
|
||||||
|
|
||||||
|
def _get_librespot():
|
||||||
|
while True:
|
||||||
|
settings = {k: _get_setting(k, v) for k, v in _SETTINGS.items()}
|
||||||
|
backend = settings.pop('backend')
|
||||||
|
librespot_class = __import__(f'librespot_{backend}').Librespot
|
||||||
|
with librespot_class(**settings) as librespot:
|
||||||
|
with librespot.get_player(librespot=librespot, **settings) as player:
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
class Monitor(xbmc.Monitor):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._get_librespot = _get_librespot()
|
||||||
|
self.onSettingsChanged()
|
||||||
|
|
||||||
|
def onSettingsChanged(self):
|
||||||
|
log('settings changed')
|
||||||
|
next(self._get_librespot)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.waitForAbort()
|
||||||
|
log('abort requested')
|
||||||
|
self._get_librespot.close()
|
@ -1,12 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<settings>
|
<settings>
|
||||||
<category label="30100">
|
<category label="30100">
|
||||||
<setting label="30101" id="autoplay" type="bool" default="true" />
|
<setting label="30101" id="name" type="text" default="Librespot@{}" />
|
||||||
<setting label="30102" id="discovery" type="bool" default="true" />
|
<setting label="30102" id="dnd_kodi" type="bool" default="false" />
|
||||||
<setting label="30103" id="username" type="text" default="" subsetting="true" visible="eq(-1,false)" />
|
<setting label="30103" id="options" type="text" default="" />
|
||||||
<setting label="30104" id="password" type="text" default="" subsetting="true" visible="eq(-2,false)" option="hidden" />
|
<setting label="30104" id="backend" type="select" values="pulseaudio_rtp|alsa" />
|
||||||
<setting label="30105" id="name" type="text" default="Librespot@{}" />
|
<setting label="30105" id="alsa_device" type="text" default="hw:2,0" subsetting="true" visible="eq(-1,1)" />
|
||||||
<setting label="30106" id="rtp_port" type="number" default="24642" />
|
|
||||||
<setting label="30107" id="connect_port" type="number" default="0" />
|
|
||||||
</category>
|
</category>
|
||||||
</settings>
|
</settings>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user