Initial commit
This commit is contained in:
BIN
librespot/audio/__init__.zip
Normal file
BIN
librespot/audio/__init__.zip
Normal file
Binary file not shown.
1164
librespot/audio/__init__api.py
Normal file
1164
librespot/audio/__init__api.py
Normal file
File diff suppressed because it is too large
Load Diff
1033
librespot/audio/__init__old.py
Normal file
1033
librespot/audio/__init__old.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
librespot/audio/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
librespot/audio/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
librespot/audio/__pycache__/decoders.cpython-312.pyc
Normal file
BIN
librespot/audio/__pycache__/decoders.cpython-312.pyc
Normal file
Binary file not shown.
BIN
librespot/audio/__pycache__/decrypt.cpython-312.pyc
Normal file
BIN
librespot/audio/__pycache__/decrypt.cpython-312.pyc
Normal file
Binary file not shown.
BIN
librespot/audio/__pycache__/format.cpython-312.pyc
Normal file
BIN
librespot/audio/__pycache__/format.cpython-312.pyc
Normal file
Binary file not shown.
BIN
librespot/audio/__pycache__/storage.cpython-312.pyc
Normal file
BIN
librespot/audio/__pycache__/storage.cpython-312.pyc
Normal file
Binary file not shown.
81
librespot/audio/decoders.py
Normal file
81
librespot/audio/decoders.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
from librespot.audio import SuperAudioFormat
|
||||
from librespot.proto import Metadata_pb2 as Metadata
|
||||
from librespot.proto.Metadata_pb2 import AudioFile
|
||||
from librespot.structure import AudioQualityPicker
|
||||
import enum
|
||||
import logging
|
||||
import typing
|
||||
|
||||
|
||||
class AudioQuality(enum.Enum):
|
||||
NORMAL = 0x00
|
||||
HIGH = 0x01
|
||||
VERY_HIGH = 0x02
|
||||
|
||||
@staticmethod
|
||||
def get_quality(audio_format: AudioFile.Format) -> AudioQuality:
|
||||
if audio_format in [
|
||||
AudioFile.MP3_96,
|
||||
AudioFile.OGG_VORBIS_96,
|
||||
AudioFile.AAC_24_NORM,
|
||||
]:
|
||||
return AudioQuality.NORMAL
|
||||
if audio_format in [
|
||||
AudioFile.MP3_160,
|
||||
AudioFile.MP3_160_ENC,
|
||||
AudioFile.OGG_VORBIS_160,
|
||||
AudioFile.AAC_24,
|
||||
]:
|
||||
return AudioQuality.HIGH
|
||||
if audio_format in [
|
||||
AudioFile.MP3_320,
|
||||
AudioFile.MP3_256,
|
||||
AudioFile.OGG_VORBIS_320,
|
||||
AudioFile.AAC_48,
|
||||
]:
|
||||
return AudioQuality.VERY_HIGH
|
||||
raise RuntimeError("Unknown format: {}".format(format))
|
||||
|
||||
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(
|
||||
file.format) == self:
|
||||
file_list.append(file)
|
||||
return file_list
|
||||
|
||||
|
||||
class VorbisOnlyAudioQuality(AudioQualityPicker):
|
||||
logger = logging.getLogger("Librespot:Player:VorbisOnlyAudioQuality")
|
||||
preferred: AudioQuality
|
||||
|
||||
def __init__(self, preferred: AudioQuality):
|
||||
self.preferred = preferred
|
||||
|
||||
@staticmethod
|
||||
def get_vorbis_file(files: typing.List[Metadata.AudioFile]):
|
||||
for file in files:
|
||||
if file.HasField("format") and SuperAudioFormat.get(
|
||||
file.format) == SuperAudioFormat.VORBIS:
|
||||
return file
|
||||
return None
|
||||
|
||||
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:
|
||||
vorbis: Metadata.AudioFile = VorbisOnlyAudioQuality.get_vorbis_file(
|
||||
files)
|
||||
if vorbis is not None:
|
||||
self.logger.warning(
|
||||
"Using {} because preferred {} couldn't be found.".format(
|
||||
Metadata.AudioFile.Format.Name(vorbis.format),
|
||||
self.preferred))
|
||||
else:
|
||||
self.logger.fatal(
|
||||
"Couldn't find any Vorbis file, available: {}")
|
||||
return vorbis
|
||||
45
librespot/audio/decrypt.py
Normal file
45
librespot/audio/decrypt.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from __future__ import annotations
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Util import Counter
|
||||
from librespot.audio.storage import ChannelManager
|
||||
from librespot.structure import AudioDecrypt
|
||||
import io
|
||||
import time
|
||||
|
||||
|
||||
class AesAudioDecrypt(AudioDecrypt):
|
||||
audio_aes_iv = b'r\xe0g\xfb\xdd\xcb\xcfw\xeb\xe8\xbcd?c\r\x93'
|
||||
cipher = None
|
||||
decrypt_count = 0
|
||||
decrypt_total_time = 0
|
||||
iv_int = int.from_bytes(audio_aes_iv, "big")
|
||||
iv_diff = 0x100
|
||||
key: bytes
|
||||
|
||||
def __init__(self, key: bytes):
|
||||
self.key = key
|
||||
|
||||
def decrypt_chunk(self, chunk_index: int, buffer: bytes):
|
||||
new_buffer = io.BytesIO()
|
||||
iv = self.iv_int + int(ChannelManager.chunk_size * chunk_index / 16)
|
||||
start = time.time_ns()
|
||||
for i in range(0, len(buffer), 4096):
|
||||
cipher = AES.new(key=self.key,
|
||||
mode=AES.MODE_CTR,
|
||||
counter=Counter.new(128, initial_value=iv))
|
||||
count = min(4096, len(buffer) - i)
|
||||
decrypted_buffer = cipher.decrypt(buffer[i:i + count])
|
||||
new_buffer.write(decrypted_buffer)
|
||||
if count != len(decrypted_buffer):
|
||||
raise RuntimeError(
|
||||
"Couldn't process all data, actual: {}, expected: {}".
|
||||
format(len(decrypted_buffer), count))
|
||||
iv += self.iv_diff
|
||||
self.decrypt_total_time += time.time_ns() - start
|
||||
self.decrypt_count += 1
|
||||
new_buffer.seek(0)
|
||||
return new_buffer.read()
|
||||
|
||||
def decrypt_time_ms(self):
|
||||
return 0 if self.decrypt_count == 0 else int(
|
||||
(self.decrypt_total_time / self.decrypt_count) / 1000000)
|
||||
32
librespot/audio/format.py
Normal file
32
librespot/audio/format.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from librespot.proto import Metadata_pb2 as Metadata
|
||||
import enum
|
||||
|
||||
|
||||
class SuperAudioFormat(enum.Enum):
|
||||
MP3 = 0x00
|
||||
VORBIS = 0x01
|
||||
AAC = 0x02
|
||||
|
||||
@staticmethod
|
||||
def get(audio_format: Metadata.AudioFile.Format):
|
||||
if audio_format in [
|
||||
Metadata.AudioFile.Format.OGG_VORBIS_96,
|
||||
Metadata.AudioFile.Format.OGG_VORBIS_160,
|
||||
Metadata.AudioFile.Format.OGG_VORBIS_320,
|
||||
]:
|
||||
return SuperAudioFormat.VORBIS
|
||||
if audio_format in [
|
||||
Metadata.AudioFile.Format.MP3_256,
|
||||
Metadata.AudioFile.Format.MP3_320,
|
||||
Metadata.AudioFile.Format.MP3_160,
|
||||
Metadata.AudioFile.Format.MP3_96,
|
||||
Metadata.AudioFile.Format.MP3_160_ENC,
|
||||
]:
|
||||
return SuperAudioFormat.MP3
|
||||
if audio_format in [
|
||||
Metadata.AudioFile.Format.AAC_24,
|
||||
Metadata.AudioFile.Format.AAC_48,
|
||||
Metadata.AudioFile.Format.AAC_24_NORM,
|
||||
]:
|
||||
return SuperAudioFormat.AAC
|
||||
raise RuntimeError("Unknown audio format: {}".format(audio_format))
|
||||
139
librespot/audio/storage.py
Normal file
139
librespot/audio/storage.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
from librespot import util
|
||||
from librespot.crypto import Packet
|
||||
from librespot.proto.Metadata_pb2 import AudioFile
|
||||
from librespot.structure import Closeable, PacketsReceiver
|
||||
import concurrent.futures
|
||||
import io
|
||||
import logging
|
||||
import queue
|
||||
import struct
|
||||
import threading
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from librespot.core import Session
|
||||
|
||||
|
||||
class ChannelManager(Closeable, PacketsReceiver):
|
||||
channels: typing.Dict[int, Channel] = {}
|
||||
chunk_size = 128 * 1024
|
||||
executor_service = concurrent.futures.ThreadPoolExecutor()
|
||||
logger = logging.getLogger("Librespot:ChannelManager")
|
||||
seq_holder = 0
|
||||
seq_holder_lock = threading.Condition()
|
||||
__session: Session = None
|
||||
|
||||
def __init__(self, session: Session):
|
||||
self.__session = session
|
||||
|
||||
def request_chunk(self, file_id: bytes, index: int, file: AudioFile):
|
||||
start = int(index * self.chunk_size / 4)
|
||||
end = int((index + 1) * self.chunk_size / 4)
|
||||
channel = ChannelManager.Channel(self, file, index)
|
||||
self.channels[channel.chunk_id] = channel
|
||||
out = io.BytesIO()
|
||||
out.write(struct.pack(">H", channel.chunk_id))
|
||||
out.write(struct.pack(">i", 0x00000000))
|
||||
out.write(struct.pack(">i", 0x00000000))
|
||||
out.write(struct.pack(">i", 0x00004E20))
|
||||
out.write(struct.pack(">i", 0x00030D40))
|
||||
out.write(file_id)
|
||||
out.write(struct.pack(">i", start))
|
||||
out.write(struct.pack(">i", end))
|
||||
out.seek(0)
|
||||
self.__session.send(Packet.Type.stream_chunk, out.read())
|
||||
|
||||
def dispatch(self, packet: Packet) -> None:
|
||||
payload = io.BytesIO(packet.payload)
|
||||
if packet.is_cmd(Packet.Type.stream_chunk_res):
|
||||
chunk_id = struct.unpack(">H", payload.read(2))[0]
|
||||
channel = self.channels.get(chunk_id)
|
||||
if channel is None:
|
||||
self.logger.warning(
|
||||
"Couldn't find channel, id: {}, received: {}".format(
|
||||
chunk_id, len(packet.payload)))
|
||||
return
|
||||
channel.add_to_queue(payload)
|
||||
elif packet.is_cmd(Packet.Type.channel_error):
|
||||
chunk_id = struct.unpack(">H", payload.read(2))[0]
|
||||
channel = self.channels.get(chunk_id)
|
||||
if channel is None:
|
||||
self.logger.warning(
|
||||
"Dropping channel error, id: {}, code: {}".format(
|
||||
chunk_id,
|
||||
struct.unpack(">H", payload.read(2))[0]))
|
||||
return
|
||||
channel.stream_error(struct.unpack(">H", payload.read(2))[0])
|
||||
else:
|
||||
self.logger.warning(
|
||||
"Couldn't handle packet, cmd: {}, payload: {}".format(
|
||||
packet.cmd, util.bytes_to_hex(packet.payload)))
|
||||
|
||||
def close(self) -> None:
|
||||
self.executor_service.shutdown()
|
||||
|
||||
class Channel:
|
||||
channel_manager: ChannelManager
|
||||
chunk_id: int
|
||||
q = queue.Queue()
|
||||
__buffer: io.BytesIO
|
||||
__chunk_index: int
|
||||
__file: AudioFile
|
||||
__header: bool = True
|
||||
|
||||
def __init__(self, channel_manager: ChannelManager, file: AudioFile,
|
||||
chunk_index: int):
|
||||
self.__buffer = io.BytesIO()
|
||||
self.channel_manager = channel_manager
|
||||
self.__file = file
|
||||
self.__chunk_index = chunk_index
|
||||
with self.channel_manager.seq_holder_lock:
|
||||
self.chunk_id = self.channel_manager.seq_holder
|
||||
self.channel_manager.seq_holder += 1
|
||||
self.channel_manager.executor_service.submit(
|
||||
lambda: ChannelManager.Channel.Handler(self))
|
||||
|
||||
def _handle(self, payload: bytes) -> bool:
|
||||
if len(payload) == 0:
|
||||
if not self.__header:
|
||||
self.__file.write_chunk(payload, self.__chunk_index, False)
|
||||
return True
|
||||
self.channel_manager.logger.debug(
|
||||
"Received empty chunk, skipping.")
|
||||
return False
|
||||
if self.__header:
|
||||
length: int
|
||||
while len(payload.buffer) > 0:
|
||||
length = payload.read_short()
|
||||
if not length > 0:
|
||||
break
|
||||
header_id = payload.read_byte()
|
||||
header_data = payload.read(length - 1)
|
||||
self.__file.write_header(int.from_bytes(header_id, "big"),
|
||||
bytearray(header_data), False)
|
||||
self.__header = False
|
||||
else:
|
||||
self.__buffer.write(payload.read(len(payload.buffer)))
|
||||
return False
|
||||
|
||||
def add_to_queue(self, payload):
|
||||
self.q.put(payload)
|
||||
|
||||
def stream_error(self, code: int) -> None:
|
||||
self.__file.stream_error(self.__chunk_index, code)
|
||||
|
||||
class Handler:
|
||||
__channel: ChannelManager.Channel = None
|
||||
|
||||
def __init__(self, channel: ChannelManager.Channel):
|
||||
self.__channel = channel
|
||||
|
||||
def run(self) -> None:
|
||||
self.__channel.channel_manager.logger.debug(
|
||||
"ChannelManager.Handler is starting")
|
||||
with self.__channel.q.all_tasks_done:
|
||||
self.__channel.channel_manager.channels.pop(
|
||||
self.__channel.chunk_id)
|
||||
self.__channel.channel_manager.logger.debug(
|
||||
"ChannelManager.Handler is shutting down")
|
||||
Reference in New Issue
Block a user