Merge pull request #16 from raitonoberu/main

Fix some critical bugs and update the example
This commit is contained in:
こうから
2021-04-24 21:25:37 +09:00
committed by GitHub
18 changed files with 65 additions and 54 deletions

View File

@@ -29,7 +29,7 @@ from librespot.core import Session
session = Session.Builder() \ session = Session.Builder() \
.user_pass("<Username>", "<Password>") \ .user_pass("Username", "Password") \
.create() .create()
aceess_token = session.tokens().get("playlist-read") aceess_token = session.tokens().get("playlist-read")
@@ -38,16 +38,16 @@ aceess_token = session.tokens().get("playlist-read")
\*Currently, music streaming is supported, but it may cause unintended behavior. \*Currently, music streaming is supported, but it may cause unintended behavior.
```python ```python
from librespot.core import Session from librespot.core import Session
from librespot.metadata import TrackId
from librespot.player.codecs import AudioQuality, VorbisOnlyAudioQuality
session = Session.Builder() \ session = Session.Builder() \
.user_pass("<Username>", "<Password>") \ .user_pass("Username", "Password") \
.create() .create()
track_id = TrackId.from_uri("<TrackID(ex, spotify:track:xxxxxxxxxxxxxxxxxxxxxx)>") track_id = TrackId.from_uri("spotify:track:xxxxxxxxxxxxxxxxxxxxxx")
stream = session.content_feeder().load(track_id, VorbisOnlyAudioQuality(AudioQuality.AudioQuality.VERY_HIGH), False, None) stream = session.content_feeder().load(track_id, VorbisOnlyAudioQuality(AudioQuality.AudioQuality.VERY_HIGH), False, None)
# stream.input_stream.stream().read() to get one byte of the music stream # stream.input_stream.stream().read() to get one byte of the music stream
``` ```
Please read [this document](https://librespot-python.rtfd.io) for detailed specifications. Please read [this document](https://librespot-python.rtfd.io) for detailed specifications.

View File

@@ -12,7 +12,7 @@ class AbsChunkedInputStream(InputStream, HaltListener):
preload_chunk_retries: typing.Final[int] = 2 preload_chunk_retries: typing.Final[int] = 2
max_chunk_tries: typing.Final[int] = 128 max_chunk_tries: typing.Final[int] = 128
wait_lock: threading.Condition = threading.Condition() wait_lock: threading.Condition = threading.Condition()
retries: list[int] retries: typing.List[int]
retry_on_chunk_error: bool retry_on_chunk_error: bool
chunk_exception = None chunk_exception = None
wait_for_chunk: int = -1 wait_for_chunk: int = -1
@@ -22,7 +22,7 @@ class AbsChunkedInputStream(InputStream, HaltListener):
_decoded_length: int = 0 _decoded_length: int = 0
def __init__(self, retry_on_chunk_error: bool): def __init__(self, retry_on_chunk_error: bool):
self.retries: typing.Final[list[int]] = [ self.retries: typing.Final[typing.List[int]] = [
0 for _ in range(self.chunks()) 0 for _ in range(self.chunks())
] ]
self.retry_on_chunk_error = retry_on_chunk_error self.retry_on_chunk_error = retry_on_chunk_error
@@ -30,7 +30,7 @@ class AbsChunkedInputStream(InputStream, HaltListener):
def is_closed(self) -> bool: def is_closed(self) -> bool:
return self.closed return self.closed
def buffer(self) -> list[bytearray]: def buffer(self) -> typing.List[bytearray]:
raise NotImplementedError() raise NotImplementedError()
def size(self) -> int: def size(self) -> int:
@@ -83,10 +83,10 @@ class AbsChunkedInputStream(InputStream, HaltListener):
return k return k
def requested_chunks(self) -> list[bool]: def requested_chunks(self) -> typing.List[bool]:
raise NotImplementedError() raise NotImplementedError()
def available_chunks(self) -> list[bool]: def available_chunks(self) -> typing.List[bool]:
raise NotImplementedError() raise NotImplementedError()
def chunks(self) -> int: def chunks(self) -> int:

View File

@@ -7,6 +7,7 @@ from librespot.standard import BytesInputStream, ByteArrayOutputStream
import logging import logging
import queue import queue
import threading import threading
import typing
class AudioKeyManager(PacketsReceiver): class AudioKeyManager(PacketsReceiver):
@@ -15,7 +16,7 @@ class AudioKeyManager(PacketsReceiver):
_AUDIO_KEY_REQUEST_TIMEOUT: int = 20 _AUDIO_KEY_REQUEST_TIMEOUT: int = 20
_seqHolder: int = 0 _seqHolder: int = 0
_seqHolderLock: threading.Condition = threading.Condition() _seqHolderLock: threading.Condition = threading.Condition()
_callbacks: dict[int, AudioKeyManager.Callback] = {} _callbacks: typing.Dict[int, AudioKeyManager.Callback] = {}
_session: Session = None _session: Session = None
def __init__(self, session: Session): def __init__(self, session: Session):

View File

@@ -87,9 +87,9 @@ class CdnManager:
class InternalResponse: class InternalResponse:
_buffer: bytearray _buffer: bytearray
_headers: dict[str, str] _headers: typing.Dict[str, str]
def __init__(self, buffer: bytearray, headers: dict[str, str]): def __init__(self, buffer: bytearray, headers: typing.Dict[str, str]):
self._buffer = buffer self._buffer = buffer
self._headers = headers self._headers = headers
@@ -163,9 +163,9 @@ class CdnManager:
_audioDecrypt: AudioDecrypt = None _audioDecrypt: AudioDecrypt = None
_cdnUrl = None _cdnUrl = None
_size: int _size: int
_buffer: list[bytearray] _buffer: typing.List[bytearray]
_available: list[bool] _available: typing.List[bool]
_requested: list[bool] _requested: typing.List[bool]
_chunks: int _chunks: int
_internalStream: CdnManager.Streamer.InternalStream = None _internalStream: CdnManager.Streamer.InternalStream = None
_haltListener: HaltListener = None _haltListener: HaltListener = None
@@ -269,16 +269,16 @@ class CdnManager:
self.streamer: CdnManager.Streamer = streamer self.streamer: CdnManager.Streamer = streamer
super().__init__(retry_on_chunk_error) super().__init__(retry_on_chunk_error)
def buffer(self) -> list[bytearray]: def buffer(self) -> typing.List[bytearray]:
return self.streamer._buffer return self.streamer._buffer
def size(self) -> int: def size(self) -> int:
return self.streamer._size return self.streamer._size
def requested_chunks(self) -> list[bool]: def requested_chunks(self) -> typing.List[bool]:
return self.streamer._requested return self.streamer._requested
def available_chunks(self) -> list[bool]: def available_chunks(self) -> typing.List[bool]:
return self.streamer._available return self.streamer._available
def chunks(self) -> int: def chunks(self) -> int:

View File

@@ -6,5 +6,5 @@ if typing.TYPE_CHECKING:
class AudioQualityPicker: class AudioQualityPicker:
def get_file(self, files: list[Metadata.AudioFile]) -> Metadata.AudioFile: def get_file(self, files: typing.List[Metadata.AudioFile]) -> Metadata.AudioFile:
pass pass

View File

@@ -10,12 +10,13 @@ from librespot.standard import BytesInputStream, BytesOutputStream, Closeable, R
import concurrent.futures import concurrent.futures
import logging import logging
import threading import threading
import typing
class ChannelManager(Closeable, PacketsReceiver.PacketsReceiver): class ChannelManager(Closeable, PacketsReceiver.PacketsReceiver):
CHUNK_SIZE: int = 128 * 1024 CHUNK_SIZE: int = 128 * 1024
_LOGGER: logging = logging.getLogger(__name__) _LOGGER: logging = logging.getLogger(__name__)
_channels: dict[int, Channel] = {} _channels: typing.Dict[int, Channel] = {}
_seqHolder: int = 0 _seqHolder: int = 0
_seqHolderLock: threading.Condition = threading.Condition() _seqHolderLock: threading.Condition = threading.Condition()
_executorService: concurrent.futures.ThreadPoolExecutor = concurrent.futures.ThreadPoolExecutor( _executorService: concurrent.futures.ThreadPoolExecutor = concurrent.futures.ThreadPoolExecutor(

View File

@@ -26,6 +26,7 @@ import socket
import struct import struct
import threading import threading
import time import time
import typing
class Session(Closeable, SubListener, DealerClient.MessageListener): class Session(Closeable, SubListener, DealerClient.MessageListener):
@@ -60,11 +61,11 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
_authLock: threading.Condition = threading.Condition() _authLock: threading.Condition = threading.Condition()
_authLockBool: bool = False _authLockBool: bool = False
_client: requests.Session = None _client: requests.Session = None
_closeListeners: list[Session.CloseListener] = [] _closeListeners: typing.List[Session.CloseListener] = []
_closeListenersLock: threading.Condition = threading.Condition() _closeListenersLock: threading.Condition = threading.Condition()
_reconnectionListeners: list[Session.ReconnectionListener] = [] _reconnectionListeners: typing.List[Session.ReconnectionListener] = []
_reconnectionListenersLock: threading.Condition = threading.Condition() _reconnectionListenersLock: threading.Condition = threading.Condition()
_userAttributes: dict[str, str] = {} _userAttributes: typing.Dict[str, str] = {}
_conn: Session.ConnectionHolder = None _conn: Session.ConnectionHolder = None
_cipherPair: CipherPair = None _cipherPair: CipherPair = None
_receiver: Session.Receiver = None _receiver: Session.Receiver = None
@@ -379,9 +380,9 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
with self._closeListenersLock: with self._closeListenersLock:
for listener in self._closeListeners: for listener in self._closeListeners:
listener.on_closed() listener.on_closed()
self._closeListeners: list[Session.CloseListener] = [] self._closeListeners: typing.List[Session.CloseListener] = []
self._reconnectionListeners: list[Session.ReconnectionListener] = [] self._reconnectionListeners: typing.List[Session.ReconnectionListener] = []
self._LOGGER.info("Closed session. device_id: {}".format( self._LOGGER.info("Closed session. device_id: {}".format(
self._inner.device_id)) self._inner.device_id))
@@ -586,7 +587,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
self._LOGGER.info("Updated user attribute: {} -> {}".format( self._LOGGER.info("Updated user attribute: {} -> {}".format(
pair.key, pair.value)) pair.key, pair.value))
def on_message(self, uri: str, headers: dict[str, str], def on_message(self, uri: str, headers: typing.Dict[str, str],
payload: bytes) -> None: payload: bytes) -> None:
if uri == "hm://connect-state/v1/connect/logout": if uri == "hm://connect-state/v1/connect/logout":
self.close() self.close()
@@ -986,7 +987,7 @@ class Session(Closeable, SubListener, DealerClient.MessageListener):
self.session._scheduledReconnect) self.session._scheduledReconnect)
def anonymous(): def anonymous():
self._LOGGER.warning( self.session._LOGGER.warning(
"Socket timed out. Reconnecting...") "Socket timed out. Reconnecting...")
self.session._reconnect() self.session._reconnect()

View File

@@ -2,19 +2,20 @@ from __future__ import annotations
from librespot.core import Session, TimeProvider from librespot.core import Session, TimeProvider
from librespot.mercury import MercuryRequests from librespot.mercury import MercuryRequests
import logging import logging
import typing
class TokenProvider: class TokenProvider:
_LOGGER: logging = logging.getLogger(__name__) _LOGGER: logging = logging.getLogger(__name__)
_TOKEN_EXPIRE_THRESHOLD = 10 _TOKEN_EXPIRE_THRESHOLD = 10
_session: Session = None _session: Session = None
_tokens: list[TokenProvider.StoredToken] = [] _tokens: typing.List[TokenProvider.StoredToken] = []
def __init__(self, session: Session): def __init__(self, session: Session):
self._session = session self._session = session
def find_token_with_all_scopes( def find_token_with_all_scopes(
self, scopes: list[str]) -> TokenProvider.StoredToken: self, scopes: typing.List[str]) -> TokenProvider.StoredToken:
for token in self._tokens: for token in self._tokens:
if token.has_scopes(scopes): if token.has_scopes(scopes):
return token return token
@@ -55,7 +56,7 @@ class TokenProvider:
class StoredToken: class StoredToken:
expires_in: int expires_in: int
access_token: str access_token: str
scopes: list[str] scopes: typing.List[str]
timestamp: int timestamp: int
def __init__(self, obj): def __init__(self, obj):
@@ -76,7 +77,7 @@ class TokenProvider:
return False return False
def has_scopes(self, sc: list[str]) -> bool: def has_scopes(self, sc: typing.List[str]) -> bool:
for s in sc: for s in sc:
if not self.has_scope(s): if not self.has_scope(s):
return False return False

View File

@@ -2,9 +2,9 @@ from librespot.core.ApResolver import ApResolver
from librespot.metadata import AlbumId, ArtistId, EpisodeId, TrackId, ShowId from librespot.metadata import AlbumId, ArtistId, EpisodeId, TrackId, ShowId
from librespot.proto import Connect, Metadata from librespot.proto import Connect, Metadata
from librespot.standard import Closeable from librespot.standard import Closeable
from typing import Union
import logging import logging
import requests import requests
import typing
class ApiClient(Closeable): class ApiClient(Closeable):
@@ -17,8 +17,8 @@ class ApiClient(Closeable):
self._baseUrl = "https://{}".format(ApResolver.get_random_spclient()) self._baseUrl = "https://{}".format(ApResolver.get_random_spclient())
def build_request(self, method: str, suffix: str, def build_request(self, method: str, suffix: str,
headers: Union[None, dict[str, str]], headers: typing.Union[None, typing.Dict[str, str]],
body: Union[None, bytes]) -> requests.PreparedRequest: body: typing.Union[None, bytes]) -> requests.PreparedRequest:
request = requests.PreparedRequest() request = requests.PreparedRequest()
request.method = method request.method = method
request.data = body request.data = body
@@ -30,9 +30,9 @@ class ApiClient(Closeable):
request.url = self._baseUrl + suffix request.url = self._baseUrl + suffix
return request return request
def send(self, method: str, suffix: str, headers: Union[None, dict[str, def send(self, method: str, suffix: str, headers: typing.Union[None, typing.Dict[str,
str]], str]],
body: Union[None, bytes]) -> requests.Response: body: typing.Union[None, bytes]) -> requests.Response:
resp = self._session.client().send( resp = self._session.client().send(
self.build_request(method, suffix, headers, body)) self.build_request(method, suffix, headers, body))
return resp return resp

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from librespot.standard.Closeable import Closeable from librespot.standard.Closeable import Closeable
import typing
class DealerClient(Closeable): class DealerClient(Closeable):
@@ -14,6 +15,6 @@ class DealerClient(Closeable):
pass pass
class MessageListener: class MessageListener:
def on_message(self, uri: str, headers: dict[str, str], def on_message(self, uri: str, headers: typing.Dict[str, str],
payload: bytes): payload: bytes):
pass pass

View File

@@ -17,11 +17,11 @@ class MercuryClient(PacketsReceiver.PacketsReceiver, Closeable):
_MERCURY_REQUEST_TIMEOUT: int = 3 _MERCURY_REQUEST_TIMEOUT: int = 3
_seqHolder: int = 1 _seqHolder: int = 1
_seqHolderLock: threading.Condition = threading.Condition() _seqHolderLock: threading.Condition = threading.Condition()
_callbacks: dict[int, Callback] = {} _callbacks: typing.Dict[int, Callback] = {}
_removeCallbackLock: threading.Condition = threading.Condition() _removeCallbackLock: threading.Condition = threading.Condition()
_subscriptions: list[MercuryClient.InternalSubListener] = [] _subscriptions: typing.List[MercuryClient.InternalSubListener] = []
_subscriptionsLock: threading.Condition = threading.Condition() _subscriptionsLock: threading.Condition = threading.Condition()
_partials: dict[int, bytes] = {} _partials: typing.Dict[int, bytes] = {}
_session: Session = None _session: Session = None
def __init__(self, session: Session): def __init__(self, session: Session):
@@ -246,10 +246,10 @@ class MercuryClient(PacketsReceiver.PacketsReceiver, Closeable):
class Response: class Response:
uri: str uri: str
payload: list[bytes] payload: typing.List[bytes]
status_code: int status_code: int
def __init__(self, header: Mercury.Header, payload: list[bytes]): def __init__(self, header: Mercury.Header, payload: typing.List[bytes]):
self.uri = header.uri self.uri = header.uri
self.status_code = header.status_code self.status_code = header.status_code
self.payload = payload[1:] self.payload = payload[1:]

View File

@@ -1,11 +1,12 @@
from librespot.proto import Mercury from librespot.proto import Mercury
import typing
class RawMercuryRequest: class RawMercuryRequest:
header: Mercury.Header header: Mercury.Header
payload: list[bytes] payload: typing.List[bytes]
def __init__(self, header: Mercury.Header, payload: list[bytes]): def __init__(self, header: Mercury.Header, payload: typing.List[bytes]):
self.header = header self.header = header
self.payload = payload self.payload = payload
@@ -40,7 +41,7 @@ class RawMercuryRequest:
class Builder: class Builder:
header_dict: dict header_dict: dict
payload: list[bytes] payload: typing.List[bytes]
def __init__(self): def __init__(self):
self.header_dict = {} self.header_dict = {}

View File

@@ -7,7 +7,7 @@ from librespot.common import Utils
from librespot.metadata import SpotifyId from librespot.metadata import SpotifyId
class ShowId(SpotifyId.SpotifyId): class ShowId(SpotifyId):
_PATTERN = re.compile("spotify:show:(.{22})") _PATTERN = re.compile("spotify:show:(.{22})")
_BASE62 = Base62.create_instance_with_inverted_character_set() _BASE62 = Base62.create_instance_with_inverted_character_set()
_hexId: str _hexId: str

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import logging import logging
import sched import sched
import time import time
import typing
from librespot.core.Session import Session from librespot.core.Session import Session
from librespot.player import PlayerConfiguration from librespot.player import PlayerConfiguration
@@ -22,7 +23,7 @@ class Player(Closeable, PlayerSession.Listener, AudioSink.Listener):
_conf: PlayerConfiguration = None _conf: PlayerConfiguration = None
_events: Player.EventsDispatcher = None _events: Player.EventsDispatcher = None
_sink: AudioSink = None _sink: AudioSink = None
_metrics: dict[str, PlaybackMetrics] = {} _metrics: typing.Dict[str, PlaybackMetrics] = {}
_state: StateWrapper = None _state: StateWrapper = None
_playerSession: PlayerSession = None _playerSession: PlayerSession = None
_releaseLineFuture = None _releaseLineFuture = None

View File

@@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
import typing
from librespot.core import Session from librespot.core import Session
from librespot.dealer import DealerClient from librespot.dealer import DealerClient
from librespot.player import Player from librespot.player import Player
@@ -53,5 +55,5 @@ class StateWrapper(DeviceStateHandler.Listener, DealerClient.MessageListener):
self._device.update_state(Connect.PutStateReason.NEW_DEVICE, 0, self._device.update_state(Connect.PutStateReason.NEW_DEVICE, 0,
self._state) self._state)
def on_message(self, uri: str, headers: dict[str, str], payload: bytes): def on_message(self, uri: str, headers: typing.Dict[str, str], payload: bytes):
pass pass

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from librespot.proto.Metadata import AudioFile from librespot.proto.Metadata import AudioFile
import enum import enum
import typing
class AudioQuality(enum.Enum): class AudioQuality(enum.Enum):
@@ -26,7 +27,7 @@ class AudioQuality(enum.Enum):
return AudioQuality.VERY_HIGH return AudioQuality.VERY_HIGH
raise RuntimeError("Unknown format: {}".format(format)) raise RuntimeError("Unknown format: {}".format(format))
def get_matches(self, files: list[AudioFile]) -> list[AudioFile]: def get_matches(self, files: typing.List[AudioFile]) -> typing.List[AudioFile]:
file_list = [] file_list = []
for file in files: for file in files:
if hasattr(file, "format") and AudioQuality.get_quality( if hasattr(file, "format") and AudioQuality.get_quality(

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import typing
from librespot.audio.format.AudioQualityPicker import AudioQualityPicker from librespot.audio.format.AudioQualityPicker import AudioQualityPicker
from librespot.audio.format.SuperAudioFormat import SuperAudioFormat from librespot.audio.format.SuperAudioFormat import SuperAudioFormat
@@ -16,7 +17,7 @@ class VorbisOnlyAudioQuality(AudioQualityPicker):
self.preferred = preferred self.preferred = preferred
@staticmethod @staticmethod
def get_vorbis_file(files: list[Metadata.AudioFile]): def get_vorbis_file(files: typing.List[Metadata.AudioFile]):
for file in files: for file in files:
if hasattr(file, "format") and SuperAudioFormat.get( if hasattr(file, "format") and SuperAudioFormat.get(
file.format) == SuperAudioFormat.VORBIS: file.format) == SuperAudioFormat.VORBIS:
@@ -24,8 +25,8 @@ class VorbisOnlyAudioQuality(AudioQualityPicker):
return None return None
def get_file(self, files: list[Metadata.AudioFile]): def get_file(self, files: typing.List[Metadata.AudioFile]):
matches: list[Metadata.AudioFile] = self.preferred.get_matches(files) matches: typing.List[Metadata.AudioFile] = self.preferred.get_matches(files)
vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file( vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file(
matches) matches)
if vorbis is None: if vorbis is None:

View File

@@ -19,7 +19,7 @@ class DeviceStateHandler:
_LOGGER: logging = logging.getLogger(__name__) _LOGGER: logging = logging.getLogger(__name__)
_session: Session = None _session: Session = None
_deviceInfo: Connect.DeviceInfo = None _deviceInfo: Connect.DeviceInfo = None
_listeners: list[DeviceStateHandler.Listener] = [] _listeners: typing.List[DeviceStateHandler.Listener] = []
_putState: Connect.PutStateRequest = None _putState: Connect.PutStateRequest = None
_putStateWorker: concurrent.futures.ThreadPoolExecutor = ( _putStateWorker: concurrent.futures.ThreadPoolExecutor = (
concurrent.futures.ThreadPoolExecutor()) concurrent.futures.ThreadPoolExecutor())