Skip to content

Commit

Permalink
Merge pull request #207 from jozefKruszynski/feature/favorite-mixes-i…
Browse files Browse the repository at this point in the history
…ssue-197

feat: Issue number 197 add v2 api favorite mixes
  • Loading branch information
tehkillerbee authored Nov 29, 2023
2 parents 3c91345 + 5ce4a7e commit fd81b6f
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 3 deletions.
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,10 @@ prof/
.sync-exclude.lst

# Local Netlify folder
.netlify
.netlify

# Virtual environment
.venv

# MacOS
.DS_Store
18 changes: 18 additions & 0 deletions tests/test_mix.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019-2022 morguldir
# Copyright (C) 2014 Thomas Amland
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import tidalapi

from .cover import verify_image_cover
Expand Down
7 changes: 7 additions & 0 deletions tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ def test_add_remove_favorite_video(session):
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
your favorites.
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
113 changes: 113 additions & 0 deletions tidalapi/mix.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@

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

import dateutil.parser

from tidalapi.types import JsonObj

if TYPE_CHECKING:
Expand Down Expand Up @@ -151,3 +154,113 @@ 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}")
1 change: 0 additions & 1 deletion tidalapi/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ def basic_request(
headers["authorization"] = (
self.session.token_type + " " + self.session.access_token
)

url = urljoin(self.session.config.api_location, path)
request = self.session.request_session.request(
method, url, params=request_params, data=data, headers=headers
Expand Down
13 changes: 13 additions & 0 deletions tidalapi/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ def __init__(self, config: Config = Config()):
self.parse_track = self.track().parse_track
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, "")
Expand Down Expand Up @@ -661,6 +663,17 @@ def mix(self, mix_id: Optional[str] = None) -> mix.Mix:

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

def mixv2(self, mix_id=None) -> mix.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:`.MixV2` object that has access to the session instance used.
"""

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

def get_user(
self, user_id: Optional[int] = None
) -> Union["FetchedUser", "LoggedInUser", "PlaylistCreator"]:
Expand Down
18 changes: 18 additions & 0 deletions tidalapi/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@

from copy import copy
from typing import TYPE_CHECKING, Dict, List, Optional, Union, cast
from urllib.parse import urljoin

from tidalapi.types import JsonObj

if TYPE_CHECKING:
from tidalapi.album import Album
from tidalapi.artist import Artist
from tidalapi.media import Track, Video
from tidalapi.mix import MixV2
from tidalapi.playlist import Playlist, UserPlaylist
from tidalapi.session import Session

Expand Down Expand Up @@ -196,6 +198,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 @@ -380,3 +383,18 @@ 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(
url=urljoin("https://api.tidal.com/v2/", f"{self.v2_base_url}/mixes"),
params=params,
parse=self.session.parse_v2_mix,
),
)

0 comments on commit fd81b6f

Please sign in to comment.