forked from MiczFlor/RPi-Jukebox-RFID
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
efbe969
commit bdace53
Showing
16 changed files
with
445 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
deviceId = "" ### Device ID (40 chars, leave empty for random) ### | ||
deviceName = "Phoniebox" ### Device name ### | ||
deviceType = "SPEAKER" ### Device type (COMPUTER, TABLET, SMARTPHONE, SPEAKER, TV, AVR, STB, AUDIO_DONGLE, GAME_CONSOLE, CAST_VIDEO, CAST_AUDIO, AUTOMOBILE, WEARABLE, UNKNOWN_SPOTIFY, CAR_THING, UNKNOWN) ### | ||
preferredLocale = "de" ### Preferred locale ### | ||
logLevel = "TRACE" ### Log level (OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL) ### | ||
|
||
[auth] ### Authentication ### | ||
strategy = "USER_PASS" # Strategy (USER_PASS, ZEROCONF, BLOB, FACEBOOK, STORED) | ||
username = "HERE_USERNAME" # Spotify username (BLOB, USER_PASS only) | ||
password = "HERE_PASSWORD" # Spotify password (USER_PASS only) | ||
blob = "" # Spotify authentication blob Base64-encoded (BLOB only) | ||
storeCredentials = false # Whether to store reusable credentials on disk (not a plain password) | ||
credentialsFile = "credentials.json" # Credentials file (JSON) | ||
|
||
[zeroconf] ### Zeroconf ### | ||
listenPort = 12345 # Listen on this TCP port (`-1` for random) | ||
listenAll = true # Listen on all interfaces (overrides `zeroconf.interfaces`) | ||
interfaces = "" # Listen on these interfaces (comma separated list of names) | ||
|
||
[cache] ### Cache ### | ||
enabled = false # Cache enabled | ||
dir = "./cache/" | ||
doCleanUp = true | ||
|
||
[network] ### Network ### | ||
connectionTimeout = 10 # If ping isn't received within this amount of seconds, reconnect | ||
|
||
[preload] ### Preload ### | ||
enabled = true # Preload enabled | ||
|
||
[time] ### Time correction ### | ||
synchronizationMethod = "NTP" # Time synchronization method (NTP, PING, MELODY, MANUAL) | ||
manualCorrection = 0 # Manual time correction in millis | ||
|
||
[player] ### Player ### | ||
autoplayEnabled = false # Autoplay similar songs when your music ends | ||
preferredAudioQuality = "NORMAL" # Preferred audio quality (NORMAL, HIGH, VERY_HIGH) | ||
enableNormalisation = true # Whether to apply the Spotify loudness normalisation | ||
normalisationPregain = +3.0 # Normalisation pregain in decibels (loud at +6, normal at +3, quiet at -5) | ||
initialVolume = 65536 # Initial volume (0-65536) | ||
volumeSteps = 64 # Number of volume notches | ||
logAvailableMixers = true # Log available mixers | ||
mixerSearchKeywords = "" # Mixer/backend search keywords (semicolon separated) | ||
crossfadeDuration = 0 # Crossfade overlap time (in milliseconds) | ||
output = "MIXER" # Audio output device (MIXER, PIPE, STDOUT, CUSTOM) | ||
outputClass = "" # Audio output Java class name | ||
releaseLineDelay = 20 # Release mixer line after set delay (in seconds) | ||
pipe = "" # Output raw (signed) PCM to this file (`player.output` must be PIPE) | ||
retryOnChunkError = true # Whether the player should retry fetching a chuck if it fails | ||
metadataPipe = "" # Output metadata in Shairport Sync format (https://github.com/mikebrady/shairport-sync-metadata-reader) | ||
bypassSinkVolume = false # Whether librespot-java should ignore volume events, sink volume is set to the max | ||
localFilesPath = "" # Where librespot-java should search for local files | ||
|
||
[api] ### API ### | ||
port = 24879 # API port (`api` module only) | ||
host = "0.0.0.0" # API listen interface (`api` module only) | ||
|
||
[proxy] ### Proxy ### | ||
enabled = false # Whether the proxy is enabled | ||
type = "HTTP" # The proxy type (HTTP, SOCKS) | ||
ssl = false # Connect to proxy using SSL (HTTP only) | ||
address = "" # The proxy hostname | ||
port = 0 # The proxy port | ||
auth = false # Whether authentication is enabled on the server | ||
username = "" # Basic auth username | ||
password = "" # Basic auth password | ||
|
||
[shell] ### Shell ### | ||
enabled = false # Shell events enabled | ||
executeWithBash = false # Execute the command with `bash -c` | ||
onContextChanged = "" | ||
onTrackChanged = "" | ||
onPlaybackEnded = "" | ||
onPlaybackPaused = "" | ||
onPlaybackResumed = "" | ||
onTrackSeeked = "" | ||
onMetadataAvailable = "" | ||
onVolumeChanged = "" | ||
onInactiveSession = "" | ||
onPanicState = "" | ||
onConnectionDropped = "" | ||
onConnectionEstablished = "" | ||
onStartedLoading = "" | ||
onFinishedLoading = "" |
97 changes: 97 additions & 0 deletions
97
src/jukebox/components/player/backends/spotify/http_client.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import json | ||
import logging | ||
import requests | ||
from requests.adapters import HTTPAdapter | ||
import urllib | ||
from urllib3.util.retry import Retry | ||
|
||
logger = logging.getLogger('jb.spotify.SpotifyHttpClient') | ||
|
||
|
||
class SpotifyHttpClient: | ||
def __init__(self, host: str, port=24879): | ||
self.protocol = 'http' | ||
self.host = host | ||
self.port = port | ||
self.authority = f'{self.protocol}://{self.host}:{self.port}' | ||
|
||
self.session = requests.Session() | ||
retries = Retry( | ||
total=5, | ||
backoff_factor=5, | ||
status_forcelist=[500, 502, 503, 504] | ||
) | ||
|
||
self.session.mount( | ||
self.protocol + '://', | ||
HTTPAdapter(max_retries=retries) | ||
) | ||
self.session.headers.update({'content-type': 'application/json'}) | ||
logger.debug(f'Spotify HTTP Client initialized. Will connect to {self.authority}') | ||
|
||
def close(self): | ||
logger.debug("Exiting Spotify HTTP session") | ||
self._post_request('/instance/close') | ||
|
||
def _request(self, request_func, path: str): | ||
try: | ||
url = urllib.parse.urljoin(self.authority, path) | ||
logger.debug(f'Requesting "{self.authority}"') | ||
|
||
response = request_func(url) | ||
response.raise_for_status() | ||
|
||
except requests.HTTPError as http_error: | ||
response = {} | ||
logger.error(f'HTTPError: {http_error}') | ||
|
||
except Exception as error: | ||
response = {} | ||
logger.error(f'Error {error}') | ||
|
||
if response.content: | ||
logger.debug(f"Request response.content: {response.content}") | ||
return json.loads(response.content) | ||
else: | ||
logger.debug("Request response.content empty") | ||
return {} | ||
|
||
# no JSON returned | ||
|
||
def _get_request(self, path: str): | ||
response = self._request(self.session.get, path) | ||
return response | ||
|
||
def _post_request(self, path: str): | ||
response = self._request(self.session.post, path) | ||
return response | ||
|
||
def get_status(self): | ||
# json = self._get_request('/web-api/v1//me/player') | ||
response_json = self._post_request('/player/current') | ||
logger.debug(response_json) | ||
return response_json | ||
|
||
def play_uri(self, uri: str, play: bool = True, shuffle: bool = False): | ||
return self._post_request(f'/player/load?uri={uri}&play={play}&shuffle={shuffle}') | ||
|
||
def play(self): | ||
return self._post_request('/player/resume') | ||
|
||
def pause(self): | ||
return self._post_request('/player/pause') | ||
|
||
def prev(self): | ||
return self._post_request('/player/prev') | ||
|
||
def next(self): | ||
return self._post_request('/player/next') | ||
|
||
def seek(self, new_time: int): | ||
return self._post_request(f'/player/seek?pos={new_time}') | ||
|
||
def shuffle(self, val: bool): | ||
return self._post_request(f'/player/shuffle?val={val}') | ||
|
||
def repeat(self, val: str): | ||
return self._post_request(f'/player/repeat?val={val}') |
104 changes: 104 additions & 0 deletions
104
src/jukebox/components/player/backends/spotify/interfacing_spotify.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# Copyright: 2022 | ||
# SPDX License Identifier: MIT License | ||
|
||
import logging | ||
import os.path | ||
|
||
from ruamel import yaml | ||
|
||
import jukebox.plugs as plugin | ||
import jukebox.cfghandler | ||
from components.player.backends.spotify.http_client import SpotifyHttpClient | ||
from components.player.backends.spotify.ws_client import SpotifyWsClient | ||
|
||
logger = logging.getLogger('jb.spotify') | ||
cfg = jukebox.cfghandler.get_handler('jukebox') | ||
|
||
|
||
def sanitize(path: str): | ||
return os.path.normpath(path).lstrip('./') | ||
|
||
|
||
class SPOTBackend: | ||
def __init__(self, player_status): | ||
host = cfg.getn('playerspot', 'host') | ||
self.player_status = player_status | ||
|
||
self.http_client = SpotifyHttpClient(host) | ||
|
||
self.ws_client = SpotifyWsClient( | ||
host=host, | ||
player_status=self.player_status | ||
) | ||
self.ws_client.connect() | ||
|
||
self.collection_file_location = cfg.setndefault('playerspot', 'collection_file', | ||
value="../../shared/audio/spotify/spotify_collection.yaml") | ||
self.spotify_collection_data = self._read_data_file() | ||
|
||
def _read_data_file(self) -> dict: | ||
try: | ||
with open(self.collection_file_location, "r") as collection_file: | ||
return yaml.safe_load(collection_file.read()) | ||
except Exception as err: | ||
logger.error(f"Could not open spotify collection file {self.collection_file_location}") | ||
logger.debug(f"Error: {err}") | ||
logger.debug("Continuing with empty dictionary") | ||
return {} | ||
|
||
def play(self): | ||
self.http_client.play() | ||
|
||
def pause(self): | ||
self.http_client.pause() | ||
|
||
def stop(self): | ||
try: | ||
is_playing = self.http_client.get_status()['current'] | ||
logger.debug(f"Current player playing status: {is_playing}") | ||
if is_playing: | ||
self.http_client.pause() | ||
except Exception as err: | ||
logger.debug("No status information if Spotify is playing something.") | ||
|
||
def prev(self): | ||
self.http_client.prev() | ||
|
||
def next(self): | ||
self.http_client.next() | ||
|
||
def toggle(self): | ||
pass | ||
|
||
def get_queue(self): | ||
pass | ||
|
||
@plugin.tag | ||
def play_uri(self, uri: str, **kwargs): | ||
"""Decode URI and forward play call | ||
spotify:playlist:0 | ||
--> search in the yaml-file for the type "playlist" and play the first uri | ||
""" | ||
player_type, index = uri.split(':', 1) | ||
if player_type != 'spotify': | ||
raise KeyError(f"URI prefix must be 'spotify' not '{player_type}") | ||
|
||
self.http_client.play_uri(uri) | ||
|
||
@plugin.tag | ||
def get_status(self): | ||
self.http_client.get_status() | ||
|
||
# ----------------------------------------------------- | ||
# Queue / URI state (save + restore e.g. random, resume, ...) | ||
|
||
def save_state(self): | ||
"""Save the configuration and state of the current URI playback to the URIs state file""" | ||
pass | ||
|
||
def _restore_state(self): | ||
""" | ||
Restore the configuration state and last played status for current active URI | ||
""" | ||
pass |
Oops, something went wrong.