119 lines
2.9 KiB
Python
119 lines
2.9 KiB
Python
from attrs import define
|
|
from typing import Callable
|
|
|
|
from .driver import SoundCloudDriver
|
|
from .song import SongItem
|
|
|
|
import m3u8
|
|
|
|
|
|
@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
|
|
)
|