Skip to content

Commit 8da768d

Browse files
rimma-kubanovaRimma KubanovaamCap1712
authored
Import Playlists from SoundCloud (#143)
* added soundcloud import * added soundcloud import * added soundcloud import * added title spliter * added title spliter * removed logs and cleaned the code * added rate limit to apple/soundcloud endpoints * defined rate limit for better clarification * added http session with retry && refactored API calls * minor cleanup fix authorization header, formatting fixes, and remove unused dependency --------- Co-authored-by: Rimma Kubanova <[email protected]> Co-authored-by: Kartik Ohri <[email protected]>
1 parent eb2182e commit 8da768d

File tree

6 files changed

+86
-33
lines changed

6 files changed

+86
-33
lines changed

troi/patches/playlist_from_ms.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from troi.musicbrainz.recording_lookup import RecordingLookupElement
77
from troi.tools.apple_lookup import get_tracks_from_apple_playlist
88
from troi.tools.spotify_lookup import get_tracks_from_spotify_playlist
9+
from troi.tools.soundcloud_lookup import get_tracks_from_soundcloud_playlist
910

1011

1112
class ImportPlaylistPatch(Patch):
@@ -46,18 +47,20 @@ def create(self, inputs):
4647
playlist_id = inputs["playlist_id"]
4748
music_service = inputs["music_service"]
4849
apple_user_token = inputs["apple_user_token"]
49-
50+
5051
if apple_user_token == "":
5152
apple_user_token = None
52-
53+
5354
if music_service == "apple_music" and apple_user_token is None:
5455
raise RuntimeError("Authentication is required")
55-
56+
5657
# this one only used to get track name and desc
5758
if music_service == "spotify":
5859
tracks, name, desc = get_tracks_from_spotify_playlist(ms_token, playlist_id)
5960
elif music_service == "apple_music":
6061
tracks, name, desc = get_tracks_from_apple_playlist(ms_token, apple_user_token, playlist_id)
62+
elif music_service == "soundcloud":
63+
tracks, name, desc = get_tracks_from_soundcloud_playlist(ms_token, playlist_id)
6164

6265
source = RecordingsFromMusicServiceElement(token=ms_token, playlist_id=playlist_id, music_service=music_service, apple_user_token=apple_user_token)
6366

troi/tools/apple_lookup.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,30 @@
1-
import requests
2-
3-
from troi.tools.spotify_lookup import APPLE_MUSIC_URL
4-
5-
6-
def convert_apple_tracks_to_json(apple_tracks):
7-
tracks= []
8-
for track in apple_tracks:
9-
tracks.append({
10-
"recording_name": track['attributes']['name'],
11-
"artist_name": track['attributes']['artistName'],
12-
})
13-
return tracks
1+
from .utils import create_http_session
142

3+
APPLE_MUSIC_URL = f"https://api.music.apple.com/"
154

165
def get_tracks_from_apple_playlist(developer_token, user_token, playlist_id):
176
""" Get tracks from the Apple Music playlist.
187
"""
8+
http = create_http_session()
9+
1910
headers = {
2011
"Authorization": f"Bearer {developer_token}",
2112
"Music-User-Token": user_token
2213
}
23-
response = requests.get(APPLE_MUSIC_URL+f"v1/me/library/playlists/{playlist_id}?include=tracks", headers=headers)
24-
if response.status_code == 200:
25-
response = response.json()
26-
tracks = response["data"][0]["relationships"]["tracks"]["data"]
27-
name = response["data"][0]["attributes"]["name"]
28-
description = response["data"][0]["attributes"].get("description", {}).get("standard", "")
29-
else:
30-
response.raise_for_status()
31-
return tracks, name, description
14+
response = http.get(APPLE_MUSIC_URL+f"v1/me/library/playlists/{playlist_id}?include=tracks", headers=headers)
15+
response.raise_for_status()
16+
17+
response = response.json()
18+
tracks = response["data"][0]["relationships"]["tracks"]["data"]
19+
name = response["data"][0]["attributes"]["name"]
20+
description = response["data"][0]["attributes"].get("description", {}).get("standard", "")
21+
22+
mapped_tracks = [
23+
{
24+
"recording_name": track['attributes']['name'],
25+
"artist_name": track['attributes']['artistName']
26+
}
27+
for track in tracks
28+
]
29+
30+
return mapped_tracks, name, description

troi/tools/common_lookup.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
import requests
44
from more_itertools import chunked
55

6-
from troi.tools.apple_lookup import get_tracks_from_apple_playlist, convert_apple_tracks_to_json
7-
from troi.tools.spotify_lookup import get_tracks_from_spotify_playlist, convert_spotify_tracks_to_json
6+
from troi.tools.apple_lookup import get_tracks_from_apple_playlist
7+
from troi.tools.spotify_lookup import get_tracks_from_spotify_playlist
8+
from troi.tools.soundcloud_lookup import get_tracks_from_soundcloud_playlist
89

910
MAX_LOOKUPS_PER_POST = 50
1011
MBID_LOOKUP_URL = "https://api.listenbrainz.org/1/metadata/lookup/"
@@ -16,19 +17,19 @@ def music_service_tracks_to_mbid(token, playlist_id, music_service, apple_user_t
1617
""" Convert Spotify playlist tracks to a list of MBID tracks.
1718
"""
1819
if music_service == "spotify":
19-
tracks_from_playlist, name, desc = get_tracks_from_spotify_playlist(token, playlist_id)
20-
tracks = convert_spotify_tracks_to_json(tracks_from_playlist)
20+
tracks, name, desc = get_tracks_from_spotify_playlist(token, playlist_id)
2121
elif music_service == "apple_music":
22-
tracks_from_playlist, name, desc = get_tracks_from_apple_playlist(token, apple_user_token, playlist_id)
23-
tracks = convert_apple_tracks_to_json(tracks_from_playlist)
22+
tracks, name, desc = get_tracks_from_apple_playlist(token, apple_user_token, playlist_id)
23+
elif music_service == "soundcloud":
24+
tracks, name, desc = get_tracks_from_soundcloud_playlist(token, playlist_id)
2425
else:
2526
raise ValueError("Unknown music service")
2627

2728
track_lists = list(chunked(tracks, MAX_LOOKUPS_PER_POST))
28-
return mbid_mapping_spotify(track_lists)
29+
return mbid_mapping_tracks(track_lists)
2930

3031

31-
def mbid_mapping_spotify(track_lists):
32+
def mbid_mapping_tracks(track_lists):
3233
""" Given a track_name and artist_name, try to find MBID for these tracks from mbid lookup.
3334
"""
3435
track_mbids = []

troi/tools/soundcloud_lookup.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from .utils import create_http_session
2+
3+
SOUNDCLOUD_URL = f"https://api.soundcloud.com/"
4+
5+
def get_tracks_from_soundcloud_playlist(developer_token, playlist_id):
6+
""" Get tracks from the Soundcloud playlist.
7+
"""
8+
http = create_http_session()
9+
10+
headers = {
11+
"Authorization": f"OAuth {developer_token}",
12+
}
13+
response = http.get(f"{SOUNDCLOUD_URL}/playlists/{playlist_id}", headers=headers)
14+
response.raise_for_status()
15+
16+
response = response.json()
17+
tracks = response["tracks"]
18+
name = response["title"]
19+
description = response["description"]
20+
21+
mapped_tracks = [
22+
{
23+
"recording_name": track['title'].split(" - ")[1] if " - " in track['title'] else track['title'],
24+
"artist_name": track['title'].split(" - ")[0] if " - " in track['title'] else track['user']['username']
25+
}
26+
for track in tracks
27+
]
28+
29+
return mapped_tracks, name, description

troi/tools/spotify_lookup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ def get_tracks_from_spotify_playlist(spotify_token, playlist_id):
182182
name = playlist_info["name"]
183183
description = playlist_info["description"]
184184

185+
tracks = convert_spotify_tracks_to_json(tracks)
185186
return tracks, name, description
186187

187188

troi/tools/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import requests
2+
from requests.adapters import HTTPAdapter
3+
from urllib3.util.retry import Retry
4+
5+
6+
def create_http_session():
7+
""" Create an HTTP session with retry strategy for handling rate limits and server errors.
8+
"""
9+
retry_strategy = Retry(
10+
total=3,
11+
status_forcelist=[429, 500, 502, 503, 504],
12+
allowed_methods=["HEAD", "GET", "OPTIONS"],
13+
backoff_factor=1
14+
)
15+
16+
adapter = HTTPAdapter(max_retries=retry_strategy)
17+
http = requests.Session()
18+
http.mount("https://", adapter)
19+
http.mount("http://", adapter)
20+
return http

0 commit comments

Comments
 (0)