From 987de07fc5b5fd0cee7801b49283b87a97cb6ce0 Mon Sep 17 00:00:00 2001 From: NiceAesth Date: Sat, 11 Mar 2023 15:44:46 +0200 Subject: [PATCH 1/4] feat: add typing; add makefile; add pipfile --- .gitignore | 4 + .pre-commit-config.yaml | 36 ++++ .readthedocs.yaml | 4 +- Makefile | 18 ++ Pipfile | 18 ++ README.md | 46 ++--- docs/api/enums.md | 2 +- docs/api/events.md | 2 +- docs/api/exceptions.md | 2 +- docs/api/filters.md | 2 +- docs/api/index.md | 2 +- docs/api/objects.md | 2 +- docs/api/player.md | 2 +- docs/api/pool.md | 2 +- docs/api/queue.md | 2 +- docs/api/utils.md | 2 +- docs/conf.py | 9 +- docs/faq.md | 4 +- docs/hdi/events.md | 5 +- docs/hdi/filters.md | 3 +- docs/hdi/index.md | 1 - docs/hdi/node.md | 6 +- docs/hdi/player.md | 20 +- docs/hdi/pool.md | 18 +- docs/hdi/queue.md | 14 +- docs/index.md | 6 +- docs/installation.md | 10 +- docs/quickstart.md | 42 ++--- docs/requirements_rtd.txt | 6 +- examples/advanced.py | 27 ++- examples/basic.py | 15 +- pomice/__init__.py | 2 +- pomice/applemusic/__init__.py | 3 +- pomice/applemusic/client.py | 114 +++++++----- pomice/applemusic/exceptions.py | 6 + pomice/applemusic/objects.py | 16 +- pomice/enums.py | 42 +++-- pomice/events.py | 79 +++++--- pomice/exceptions.py | 23 +++ pomice/filters.py | 155 ++++++++++------ pomice/objects.py | 151 +++++++-------- pomice/player.py | 230 +++++++++++++---------- pomice/pool.py | 316 +++++++++++++++++++------------- pomice/queue.py | 72 +++++--- pomice/routeplanner.py | 9 +- pomice/spotify/__init__.py | 3 +- pomice/spotify/client.py | 84 +++++---- pomice/spotify/exceptions.py | 6 + pomice/spotify/objects.py | 45 +++-- pomice/utils.py | 121 +++++++----- pyproject.toml | 12 +- setup.py | 11 +- 52 files changed, 1103 insertions(+), 729 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 Makefile create mode 100644 Pipfile diff --git a/.gitignore b/.gitignore index e5713fc..38b963d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,7 @@ docs/_build/ build/ .gitpod.yml .python-verson +Pipfile.lock +.mypy_cache/ +.vscode/ +.venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..81dd067 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-ast + - id: check-builtin-literals + - id: debug-statements + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace +- repo: https://github.com/pre-commit/mirrors-autopep8 + rev: v2.0.2 + hooks: + - id: autopep8 +- repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: [--py37-plus, --keep-runtime-typing] +- repo: https://github.com/asottile/reorder_python_imports + rev: v3.9.0 + hooks: + - id: reorder-python-imports +- repo: https://github.com/asottile/add-trailing-comma + rev: v2.4.0 + hooks: + - id: add-trailing-comma +- repo: https://github.com/hadialqattan/pycln + rev: v2.1.3 + hooks: + - id: pycln + +default_language_version: + python: python3.8 \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5f6bf3f..8f8e770 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,7 +4,7 @@ build: os: ubuntu-22.04 tools: python: "3.8" - + sphinx: configuration: docs/conf.py @@ -12,4 +12,4 @@ sphinx: python: install: - - requirements: docs/requirements_rtd.txt \ No newline at end of file + - requirements: docs/requirements_rtd.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b6ec5ab --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +prepare: + pipenv install --dev + pipenv run pre-commit install + +shell: + pipenv shell + +lint: + pipenv run pre-commit run --all-files + +test: + pipenv run mypy + +serve-docs: + @cd docs;\ + make html;\ + cd build/html;\ + python -m http.server;\ \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..c76e209 --- /dev/null +++ b/Pipfile @@ -0,0 +1,18 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +orjson = "*" +"discord.py" = {extras = ["voice"], version = "*"} + +[dev-packages] +mypy = "*" +pre-commit = "*" +furo = "*" +sphinx = "*" +myst-parser = "*" + +[requires] +python_version = "3.8" diff --git a/README.md b/README.md index 2ef377a..ebb45a0 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ ![](https://raw.githubusercontent.com/cloudwithax/pomice/main/banner.jpg) -[![GPL](https://img.shields.io/badge/license-GPL-2f2f2f)](https://github.com/cloudwithax/pomice/blob/main/LICENSE) ![](https://img.shields.io/badge/python-3.8-2f2f2f) +[![GPL](https://img.shields.io/badge/license-GPL-2f2f2f)](https://github.com/cloudwithax/pomice/blob/main/LICENSE) ![](https://img.shields.io/badge/python-3.8-2f2f2f) [![Discord](https://img.shields.io/discord/899324069235810315)](https://discord.gg/r64qjTSHG8) [![Read the Docs](https://readthedocs.org/projects/pomice/badge/?version=latest)](https://pomice.readthedocs.io/en/latest/) -Pomice is a fully asynchronous Python library designed for communicating with [Lavalink](https://github.com/freyacodes/Lavalink) seamlessly within the [discord.py](https://github.com/Rapptz/discord.py) library. It features 100% API coverage of the entire [Lavalink](https://github.com/freyacodes/Lavalink) spec that can be accessed with easy-to-understand functions. We also include Spotify and Apple Music querying capabilites using built-in custom clients, making it easier to develop your next big music bot. +Pomice is a fully asynchronous Python library designed for communicating with [Lavalink](https://github.com/freyacodes/Lavalink) seamlessly within the [discord.py](https://github.com/Rapptz/discord.py) library. It features 100% API coverage of the entire [Lavalink](https://github.com/freyacodes/Lavalink) spec that can be accessed with easy-to-understand functions. We also include Spotify and Apple Music querying capabilites using built-in custom clients, making it easier to develop your next big music bot. # Install @@ -45,63 +45,63 @@ from discord.ext import commands URL_REG = re.compile(r'https?://(?:www\.)?.+') class MyBot(commands.Bot): - + def __init__(self) -> None: super().__init__(command_prefix='!', activity=discord.Activity(type=discord.ActivityType.listening, name='to music!')) - + self.add_cog(Music(self)) - + async def on_ready(self) -> None: print("I'm online!") await self.cogs["Music"].start_nodes() - - + + class Music(commands.Cog): - + def __init__(self, bot) -> None: self.bot = bot - + self.pomice = pomice.NodePool() - + async def start_nodes(self): - await self.pomice.create_node(bot=self.bot, host='127.0.0.1', port='3030', + await self.pomice.create_node(bot=self.bot, host='127.0.0.1', port='3030', password='youshallnotpass', identifier='MAIN') print(f"Node is ready!") - + @commands.command(name='join', aliases=['connect']) async def join(self, ctx: commands.Context, *, channel: discord.TextChannel = None) -> None: - + if not channel: channel = getattr(ctx.author.voice, 'channel', None) if not channel: raise commands.CheckFailure('You must be in a voice channel to use this command' 'without specifying the channel argument.') - + await ctx.author.voice.channel.connect(cls=pomice.Player) await ctx.send(f'Joined the voice channel `{channel}`') - + @commands.command(name='play') async def play(self, ctx, *, search: str) -> None: - - if not ctx.voice_client: - await ctx.invoke(self.join) - player = ctx.voice_client + if not ctx.voice_client: + await ctx.invoke(self.join) + + player = ctx.voice_client results = await player.get_tracks(query=f'{search}') - + if not results: raise commands.CommandError('No results were found for that search term.') - + if isinstance(results, pomice.Playlist): await player.play(track=results.tracks[0]) else: await player.play(track=results[0]) - + bot = MyBot() bot.run("token here") ``` @@ -117,7 +117,7 @@ What experience do I need? Why is it saying "No module named pomice found"? -- You need to [install](#Install) the package before you can use it +- You need to [install](#Install) the package before you can use it # Contributors diff --git a/docs/api/enums.md b/docs/api/enums.md index abc650d..f210300 100644 --- a/docs/api/enums.md +++ b/docs/api/enums.md @@ -6,4 +6,4 @@ Enums :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/api/events.md b/docs/api/events.md index 838a3b3..57dafd4 100644 --- a/docs/api/events.md +++ b/docs/api/events.md @@ -6,4 +6,4 @@ Events :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md index 319e03b..b565823 100644 --- a/docs/api/exceptions.md +++ b/docs/api/exceptions.md @@ -6,4 +6,4 @@ Exceptions :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/api/filters.md b/docs/api/filters.md index 1af5063..9b8e347 100644 --- a/docs/api/filters.md +++ b/docs/api/filters.md @@ -6,4 +6,4 @@ Filters :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/api/index.md b/docs/api/index.md index 22f74f5..7e48d6d 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -15,4 +15,4 @@ player.md pool.md queue.md utils.md -``` \ No newline at end of file +``` diff --git a/docs/api/objects.md b/docs/api/objects.md index e3ceef0..50e8b88 100644 --- a/docs/api/objects.md +++ b/docs/api/objects.md @@ -6,4 +6,4 @@ Objects :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/api/player.md b/docs/api/player.md index ac7b752..1bc8a07 100644 --- a/docs/api/player.md +++ b/docs/api/player.md @@ -6,4 +6,4 @@ Player :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/api/pool.md b/docs/api/pool.md index 155f42a..81265b1 100644 --- a/docs/api/pool.md +++ b/docs/api/pool.md @@ -6,4 +6,4 @@ Pool :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/api/queue.md b/docs/api/queue.md index 58e7be9..dc7e303 100644 --- a/docs/api/queue.md +++ b/docs/api/queue.md @@ -6,4 +6,4 @@ Queue :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/api/utils.md b/docs/api/utils.md index 9d3e89d..91de8b3 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -6,4 +6,4 @@ Utils :members: :undoc-members: :show-inheritance: -``` \ No newline at end of file +``` diff --git a/docs/conf.py b/docs/conf.py index fb31719..0c649b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,12 +2,12 @@ import importlib import inspect import os import sys -from typing import Any, Dict +from typing import Any +from typing import Dict sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.abspath('..')) - project = 'Pomice' copyright = '2023, cloudwithax' author = 'cloudwithax' @@ -19,7 +19,7 @@ extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.linkcode', - 'myst_parser' + 'myst_parser', ] myst_enable_extensions = [ @@ -84,6 +84,7 @@ html_theme_options: Dict[str, Any] = { # Grab lines from source files and embed into the docs # so theres a point of reference + def linkcode_resolve(domain, info): # i absolutely MUST add this here or else # the docs will not build. fuck sphinx @@ -92,7 +93,6 @@ def linkcode_resolve(domain, info): return None if not info['module']: return None - mod = importlib.import_module(info["module"]) if "." in info["fullname"]: @@ -117,4 +117,3 @@ def linkcode_resolve(domain, info): return f"https://github.com/cloudwithax/pomice/blob/main/{file}#L{start}-L{end}" except: pass - diff --git a/docs/faq.md b/docs/faq.md index b415a54..1f06468 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -8,7 +8,7 @@ Here are some common issues: If you are experiencing the first issue, you can download Lavalink [here.](https://github.com/freyacodes/Lavalink/releases/latest) -As for the other listed issues, either consult the Lavalink docs or go through the proper support channels for your specfic issue at hand. +As for the other listed issues, either consult the Lavalink docs or go through the proper support channels for your specfic issue at hand. For any other issues not listed here, please consult your preferred resource for more information. @@ -24,4 +24,4 @@ Refer to the [Installation](installation.md) section. If you are interested in learning how Pomice works, refer to the [API Reference](api/index.md) section. -If you want a quick example, refer to the [Quickstart](quickstart.md) section. +If you want a quick example, refer to the [Quickstart](quickstart.md) section. diff --git a/docs/hdi/events.md b/docs/hdi/events.md index e8166be..50dfb91 100644 --- a/docs/hdi/events.md +++ b/docs/hdi/events.md @@ -45,8 +45,5 @@ and properties for further evaluation. They also carry a `Track` object so you c `Event.WebsocketClosedEvent()` carries a payload object that contains a `Guild` object, the code number, the reason for disconnect and whether or not it was by the remote, or the node. -`Event.WebsocketOpenEvent()` carries a target, which is usually the node IP, and the SSRC, a 32-bit integer uniquely identifying the source of the RTP packets sent from +`Event.WebsocketOpenEvent()` carries a target, which is usually the node IP, and the SSRC, a 32-bit integer uniquely identifying the source of the RTP packets sent from Lavalink. - - - diff --git a/docs/hdi/filters.md b/docs/hdi/filters.md index 4eca67e..4adff3d 100644 --- a/docs/hdi/filters.md +++ b/docs/hdi/filters.md @@ -23,7 +23,7 @@ Here are the different types and what they do: * - Distortion - `pomice.Distortion()` - Generates a distortion effect on a track. - + * - Equalizer - `pomice.Equalizer()` - Represents a 15 band equalizer. You can adjust the dynamic of the sound using this filter. @@ -184,4 +184,3 @@ After you have initialized your function, you can optionally include the `fast_a await Player.reset_filters(fast_apply=) ``` - diff --git a/docs/hdi/index.md b/docs/hdi/index.md index 9a6d965..8b5385d 100644 --- a/docs/hdi/index.md +++ b/docs/hdi/index.md @@ -14,4 +14,3 @@ filters.md queue.md events.md `` - diff --git a/docs/hdi/node.md b/docs/hdi/node.md index df769bd..0b30cd1 100644 --- a/docs/hdi/node.md +++ b/docs/hdi/node.md @@ -19,7 +19,7 @@ There are also properties the `Node` class has to access certain values: - Description * - `Node.bot` - - `Union[Client, Bot]` + - `Client` - Returns the discord.py client linked to this node. * - `Node.is_connected` @@ -132,7 +132,7 @@ If you want to enable it, refer to [](pool.md#adding-a-node) You should get a list of `Track` in return after running this function for you to then do whatever you want with it. Ideally, you should be putting all tracks into some sort of a queue. If you would like to learn about how to use -our queue implementation, you can refer to [](queue.md) +our queue implementation, you can refer to [](queue.md) ## Getting recommendations @@ -179,4 +179,4 @@ await Node.get_recommendations( You should get a list of `Track` in return after running this function for you to then do whatever you want with it. Ideally, you should be putting all tracks into some sort of a queue. If you would like to learn about how to use -our queue implementation, you can refer to [](queue.md) \ No newline at end of file +our queue implementation, you can refer to [](queue.md) diff --git a/docs/hdi/player.md b/docs/hdi/player.md index 0a050de..ca54ffa 100644 --- a/docs/hdi/player.md +++ b/docs/hdi/player.md @@ -28,7 +28,7 @@ There are also properties the `Player` class has to access certain values: - Description * - `Player.bot` - - `Union[Client, commands.Bot]` + - `Client` - Returns the bot associated with this player instance. * - `Player.current` @@ -136,7 +136,7 @@ If you want to enable it, refer to [](pool.md#adding-a-node) You should get a list of `Track` in return after running this function for you to then do whatever you want with it. Ideally, you should be putting all tracks into some sort of a queue. If you would like to learn about how to use -our queue implementation, you can refer to [](queue.md) +our queue implementation, you can refer to [](queue.md) ## Getting recommendations @@ -183,7 +183,7 @@ await Player.get_recommendations( You should get a list of `Track` in return after running this function for you to then do whatever you want with it. Ideally, you should be putting all tracks into some sort of a queue. If you would like to learn about how to use -our queue implementation, you can refer to [](queue.md) +our queue implementation, you can refer to [](queue.md) ## Connecting a player @@ -274,7 +274,7 @@ await Player.play( After running this function, it should return the `Track` you specified when running the function. This means the track is now playing. -### Seeking to a position +### Seeking to a position To seek to a position, we need to use `Player.seek()` @@ -466,15 +466,3 @@ After you have initialized your function, you can optionally include the `fast_a await Player.reset_filters(fast_apply=) ``` - - - - - - - - - - - - diff --git a/docs/hdi/pool.md b/docs/hdi/pool.md index 1fc49bb..e328e00 100644 --- a/docs/hdi/pool.md +++ b/docs/hdi/pool.md @@ -1,6 +1,6 @@ # Use the NodePool class -The `NodePool` class is the first class you will use when using Pomice. +The `NodePool` class is the first class you will use when using Pomice. The `NodePool` Class has three main functions you can use: @@ -43,7 +43,7 @@ After you have initialized your function, we need to fill in the proper paramete * - `identifier` - `str` - - The identifier your `Node` object uses to distinguish itself. + - The identifier your `Node` object uses to distinguish itself. * - `password` - `str` @@ -63,7 +63,7 @@ After you have initialized your function, we need to fill in the proper paramete * - `fallback` - `bool` - - Set this value to `True` if you want Pomice to automatically switch all players to another available node if one disconnects. + - Set this value to `True` if you want Pomice to automatically switch all players to another available node if one disconnects. You must have two or more nodes to be able to do this. ::: @@ -148,15 +148,3 @@ await NodePool.disconnect() ``` After running this function, all nodes in the pool should disconnect and no longer be available to use. - - - - - - - - - - - - diff --git a/docs/hdi/queue.md b/docs/hdi/queue.md index 884b094..ce11665 100644 --- a/docs/hdi/queue.md +++ b/docs/hdi/queue.md @@ -22,7 +22,7 @@ from pomice import Player, Queue class CustomPlayer(Player): ... - self.queue = Queue() + self.queue = Queue() ``` @@ -52,7 +52,7 @@ To get a track from the queue, we need to do a few things. To get a track using its position within the queue, you first need to get the position as a number, also known as its index. If you dont have the index and instead want to search for its index using keywords, you have to implement a fuzzy searching algorithm to find said track using a search query as an input and have it compare that query against the titles of the tracks in the queue. After that, you can get the `Track` object by [getting it with its index](queue.md#getting-track-with-its-index) -### Getting index of track +### Getting index of track If you have the `Track` object and want to get its index within the queue, we can use `Queue.find_position()` @@ -222,13 +222,3 @@ Your `Track` object must be in the queue if you want to jump to it. Make sure yo ::: After running this function, any items before the specified item will be removed, effectively "jumping" to the specified item in the queue. The next item obtained using `Queue.get()` will be your specified track. - - - - - - - - - - diff --git a/docs/index.md b/docs/index.md index 87423ac..1009df0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ hide-toc: true -Pomice is a fully asynchronous Python library designed for communicating with [Lavalink](https://github.com/freyacodes/Lavalink) seamlessly within the [discord.py](https://github.com/Rapptz/discord.py) library. It features 100% API coverage of the entire [Lavalink](https://github.com/freyacodes/Lavalink) spec that can be accessed with easy-to-understand functions. We also include Spotify and Apple Music querying capabilites using built-in custom clients, making it easier to develop your next big music bot. +Pomice is a fully asynchronous Python library designed for communicating with [Lavalink](https://github.com/freyacodes/Lavalink) seamlessly within the [discord.py](https://github.com/Rapptz/discord.py) library. It features 100% API coverage of the entire [Lavalink](https://github.com/freyacodes/Lavalink) spec that can be accessed with easy-to-understand functions. We also include Spotify and Apple Music querying capabilites using built-in custom clients, making it easier to develop your next big music bot. ## Quick Links: @@ -38,7 +38,3 @@ hdi/index.md :hidden: api/index.md ``` - - - - diff --git a/docs/installation.md b/docs/installation.md index 024a6ee..bfdbc9e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,12 +2,12 @@ This library is designed to work with the Lavalink audio delivery system, which directly interfaces with Discord to provide buttery smooth audio without -wasting your precious system resources. +wasting your precious system resources. Pomice is made with convenience to the user, in that everything is easy to use and is out of your way, while also being customizable. -In order to start using this library, please download a Lavalink node to start, +In order to start using this library, please download a Lavalink node to start, you can get it [here](https://github.com/freyacodes/Lavalink/releases/latest) After you have your Lavalink node set up, you can install Pomice: @@ -21,12 +21,10 @@ start coding with it without a hitch. After you installed Pomice, get familiar with how it works by starting out with [an example.](quickstart.md) -If you need more than just a quick example, get our drop-in [advanced cog](https://github.com/cloudwithax/pomice/blob/main/examples/advanced.py) -to take advantage of all of Pomice's features. +If you need more than just a quick example, get our drop-in [advanced cog](https://github.com/cloudwithax/pomice/blob/main/examples/advanced.py) +to take advantage of all of Pomice's features. You are free to use this as a base to add on to for any music features you want to implement within your application. If you want to jump into the library and learn how to do everything you need, refer to the [How Do I?](hdi/index.md) section. If you want a deeper look into how the library works beyond the [How Do I?](hdi/index.md) guide, refer to the [API Reference](api/index.md) section. - - diff --git a/docs/quickstart.md b/docs/quickstart.md index d050548..a7be045 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -13,63 +13,63 @@ from discord.ext import commands URL_REG = re.compile(r'https?://(?:www\.)?.+') class MyBot(commands.Bot): - + def __init__(self) -> None: super().__init__(command_prefix='!', activity=discord.Activity(type=discord.ActivityType.listening, name='to music!')) - + self.add_cog(Music(self)) - + async def on_ready(self) -> None: print("I'm online!") await self.cogs["Music"].start_nodes() - - + + class Music(commands.Cog): - + def __init__(self, bot) -> None: self.bot = bot - + self.pomice = pomice.NodePool() - + async def start_nodes(self): - await self.pomice.create_node(bot=self.bot, host='127.0.0.1', port='3030', + await self.pomice.create_node(bot=self.bot, host='127.0.0.1', port='3030', password='youshallnotpass', identifier='MAIN') print(f"Node is ready!") - + @commands.command(name='join', aliases=['connect']) async def join(self, ctx: commands.Context, *, channel: discord.TextChannel = None) -> None: - + if not channel: channel = getattr(ctx.author.voice, 'channel', None) if not channel: raise commands.CheckFailure('You must be in a voice channel to use this command' 'without specifying the channel argument.') - + await ctx.author.voice.channel.connect(cls=pomice.Player) await ctx.send(f'Joined the voice channel `{channel}`') - + @commands.command(name='play') async def play(self, ctx, *, search: str) -> None: - - if not ctx.voice_client: - await ctx.invoke(self.join) - player = ctx.voice_client + if not ctx.voice_client: + await ctx.invoke(self.join) + + player = ctx.voice_client results = await player.get_tracks(query=f'{search}') - + if not results: raise commands.CommandError('No results were found for that search term.') - + if isinstance(results, pomice.Playlist): await player.play(track=results.tracks[0]) else: await player.play(track=results[0]) - + bot = MyBot() bot.run("token here") -``` \ No newline at end of file +``` diff --git a/docs/requirements_rtd.txt b/docs/requirements_rtd.txt index f99f6d2..fa93f6f 100644 --- a/docs/requirements_rtd.txt +++ b/docs/requirements_rtd.txt @@ -1,5 +1,5 @@ -discord.py[voice] aiohttp -orjson +discord.py[voice] +furo myst_parser -furo \ No newline at end of file +orjson diff --git a/examples/advanced.py b/examples/advanced.py index 4f78ff1..3d738ce 100644 --- a/examples/advanced.py +++ b/examples/advanced.py @@ -4,13 +4,13 @@ This is in the form of a drop-in cog you can use and modify to your liking. This example aims to include everything you would need to make a fully functioning music bot, from a queue system, advanced queue control and more. """ +import math +from contextlib import suppress import discord -import pomice -import math - from discord.ext import commands -from contextlib import suppress + +import pomice class Player(pomice.Player): @@ -44,7 +44,6 @@ class Player(pomice.Player): with suppress(discord.HTTPException): await self.controller.delete() - # Queue up the next track, else teardown the player try: track: pomice.Track = self.queue.get() @@ -56,13 +55,16 @@ class Player(pomice.Player): # Call the controller (a.k.a: The "Now Playing" embed) and check if one exists if track.is_stream: - embed = discord.Embed(title="Now playing", description=f":red_circle: **LIVE** [{track.title}]({track.uri}) [{track.requester.mention}]") + embed = discord.Embed( + title="Now playing", description=f":red_circle: **LIVE** [{track.title}]({track.uri}) [{track.requester.mention}]", + ) self.controller = await self.context.send(embed=embed) else: - embed = discord.Embed(title=f"Now playing", description=f"[{track.title}]({track.uri}) [{track.requester.mention}]") + embed = discord.Embed( + title=f"Now playing", description=f"[{track.title}]({track.uri}) [{track.requester.mention}]", + ) self.controller = await self.context.send(embed=embed) - async def teardown(self): """Clear internal states, remove player controller and disconnect.""" with suppress((discord.HTTPException), (KeyError)): @@ -76,8 +78,6 @@ class Player(pomice.Player): self.dj = ctx.author - - class Music(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot @@ -100,7 +100,7 @@ class Music(commands.Cog): host="127.0.0.1", port="3030", password="youshallnotpass", - identifier="MAIN" + identifier="MAIN", ) print(f"Node is ready!") @@ -122,7 +122,6 @@ class Music(commands.Cog): return player.dj == ctx.author or ctx.author.guild_permissions.kick_members - # The following are events from pomice.events # We are using these so that if the track either stops or errors, # we can just skip to the next track @@ -195,8 +194,6 @@ class Music(commands.Cog): if not player.is_playing: await player.do_next() - - @commands.command(aliases=['pau', 'pa']) async def pause(self, ctx: commands.Context): """Pause the currently playing song.""" @@ -345,6 +342,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)) - diff --git a/examples/basic.py b/examples/basic.py index 7268870..8e0a218 100644 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,13 +1,16 @@ import discord -import pomice from discord.ext import commands +import pomice + class MyBot(commands.Bot): def __init__(self) -> None: super().__init__( command_prefix="!", - activity=discord.Activity(type=discord.ActivityType.listening, name="to music!") + activity=discord.Activity( + type=discord.ActivityType.listening, name="to music!", + ), ) self.add_cog(Music(self)) @@ -33,7 +36,7 @@ class Music(commands.Cog): host="127.0.0.1", port="3030", password="youshallnotpass", - identifier="MAIN" + identifier="MAIN", ) print(f"Node is ready!") @@ -44,7 +47,7 @@ class Music(commands.Cog): if not channel: raise commands.CheckFailure( "You must be in a voice channel to use this command " - "without specifying the channel argument." + "without specifying the channel argument.", ) # With the release of discord.py 1.7, you can now add a compatible @@ -78,7 +81,9 @@ class Music(commands.Cog): results = await player.get_tracks(search) if not results: - raise commands.CommandError("No results were found for that search term.") + raise commands.CommandError( + "No results were found for that search term.", + ) if isinstance(results, pomice.Playlist): await player.play(track=results.tracks[0]) diff --git a/pomice/__init__.py b/pomice/__init__.py index 6671545..e6bc81a 100644 --- a/pomice/__init__.py +++ b/pomice/__init__.py @@ -17,7 +17,7 @@ if not discord.version_info.major >= 2: 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'" + "using 'pip install discord.py'", ) __version__ = "2.2a" diff --git a/pomice/applemusic/__init__.py b/pomice/applemusic/__init__.py index abd0722..150a710 100644 --- a/pomice/applemusic/__init__.py +++ b/pomice/applemusic/__init__.py @@ -1,5 +1,4 @@ """Apple Music module for Pomice, made possible by cloudwithax 2023""" - +from .client import Client from .exceptions import * from .objects import * -from .client import Client diff --git a/pomice/applemusic/client.py b/pomice/applemusic/client.py index 8d11735..867e3fa 100644 --- a/pomice/applemusic/client.py +++ b/pomice/applemusic/client.py @@ -1,19 +1,27 @@ from __future__ import annotations +import base64 import re +from datetime import datetime +from typing import Dict +from typing import List +from typing import Union + import aiohttp import orjson as json -import base64 -from datetime import datetime -from .objects import * from .exceptions import * +from .objects import * + +__all__ = ( + "Client", +) AM_URL_REGEX = re.compile( - r"https?://music.apple.com/(?P[a-zA-Z]{2})/(?Palbum|playlist|song|artist)/(?P.+)/(?P[^?]+)" + 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.+)" + 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" @@ -26,37 +34,49 @@ class Client: """ def __init__(self) -> None: - self.token: str = None - self.expiry: datetime = None - self.session: aiohttp.ClientSession = None - self.headers = None + self.expiry: datetime = datetime(1970, 1, 1) + self.token: str = "" + self.headers: Dict[str, str] = {} + self.session: aiohttp.ClientSession = None # type: ignore - async def request_token(self): + async def request_token(self) -> None: if not self.session: 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}" + f"Error while fetching results: {resp.status} {resp.reason}", ) text = await resp.text() - result = re.search('"(eyJ.+?)"', text).group(1) + match = re.search('"(eyJ.+?)"', text) + if not match: + raise AppleMusicRequestException( + "Could not find token in response.", + ) + result = match.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_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) -> Union[Album, Playlist, Song, Artist]: if not self.token or datetime.utcnow() > self.expiry: await self.request_token() result = AM_URL_REGEX.match(query) + if not result: + raise InvalidAppleMusicURL( + "The Apple Music link provided is not valid.", + ) country = result.group("country") type = result.group("type") @@ -75,7 +95,7 @@ class Client: 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}" + f"Error while fetching results: {resp.status} {resp.reason}", ) data: dict = await resp.json(loads=json.loads) @@ -84,53 +104,57 @@ class Client: if type == "song": return Song(data) - elif type == "album": + if type == "album": return Album(data) - elif type == "artist": + if type == "artist": async with self.session.get( - f"{request_url}/view/top-songs", headers=self.headers + 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}" + f"Error while fetching results: {resp.status} {resp.reason}", ) top_tracks: dict = await resp.json(loads=json.loads) - tracks: dict = top_tracks["data"] + artist_tracks: dict = top_tracks["data"] - return Artist(data, tracks=tracks) + return Artist(data, tracks=artist_tracks) - else: - track_data: dict = data["relationships"]["tracks"] + track_data: dict = data["relationships"]["tracks"] + album_tracks: List[Song] = [ + Song(track) + for track in track_data["data"] + ] - tracks = [Song(track) for track in track_data.get("data")] + if not len(album_tracks): + raise AppleMusicRequestException( + "This playlist is empty and therefore cannot be queued.", + ) - if not len(tracks): - raise AppleMusicRequestException( - "This playlist is empty and therefore cannot be queued." - ) + _next = track_data.get("next") + if _next: + next_page_url = AM_BASE_URL + _next - 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}", + ) - 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) - next_data: dict = await resp.json(loads=json.loads) + album_tracks.extend(Song(track) for track in next_data["data"]) - 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 + _next = next_data.get("next") + if _next: + next_page_url = AM_BASE_URL + _next + else: + next_page_url = None - return Playlist(data, tracks) + return Playlist(data, album_tracks) - async def close(self): + async def close(self) -> None: if self.session: await self.session.close() - self.session = None + self.session = None # type: ignore diff --git a/pomice/applemusic/exceptions.py b/pomice/applemusic/exceptions.py index 6d60fa3..5c3ff88 100644 --- a/pomice/applemusic/exceptions.py +++ b/pomice/applemusic/exceptions.py @@ -1,3 +1,9 @@ +__all__ = ( + "AppleMusicRequestException", + "InvalidAppleMusicURL", +) + + class AppleMusicRequestException(Exception): """An error occurred when making a request to the Apple Music API""" diff --git a/pomice/applemusic/objects.py b/pomice/applemusic/objects.py index 67e5643..9a1e253 100644 --- a/pomice/applemusic/objects.py +++ b/pomice/applemusic/objects.py @@ -1,7 +1,13 @@ """Module for managing Apple Music objects""" - from typing import List +__all__ = ( + "Song", + "Playlist", + "Album", + "Artist", +) + class Song: """The base class for an Apple Music song""" @@ -55,7 +61,9 @@ class Album: 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.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"]}', @@ -75,7 +83,9 @@ class Artist: 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.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}", diff --git a/pomice/enums.py b/pomice/enums.py index 93e0eea..6b3a902 100644 --- a/pomice/enums.py +++ b/pomice/enums.py @@ -1,7 +1,17 @@ import re - from enum import Enum +__all__ = ( + "SearchType", + "TrackType", + "PlaylistType", + "NodeAlgorithm", + "LoopMode", + "RouteStrategy", + "RouteIPType", + "URLRegex", +) + class SearchType(Enum): """ @@ -185,43 +195,51 @@ class URLRegex: """ SPOTIFY_URL = re.compile( - r"https?://open.spotify.com/(?Palbum|playlist|track|artist)/(?P[a-zA-Z0-9]+)" + 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_.]+)+" + 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+)?$" + r"(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$", ) YOUTUBE_PLAYLIST_URL = re.compile( - r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))/playlist\?list=.*" + r"^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))/playlist\?list=.*", ) - YOUTUBE_VID_IN_PLAYLIST = re.compile(r"(?P