Skip to content

Commit 8b3fba3

Browse files
authored
Merge pull request #223 from tamland/138-add-oauth-login-from-file-helper-functions
feature/138 add oauth login from file helper functions
2 parents ea5b1e6 + cb82f5f commit 8b3fba3

File tree

5 files changed

+251
-26
lines changed

5 files changed

+251
-26
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,10 @@ prof/
7575
.venv
7676

7777
# MacOS
78-
.DS_Store
78+
.DS_Store
79+
80+
# OAuth json session files
81+
*tidal-oauth*
82+
83+
# Misc. csv. files that might be generated when executing examples
84+
*.csv

README.rst

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ Unofficial Python API for TIDAL music streaming service.
1111

1212
Requires Python 3.9 or higher.
1313

14-
0.7.x Migration guide
15-
---------------------
16-
The 0.7.x rewrite is now complete, see the `migration guide <https://tidalapi.netlify.app/migration.html#migrating-from-0-6-x-0-7-x>`_ for dealing with it
17-
1814
Installation
1915
------------
2016

@@ -29,27 +25,7 @@ Install from `PyPI <https://pypi.python.org/pypi/tidalapi/>`_ using ``pip``:
2925
Example usage
3026
-------------
3127

32-
.. code-block:: python
33-
34-
import tidalapi
35-
36-
session = tidalapi.Session()
37-
# Will run until you visit the printed url and link your account
38-
session.login_oauth_simple()
39-
# Override the required playback quality, if necessary
40-
# Note: Set the quality according to your subscription.
41-
# Normal: Quality.low_320k
42-
# HiFi: Quality.high_lossless
43-
# HiFi+ Quality.hi_res_lossless
44-
session.audio_quality = Quality.low_320k
45-
46-
album = session.album(66236918)
47-
tracks = album.tracks()
48-
for track in tracks:
49-
print(track.name)
50-
for artist in track.artists:
51-
print(' by: ', artist.name)
52-
28+
For examples on how to use the api, see the `examples` directory.
5329

5430
Documentation
5531
-------------

examples/simple.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright (C) 2023- The Tidalapi Developers
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU Lesser General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
"""simple.py: A simple example script that describes how to get started using tidalapi"""
19+
20+
import tidalapi
21+
from tidalapi import Quality
22+
from pathlib import Path
23+
24+
oauth_file1 = Path("tidal-oauth-user.json")
25+
26+
session = tidalapi.Session()
27+
# Will run until you visit the printed url and link your account
28+
session.login_oauth_file(oauth_file1)
29+
# Override the required playback quality, if necessary
30+
# Note: Set the quality according to your subscription.
31+
# Normal: Quality.low_320k
32+
# HiFi: Quality.high_lossless
33+
# HiFi+ Quality.hi_res_lossless
34+
session.audio_quality = Quality.low_320k
35+
36+
album = session.album(66236918) # Electric For Life Episode 099
37+
tracks = album.tracks()
38+
print(album.name)
39+
# list album tracks
40+
for track in tracks:
41+
print(track.name)
42+
for artist in track.artists:
43+
print(' by: ', artist.name)

examples/transfer_favorites.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright (C) 2023- The Tidalapi Developers
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU Lesser General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
"""transfer_favorites.py: Use this script to transfer your Tidal favourites from Tidal user A to Tidal user B"""
19+
import logging
20+
from pathlib import Path
21+
import csv
22+
import time
23+
import sys
24+
25+
import tidalapi
26+
27+
logger = logging.getLogger(__name__)
28+
logger.setLevel(logging.INFO)
29+
logger.addHandler(logging.StreamHandler(sys.stdout))
30+
31+
oauth_file1 = Path("tidal-oauth-user.json")
32+
oauth_file2 = Path("tidal-oauth-userB.json")
33+
34+
35+
class TidalSession:
36+
def __init__(self):
37+
self._active_session = tidalapi.Session()
38+
39+
def get_uid(self):
40+
return self._active_session.user.id
41+
42+
def get_session(self):
43+
return self._active_session
44+
45+
46+
class TidalTransfer:
47+
def __init__(self):
48+
self.session_src = TidalSession()
49+
self.session_dst = TidalSession()
50+
51+
def export_csv(self, my_tracks, my_albums, my_artists, my_playlists):
52+
logger.info("Exporting user A favorites to csv...")
53+
# save to csv file
54+
with open("fav_tracks.csv", "w") as file:
55+
wr = csv.writer(file, quoting=csv.QUOTE_ALL)
56+
for track in my_tracks:
57+
wr.writerow(
58+
[
59+
track.id,
60+
track.user_date_added,
61+
track.artist.name,
62+
track.album.name,
63+
]
64+
)
65+
with open("fav_albums.csv", "w") as file:
66+
wr = csv.writer(file, quoting=csv.QUOTE_ALL)
67+
for album in my_albums:
68+
wr.writerow(
69+
[album.id, album.user_date_added, album.artist.name, album.name]
70+
)
71+
with open("fav_artists.csv", "w") as file:
72+
wr = csv.writer(file, quoting=csv.QUOTE_ALL)
73+
for artist in my_artists:
74+
wr.writerow([artist.id, artist.user_date_added, artist.name])
75+
with open("fav_playlists.csv", "w") as file:
76+
wr = csv.writer(file, quoting=csv.QUOTE_ALL)
77+
for playlist in my_playlists:
78+
wr.writerow(
79+
[playlist.id, playlist.created, playlist.type, playlist.name]
80+
)
81+
82+
def do_transfer(self):
83+
# do login for src and dst Tidal account
84+
session_src = self.session_src.get_session()
85+
session_dst = self.session_dst.get_session()
86+
logger.info("Login to user A (source)...")
87+
if not session_src.login_oauth_file(oauth_file1):
88+
logger.error("Login to Tidal user...FAILED!")
89+
exit(1)
90+
logger.info("Login to user B (destination)...")
91+
if not session_dst.login_oauth_file(oauth_file2):
92+
logger.error("Login to Tidal user...FAILED!")
93+
exit(1)
94+
95+
# get current user favourites (source)
96+
my_tracks = session_src.user.favorites.tracks()
97+
my_albums = session_src.user.favorites.albums()
98+
my_artists = session_src.user.favorites.artists()
99+
my_playlists = session_src.user.playlist_and_favorite_playlists()
100+
# my_mixes = self._active_session.user.mixes()
101+
102+
# export to csv
103+
self.export_csv(my_tracks, my_albums, my_artists, my_playlists)
104+
105+
# add favourites to new user
106+
logger.info("Adding favourites to Tidal user B...")
107+
for idx, track in enumerate(my_tracks):
108+
logger.info("Adding track {}/{}".format(idx, len(my_tracks)))
109+
try:
110+
session_dst.user.favorites.add_track(track.id)
111+
time.sleep(0.1)
112+
except:
113+
logger.error("error while adding track {} {}".format(track.id, track.name))
114+
115+
for idx, album in enumerate(my_albums):
116+
logger.info("Adding album {}/{}".format(idx, len(my_albums)))
117+
try:
118+
session_dst.user.favorites.add_album(album.id)
119+
time.sleep(0.1)
120+
except:
121+
logger.error("error while adding album {} {}".format(album.id, album.name))
122+
123+
for idx, artist in enumerate(my_artists):
124+
logger.info("Adding artist {}/{}".format(idx, len(my_artists)))
125+
try:
126+
session_dst.user.favorites.add_artist(artist.id)
127+
time.sleep(0.1)
128+
except:
129+
logger.error("error while adding artist {} {}".format(artist.id, artist.name))
130+
131+
for idx, playlist in enumerate(my_playlists):
132+
logger.info("Adding playlist {}/{}".format(idx, len(my_playlists)))
133+
try:
134+
session_dst.user.favorites.add_playlist(playlist.id)
135+
time.sleep(0.1)
136+
except:
137+
logger.error(
138+
"error while adding playlist {} {}".format(
139+
playlist.id, playlist.name
140+
)
141+
)
142+
143+
144+
if __name__ == "__main__":
145+
TidalTransfer().do_transfer()

tidalapi/session.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import base64
2222
import concurrent.futures
2323
import datetime
24+
import json
2425
import logging
2526
import random
2627
import time
2728
import uuid
2829
from dataclasses import dataclass
2930
from enum import Enum
31+
from pathlib import Path
3032
from typing import (
3133
TYPE_CHECKING,
3234
Any,
@@ -399,13 +401,42 @@ def login(self, username: str, password: str) -> bool:
399401
self.user = user.User(self, user_id=body["userId"]).factory()
400402
return True
401403

404+
def login_oauth_file(self, oauth_file: Path) -> bool:
405+
"""Logs in to the TIDAL api using an existing OAuth session file. If no OAuth
406+
session json file exists, a new one will be created after successful login.
407+
408+
:param oauth_file: The OAuth session json file
409+
:return: Returns true if we think the login was successful.
410+
"""
411+
try:
412+
# attempt to reload existing session from file
413+
with open(oauth_file) as f:
414+
log.info("Loading OAuth session from %s...", oauth_file)
415+
data = json.load(f)
416+
self._load_oauth_session_from_file(**data)
417+
except Exception as e:
418+
log.info("Could not load OAuth session from %s: %s", oauth_file, e)
419+
420+
if not self.check_login():
421+
log.info("Creating new OAuth session...")
422+
self.login_oauth_simple()
423+
424+
if self.check_login():
425+
log.info("TIDAL Login OK")
426+
self._save_oauth_session_to_file(oauth_file)
427+
return True
428+
else:
429+
log.info("TIDAL Login KO")
430+
return False
431+
402432
def login_oauth_simple(self, function: Callable[[str], None] = print) -> None:
403433
"""Login to TIDAL using a remote link. You can select what function you want to
404434
use to display the link.
405435
406436
:param function: The function you want to display the link with
407437
:raises: TimeoutError: If the login takes too long
408438
"""
439+
409440
login, future = self.login_oauth()
410441
text = "Visit https://{0} to log in, the code will expire in {1} seconds"
411442
function(text.format(login.verification_uri_complete, login.expires_in))
@@ -423,6 +454,30 @@ def login_oauth(self) -> Tuple[LinkLogin, concurrent.futures.Future[Any]]:
423454
login, future = self._login_with_link()
424455
return login, future
425456

457+
def _save_oauth_session_to_file(self, oauth_file: Path):
458+
# create a new session
459+
if self.check_login():
460+
# store current OAuth session
461+
data = {
462+
"token_type": {"data": self.token_type},
463+
"session_id": {"data": self.session_id},
464+
"access_token": {"data": self.access_token},
465+
"refresh_token": {"data": self.refresh_token},
466+
}
467+
with oauth_file.open("w") as outfile:
468+
json.dump(data, outfile)
469+
self._oauth_saved = True
470+
471+
def _load_oauth_session_from_file(self, **data):
472+
assert self, "No session loaded"
473+
args = {
474+
"token_type": data.get("token_type", {}).get("data"),
475+
"access_token": data.get("access_token", {}).get("data"),
476+
"refresh_token": data.get("refresh_token", {}).get("data"),
477+
}
478+
479+
self.load_oauth_session(**args)
480+
426481
def _login_with_link(self) -> Tuple[LinkLogin, concurrent.futures.Future[Any]]:
427482
url = "https://auth.tidal.com/v1/oauth2/device_authorization"
428483
params = {"client_id": self.config.client_id, "scope": "r_usr w_usr w_sub"}

0 commit comments

Comments
 (0)