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
commit b779a6a7d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 65 additions and 54 deletions

View File

@ -29,7 +29,7 @@ from librespot.core import Session
session = Session.Builder() \
.user_pass("<Username>", "<Password>") \
.user_pass("Username", "Password") \
.create()
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.
```python
from librespot.core import Session
from librespot.metadata import TrackId
from librespot.player.codecs import AudioQuality, VorbisOnlyAudioQuality
session = Session.Builder() \
.user_pass("<Username>", "<Password>") \
.user_pass("Username", "Password") \
.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.input_stream.stream().read() to get one byte of the music stream
```
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
max_chunk_tries: typing.Final[int] = 128
wait_lock: threading.Condition = threading.Condition()
retries: list[int]
retries: typing.List[int]
retry_on_chunk_error: bool
chunk_exception = None
wait_for_chunk: int = -1
@ -22,7 +22,7 @@ class AbsChunkedInputStream(InputStream, HaltListener):
_decoded_length: int = 0
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())
]
self.retry_on_chunk_error = retry_on_chunk_error
@ -30,7 +30,7 @@ class AbsChunkedInputStream(InputStream, HaltListener):
def is_closed(self) -> bool:
return self.closed
def buffer(self) -> list[bytearray]:
def buffer(self) -> typing.List[bytearray]:
raise NotImplementedError()
def size(self) -> int:
@ -83,10 +83,10 @@ class AbsChunkedInputStream(InputStream, HaltListener):
return k
def requested_chunks(self) -> list[bool]:
def requested_chunks(self) -> typing.List[bool]:
raise NotImplementedError()
def available_chunks(self) -> list[bool]:
def available_chunks(self) -> typing.List[bool]:
raise NotImplementedError()
def chunks(self) -> int:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,19 +2,20 @@ from __future__ import annotations
from librespot.core import Session, TimeProvider
from librespot.mercury import MercuryRequests
import logging
import typing
class TokenProvider:
_LOGGER: logging = logging.getLogger(__name__)
_TOKEN_EXPIRE_THRESHOLD = 10
_session: Session = None
_tokens: list[TokenProvider.StoredToken] = []
_tokens: typing.List[TokenProvider.StoredToken] = []
def __init__(self, session: Session):
self._session = session
def find_token_with_all_scopes(
self, scopes: list[str]) -> TokenProvider.StoredToken:
self, scopes: typing.List[str]) -> TokenProvider.StoredToken:
for token in self._tokens:
if token.has_scopes(scopes):
return token
@ -55,7 +56,7 @@ class TokenProvider:
class StoredToken:
expires_in: int
access_token: str
scopes: list[str]
scopes: typing.List[str]
timestamp: int
def __init__(self, obj):
@ -76,7 +77,7 @@ class TokenProvider:
return False
def has_scopes(self, sc: list[str]) -> bool:
def has_scopes(self, sc: typing.List[str]) -> bool:
for s in sc:
if not self.has_scope(s):
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.proto import Connect, Metadata
from librespot.standard import Closeable
from typing import Union
import logging
import requests
import typing
class ApiClient(Closeable):
@ -17,8 +17,8 @@ class ApiClient(Closeable):
self._baseUrl = "https://{}".format(ApResolver.get_random_spclient())
def build_request(self, method: str, suffix: str,
headers: Union[None, dict[str, str]],
body: Union[None, bytes]) -> requests.PreparedRequest:
headers: typing.Union[None, typing.Dict[str, str]],
body: typing.Union[None, bytes]) -> requests.PreparedRequest:
request = requests.PreparedRequest()
request.method = method
request.data = body
@ -30,9 +30,9 @@ class ApiClient(Closeable):
request.url = self._baseUrl + suffix
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]],
body: Union[None, bytes]) -> requests.Response:
body: typing.Union[None, bytes]) -> requests.Response:
resp = self._session.client().send(
self.build_request(method, suffix, headers, body))
return resp

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from librespot.standard.Closeable import Closeable
import typing
class DealerClient(Closeable):
@ -14,6 +15,6 @@ class DealerClient(Closeable):
pass
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):
pass

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,7 @@
from __future__ import annotations
import typing
from librespot.core import Session
from librespot.dealer import DealerClient
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._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

View File

@ -1,6 +1,7 @@
from __future__ import annotations
from librespot.proto.Metadata import AudioFile
import enum
import typing
class AudioQuality(enum.Enum):
@ -26,7 +27,7 @@ class AudioQuality(enum.Enum):
return AudioQuality.VERY_HIGH
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 = []
for file in files:
if hasattr(file, "format") and AudioQuality.get_quality(

View File

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

View File

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