Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added exporting playlists to Apple functionality #2968

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 62 additions & 15 deletions frontend/js/src/playlists/components/PlaylistMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable react/jsx-no-comment-textnodes */
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faSpotify } from "@fortawesome/free-brands-svg-icons";
import { faSpotify, faItunesNote } from "@fortawesome/free-brands-svg-icons";
import {
faCopy,
faFileExport,
Expand Down Expand Up @@ -36,7 +36,9 @@ function PlaylistMenu({
onPlaylistCopied,
disallowEmptyPlaylistExport,
}: PlaylistMenuProps) {
const { APIService, currentUser, spotifyAuth } = useContext(GlobalAppContext);
const { APIService, currentUser, spotifyAuth, appleAuth } = useContext(
GlobalAppContext
);
const { auth_token } = currentUser;
const playlistID = getPlaylistId(playlist);
const playlistTitle = playlist.title;
Expand Down Expand Up @@ -159,6 +161,39 @@ function PlaylistMenu({
{ toastId: "export-playlist" }
);
};
const exportToAppleMusic = async () => {
if (!auth_token) {
alertMustBeLoggedIn();
return;
}
let result;
if (playlistID) {
result = await APIService.exportPlaylistToAppleMusic(
auth_token,
playlistID
);
} else {
result = await APIService.exportJSPFPlaylistToAppleMusic(
auth_token,
playlist
);
}
const { external_url } = result;
toast.success(
<ToastMsg
title="Playlist exported to Apple Music"
message={
<>
Successfully exported playlist:{" "}
<a href={external_url} target="_blank" rel="noopener noreferrer">
{playlistTitle}
</a>
</>
}
/>,
{ toastId: "export-playlist" }
);
};
const handlePlaylistExport = async (handler: () => Promise<void>) => {
if (!playlist || (disallowEmptyPlaylistExport && !playlist.track.length)) {
toast.warn(
Expand All @@ -179,6 +214,7 @@ function PlaylistMenu({
const showSpotifyExportButton = spotifyAuth?.permission?.includes(
"playlist-modify-public"
);
const showAppleMusicExportButton = spotifyAuth;
rimma-kubanova marked this conversation as resolved.
Show resolved Hide resolved
return (
<ul
className="dropdown-menu dropdown-menu-right"
Expand Down Expand Up @@ -233,20 +269,31 @@ function PlaylistMenu({
</li>
</>
)}
<li role="separator" className="divider" />
{showSpotifyExportButton && (
<>
<li role="separator" className="divider" />
<li>
<a
id="exportPlaylistToSpotify"
role="button"
href="#"
onClick={() => handlePlaylistExport(exportToSpotify)}
>
<FontAwesomeIcon icon={faSpotify as IconProp} /> Export to Spotify
</a>
</li>
</>
<li>
<a
id="exportPlaylistToSpotify"
role="button"
href="#"
onClick={() => handlePlaylistExport(exportToSpotify)}
>
<FontAwesomeIcon icon={faSpotify as IconProp} /> Export to Spotify
</a>
</li>
)}
{showAppleMusicExportButton && (
<li>
<a
id="exportPlaylistToAppleMusic"
role="button"
href="#"
onClick={() => handlePlaylistExport(exportToAppleMusic)}
>
<FontAwesomeIcon icon={faItunesNote as IconProp} /> Export to Apple
Music
</a>
</li>
)}
<li role="separator" className="divider" />
<li>
Expand Down
33 changes: 33 additions & 0 deletions frontend/js/src/utils/APIService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,39 @@ export default class APIService {
return response.json();
};

exportPlaylistToAppleMusic = async (
userToken: string,
playlist_mbid: string
): Promise<any> => {
const url = `${this.APIBaseURI}/playlist/${playlist_mbid}/export/apple_music`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Token ${userToken}`,
"Content-Type": "application/json;charset=UTF-8",
},
});
await this.checkStatus(response);
return response.json();
};

exportJSPFPlaylistToAppleMusic = async (
userToken: string,
playlist: JSPFPlaylist
): Promise<any> => {
const url = `${this.APIBaseURI}/playlist/export-jspf/apple_music`;
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Token ${userToken}`,
"Content-Type": "application/json;charset=UTF-8",
},
body: JSON.stringify(playlist),
});
await this.checkStatus(response);
return response.json();
};

exportJSPFPlaylistToSpotify = async (
userToken: string,
playlist: JSPFPlaylist
Expand Down
20 changes: 20 additions & 0 deletions listenbrainz/troi/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,23 @@ def export_to_spotify(lb_token, spotify_token, is_public, playlist_mbid=None, js
playlist = patch.generate_playlist()
metadata = playlist.playlists[0].additional_metadata
return metadata["external_urls"]["spotify"]


def export_to_apple_music(lb_token, apple_music_token, music_user_token, is_public, playlist_mbid=None, jspf=None):
args = {
"mbid": playlist_mbid,
"jspf": jspf,
"read_only_token": lb_token,
"apple_music": {
"developer_token": apple_music_token,
"music_user_token": music_user_token,
"is_public": is_public
},
"upload": True,
"echo": False,
"min_recordings": 1
}
patch = TransferPlaylistPatch(args)
playlist = patch.generate_playlist()
metadata = playlist.playlists[0].additional_metadata
return metadata["external_urls"]["apple_music"]
41 changes: 30 additions & 11 deletions listenbrainz/webserver/views/playlist_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import listenbrainz.db.user as db_user
from listenbrainz.domain.spotify import SpotifyService, SPOTIFY_PLAYLIST_PERMISSIONS
from listenbrainz.domain.apple import AppleService
from listenbrainz.troi.export import export_to_spotify
from listenbrainz.troi.export import export_to_spotify, export_to_apple_music
from listenbrainz.troi.import_ms import import_from_spotify, import_from_apple_music
from listenbrainz.webserver import db_conn, ts_conn
from listenbrainz.metadata_cache.apple.client import Apple
Expand Down Expand Up @@ -860,22 +860,33 @@ def export_playlist(playlist_mbid, service):
if not is_valid_uuid(playlist_mbid):
log_raise_400("Provided playlist ID is invalid.")

if service != "spotify":
if service != "spotify" and service != "soundcloud" and service != "apple_music":
rimma-kubanova marked this conversation as resolved.
Show resolved Hide resolved
raise APIBadRequest(f"Service {service} is not supported. We currently only support 'spotify'.")
rimma-kubanova marked this conversation as resolved.
Show resolved Hide resolved

spotify_service = SpotifyService()
token = spotify_service.get_user(user["id"], refresh=True)
if service == "spotify":
spotify_service = SpotifyService()
token = spotify_service.get_user(user["id"], refresh=True)
elif service == "soundcloud":
soundcloud_service = SoundCloudService()
token = soundcloud_service.get_user(user["id"])
elif service == "apple_music":
apple_service = AppleService()
token = apple_service.get_user(user["id"])

if not token:
raise APIBadRequest(f"Service {service} is not linked. Please link your {service} account first.")

if not SPOTIFY_PLAYLIST_PERMISSIONS.issubset(set(token["scopes"])):
if service == 'spotify' and not SPOTIFY_PLAYLIST_PERMISSIONS.issubset(set(token["scopes"])):
raise APIBadRequest(f"Missing scopes playlist-modify-public and playlist-modify-private to export playlists."
f" Please relink your {service} account from ListenBrainz settings with appropriate scopes"
f" to use this feature.")

is_public = parse_boolean_arg("is_public", True)
try:
url = export_to_spotify(user["auth_token"], token["access_token"], is_public, playlist_mbid=playlist_mbid)
if service == "spotify":
url = export_to_spotify(user["auth_token"], token["access_token"], is_public, playlist_mbid=playlist_mbid)
elif service == "apple_music":
url = export_to_apple_music(user["auth_token"], token["access_token"], token["refresh_token"], is_public, playlist_mbid=playlist_mbid)
return jsonify({"external_url": url})
except requests.exceptions.HTTPError as exc:
error = exc.response.json()
Expand Down Expand Up @@ -1023,23 +1034,31 @@ def export_playlist_jspf(service):
"""
user = validate_auth_header()

if service != "spotify":
if service != "spotify" and service != "apple_music":
raise APIBadRequest(f"Service {service} is not supported. We currently only support 'spotify'.")
rimma-kubanova marked this conversation as resolved.
Show resolved Hide resolved

spotify_service = SpotifyService()
token = spotify_service.get_user(user["id"], refresh=True)
if service == "spotify":
spotify_service = SpotifyService()
token = spotify_service.get_user(user["id"], refresh=True)
elif service == "soundcloud":
apple_service = AppleService()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look right. Should it be this?

Suggested change
elif service == "soundcloud":
apple_service = AppleService()
elif service == "apple_music":
apple_service = AppleService()

token = apple_service.get_user(user["id"])

if not token:
raise APIBadRequest(f"Service {service} is not linked. Please link your {service} account first.")

if not SPOTIFY_PLAYLIST_PERMISSIONS.issubset(set(token["scopes"])):
if service=='spotify' and not SPOTIFY_PLAYLIST_PERMISSIONS.issubset(set(token["scopes"])):
raise APIBadRequest(f"Missing scopes playlist-modify-public and playlist-modify-private to export playlists."
f" Please relink your {service} account from ListenBrainz settings with appropriate scopes"
f" to use this feature.")

is_public = parse_boolean_arg("is_public", True)
jspf = request.json
try:
url = export_to_spotify(user["auth_token"], token["access_token"], is_public, jspf=jspf)
if service == "spotify":
url = export_to_spotify(user["auth_token"], token["access_token"], is_public, jspf=jspf)
elif service == "apple_music":
url = export_to_apple_music(user["auth_token"], token["access_token"], token["refresh_token"], is_public, jspf=jspf)
return jsonify({"external_url": url})
except requests.exceptions.HTTPError as exc:
error = exc.response.json()
Expand Down
Loading