2
0
Fork 0

minor refactor, functions to retrieve information

This commit is contained in:
Luca Bilke 2023-06-22 17:39:20 +02:00
parent 688f02c885
commit dcf2f2d2ce
No known key found for this signature in database
GPG Key ID: 7B77C51E8C779E75
3 changed files with 117 additions and 78 deletions

View File

@ -1,5 +1,8 @@
from scraper import scraper from scraper import scraper
import os
authfile = os.environ["XDG_CACHE_HOME"] + "/auth.json" NORMAL = "LOW"
s = scraper(authfile) HIGH = "HIGH"
HIFI = "LOSSLESS"
MASTER = "HI_RES"
s = scraper(quality = HIFI)

View File

@ -21,70 +21,52 @@ class Auth:
@dataclass @dataclass
class Artist: class Artist:
id: int | None id: int
name: str | None name: str
type: str | None type: str
picture: str | None picture: str
@dataclass @dataclass
class Album: class Album:
id: int | None id: int
title: str | None title: str
duration: int | None duration: int
numberOfTracks: int | None numberOfTracks: int
numberOfVideos: int | None numberOfVolumes: int
numberOfVolumes: int | None releaseDate: str
releaseDate: str | None type: str
type: str | None version: str
version: str | None cover: str
cover: str | None explicit: bool
explicit: bool | None audioQuality: str
audioQuality: str | None audioModes: str
audioModes: str | None artist: Artist
artist: Artist | None artists: list
artists: Artist | None
@dataclass
class Playlist:
uuid: str | None
title: str | None
numberOfTracks: int | None
numberOfVideos: int | None
description: str | None
duration: int | None
image: str | None
squareImage: str | None
@dataclass @dataclass
class Track: class Track:
id: int | None id: int
title: str | None title: str
duration: int | None duration: int
trackNumber: int | None number: int
volumeNumber: int | None volumeNumber: int
trackNumberOnPlaylist: int | None version: str
version: str | None isrc: str
isrc: str | None explicit: bool
explicit: bool | None audioQuality: str
audioQuality: str | None copyRight: str
copyRight: str | None artist: Artist
artist: Artist | None artists: Artist
artists: Artist | None album: Album
album: Album | None allowStreaming: bool
allowStreaming: bool | None
playlist: Playlist | None
@dataclass @dataclass
class StreamResponse: class StreamInfo:
trackid: int | None trackId: int
streamType: str | None audioQuality: str
assetPresentation: str | None codecs: str
audioMode: str | None encryptionKey: str
audioQuality: str | None url: str
videoQuality: str | None
manifestMimeType: str | None
manifest: str | None

View File

@ -1,18 +1,36 @@
CLIENT_ID = "zU4XHVVkc2tDPo4t" CLIENT_ID = "zU4XHVVkc2tDPo4t"
CLIENT_SECRET = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4" CLIENT_SECRET = "VJKhDFqJPqvsPVNBV6ukXTJmwlvbttP7wlMlrc72se4"
API_URL_BASE = "https://api.tidalhifi.com/v1"
AUTH_URL_BASE = "https://auth.tidal.com/v1/oauth2"
from objects import * from objects import *
from typing import Tuple
import requests import requests
import time import time
import random import random
import json import json
import base64
import os
class scraper: class scraper:
def __init__(self, authfile: str): def __init__(
self,
quality: str,
redownload: bool = False,
authUrlBase: str = "https://auth.tidal.com/v1/oauth2",
apiUrlBase: str = "https://api.tidalhifi.com/v1",
clientToken: tuple = (CLIENT_ID, CLIENT_SECRET),
downloadPath: str = "~/Downloads",
cachePath: str = "~/.cache",
):
self.quality = quality
self.redownload = redownload
self.authUrlBase = authUrlBase
self.apiUrlBase = apiUrlBase
self.apiUrlBase = apiUrlBase
self.clientToken = clientToken
self.downloadPath = downloadPath
self.cachePath = cachePath
authfile = self.cachePath + "/auth.json"
try: try:
with open(authfile, "rb") as f: with open(authfile, "rb") as f:
a = json.load(f) a = json.load(f)
@ -29,7 +47,10 @@ class scraper:
json.dump(self.auth.__dict__, f) json.dump(self.auth.__dict__, f)
def post(self, url: str, data: dict) -> dict: def post(self, url: str, data: dict) -> dict:
return requests.post(url, data=data, auth=(CLIENT_ID, CLIENT_SECRET)).json() return requests.post(url, data=data, auth=self.clientToken).json()
def retrieve(self, url: str, path: str) -> None:
# TODO: Write function to retrieve stream
def get(self, url: str, params: dict = {}) -> dict: def get(self, url: str, params: dict = {}) -> dict:
headers = {"authorization": f"Bearer {self.auth.accessToken}"} headers = {"authorization": f"Bearer {self.auth.accessToken}"}
@ -78,8 +99,8 @@ class scraper:
def loginByWeb(self) -> Auth: def loginByWeb(self) -> Auth:
result = self.post( result = self.post(
f"{AUTH_URL_BASE}/device_authorization", f"{self.authUrlBase}/device_authorization",
{"client_id": CLIENT_ID, "scope": "r_usr+w_usr+w_sub"}, {"client_id": self.clientToken[0], "scope": "r_usr+w_usr+w_sub"},
) )
if "status" in result and result["status"] != 200: if "status" in result and result["status"] != 200:
raise Exception("Client ID not accepted by Tidal") raise Exception("Client ID not accepted by Tidal")
@ -99,9 +120,9 @@ class scraper:
while elapsed < timeout and not auth: while elapsed < timeout and not auth:
elapsed = time.time() - start elapsed = time.time() - start
result = self.post( result = self.post(
f"{AUTH_URL_BASE}/token", f"{self.authUrlBase}/token",
{ {
"client_id": CLIENT_ID, "client_id": self.clientToken[0],
"device_code": login.deviceCode, "device_code": login.deviceCode,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"scope": "r_usr+w_usr+w_sub", "scope": "r_usr+w_usr+w_sub",
@ -126,16 +147,49 @@ class scraper:
return auth return auth
raise Exception("Failed to log in") raise Exception("Failed to log in")
def getTracks(self, obj) -> list: def getTracks(self, album: Album) -> list:
url = API_URL_BASE return self.getItems(f"{self.apiUrlBase}/albums/{str(album.id)}/items")
if type(obj) is Album:
url += f"/albums/{str(obj.id)}/items" def getAlbumFsPath(self, album: Album) -> str:
elif type(obj) is Playlist: return self.downloadPath + album.title
url += f"/playlists/{obj.uuid}/items"
else: def getTrackFsPath(self, track) -> str:
raise Exception("Tried to get tracks from incorrect object") return f"{self.downloadPath}/{self.getAlbumFsPath(track.album)}/[{track.number}] {track.title}"
return self.getItems(url)
def getStreamInfo(self, track: Track) -> StreamInfo:
response = self.get(
f"tracks/{str(track.id)}/playbackinfopostpaywall",
{
"audioquality": self.quality,
"playbackmode": "STREAM",
"assetpresentation": "FULL",
},
)
if "vnd.tidal.bt" in response["manifestMimeType"]:
manifest = json.loads(
base64.b64decode(response["manifest"]).decode("utf-8")
)
return StreamInfo(
response["trackid"],
response["audioQuality"],
manifest["codecs"],
manifest["keyId"] if "keyId" in manifest else "",
manifest["urls"][0],
)
raise Exception("Can't read manifest of type {response['manifestMimeType']}")
def downloadTrack(self, track, partSize=1048576) -> Tuple[bool, str]:
try:
stream = self.getStreamInfo(track.id)
path = self.getTrackFsPath(track)
print(f"Starting download of track \"{track.title}\"")
if not self.redownload and os.path.exists(path):
print(f"Skipping download, \"{track.title}\" already exists")
return True, "exists"
def downloadAlbum(self, album: Album): def downloadAlbum(self, album: Album):
tracks = self.getTracks(album) tracks = self.getTracks(album)
# TODO: Continue working here for i, track in enumerate(tracks):
self.downloadTrack(track)