Change Directory
This commit is contained in:
33
librespot/core/ApResolver.py
Normal file
33
librespot/core/ApResolver.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import queue
|
||||
import random
|
||||
import requests
|
||||
|
||||
|
||||
class ApResolver:
|
||||
base_url = "http://apresolve.spotify.com/"
|
||||
|
||||
@staticmethod
|
||||
def request(service_type: str):
|
||||
response = requests.get("{}?type={}".format(ApResolver.base_url,
|
||||
service_type))
|
||||
return response.json()
|
||||
|
||||
@staticmethod
|
||||
def get_random_of(service_type: str):
|
||||
pool = ApResolver.request(service_type)
|
||||
urls = pool.get(service_type)
|
||||
if urls is None or len(urls) == 0:
|
||||
raise RuntimeError()
|
||||
return random.choice(urls)
|
||||
|
||||
@staticmethod
|
||||
def get_random_dealer() -> str:
|
||||
return ApResolver.get_random_of("dealer")
|
||||
|
||||
@staticmethod
|
||||
def get_random_spclient() -> str:
|
||||
return ApResolver.get_random_of("spclient")
|
||||
|
||||
@staticmethod
|
||||
def get_random_accesspoint() -> str:
|
||||
return ApResolver.get_random_of("accesspoint")
|
||||
103
librespot/core/EventService.py
Normal file
103
librespot/core/EventService.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from __future__ import annotations
|
||||
import concurrent.futures
|
||||
import enum
|
||||
import time
|
||||
import typing
|
||||
import logging
|
||||
|
||||
from librespot.core import Session
|
||||
from librespot.mercury import RawMercuryRequest
|
||||
from librespot.standard import ByteArrayOutputStream
|
||||
|
||||
|
||||
class EventService:
|
||||
_session: Session
|
||||
_LOGGER: logging = logging.getLogger(__name__)
|
||||
_worker: concurrent.futures.ThreadPoolExecutor = concurrent.futures.ThreadPoolExecutor(
|
||||
)
|
||||
|
||||
def __init__(self, session: Session):
|
||||
self._session = session
|
||||
|
||||
def _worker_callback(self, event_builder: EventService.EventBuilder):
|
||||
try:
|
||||
body = event_builder.to_array()
|
||||
resp = self._session.mercury().send_sync(RawMercuryRequest.Builder(
|
||||
).set_uri("hm://event-service/v1/events").set_method(
|
||||
"POST").add_user_field("Accept-Language", "en").add_user_field(
|
||||
"X-ClientTimeStamp",
|
||||
int(time.time() * 1000)).add_payload_part(body).build())
|
||||
|
||||
self._LOGGER.debug("Event sent. body: {}, result: {}".format(
|
||||
body, resp.status_code))
|
||||
except IOError as ex:
|
||||
self._LOGGER.error("Failed sending event: {} {}".format(
|
||||
event_builder, ex))
|
||||
|
||||
def send_event(self,
|
||||
event_or_builder: typing.Union[EventService.GenericEvent,
|
||||
EventService.EventBuilder]):
|
||||
if type(event_or_builder) is EventService.GenericEvent:
|
||||
builder = event_or_builder.build()
|
||||
elif type(event_or_builder) is EventService.EventBuilder:
|
||||
builder = event_or_builder
|
||||
else:
|
||||
TypeError()
|
||||
self._worker.submit(lambda: self._worker_callback(builder))
|
||||
|
||||
def language(self, lang: str):
|
||||
event = EventService.EventBuilder(EventService.Type.LANGUAGE)
|
||||
event.append(s=lang)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
class Type(enum.Enum):
|
||||
LANGUAGE = ("812", 1)
|
||||
FETCHED_FILE_ID = ("274", 3)
|
||||
NEW_SESSION_ID = ("557", 3)
|
||||
NEW_PLAYBACK_ID = ("558", 1)
|
||||
TRACK_PLAYED = ("372", 1)
|
||||
TRACK_TRANSITION = ("12", 37)
|
||||
CDN_REQUEST = ("10", 20)
|
||||
|
||||
_eventId: str
|
||||
_unknown: str
|
||||
|
||||
def __init__(self, event_id: str, unknown: str):
|
||||
self._eventId = event_id
|
||||
self._unknown = unknown
|
||||
|
||||
class GenericEvent:
|
||||
def build(self) -> EventService.EventBuilder:
|
||||
pass
|
||||
|
||||
class EventBuilder:
|
||||
body: ByteArrayOutputStream = ByteArrayOutputStream(256)
|
||||
|
||||
def __init__(self, type: EventService.Type):
|
||||
self.append_no_delimiter(type.value[0])
|
||||
self.append(type.value[1])
|
||||
|
||||
def append_no_delimiter(self, s: str = None) -> None:
|
||||
if s is None:
|
||||
s = ""
|
||||
|
||||
self.body.write(buffer=bytearray(s.encode()))
|
||||
|
||||
def append(self,
|
||||
c: int = None,
|
||||
s: str = None) -> EventService.EventBuilder:
|
||||
if c is None and s is None or c is not None and s is not None:
|
||||
raise TypeError()
|
||||
if c is not None:
|
||||
self.body.write(byte=0x09)
|
||||
self.body.write(byte=c)
|
||||
return self
|
||||
elif s is not None:
|
||||
self.body.write(byte=0x09)
|
||||
self.append_no_delimiter(s)
|
||||
return self
|
||||
|
||||
def to_array(self) -> bytearray:
|
||||
return self.body.to_byte_array()
|
||||
6
librespot/core/PacketsReceiver.py
Normal file
6
librespot/core/PacketsReceiver.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from librespot.crypto.Packet import Packet
|
||||
|
||||
|
||||
class PacketsReceiver:
|
||||
def dispatch(self, packet: Packet):
|
||||
pass
|
||||
10
librespot/core/SearchManager.py
Normal file
10
librespot/core/SearchManager.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
from librespot.core import Session
|
||||
|
||||
|
||||
class SearchManager:
|
||||
_BASE_URL: str = "hm://searchview/km/v4/search/"
|
||||
_session: Session
|
||||
|
||||
def __init__(self, session: Session):
|
||||
self._session = session
|
||||
1048
librespot/core/Session.py
Normal file
1048
librespot/core/Session.py
Normal file
File diff suppressed because it is too large
Load Diff
36
librespot/core/TimeProvider.py
Normal file
36
librespot/core/TimeProvider.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import math
|
||||
import time
|
||||
|
||||
|
||||
class TimeProvider:
|
||||
offset = 0
|
||||
method = 0x00
|
||||
|
||||
def init(self, conf=None, session=None):
|
||||
if conf is None and session is None:
|
||||
return
|
||||
if conf is not None:
|
||||
self.method = conf.time_synchronization_method
|
||||
if conf.time_synchronization_method == TimeProvider.Method.ntp:
|
||||
self.update_with_ntp()
|
||||
if conf.time_synchronization_method == TimeProvider.Method.manual:
|
||||
self.offset = conf.time_manual_correction
|
||||
if session is not None:
|
||||
if self.method != TimeProvider.Method.melody:
|
||||
return
|
||||
self.update_melody(session)
|
||||
|
||||
def current_time_millis(self):
|
||||
return math.floor(time.time() * 1000) + self.offset
|
||||
|
||||
def update_melody(self, session):
|
||||
pass
|
||||
|
||||
def update_with_ntp(self):
|
||||
pass
|
||||
|
||||
class Method:
|
||||
ntp = 0x00
|
||||
ping = 0x01
|
||||
melody = 0x02
|
||||
manual = 0x03
|
||||
84
librespot/core/TokenProvider.py
Normal file
84
librespot/core/TokenProvider.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
from librespot.core import Session, TimeProvider
|
||||
from librespot.mercury import MercuryRequests
|
||||
import logging
|
||||
|
||||
|
||||
class TokenProvider:
|
||||
_LOGGER: logging = logging.getLogger(__name__)
|
||||
_TOKEN_EXPIRE_THRESHOLD = 10
|
||||
_session: Session = None
|
||||
_tokens: list[TokenProvider.StoredToken] = []
|
||||
|
||||
def __init__(self, session: Session):
|
||||
self._session = session
|
||||
|
||||
def find_token_with_all_scopes(
|
||||
self, scopes: list[str]) -> TokenProvider.StoredToken:
|
||||
for token in self._tokens:
|
||||
if token.has_scopes(scopes):
|
||||
return token
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
return None
|
||||
|
||||
def get_token(self, *scopes) -> TokenProvider.StoredToken:
|
||||
scopes = list(scopes)
|
||||
if len(scopes) == 0:
|
||||
raise RuntimeError()
|
||||
|
||||
token = self.find_token_with_all_scopes(scopes)
|
||||
if token is not None:
|
||||
if token.expired():
|
||||
self._tokens.remove(token)
|
||||
else:
|
||||
return token
|
||||
|
||||
self._LOGGER.debug(
|
||||
"Token expired or not suitable, requesting again. scopes: {}, old_token: {}"
|
||||
.format(scopes, token))
|
||||
resp = self._session.mercury().send_sync_json(
|
||||
MercuryRequests.request_token(self._session.device_id(),
|
||||
",".join(scopes)))
|
||||
token = TokenProvider.StoredToken(resp)
|
||||
|
||||
self._LOGGER.debug(
|
||||
"Updated token successfully! scopes: {}, new_token: {}".format(
|
||||
scopes, token))
|
||||
self._tokens.append(token)
|
||||
|
||||
return token
|
||||
|
||||
def get(self, scope: str) -> str:
|
||||
return self.get_token(scope).access_token
|
||||
|
||||
class StoredToken:
|
||||
expires_in: int
|
||||
access_token: str
|
||||
scopes: list[str]
|
||||
timestamp: int
|
||||
|
||||
def __init__(self, obj):
|
||||
self.timestamp = TimeProvider.TimeProvider().current_time_millis()
|
||||
self.expires_in = obj["expiresIn"]
|
||||
self.access_token = obj["accessToken"]
|
||||
self.scopes = obj["scope"]
|
||||
|
||||
def expired(self) -> bool:
|
||||
return self.timestamp + (
|
||||
self.expires_in - TokenProvider._TOKEN_EXPIRE_THRESHOLD
|
||||
) * 1000 < TimeProvider.TimeProvider().current_time_millis()
|
||||
|
||||
def has_scope(self, scope: str) -> bool:
|
||||
for s in self.scopes:
|
||||
if s == scope:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_scopes(self, sc: list[str]) -> bool:
|
||||
for s in sc:
|
||||
if not self.has_scope(s):
|
||||
return False
|
||||
|
||||
return True
|
||||
7
librespot/core/__init__.py
Normal file
7
librespot/core/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from librespot.core.ApResolver import ApResolver
|
||||
from librespot.core.EventService import EventService
|
||||
from librespot.core.PacketsReceiver import PacketsReceiver
|
||||
from librespot.core.SearchManager import SearchManager
|
||||
from librespot.core.Session import Session
|
||||
from librespot.core.TimeProvider import TimeProvider
|
||||
from librespot.core.TokenProvider import TokenProvider
|
||||
Reference in New Issue
Block a user