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 ctx.send(f'Set the volume to **{vol}**%', delete_after=7)
async def setup(bot: commands.Bot):
await bot.add_cog(Music(bot))

View File

@ -1,12 +1,16 @@
import re
import aiohttp
import json
import orjson as json
import base64
from datetime import datetime
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_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_BASE_URL = "https://api.music.apple.com"
class Client:
"""The base Apple Music client for Pomice.
@ -16,14 +20,17 @@ class Client:
def __init__(self) -> None:
self.token: str = None
self.origin: str = None
self.session: aiohttp.ClientSession = None
self.expiry: datetime = None
self.session: aiohttp.ClientSession = aiohttp.ClientSession()
self.headers = None
async def request_token(self):
self.session = aiohttp.ClientSession()
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()
result = re.search("\"(eyJ.+?)\"", text).group(1)
self.token = result
@ -31,10 +38,14 @@ class Client:
'Authorization': f"Bearer {result}",
'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):
if not self.token:
if not self.token or datetime.utcnow() > self.expiry:
await self.request_token()
result = AM_URL_REGEX.match(query)
@ -47,39 +58,66 @@ class Client:
# apple music likes to generate links for singles off an album
# by adding a param at the end of the url
# so we're gonna scan for that and correct it
id = sia_result.group("id2")
type = "song"
request_url = AM_REQ_URL.format(country=country, type=type, id=id)
else:
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:
print(resp.status)
data = await resp.json()
if resp.status != 200:
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:
file.write(json.dumps(data))
data = data["data"][0]
if type == "playlist":
return Playlist(data)
if type == "song":
return Song(data)
elif type == "album":
return Album(data)
elif type == "song":
return Song(data)
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:
"""The base class for an Apple Music song"""
def __init__(self, data: dict) -> None:
self.track_data = ["data"][0]
self.name = self.track_data["attributes"]["name"]
self.url = self.track_data["atrributes"]["url"]
self.isrc = self.track_data["atrributes"]["isrc"]
self.length = self.track_data["atrributes"]["durationInMillis"]
self.id = self.track_data["id"]
self.artists = self.track_data["atrributes"]["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.name: str = data["attributes"]["name"]
self.url: str = data["attributes"]["url"]
self.isrc: str = data["attributes"]["isrc"]
self.length: float = data["attributes"]["durationInMillis"]
self.id: str = data["id"]
self.artists: str = data["attributes"]["artistName"]
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:
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}>"
)
class Playlist:
def __init__(self, data: dict) -> None:
pass
"""The base class for an Apple Music playlist"""
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:
return (
@ -28,8 +45,18 @@ class Playlist:
class Album:
"""The base class for an Apple Music album"""
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:
return (
@ -40,8 +67,17 @@ class Album:
class Artist:
def __init__(self, data: dict) -> None:
pass
"""The base class for an Apple Music artist"""
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:
return (

View File

@ -7,6 +7,11 @@ from discord.ext import commands
from .enums import SearchType
from .filters import Filter
from . import (
spotify,
applemusic
)
SOUNDCLOUD_URL_REGEX = re.compile(
r"^(https?:\/\/)?(www.)?(m\.)?soundcloud\.com\/[\w\-\.]+(\/)+[\w\-\.]+/?$"
)
@ -24,8 +29,10 @@ class Track:
info: dict,
ctx: Optional[commands.Context] = None,
spotify: bool = False,
apple_music: bool = False,
am_track: applemusic.Song = None,
search_type: SearchType = SearchType.ytsearch,
spotify_track = None,
spotify_track: spotify.Track = None,
filters: Optional[List[Filter]] = None,
timestamp: Optional[float] = None,
requester: Optional[Union[Member, User]] = None
@ -33,12 +40,17 @@ class Track:
self.track_id = track_id
self.info = info
self.spotify = spotify
self.apple_music = apple_music
self.filters: List[Filter] = filters
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.spotify_track = spotify_track
self.am_track = am_track
self.title = info.get("title")
self.author = info.get("author")
@ -95,13 +107,17 @@ class Playlist:
tracks: list,
ctx: Optional[commands.Context] = None,
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.tracks_raw = tracks
self.spotify = spotify
self.name = playlist_info.get("name")
self.spotify_playlist = spotify_playlist
self.apple_music = apple_music
self.am_playlist = am_playlist
self._thumbnail = None
self._uri = None
@ -110,6 +126,12 @@ class Playlist:
self.tracks = tracks
self._thumbnail = self.spotify_playlist.image
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:
self.tracks = [
Track(track_id=track["track"], info=track["info"], ctx=ctx)
@ -133,10 +155,10 @@ class Playlist:
@property
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
@property
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

View File

@ -33,6 +33,11 @@ class Filters:
"""Property which checks if any applied filters were preloaded"""
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
def empty(self):

View File

@ -380,7 +380,61 @@ class Node:
"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):

View File

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