100 lines
2.7 KiB
Python
100 lines
2.7 KiB
Python
from typing import Callable
|
|
|
|
import m3u8
|
|
from attrs import define
|
|
|
|
from .driver import SoundCloudDriver
|
|
from .song import SongItem
|
|
|
|
|
|
@define
|
|
class SoundCloudBytestream:
|
|
file: bytes
|
|
filename: str
|
|
duration: int
|
|
song: SongItem
|
|
|
|
@classmethod
|
|
def from_bytes(cls, bytes_: bytes, filename: str, duration: int, song: SongItem):
|
|
return cls(
|
|
file=bytes_, filename=filename, duration=int(duration / 1000), song=song
|
|
)
|
|
|
|
|
|
@define
|
|
class Downloader:
|
|
driver: SoundCloudDriver
|
|
download_url: str
|
|
duration: int
|
|
filename: str
|
|
method: Callable
|
|
song: SongItem
|
|
|
|
@classmethod
|
|
async def build(cls, song_id: str, driver: SoundCloudDriver):
|
|
track = await driver.get_track(song_id)
|
|
song = SongItem.from_soundcloud(track)
|
|
|
|
if url := cls._try_get_progressive(track["media"]["transcodings"]):
|
|
method = cls._progressive
|
|
else:
|
|
url = track["media"]["transcodings"][0]["url"]
|
|
method = (
|
|
cls._hls
|
|
if (track["media"]["transcodings"][0]["format"]["protocol"] == "hls")
|
|
else cls._progressive
|
|
)
|
|
|
|
return cls(
|
|
driver=driver,
|
|
duration=track["duration"],
|
|
method=method,
|
|
download_url=url,
|
|
filename=f'{track["title"]}.mp3',
|
|
song=song,
|
|
)
|
|
|
|
@staticmethod
|
|
def _try_get_progressive(urls: list) -> str | None:
|
|
for transcode in urls:
|
|
if transcode["format"]["protocol"] == "progressive":
|
|
return transcode["url"]
|
|
|
|
async def _progressive(self, url: str) -> bytes:
|
|
return await self.driver.engine.read_data(
|
|
url=(await self.driver.engine.get(url))["url"]
|
|
)
|
|
|
|
async def _hls(self, url: str) -> bytes:
|
|
m3u8_obj = m3u8.loads(
|
|
(
|
|
await self.driver.engine.read_data(
|
|
(await self.driver.engine.get(url=url))["url"]
|
|
)
|
|
).decode()
|
|
)
|
|
|
|
content = bytearray()
|
|
for segment in m3u8_obj.files:
|
|
content.extend(
|
|
await self.driver.engine.read_data(url=segment, append_client_id=False)
|
|
)
|
|
|
|
return content
|
|
|
|
async def to_bytestream(self) -> SoundCloudBytestream:
|
|
return SoundCloudBytestream.from_bytes(
|
|
bytes_=await self.method(self, self.download_url),
|
|
filename=self.filename,
|
|
duration=self.duration,
|
|
song=self.song,
|
|
)
|
|
|
|
|
|
@define
|
|
class DownloaderBuilder:
|
|
driver: SoundCloudDriver
|
|
|
|
async def from_id(self, song_id: str):
|
|
return await Downloader.build(song_id=song_id, driver=self.driver)
|