diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 0000000..6fba074 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,10 @@ +# This configuration file was automatically generated by Gitpod. +# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) +# and commit this file to your remote git repository to share the goodness with others. + +# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart + +tasks: + - init: pip install . + + diff --git a/build/lib/pomice/__init__.py b/build/lib/pomice/__init__.py new file mode 100644 index 0000000..d0fd69c --- /dev/null +++ b/build/lib/pomice/__init__.py @@ -0,0 +1,34 @@ +""" +Pomice +~~~~~~ +The modern Lavalink wrapper designed for discord.py. + +:copyright: 2023, cloudwithax +:license: GPL-3.0 +""" +import discord + +if not discord.version_info.major >= 2: + class DiscordPyOutdated(Exception): + pass + + raise DiscordPyOutdated( + "You must have discord.py (v2.0 or greater) to use this library. " + "Uninstall your current version and install discord.py 2.0 " + "using 'pip install discord.py'" + ) + +__version__ = "2.1.1" +__title__ = "pomice" +__author__ = "cloudwithax" + +from .enums import * +from .events import * +from .exceptions import * +from .filters import * +from .objects import * +from .queue import * +from .player import * +from .pool import * +from .routeplanner import * + diff --git a/build/lib/pomice/applemusic/__init__.py b/build/lib/pomice/applemusic/__init__.py new file mode 100644 index 0000000..1c9005c --- /dev/null +++ b/build/lib/pomice/applemusic/__init__.py @@ -0,0 +1,5 @@ +"""Apple Music module for Pomice, made possible by cloudwithax 2023""" + +from .exceptions import * +from .objects import * +from .client import Client \ No newline at end of file diff --git a/build/lib/pomice/applemusic/client.py b/build/lib/pomice/applemusic/client.py new file mode 100644 index 0000000..e0b00b3 --- /dev/null +++ b/build/lib/pomice/applemusic/client.py @@ -0,0 +1,124 @@ +import re +import aiohttp +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[a-zA-Z]{2})/(?Palbum|playlist|song|artist)/(?P.+)/(?P[^?]+)") +AM_SINGLE_IN_ALBUM_REGEX = re.compile(r"https?://music.apple.com/(?P[a-zA-Z]{2})/(?Palbum|playlist|song|artist)/(?P.+)/(?P.+)(\?i=)(?P.+)") +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. + This will do all the heavy lifting of getting tracks from Apple Music + and translating it to a valid Lavalink track. No client auth is required here. + """ + + def __init__(self) -> None: + self.token: str = None + self.expiry: datetime = None + self.session: aiohttp.ClientSession = aiohttp.ClientSession() + self.headers = None + + + async def request_token(self): + 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 + self.headers = { + '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 or datetime.utcnow() > self.expiry: + await self.request_token() + + result = AM_URL_REGEX.match(query) + + country = result.group("country") + type = result.group("type") + id = result.group("id") + + if type == "album" and (sia_result := AM_SINGLE_IN_ALBUM_REGEX.match(query)): + # 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) + + + async with self.session.get(request_url, headers=self.headers) as resp: + if resp.status != 200: + raise AppleMusicRequestException( + f"Error while fetching results: {resp.status} {resp.reason}" + ) + data: dict = await resp.json(loads=json.loads) + + data = data["data"][0] + + + if type == "song": + return Song(data) + + elif type == "album": + return Album(data) + + elif type == "artist": + 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: + + track_data: dict = data["relationships"]["tracks"] + + tracks = [Song(track) for track in track_data.get("data")] + + if not len(tracks): + raise AppleMusicRequestException("This playlist is empty and therefore cannot be queued.") + + if track_data.get("next"): + next_page_url = AM_BASE_URL + track_data.get("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.get("next") + else: + next_page_url = None + + + + return Playlist(data, tracks) \ No newline at end of file diff --git a/build/lib/pomice/applemusic/exceptions.py b/build/lib/pomice/applemusic/exceptions.py new file mode 100644 index 0000000..f9c1f7d --- /dev/null +++ b/build/lib/pomice/applemusic/exceptions.py @@ -0,0 +1,8 @@ +class AppleMusicRequestException(Exception): + """An error occurred when making a request to the Apple Music API""" + pass + + +class InvalidAppleMusicURL(Exception): + """An invalid Apple Music URL was passed""" + pass diff --git a/build/lib/pomice/applemusic/objects.py b/build/lib/pomice/applemusic/objects.py new file mode 100644 index 0000000..c7cd7c1 --- /dev/null +++ b/build/lib/pomice/applemusic/objects.py @@ -0,0 +1,87 @@ +"""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.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"" + ) + + +class Playlist: + """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 ( + f"" + ) + + +class Album: + """The base class for an Apple Music album""" + def __init__(self, data: dict) -> None: + 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 ( + f"" + ) + + + +class Artist: + """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 ( + f"" + ) \ No newline at end of file diff --git a/build/lib/pomice/enums.py b/build/lib/pomice/enums.py new file mode 100644 index 0000000..54217b3 --- /dev/null +++ b/build/lib/pomice/enums.py @@ -0,0 +1,260 @@ +import re + +from enum import Enum + + +class SearchType(Enum): + """ + The enum for the different search types for Pomice. + This feature is exclusively for the Spotify search feature of Pomice. + If you are not using this feature, this class is not necessary. + + SearchType.ytsearch searches using regular Youtube, + which is best for all scenarios. + + SearchType.ytmsearch searches using YouTube Music, + which is best for getting audio-only results. + + SearchType.scsearch searches using SoundCloud, + which is an alternative to YouTube or YouTube Music. + """ + ytsearch = "ytsearch" + ytmsearch = "ytmsearch" + scsearch = "scsearch" + + def __str__(self) -> str: + return self.value + + +class TrackType(Enum): + """ + The enum for the different track types for Pomice. + + TrackType.YOUTUBE defines that the track is from YouTube + + TrackType.SOUNDCLOUD defines that the track is from SoundCloud. + + TrackType.SPOTIFY defines that the track is from Spotify + + TrackType.APPLE_MUSIC defines that the track is from Apple Music. + + TrackType.HTTP defines that the track is from an HTTP source. + """ + + # We don't have to define anything special for these, since these just serve as flags + YOUTUBE = "youtube_track" + SOUNDCLOUD = "soundcloud_track" + SPOTIFY = "spotify_track" + APPLE_MUSIC = "apple_music_track" + HTTP = "http_source" + + def __str__(self) -> str: + return self.value + +class PlaylistType(Enum): + """ + The enum for the different playlist types for Pomice. + + PlaylistType.YOUTUBE defines that the playlist is from YouTube + + PlaylistType.SOUNDCLOUD defines that the playlist is from SoundCloud. + + PlaylistType.SPOTIFY defines that the playlist is from Spotify + + PlaylistType.APPLE_MUSIC defines that the playlist is from Apple Music. + """ + + # We don't have to define anything special for these, since these just serve as flags + YOUTUBE = "youtube_playlist" + SOUNDCLOUD = "soundcloud_playlist" + SPOTIFY = "spotify_playlist" + APPLE_MUSIC = "apple_music_list" + + def __str__(self) -> str: + return self.value + + + +class NodeAlgorithm(Enum): + """ + The enum for the different node algorithms in Pomice. + + The enums in this class are to only differentiate different + methods, since the actual method is handled in the + get_best_node() method. + + NodeAlgorithm.by_ping returns a node based on it's latency, + preferring a node with the lowest response time + + + NodeAlgorithm.by_players return a nodes based on how many players it has. + This algorithm prefers nodes with the least amount of players. + """ + + # We don't have to define anything special for these, since these just serve as flags + by_ping = "BY_PING" + by_players = "BY_PLAYERS" + + def __str__(self) -> str: + return self.value + +class LoopMode(Enum): + """ + The enum for the different loop modes. + This feature is exclusively for the queue utility of pomice. + If you are not using this feature, this class is not necessary. + + LoopMode.TRACK sets the queue loop to the current track. + + LoopMode.QUEUE sets the queue loop to the whole queue. + + """ + # We don't have to define anything special for these, since these just serve as flags + TRACK = "track" + QUEUE = "queue" + + + def __str__(self) -> str: + return self.value + +class PlatformRecommendation(Enum): + + """ + The enum for choosing what platform you want for recommendations. + This feature is exclusively for the recommendations function. + If you are not using this feature, this class is not necessary. + + PlatformRecommendation.SPOTIFY sets the recommendations to come from Spotify + + PlatformRecommendation.YOUTUBE sets the recommendations to come from YouTube + + """ + + # We don't have to define anything special for these, since these just serve as flags + SPOTIFY = "spotify" + YOUTUBE = "youtube" + + + def __str__(self) -> str: + return self.value + + +class RouteStrategy(Enum): + """ + The enum for specifying the route planner strategy for Lavalink. + This feature is exclusively for the RoutePlanner class. + If you are not using this feature, this class is not necessary. + + RouteStrategy.ROTATE_ON_BAN specifies that the node is rotating IPs + whenever they get banned by Youtube. + + RouteStrategy.LOAD_BALANCE specifies that the node is selecting + random IPs to balance out requests between them. + + RouteStrategy.NANO_SWITCH specifies that the node is switching + between IPs every CPU clock cycle. + + RouteStrategy.ROTATING_NANO_SWITCH specifies that the node is switching + between IPs every CPU clock cycle and is rotating between IP blocks on + ban. + + """ + + ROTATE_ON_BAN = "RotatingIpRoutePlanner" + LOAD_BALANCE = "BalancingIpRoutePlanner" + NANO_SWITCH = "NanoIpRoutePlanner" + ROTATING_NANO_SWITCH = "RotatingNanoIpRoutePlanner" + + +class RouteIPType(Enum): + """ + The enum for specifying the route planner IP block type for Lavalink. + This feature is exclusively for the RoutePlanner class. + If you are not using this feature, this class is not necessary. + + RouteIPType.IPV4 specifies that the IP block type is IPV4 + + RouteIPType.IPV6 specifies that the IP block type is IPV6 + """ + + IPV4 = "Inet4Address" + IPV6 = "Inet6Address" + + +class URLRegex(): + """ + The enums for all the URL Regexes in use by Pomice. + + URLRegex.SPOTIFY_URL returns the Spotify URL Regex. + + URLRegex.DISCORD_MP3_URL returns the Discord MP3 URL Regex. + + URLRegex.YOUTUBE_URL returns the Youtube URL Regex. + + URLRegex.YOUTUBE_PLAYLIST returns the Youtube Playlist Regex. + + URLRegex.YOUTUBE_TIMESTAMP returns the Youtube Timestamp Regex. + + URLRegex.AM_URL returns the Apple Music URL Regex. + + URLRegex.SOUNDCLOUD_URL returns the SoundCloud URL Regex. + + URLRegex.BASE_URL returns the standard URL Regex. + + """ + SPOTIFY_URL = re.compile( + r"https?://open.spotify.com/(?Palbum|playlist|track|artist)/(?P[a-zA-Z0-9]+)" + ) + + DISCORD_MP3_URL = re.compile( + r"https?://cdn.discordapp.com/attachments/(?P[0-9]+)/" + r"(?P[0-9]+)/(?P[a-zA-Z0-9_.]+)+" + ) + + YOUTUBE_URL = re.compile( + r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))" + r"(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$" + ) + + YOUTUBE_PLAYLIST_URL = re.compile( + r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))/playlist\?list=.*" + ) + + YOUTUBE_VID_IN_PLAYLIST = re.compile( + r"(?P