-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A new Patch for importing playlists from Spotify (#141)
* added new patch for import * fixed token * fixed token * fixed token * fixed token * fixed track fetching * added comments * TEST: added chunking * fixed chunking * integrated apple music * fixed typo * fixed missing parameter * Refactor lookup functions --------- Co-authored-by: Rimma Kubanova <[email protected]> Co-authored-by: Kartik Ohri <[email protected]>
- Loading branch information
1 parent
2b29c99
commit e5503c2
Showing
5 changed files
with
212 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import json | ||
|
||
from troi import Playlist | ||
from troi.patch import Patch | ||
from troi.playlist import RecordingsFromMusicServiceElement, PlaylistMakerElement | ||
from troi.musicbrainz.recording_lookup import RecordingLookupElement | ||
from troi.tools.apple_lookup import get_tracks_from_apple_playlist | ||
from troi.tools.spotify_lookup import get_tracks_from_spotify_playlist | ||
|
||
|
||
class ImportPlaylistPatch(Patch): | ||
|
||
@staticmethod | ||
def inputs(): | ||
""" | ||
A patch that retrieves an existing playlist from Spotify for use in Troi. | ||
\b | ||
MS_TOKEN is the music service token from which the playlist is retrieved. For now, only Spotify tokens are accepted. | ||
PLAYLIST_ID is the playlist id to retrieve the tracks from it. | ||
MUSIC_SERVICE is the music service from which the playlist is retrieved | ||
APPLE_USER_TOKEN is the apple user token. Optional, if music services is not Apple Music | ||
""" | ||
return [ | ||
{"type": "argument", "args": ["ms_token"], "kwargs": {"required": False}}, | ||
{"type": "argument", "args": ["playlist_id"], "kwargs": {"required": False}}, | ||
{"type": "argument", "args": ["music_service"], "kwargs": {"required": False}}, | ||
{"type": "argument", "args": ["apple_user_token"], "kwargs": {"required": False}}, | ||
] | ||
|
||
@staticmethod | ||
def outputs(): | ||
return [Playlist] | ||
|
||
@staticmethod | ||
def slug(): | ||
return "import-playlist" | ||
|
||
@staticmethod | ||
def description(): | ||
return "Retrieve a playlist from the Music Services (Spotify/Apple Music/SoundCloud)" | ||
|
||
def create(self, inputs): | ||
|
||
ms_token = inputs["ms_token"] | ||
playlist_id = inputs["playlist_id"] | ||
music_service = inputs["music_service"] | ||
apple_user_token = inputs["apple_user_token"] | ||
|
||
if apple_user_token == "": | ||
apple_user_token = None | ||
|
||
if music_service == "apple_music" and apple_user_token is None: | ||
raise RuntimeError("Authentication is required") | ||
|
||
# this one only used to get track name and desc | ||
if music_service == "spotify": | ||
tracks, name, desc = get_tracks_from_spotify_playlist(ms_token, playlist_id) | ||
elif music_service == "apple_music": | ||
tracks, name, desc = get_tracks_from_apple_playlist(ms_token, apple_user_token, playlist_id) | ||
|
||
source = RecordingsFromMusicServiceElement(token=ms_token, playlist_id=playlist_id, music_service=music_service, apple_user_token=apple_user_token) | ||
|
||
rec_lookup = RecordingLookupElement() | ||
rec_lookup.set_sources(source) | ||
|
||
pl_maker = PlaylistMakerElement(name, desc, patch_slug=self.slug()) | ||
pl_maker.set_sources(rec_lookup) | ||
|
||
return pl_maker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import requests | ||
|
||
from troi.tools.spotify_lookup import APPLE_MUSIC_URL | ||
|
||
|
||
def convert_apple_tracks_to_json(apple_tracks): | ||
tracks= [] | ||
for track in apple_tracks: | ||
tracks.append({ | ||
"recording_name": track['attributes']['name'], | ||
"artist_name": track['attributes']['artistName'], | ||
}) | ||
return tracks | ||
|
||
|
||
def get_tracks_from_apple_playlist(developer_token, user_token, playlist_id): | ||
""" Get tracks from the Apple Music playlist. | ||
""" | ||
headers = { | ||
"Authorization": f"Bearer {developer_token}", | ||
"Music-User-Token": user_token | ||
} | ||
response = requests.get(APPLE_MUSIC_URL+f"v1/me/library/playlists/{playlist_id}?include=tracks", headers=headers) | ||
if response.status_code == 200: | ||
response = response.json() | ||
tracks = response["data"][0]["relationships"]["tracks"]["data"] | ||
name = response["data"][0]["attributes"]["name"] | ||
description = response["data"][0]["attributes"]["description"]["standard"] | ||
else: | ||
response.raise_for_status() | ||
return tracks, name, description |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import logging | ||
|
||
import requests | ||
from more_itertools import chunked | ||
|
||
from troi.tools.apple_lookup import get_tracks_from_apple_playlist, convert_apple_tracks_to_json | ||
from troi.tools.spotify_lookup import get_tracks_from_spotify_playlist, convert_spotify_tracks_to_json | ||
|
||
MAX_LOOKUPS_PER_POST = 50 | ||
MBID_LOOKUP_URL = "https://api.listenbrainz.org/1/metadata/lookup/" | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def music_service_tracks_to_mbid(token, playlist_id, music_service, apple_user_token=None): | ||
""" Convert Spotify playlist tracks to a list of MBID tracks. | ||
""" | ||
if music_service == "spotify": | ||
tracks_from_playlist, name, desc = get_tracks_from_spotify_playlist(token, playlist_id) | ||
tracks = convert_spotify_tracks_to_json(tracks_from_playlist) | ||
elif music_service == "apple_music": | ||
tracks_from_playlist, name, desc = get_tracks_from_apple_playlist(token, apple_user_token, playlist_id) | ||
tracks = convert_apple_tracks_to_json(tracks_from_playlist) | ||
else: | ||
raise ValueError("Unknown music service") | ||
|
||
track_lists = list(chunked(tracks, MAX_LOOKUPS_PER_POST)) | ||
return mbid_mapping_spotify(track_lists) | ||
|
||
|
||
def mbid_mapping_spotify(track_lists): | ||
""" Given a track_name and artist_name, try to find MBID for these tracks from mbid lookup. | ||
""" | ||
track_mbids = [] | ||
for tracks in track_lists: | ||
params = { | ||
"recordings": tracks | ||
} | ||
response = requests.post(MBID_LOOKUP_URL, json=params) | ||
if response.status_code == 200: | ||
data = response.json() | ||
for d in data: | ||
if d is not None and "recording_mbid" in d: | ||
track_mbids.append(d["recording_mbid"]) | ||
else: | ||
logger.error("Error occurred: %s", response.text) | ||
return track_mbids |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters