Initial commit

This commit is contained in:
unknown
2025-11-22 03:39:04 +01:00
parent 912dfd504d
commit 4821831e6f
118 changed files with 29486 additions and 3 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

View 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
View 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
View 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")