Skip to content

Commit

Permalink
feat: Issue number 197 add v2 api favorite mixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jozefKruszynski committed Oct 22, 2023
1 parent a11775b commit 874d117
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 7 deletions.
5 changes: 5 additions & 0 deletions tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ def test_add_remove_favorite_video(session):
video_id = 160850422
add_remove(video_id, favorites.add_video, favorites.remove_video, favorites.videos)

def test_get_favorite_mixes(session):
favorites = session.user.favorites
mixes = favorites.mixes()
assert len(mixes) > 0
assert isinstance(mixes[0], tidalapi.MixV2)

def add_remove(object_id, add, remove, objects):
"""Add and remove an item from favorites. Skips the test if the item was already in
Expand Down
2 changes: 1 addition & 1 deletion tidalapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from .artist import Artist, Role # noqa: F401
from .genre import Genre # noqa: F401
from .media import Quality, Track, Video, VideoQuality # noqa: F401
from .mix import Mix # noqa: F401
from .mix import Mix, MixV2 # noqa: F401
from .page import Page # noqa: F401
from .playlist import Playlist, UserPlaylist # noqa: F401
from .request import Requests # noqa: F401
Expand Down
114 changes: 114 additions & 0 deletions tidalapi/mix.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@

import copy
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import TYPE_CHECKING, List, Optional, Union

from tidalapi.types import JsonObj

import dateutil.parser

if TYPE_CHECKING:
from tidalapi.media import Track, Video
from tidalapi.session import Session
Expand Down Expand Up @@ -151,3 +154,114 @@ def image(self, dimensions: int = 320) -> str:
return self.images.large

raise ValueError(f"Invalid resolution {dimensions} x {dimensions}")

@dataclass
class TextInfo:
text: str
color: str

class MixV2:
"""A mix from TIDALs v2 api endpoint, weirdly, it is used in only one place currently.
"""
date_added: Optional[datetime] = None
title: str = ""
id: str = ""
mix_type: Optional[MixType] = None
images: Optional[ImageResponse] = None
detail_images: Optional[ImageResponse] = None
master = False
title_text_info: Optional[TextInfo] = None
sub_title_text_info: Optional[TextInfo] = None
sub_title: str = ""
updated: Optional[datetime] = None

def __init__(self, session: Session, mix_id: str):
self.session = session
self.request = session.request
if mix_id is not None:
self.get(mix_id)

def get(self, mix_id: Optional[str] = None) -> "Mix":
"""Returns information about a mix, and also replaces the mix object used to
call this function.
:param mix_id: TIDAL's identifier of the mix
:return: A :class:`Mix` object containing all the information about the mix
"""
if mix_id is None:
mix_id = self.id

params = {"mixId": mix_id, "deviceType": "BROWSER"}
parse = self.session.parse_page
result = self.request.map_request("pages/mix", parse=parse, params=params)
assert not isinstance(result, list)
self._retrieved = True
self.__dict__.update(result.categories[0].__dict__)
self._items = result.categories[1].items
return self

def parse(self, json_obj: JsonObj) -> "MixV2":
"""Parse a mix into a :class:`MixV2`, replaces the calling object.
:param json_obj: The json of a mix to be parsed
:return: A copy of the parsed mix
"""
date_added = json_obj.get("dateAdded")
self.date_added = (
dateutil.parser.isoparse(date_added) if date_added else None
)
self.title = json_obj["title"]
self.id = json_obj["id"]
self.title = json_obj["title"]
self.mix_type = MixType(json_obj["mixType"])
images = json_obj["images"]
self.images = ImageResponse(
small=images["SMALL"]["url"],
medium=images["MEDIUM"]["url"],
large=images["LARGE"]["url"],
)
detail_images = json_obj["detailImages"]
self.detail_images = ImageResponse(
small=detail_images["SMALL"]["url"],
medium=detail_images["MEDIUM"]["url"],
large=detail_images["LARGE"]["url"],
)
self.master = json_obj["master"]
title_text_info = json_obj["titleTextInfo"]
self.title_text_info = TextInfo(
text=title_text_info["text"],
color=title_text_info["color"],
)
sub_title_text_info = json_obj["subTitleTextInfo"]
self.sub_title_text_info = TextInfo(
text=sub_title_text_info["text"],
color=sub_title_text_info["color"],
)
self.sub_title = json_obj["subTitle"]
updated = json_obj.get("updated")
self.date_added = (
dateutil.parser.isoparse(updated) if date_added else None
)

return copy.copy(self)

def image(self, dimensions: int = 320) -> str:
"""A URL to a Mix picture.
:param dimensions: The width and height the requested image should be
:type dimensions: int
:return: A url to the image
Original sizes: 320x320, 640x640, 1500x1500
"""
if not self.images:
raise ValueError("No images present.")

if dimensions == 320:
return self.images.small
elif dimensions == 640:
return self.images.medium
elif dimensions == 1500:
return self.images.large

raise ValueError(f"Invalid resolution {dimensions} x {dimensions}")
10 changes: 6 additions & 4 deletions tidalapi/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def __init__(self, session):
self.session = session
self.config = session.config

def basic_request(self, method, path, params=None, data=None, headers=None):
def basic_request(self, method, path, api_version="v1/", params=None, data=None, headers=None):
request_params = {
"sessionId": self.session.session_id,
"countryCode": self.session.country_code,
Expand All @@ -56,7 +56,7 @@ def basic_request(self, method, path, params=None, data=None, headers=None):
self.session.token_type + " " + self.session.access_token
)

url = urljoin(self.session.config.api_location, path)
url = urljoin(f"{self.session.config.api_location}{api_version}", path)
request = self.session.request_session.request(
method, url, params=request_params, data=data, headers=headers
)
Expand Down Expand Up @@ -86,6 +86,7 @@ def request(
self,
method: Literal["GET", "POST", "PUT", "DELETE"],
path: str,
api_version: str = "v1/",
params: Optional[Params] = None,
data: Optional[JsonObj] = None,
headers: Optional[Mapping[str, str]] = None,
Expand All @@ -102,7 +103,7 @@ def request(
:return: The json data at specified api endpoint.
"""

request = self.basic_request(method, path, params, data, headers)
request = self.basic_request(method, path, api_version, params, data, headers)
log.debug("request: %s", request.request.url)
request.raise_for_status()
if request.content:
Expand All @@ -112,6 +113,7 @@ def request(
def map_request(
self,
url: str,
api_version: str = "v1/",
params: Optional[Params] = None,
parse: Optional[Callable] = None,
):
Expand All @@ -126,7 +128,7 @@ def map_request(
:return: The object(s) at the url, with the same type as the class of the parse
method.
"""
json_obj = self.request("GET", url, params).json()
json_obj = self.request("GET", url, api_version, params).json()

return self.map_json(json_obj, parse=parse)

Expand Down
15 changes: 13 additions & 2 deletions tidalapi/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def __init__(
):
self.quality = quality.value
self.video_quality = video_quality.value
self.api_location = "https://api.tidal.com/v1/"
self.api_location = "https://api.tidal.com/"
self.image_url = "https://resources.tidal.com/images/%s/%ix%i.jpg"
self.video_url = "https://resources.tidal.com/videos/%s/%ix%i.mp4"

Expand Down Expand Up @@ -228,6 +228,7 @@ def __init__(self, config=Config()):
self.parse_video = self.video().parse_video
self.parse_media = self.track().parse_media
self.parse_mix = self.mix().parse
self.parse_v2_mix = self.mixv2().parse

self.parse_user = user.User(self, None).parse
self.page = page.Page(self, None)
Expand Down Expand Up @@ -343,7 +344,7 @@ def login(self, username, password):
:param password: The password to your TIDAL account
:return: Returns true if we think the login was successful.
"""
url = urljoin(self.config.api_location, "login/username")
url = urljoin(self.config.api_location, "v1/login/username")
headers: dict[str, str] = {"X-Tidal-Token": self.config.api_token}
payload = {
"username": username,
Expand Down Expand Up @@ -618,6 +619,16 @@ def mix(self, mix_id=None) -> tidalapi.Mix:
"""

return mix.Mix(session=self, mix_id=mix_id)

def mixv2(self, mix_id=None) -> tidalapi.MixV2:
"""Function to create a mix object with access to the session instance smoothly
Calls :class:`tidalapi.MixV2(session=session, mix_id=mix_id) <.Album>` internally.
:param mix_id: (Optional) The TIDAL id of the Mix. You may want access to the mix methods without an id.
:return: Returns a :class:`.Mix` object that has access to the session instance used.
"""

return mix.MixV2(session=self, mix_id=mix_id)

def get_user(
self, user_id=None
Expand Down
15 changes: 15 additions & 0 deletions tidalapi/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from tidalapi.media import Track, Video
from tidalapi.playlist import Playlist, UserPlaylist
from tidalapi.session import Session
from tidalapi.mix import Mix, MixV2


class User:
Expand Down Expand Up @@ -188,6 +189,7 @@ def __init__(self, session: "Session", user_id: int):
self.session = session
self.requests = session.request
self.base_url = f"users/{user_id}/favorites"
self.v2_base_url = "favorites"

def add_album(self, album_id: str) -> bool:
"""Adds an album to the users favorites.
Expand Down Expand Up @@ -356,3 +358,16 @@ def videos(self) -> List["Video"]:
f"{self.base_url}/videos", parse=self.session.parse_media
),
)

def mixes(self, limit: Optional[int] = 50, offset: int = 0) -> List["MixV2"]:
"""Get the users favorite tracks.
:return: A :class:`list` of :class:`~tidalapi.media.Track` objects containing all of the favorite tracks.
"""
params = {"limit": limit, "offset": offset}
return cast(
List["MixV2"],
self.requests.map_request(
f"{self.v2_base_url}/mixes", api_version="v2/", params=params, parse=self.session.parse_v2_mix
),
)

0 comments on commit 874d117

Please sign in to comment.