minor refactor, functions to retrieve information
This commit is contained in:
parent
688f02c885
commit
dcf2f2d2ce
|
@ -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)
|
||||||
|
|
94
objects.py
94
objects.py
|
@ -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
|
|
||||||
|
|
92
scraper.py
92
scraper.py
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue