wrap up apple music, v2 is done

This commit is contained in:
cloudwithax 2023-02-05 22:51:56 -05:00
parent c1a9d7603f
commit 14c82c1b56
7 changed files with 224 additions and 72 deletions

View File

@ -345,9 +345,6 @@ class Music(commands.Cog):
await player.set_volume(vol) await player.set_volume(vol)
await ctx.send(f'Set the volume to **{vol}**%', delete_after=7) await ctx.send(f'Set the volume to **{vol}**%', delete_after=7)
async def setup(bot: commands.Bot): async def setup(bot: commands.Bot):
await bot.add_cog(Music(bot)) await bot.add_cog(Music(bot))

View File

@ -1,12 +1,16 @@
import re import re
import aiohttp import aiohttp
import json import orjson as json
import base64
from datetime import datetime
from .objects import * from .objects import *
from .exceptions import *
AM_URL_REGEX = re.compile(r"https?://music.apple.com/(?P<country>[a-zA-Z]{2})/(?P<type>album|playlist|song|artist)/(?P<name>.+)/(?P<id>[^?]+)") AM_URL_REGEX = re.compile(r"https?://music.apple.com/(?P<country>[a-zA-Z]{2})/(?P<type>album|playlist|song|artist)/(?P<name>.+)/(?P<id>[^?]+)")
AM_SINGLE_IN_ALBUM_REGEX = re.compile(r"https?://music.apple.com/(?P<country>[a-zA-Z]{2})/(?P<type>album|playlist|song|artist)/(?P<name>.+)/(?P<id>.+)(\?i=)(?P<id2>.+)") AM_SINGLE_IN_ALBUM_REGEX = re.compile(r"https?://music.apple.com/(?P<country>[a-zA-Z]{2})/(?P<type>album|playlist|song|artist)/(?P<name>.+)/(?P<id>.+)(\?i=)(?P<id2>.+)")
AM_REQ_URL = "https://api.music.apple.com/v1/catalog/{country}/{type}s/{id}" AM_REQ_URL = "https://api.music.apple.com/v1/catalog/{country}/{type}s/{id}"
AM_BASE_URL = "https://api.music.apple.com"
class Client: class Client:
"""The base Apple Music client for Pomice. """The base Apple Music client for Pomice.
@ -16,14 +20,17 @@ class Client:
def __init__(self) -> None: def __init__(self) -> None:
self.token: str = None self.token: str = None
self.origin: str = None self.expiry: datetime = None
self.session: aiohttp.ClientSession = None self.session: aiohttp.ClientSession = aiohttp.ClientSession()
self.headers = None self.headers = None
async def request_token(self): async def request_token(self):
self.session = aiohttp.ClientSession()
async with self.session.get("https://music.apple.com/assets/index.919fe17f.js") as resp: async with self.session.get("https://music.apple.com/assets/index.919fe17f.js") as resp:
if resp.status != 200:
raise AppleMusicRequestException(
f"Error while fetching results: {resp.status} {resp.reason}"
)
text = await resp.text() text = await resp.text()
result = re.search("\"(eyJ.+?)\"", text).group(1) result = re.search("\"(eyJ.+?)\"", text).group(1)
self.token = result self.token = result
@ -31,10 +38,14 @@ class Client:
'Authorization': f"Bearer {result}", 'Authorization': f"Bearer {result}",
'Origin': 'https://apple.com', 'Origin': 'https://apple.com',
} }
token_split = self.token.split(".")[1]
token_json = base64.b64decode(token_split + '=' * (-len(token_split) % 4)).decode()
token_data = json.loads(token_json)
self.expiry = datetime.fromtimestamp(token_data["exp"])
async def search(self, query: str): async def search(self, query: str):
if not self.token: if not self.token or datetime.utcnow() > self.expiry:
await self.request_token() await self.request_token()
result = AM_URL_REGEX.match(query) result = AM_URL_REGEX.match(query)
@ -47,39 +58,66 @@ class Client:
# apple music likes to generate links for singles off an album # apple music likes to generate links for singles off an album
# by adding a param at the end of the url # by adding a param at the end of the url
# so we're gonna scan for that and correct it # so we're gonna scan for that and correct it
id = sia_result.group("id2") id = sia_result.group("id2")
type = "song" type = "song"
request_url = AM_REQ_URL.format(country=country, type=type, id=id) request_url = AM_REQ_URL.format(country=country, type=type, id=id)
else: else:
request_url = AM_REQ_URL.format(country=country, type=type, id=id) request_url = AM_REQ_URL.format(country=country, type=type, id=id)
print(request_url)
print(self.token)
async with self.session.get(request_url, headers=self.headers) as resp: async with self.session.get(request_url, headers=self.headers) as resp:
print(resp.status) if resp.status != 200:
data = await resp.json() raise AppleMusicRequestException(
f"Error while fetching results: {resp.status} {resp.reason}"
)
data: dict = await resp.json(loads=json.loads)
with open('yes.txt', 'w') as file: data = data["data"][0]
file.write(json.dumps(data))
if type == "playlist":
return Playlist(data) if type == "song":
return Song(data)
elif type == "album": elif type == "album":
return Album(data) return Album(data)
elif type == "song":
return Song(data)
elif type == "artist": elif type == "artist":
return Artist(data) async with self.session.get(f"{request_url}/view/top-songs", headers=self.headers) as resp:
if resp.status != 200:
raise AppleMusicRequestException(
f"Error while fetching results: {resp.status} {resp.reason}"
)
top_tracks: dict = await resp.json(loads=json.loads)
tracks: dict = top_tracks["data"]
return Artist(data, tracks=tracks)
else:
tracks = [Song(track) for track in data["relationships"]["tracks"]["data"]]
if not len(tracks):
raise AppleMusicRequestException("This playlist is empty and therefore cannot be queued.")
if data["relationships"]["tracks"]["next"]:
next_page_url = AM_BASE_URL + data["relationships"]["tracks"]["next"]
while next_page_url is not None:
async with self.session.get(next_page_url, headers=self.headers) as resp:
if resp.status != 200:
raise AppleMusicRequestException(
f"Error while fetching results: {resp.status} {resp.reason}"
)
next_data: dict = await resp.json(loads=json.loads)
tracks += [Song(track) for track in next_data["data"]]
if next_data.get("next"):
next_page_url = AM_BASE_URL + next_data["next"]
else:
next_page_url = None
return Playlist(data, tracks)
await self.session.close()

View File

@ -1,24 +1,41 @@
"""Module for managing Apple Music objects"""
from typing import List
class Song: class Song:
def __init__(self, data: dict) -> None: """The base class for an Apple Music song"""
self.track_data = ["data"][0] def __init__(self, data: dict) -> None:
self.name = self.track_data["attributes"]["name"] self.name: str = data["attributes"]["name"]
self.url = self.track_data["atrributes"]["url"] self.url: str = data["attributes"]["url"]
self.isrc = self.track_data["atrributes"]["isrc"] self.isrc: str = data["attributes"]["isrc"]
self.length = self.track_data["atrributes"]["durationInMillis"] self.length: float = data["attributes"]["durationInMillis"]
self.id = self.track_data["id"] self.id: str = data["id"]
self.artists = self.track_data["atrributes"]["artistName"] self.artists: str = data["attributes"]["artistName"]
self.image = self.track_data["atrributes"]["artwork"]["url"].replace("{w}x{h}", f'{self.track_data["atrributes"]["artwork"]["width"]}x{self.track_data["atrributes"]["artwork"]["height"]}') self.image: str = data["attributes"]["artwork"]["url"].replace(
"{w}x{h}",
f'{data["attributes"]["artwork"]["width"]}x{data["attributes"]["artwork"]["height"]}'
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
f"<Pomice.applemusic.Track name={self.name} artists={self.artists} " f"<Pomice.applemusic.Song name={self.name} artists={self.artists} "
f"length={self.length} id={self.id} isrc={self.isrc}>" f"length={self.length} id={self.id} isrc={self.isrc}>"
) )
class Playlist: class Playlist:
def __init__(self, data: dict) -> None: """The base class for an Apple Music playlist"""
pass def __init__(self, data: dict, tracks: List[Song]) -> None:
self.name: str = data["attributes"]["name"]
self.owner: str = data["attributes"]["curatorName"]
self.id: str = data["id"]
self.tracks: List[Song] = tracks
self.total_tracks: int = len(tracks)
self.url: str = data["attributes"]["url"]
# we'll use the first song's image as the image for the playlist
# because apple dynamically generates playlist covers client-side
self.image = self.tracks[0].image
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -28,8 +45,18 @@ class Playlist:
class Album: class Album:
"""The base class for an Apple Music album"""
def __init__(self, data: dict) -> None: def __init__(self, data: dict) -> None:
pass self.name: str = data["attributes"]["name"]
self.url: str = data["attributes"]["url"]
self.id: str = data["id"]
self.artists: str = data["attributes"]["artistName"]
self.total_tracks: int = data["attributes"]["trackCount"]
self.tracks: List[Song] = [Song(track) for track in data["relationships"]["tracks"]["data"]]
self.image: str = data["attributes"]["artwork"]["url"].replace(
"{w}x{h}",
f'{data["attributes"]["artwork"]["width"]}x{data["attributes"]["artwork"]["height"]}'
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -40,8 +67,17 @@ class Album:
class Artist: class Artist:
def __init__(self, data: dict) -> None: """The base class for an Apple Music artist"""
pass def __init__(self, data: dict, tracks: dict) -> None:
self.name: str = f'Top tracks for {data["attributes"]["name"]}'
self.url: str = data["attributes"]["url"]
self.id: str = data["id"]
self.genres: str = ", ".join(genre for genre in data["attributes"]["genreNames"])
self.tracks: List[Song] = [Song(track) for track in tracks]
self.image: str = data["attributes"]["artwork"]["url"].replace(
"{w}x{h}",
f'{data["attributes"]["artwork"]["width"]}x{data["attributes"]["artwork"]["height"]}'
)
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (

View File

@ -7,6 +7,11 @@ from discord.ext import commands
from .enums import SearchType from .enums import SearchType
from .filters import Filter from .filters import Filter
from . import (
spotify,
applemusic
)
SOUNDCLOUD_URL_REGEX = re.compile( SOUNDCLOUD_URL_REGEX = re.compile(
r"^(https?:\/\/)?(www.)?(m\.)?soundcloud\.com\/[\w\-\.]+(\/)+[\w\-\.]+/?$" r"^(https?:\/\/)?(www.)?(m\.)?soundcloud\.com\/[\w\-\.]+(\/)+[\w\-\.]+/?$"
) )
@ -24,8 +29,10 @@ class Track:
info: dict, info: dict,
ctx: Optional[commands.Context] = None, ctx: Optional[commands.Context] = None,
spotify: bool = False, spotify: bool = False,
apple_music: bool = False,
am_track: applemusic.Song = None,
search_type: SearchType = SearchType.ytsearch, search_type: SearchType = SearchType.ytsearch,
spotify_track = None, spotify_track: spotify.Track = None,
filters: Optional[List[Filter]] = None, filters: Optional[List[Filter]] = None,
timestamp: Optional[float] = None, timestamp: Optional[float] = None,
requester: Optional[Union[Member, User]] = None requester: Optional[Union[Member, User]] = None
@ -33,12 +40,17 @@ class Track:
self.track_id = track_id self.track_id = track_id
self.info = info self.info = info
self.spotify = spotify self.spotify = spotify
self.apple_music = apple_music
self.filters: List[Filter] = filters self.filters: List[Filter] = filters
self.timestamp: Optional[float] = timestamp self.timestamp: Optional[float] = timestamp
self.original: Optional[Track] = None if spotify else self if spotify or apple_music:
self.original: Optional[Track] = None
else:
self.original = self
self._search_type = search_type self._search_type = search_type
self.spotify_track = spotify_track self.spotify_track = spotify_track
self.am_track = am_track
self.title = info.get("title") self.title = info.get("title")
self.author = info.get("author") self.author = info.get("author")
@ -95,13 +107,17 @@ class Playlist:
tracks: list, tracks: list,
ctx: Optional[commands.Context] = None, ctx: Optional[commands.Context] = None,
spotify: bool = False, spotify: bool = False,
spotify_playlist = None spotify_playlist: spotify.Playlist = None,
apple_music: bool = False,
am_playlist: applemusic.Playlist = None
): ):
self.playlist_info = playlist_info self.playlist_info = playlist_info
self.tracks_raw = tracks self.tracks_raw = tracks
self.spotify = spotify self.spotify = spotify
self.name = playlist_info.get("name") self.name = playlist_info.get("name")
self.spotify_playlist = spotify_playlist self.spotify_playlist = spotify_playlist
self.apple_music = apple_music
self.am_playlist = am_playlist
self._thumbnail = None self._thumbnail = None
self._uri = None self._uri = None
@ -110,6 +126,12 @@ class Playlist:
self.tracks = tracks self.tracks = tracks
self._thumbnail = self.spotify_playlist.image self._thumbnail = self.spotify_playlist.image
self._uri = self.spotify_playlist.uri self._uri = self.spotify_playlist.uri
elif self.apple_music:
self.tracks = tracks
self._thumbnail = self.am_playlist.image
self._uri = self.am_playlist.url
else: else:
self.tracks = [ self.tracks = [
Track(track_id=track["track"], info=track["info"], ctx=ctx) Track(track_id=track["track"], info=track["info"], ctx=ctx)
@ -133,10 +155,10 @@ class Playlist:
@property @property
def uri(self) -> Optional[str]: def uri(self) -> Optional[str]:
"""Spotify album/playlist URI, or None if not a Spotify object.""" """Returns either an Apple Music/Spotify URL/URI, or None if its neither of those."""
return self._uri return self._uri
@property @property
def thumbnail(self) -> Optional[str]: def thumbnail(self) -> Optional[str]:
"""Spotify album/playlist thumbnail, or None if not a Spotify object.""" """Returns either an Apple Music/Spotify album/playlist thumbnail, or None if its neither of those."""
return self._thumbnail return self._thumbnail

View File

@ -33,6 +33,11 @@ class Filters:
"""Property which checks if any applied filters were preloaded""" """Property which checks if any applied filters were preloaded"""
return any(f for f in self._filters if f.preload == True) return any(f for f in self._filters if f.preload == True)
@property
def has_global(self):
"""Property which checks if any applied filters are global"""
return any(f for f in self._filters if f.preload == False)
@property @property
def empty(self): def empty(self):

View File

@ -380,7 +380,61 @@ class Node:
"Please set apple_music to True in your Node class." "Please set apple_music to True in your Node class."
) )
await self._apple_music_client.search(query=query) apple_music_results = await self._apple_music_client.search(query=query)
if isinstance(apple_music_results, applemusic.Song):
return [
Track(
track_id=apple_music_results.id,
ctx=ctx,
search_type=search_type,
apple_music=True,
am_track=apple_music_results,
filters=filters,
info={
"title": apple_music_results.name,
"author": apple_music_results.artists,
"length": apple_music_results.length,
"identifier": apple_music_results.id,
"uri": apple_music_results.url,
"isStream": False,
"isSeekable": True,
"position": 0,
"thumbnail": apple_music_results.image,
"isrc": apple_music_results.isrc
}
)
]
tracks = [
Track(
track_id=track.id,
ctx=ctx,
search_type=search_type,
apple_music=True,
am_track=track,
filters=filters,
info={
"title": track.name,
"author": track.artists,
"length": track.length,
"identifier": track.id,
"uri": track.url,
"isStream": False,
"isSeekable": True,
"position": 0,
"thumbnail": track.image,
"isrc": track.isrc
}
) for track in apple_music_results.tracks
]
return Playlist(
playlist_info={"name": apple_music_results.name, "selectedTrack": 0},
tracks=tracks,
ctx=ctx,
apple_music=True,
am_playlist=apple_music_results
)
elif SPOTIFY_URL_REGEX.match(query): elif SPOTIFY_URL_REGEX.match(query):

View File

@ -6,24 +6,24 @@ class Track:
def __init__(self, data: dict, image = None) -> None: def __init__(self, data: dict, image = None) -> None:
self.name: str = data["name"] self.name: str = data["name"]
self.artists = ", ".join(artist["name"] for artist in data["artists"]) self.artists: str = ", ".join(artist["name"] for artist in data["artists"])
self.length: float = data["duration_ms"] self.length: float = data["duration_ms"]
self.id: str = data["id"] self.id: str = data["id"]
if data.get("external_ids"): if data.get("external_ids"):
self.isrc = data["external_ids"]["isrc"] self.isrc: str = data["external_ids"]["isrc"]
else: else:
self.isrc = None self.isrc = None
if data.get("album") and data["album"].get("images"): if data.get("album") and data["album"].get("images"):
self.image = data["album"]["images"][0]["url"] self.image: str = data["album"]["images"][0]["url"]
else: else:
self.image = image self.image: str = image
if data["is_local"]: if data["is_local"]:
self.uri = None self.uri = None
else: else:
self.uri = data["external_urls"]["spotify"] self.uri: str = data["external_urls"]["spotify"]
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -35,13 +35,13 @@ class Playlist:
"""The base class for a Spotify playlist""" """The base class for a Spotify playlist"""
def __init__(self, data: dict, tracks: List[Track]) -> None: def __init__(self, data: dict, tracks: List[Track]) -> None:
self.name = data["name"] self.name: str = data["name"]
self.tracks = tracks self.tracks = tracks
self.owner = data["owner"]["display_name"] self.owner: str = data["owner"]["display_name"]
self.total_tracks = data["tracks"]["total"] self.total_tracks: int = data["tracks"]["total"]
self.id = data["id"] self.id: str = data["id"]
if data.get("images") and len(data["images"]): if data.get("images") and len(data["images"]):
self.image = data["images"][0]["url"] self.image: str = data["images"][0]["url"]
else: else:
self.image = None self.image = None
self.uri = data["external_urls"]["spotify"] self.uri = data["external_urls"]["spotify"]
@ -56,13 +56,13 @@ class Album:
"""The base class for a Spotify album""" """The base class for a Spotify album"""
def __init__(self, data: dict) -> None: def __init__(self, data: dict) -> None:
self.name = data["name"] self.name: str = data["name"]
self.artists = ", ".join(artist["name"] for artist in data["artists"]) self.artists: str = ", ".join(artist["name"] for artist in data["artists"])
self.image = data["images"][0]["url"] self.image: str = data["images"][0]["url"]
self.tracks = [Track(track, image=self.image) for track in data["tracks"]["items"]] self.tracks = [Track(track, image=self.image) for track in data["tracks"]["items"]]
self.total_tracks = data["total_tracks"] self.total_tracks: int = data["total_tracks"]
self.id = data["id"] self.id: str = data["id"]
self.uri = data["external_urls"]["spotify"] self.uri: str = data["external_urls"]["spotify"]
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (
@ -74,13 +74,13 @@ class Artist:
"""The base class for a Spotify artist""" """The base class for a Spotify artist"""
def __init__(self, data: dict, tracks: dict) -> None: def __init__(self, data: dict, tracks: dict) -> None:
self.name = f"Top tracks for {data['name']}" # Setting that because its only playing top tracks self.name: str = f"Top tracks for {data['name']}" # Setting that because its only playing top tracks
self.genres = ", ".join(genre for genre in data["genres"]) self.genres: str = ", ".join(genre for genre in data["genres"])
self.followers = data["followers"]["total"] self.followers: int = data["followers"]["total"]
self.image = data["images"][0]["url"] self.image: str = data["images"][0]["url"]
self.tracks = [Track(track, image=self.image) for track in tracks] self.tracks = [Track(track, image=self.image) for track in tracks]
self.id = data["id"] self.id: str = data["id"]
self.uri = data["external_urls"]["spotify"] self.uri: str = data["external_urls"]["spotify"]
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return (