Skip to content

Commit

Permalink
Merge pull request #228 from tamland/v0.7.4-prep
Browse files Browse the repository at this point in the history
V0.7.4 prep
  • Loading branch information
tehkillerbee authored Jan 28, 2024
2 parents 4a08d05 + 5fc1747 commit 831d050
Show file tree
Hide file tree
Showing 16 changed files with 147 additions and 52 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,7 @@ prof/
*tidal-oauth*

# Misc. csv. files that might be generated when executing examples
*.csv
*.csv

# Json session files
tidal*.json
14 changes: 14 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
History
=======

v0.7.4
------
* Load/store OAuth/PKCE session to file - tehkillerbee_
* Add PKCE login for HiRes - exislow_, arnesongit_
* Include request response on error. Print as warning - tehkillerbee_
* Fix tests - tehkillerbee_
* Bugfixes (artist.get_similar) - tehkillerbee_
* Favourite mixes refactoring - jozefKruszynski_
* Add typings for Playlist, UserPlaylist, Pages - arusahni_
* Update favorites.tracks to accept order and orderDirection params - Jimmyscene_

v0.7.3
------
* Official support for HI_RES FLAC quality - tehkillerbee_
Expand Down Expand Up @@ -138,6 +149,9 @@ v0.6.2
.. _PretzelVector: https://github.com/PretzelVector
.. _tehkillerbee: https://github.com/tehkillerbee
.. _JoshMock: https://github.com/JoshMock
.. _exislow: https://github.com/exislow
.. _arnesongit: https://github.com/arnesongit
.. _Jimmyscene: https://github.com/Jimmyscene



4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ Install from `PyPI <https://pypi.python.org/pypi/tidalapi/>`_ using ``pip``:
Example usage
Usage
-------------

For examples on how to use the api, see the `examples` directory.
For examples on how to use the api, see the `examples <https://github.com/tamland/python-tidal/tree/master/examples>`_ directory.

Documentation
-------------
Expand Down
46 changes: 46 additions & 0 deletions examples/pkce_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-

# Copyright (C) 2023- The Tidalapi Developers
#
# 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/>.
#
"""simple.py: A simple example script that describes how to get started using tidalapi"""

import tidalapi
from tidalapi import Quality
from pathlib import Path

session_file1 = Path("tidal-session-pkce.json")

session = tidalapi.Session()
# Load session from file; create a new session if necessary
session.login_session_file(session_file1, do_pkce=True)

# Override the required playback quality, if necessary
# Note: Set the quality according to your subscription.
# Low: Quality.low_96k
# Normal: Quality.low_320k
# HiFi: Quality.high_lossless
# HiFi+ Quality.hi_res_lossless
session.audio_quality = Quality.hi_res_lossless.value

album = session.album("110827651") # Let's Rock // The Black Keys
tracks = album.tracks()
# list album tracks
for track in tracks:
print(track.name)
# MPEG-DASH Stream is only supported when HiRes mode is used!
print(track.get_stream())
for artist in track.artists:
print(' by: ', artist.name)
12 changes: 8 additions & 4 deletions examples/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,27 @@
from tidalapi import Quality
from pathlib import Path

oauth_file1 = Path("tidal-oauth-user.json")
session_file1 = Path("tidal-session-oauth.json")

session = tidalapi.Session()
# Will run until you visit the printed url and link your account
session.login_oauth_file(oauth_file1)
# Load session from file; create a new session if necessary
session.login_session_file(session_file1)

# Override the required playback quality, if necessary
# Note: Set the quality according to your subscription.
# Low: Quality.low_96k
# Normal: Quality.low_320k
# HiFi: Quality.high_lossless
# HiFi+ Quality.hi_res_lossless
session.audio_quality = Quality.low_320k

album = session.album(66236918) # Electric For Life Episode 099
album = session.album("110827651") # Let's Rock // The Black Keys
tracks = album.tracks()
print(album.name)
# list album tracks
for track in tracks:
print(track.name)
print(track.get_url())
# print(track.get_stream())
for artist in track.artists:
print(' by: ', artist.name)
8 changes: 4 additions & 4 deletions examples/transfer_favorites.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(sys.stdout))

oauth_file1 = Path("tidal-oauth-user.json")
oauth_file2 = Path("tidal-oauth-userB.json")
oauth_file1 = Path("tidal-session.json")
oauth_file2 = Path("tidal-session-B.json")


class TidalSession:
Expand Down Expand Up @@ -84,11 +84,11 @@ def do_transfer(self):
session_src = self.session_src.get_session()
session_dst = self.session_dst.get_session()
logger.info("Login to user A (source)...")
if not session_src.login_oauth_file(oauth_file1):
if not session_src.login_session_file(oauth_file1):
logger.error("Login to Tidal user...FAILED!")
exit(1)
logger.info("Login to user B (destination)...")
if not session_dst.login_oauth_file(oauth_file2):
if not session_dst.login_session_file(oauth_file2):
logger.error("Login to Tidal user...FAILED!")
exit(1)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "tidalapi"
version = "0.7.3"
version = "0.7.4"
description = "Unofficial API for TIDAL music streaming service."
authors = ["Thomas Amland <thomas.amland@googlemail.com>"]
maintainers = ["tehkillerbee <josaksel.dk@gmail.com>"]
Expand Down
7 changes: 6 additions & 1 deletion tests/test_album.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,12 @@ def test_default_image_used_if_no_cover_art(mocker):
def test_similar(session):
album = session.album(108043414)
for alb in album.similar():
assert isinstance(alb.similar()[0], tidalapi.Album)
if alb.id == 64522277:
# Album with no similar albums should trigger AttributeError (response: 404)
with pytest.raises(AttributeError):
alb.similar()
else:
assert isinstance(alb.similar()[0], tidalapi.Album)


def test_review(session):
Expand Down
7 changes: 3 additions & 4 deletions tests/test_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,10 @@ def test_lyrics(session):

def test_no_lyrics(session):
track = session.track(17626400)
with pytest.raises(requests.HTTPError) as exception:
# Tracks with no lyrics should trigger AttributeError (response: 404)
with pytest.raises(AttributeError):
track.lyrics()

assert exception.value.response.status_code == 404


def test_right_to_left(session):
lyrics = session.track(95948697).lyrics()
Expand All @@ -97,7 +96,7 @@ def test_track_with_album(session):

def test_track_streaming(session):
track = session.track(62392768)
stream = track.stream()
stream = track.get_stream()
assert stream.audio_mode == "STEREO"
assert (
stream.audio_quality == tidalapi.Quality.low_320k.value
Expand Down
5 changes: 2 additions & 3 deletions tests/test_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ def test_explore(session):
assert explore


@pytest.mark.xfail
def test_get_explore_items(session):
explore = session.explore()
iterator = iter(explore)
Expand Down Expand Up @@ -79,7 +78,7 @@ def test_page_iterator(session):

def test_get_video_items(session):
videos = session.videos()
mix = videos.categories[1].items[0].get()
mix = videos.categories[1].items[0]
for item in mix.items():
assert isinstance(item, tidalapi.Video)

Expand Down Expand Up @@ -110,7 +109,7 @@ def test_genres(session):
def test_moods(session):
moods = session.moods()
first = next(iter(moods))
assert first.title == "Collabs"
assert first.title == "For DJs"
assert isinstance(next(iter(first.get())), tidalapi.Playlist)


Expand Down
4 changes: 2 additions & 2 deletions tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ def test_search(session):
assert "Walker" in top_hit.name


@pytest.mark.xfail(reason="Tidal now returns a video.")
def test_type_search(session):
search = session.search("Hello", [Playlist, Video])
assert isinstance(search["top_hit"], Playlist)
# Top hit may be either a Playlist or Video
assert isinstance(search["top_hit"], (Playlist, Video))

assert len(search["artists"]) == 0
assert len(search["albums"]) == 0
Expand Down
2 changes: 1 addition & 1 deletion tidalapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
User,
)

__version__ = "0.7.3"
__version__ = "0.7.4"
16 changes: 10 additions & 6 deletions tidalapi/album.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,19 @@ def page(self) -> "Page":
return self.session.page.get("pages/album", params={"albumId": self.id})

def similar(self) -> List["Album"]:
"""Retrieve albums similar to the current one.
"""Retrieve albums similar to the current one. AttributeError is raised, when no
similar albums exists.
:return: A :any:`list` of similar albums
"""
albums = self.requests.map_request(
"albums/%s/similar" % self.id, parse=self.session.parse_album
)
assert isinstance(albums, list)
return cast(List["Album"], albums)
json_obj = self.requests.map_request("albums/%s/similar" % self.id)
if json_obj.get("status"):
assert json_obj.get("status") == 404
raise AttributeError("No similar albums exist for this album")
else:
albums = self.requests.map_json(json_obj, parse=self.session.parse_album)
assert isinstance(albums, list)
return cast(List["Album"], albums)

def review(self) -> str:
"""Retrieve the album review.
Expand Down
17 changes: 11 additions & 6 deletions tidalapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def _get(self, media_id: str) -> "Track":
return cast("Track", track)

def get_url(self) -> str:
assert not self.session.is_pkce
params = {
"urlusagemode": "STREAM",
"audioquality": self.session.config.quality,
Expand All @@ -212,11 +213,15 @@ def lyrics(self) -> "Lyrics":
:return: A :class:`Lyrics` object containing the lyrics
:raises: A :class:`requests.HTTPError` if there aren't any lyrics
"""
lyrics = self.requests.map_request(
"tracks/%s/lyrics" % self.id, parse=Lyrics().parse
)
assert not isinstance(lyrics, list)
return cast("Lyrics", lyrics)

json_obj = self.requests.map_request("tracks/%s/lyrics" % self.id)
if json_obj.get("status"):
assert json_obj.get("status") == 404
raise AttributeError("No lyrics exists for this track")
else:
lyrics = self.requests.map_json(json_obj, parse=Lyrics().parse)
assert not isinstance(lyrics, list)
return cast("Lyrics", lyrics)

def get_track_radio(self, limit: int = 100) -> List["Track"]:
"""Queries TIDAL for the track radio, which is a mix of tracks that are similar
Expand All @@ -231,7 +236,7 @@ def get_track_radio(self, limit: int = 100) -> List["Track"]:
assert isinstance(tracks, list)
return cast(List["Track"], tracks)

def stream(self) -> "Stream":
def get_stream(self) -> "Stream":
"""Retrieves the track streaming object, allowing for audio transmission.
:return: A :class:`Stream` object which holds audio file properties and
Expand Down
9 changes: 6 additions & 3 deletions tidalapi/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,16 @@ def map_request(
:param url: TIDAL api endpoint that contains the data
:param params: TIDAL parameters to use when getting the data
:param parse: The method used to parse the data at the url
:param parse: (Optional) The method used to parse the data at the url. If not
set, jsonObj will be returned
: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()

return self.map_json(json_obj, parse=parse)
if parse:
return self.map_json(json_obj, parse=parse)
else:
return json_obj

@classmethod
def map_json(
Expand Down
Loading

0 comments on commit 831d050

Please sign in to comment.