diff --git a/.gitignore b/.gitignore index d50a09f..e3234c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,14 @@ -.env +# Python-generated files __pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Other stuff +.env +.python-version diff --git a/README.md b/README.md index 1ef5ab2..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,2 +0,0 @@ -# simple-discord-music-bot -A simple discord music bot. Run it yourself! diff --git a/assets/unknown.png b/assets/unknown.png deleted file mode 100644 index 69a2583..0000000 Binary files a/assets/unknown.png and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index 2dc5f5e..b7b4bf2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,11 @@ [project] -name = "oden-music-bot" +name = "oden" version = "0.1.0" description = "A simple music bot." readme = "README.md" +authors = [ + { name = "G2-Games", email = "ke0bhogsg@gmail.com" } +] requires-python = ">=3.13" dependencies = [ "audioop-lts>=0.2.1", @@ -13,3 +16,10 @@ dependencies = [ "stringprogressbar>=1.1.1", "yt-dlp>=2025.1.15", ] + +[project.scripts] +oden = "oden:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/src/oden/__init__.py b/src/oden/__init__.py new file mode 100644 index 0000000..f21634f --- /dev/null +++ b/src/oden/__init__.py @@ -0,0 +1 @@ +import oden.main diff --git a/src/main.py b/src/oden/main.py similarity index 93% rename from src/main.py rename to src/oden/main.py index 5cb024d..f5d7443 100644 --- a/src/main.py +++ b/src/oden/main.py @@ -1,9 +1,14 @@ import io -import discord, subprocess, glob, os, os.path, ffmpeg -import re, time, datetime, yt_dlp, typing, functools -import logging, json +import discord +import subprocess +import glob +import os +import os.path +import time +import datetime +import yt_dlp +import logging from time import strftime, gmtime -from yt_dlp import YoutubeDL from asyncio import sleep from discord.ext import commands from StringProgressBar import progressBar @@ -15,12 +20,12 @@ from typing import Optional, TypedDict ## LOCAL IMPORTS ## ##### -import utils -from utils import QueueItem +import oden.utils as utils +from oden.utils import QueueItem logging.basicConfig() -ServerInfo = TypedDict('ServerInfo', { +ServerInfo = TypedDict("ServerInfo", { "loop": bool, "paused": bool, "elapsed": int, @@ -108,7 +113,7 @@ async def play(ctx, *, query: Optional[str] = None): # Make sure the file is either audio or video filetype = song.content_type - if filetype is None or (filetype.split('/')[0] != "audio" and filetype.split('/')[0] != "video"): + if filetype is None or (filetype.split("/")[0] != "audio" and filetype.split("/")[0] != "video"): success_string += ":no_entry_sign: — `" + song.filename + "`, invalid file!\n" continue @@ -138,7 +143,7 @@ async def play(ctx, *, query: Optional[str] = None): notice = await ctx.send(f":mag_right: Searching for \"{query}\" ...", suppress_embeds=True) # Search metadata for youtube video - with yt_dlp.YoutubeDL({'quiet': True}) as ydl: + with yt_dlp.YoutubeDL({"quiet": True}) as ydl: search_list = ydl.extract_info(f"ytsearch:{query}", download = False) if search_list is None or len(search_list["entries"]) == 0: await notice.edit(content=":question: No songs found for query, try something else!", delete_after=3) @@ -177,7 +182,7 @@ async def play(ctx, *, query: Optional[str] = None): return # Search metadata for youtube video - with yt_dlp.YoutubeDL({'quiet': True}) as ydl: + with yt_dlp.YoutubeDL({"quiet": True}) as ydl: info = ydl.extract_info(query, download = False) pprint(info) @@ -226,19 +231,19 @@ async def play(ctx, *, query: Optional[str] = None): current_item = queue[queue_position] # Set song variables - song_id = current_item['id'] - song_url = current_item['url'] - song_name = current_item['name'] - song_thumb = current_item['thumbnail'] - song_duration = current_item['duration'] - song_thumb_url = current_item['thumbnail_url'] + song_id = current_item["id"] + song_url = current_item["url"] + song_name = current_item["name"] + song_thumb = current_item["thumbnail"] + song_duration = current_item["duration"] + song_thumb_url = current_item["thumbnail_url"] song_thumbname = str(int(time.time())) + ".png" color = 0x42f5a7 # Create the embed if current_item["artist"] and current_item["album"]: - song_desc = "Artist: " + current_item['artist'] + "\nAlbum: " + current_item['album'] + song_desc = "Artist: " + current_item["artist"] + "\nAlbum: " + current_item["album"] else: song_desc = "" @@ -255,7 +260,7 @@ async def play(ctx, *, query: Optional[str] = None): pipe = False if song_url is not None and song_url != "": song_source = subprocess.Popen( - ['yt-dlp', '-q', '-o', '-', '-x', song_url], + ["yt-dlp", "-q", "-o", "-", "-x", song_url], stdout=subprocess.PIPE, ).stdout @@ -263,7 +268,7 @@ async def play(ctx, *, query: Optional[str] = None): await notice.edit(content=":no_entry_sign: Could not get metadata.", delete_after=3) return - song_source = io.BufferedReader(song_source) + song_source = io.BufferedReader(song_source) # type: ignore pipe = True print(str(server_id) + " | " + "Playing song through yt-dlp") else: @@ -282,7 +287,7 @@ async def play(ctx, *, query: Optional[str] = None): time2 = int(time.time()) current = time2 - time1 server_info[server_id]["elapsed"] = current - bardata = progressBar.filledBar(total, current, size=20) + bardata = progressBar.filledBar(total, current, size=20) # type: ignore # Create embed embed=discord.Embed(title="▶️ Playing: " + song_name, url=song_url, description=song_desc, color=color) @@ -329,7 +334,7 @@ async def play(ctx, *, query: Optional[str] = None): # Remove all queued files and folders... This is a bit dangerous, maybe it # should be made failsafe somehow? TODO - fileList = glob.glob(os.path.join(str(server_id),'*')) + fileList = glob.glob(os.path.join(str(server_id), "*")) for filePath in fileList: try: os.remove(filePath) @@ -399,7 +404,7 @@ async def skip(ctx, command: Optional[str] = None, number: Optional[int] = None) voice_channel.stop() elif command == "to" and number is not None: if number <= 0: - await ctx.send(f":no_entry_sign: Cannot skip to negative number or 0!") + await ctx.send(":no_entry_sign: Cannot skip to negative number or 0!") return elif number >= queue_length: await ctx.send(f":no_entry_sign: Invalid skip-to number! Max is {queue_length}.") @@ -487,21 +492,21 @@ async def q(ctx, action = None, selection = None): position_string += "⠀\n" - if len(entry['name']) >= 30: - entry_cut = entry['name'][0:30] + if len(entry["name"]) >= 30: + entry_cut = entry["name"][0:30] else: - entry_cut = entry['name'] + entry_cut = entry["name"] queue_string += "**" + str(index + 1) + ":** " + entry_cut + "\n" - if entry['duration'] < 3600: - duration_string += str(strftime("%M:%S", gmtime(entry['duration']))) + "\n" + if entry["duration"] < 3600: + duration_string += str(strftime("%M:%S", gmtime(entry["duration"]))) + "\n" else: - duration_string += str(strftime("%H:%M:%S", gmtime(entry['duration']))) + "\n" + duration_string += str(strftime("%H:%M:%S", gmtime(entry["duration"]))) + "\n" if index == server_info[server_id]["queue_position"]: - total_duration += entry['duration'] - server_info[server_id]["elapsed"] + total_duration += entry["duration"] - server_info[server_id]["elapsed"] if index > server_info[server_id]["queue_position"]: - total_duration += entry['duration'] + total_duration += entry["duration"] index += 1 # Calculate the time remaining in the queue @@ -527,7 +532,7 @@ async def q(ctx, action = None, selection = None): if server_info[server_id]["queue"][selection]["thumbnail"] != "/assets/unknown.png": thumbnail = None else: - thumbnail = server_info[server_id]["queue"][selection]['thumbnail'] + thumbnail = server_info[server_id]["queue"][selection]["thumbnail"] if selection is current_position: await ctx.send(":no_entry_sign: Error, cannot remove currently playing item", delete_after=3) @@ -593,6 +598,5 @@ async def on_command_error(ctx, error): else: print(error) -if __name__ == "__main__": - # Run the bot using the Discord bot token - bot.run(os.environ['DISCORD_SECRET']) +# Run the bot using the Discord bot token +bot.run(os.environ["ODEN_DISCORD_SECRET"]) diff --git a/src/utils.py b/src/oden/utils.py similarity index 95% rename from src/utils.py rename to src/oden/utils.py index 59c1fe8..418eb90 100644 --- a/src/utils.py +++ b/src/oden/utils.py @@ -1,11 +1,11 @@ # pyright: reportUnusedExpression=true -from pprint import pprint -import uuid, os, ffmpeg -from colorthief import ColorThief +import uuid +import os +import ffmpeg ## LINTING / TYPING ## ##### -from typing import Any, Optional, TypedDict +from typing import Optional, TypedDict async def getIds(ctx): """Get server id, voice channel id, and user voice channel id""" diff --git a/uv.lock b/uv.lock index d6907c0..50684bb 100644 --- a/uv.lock +++ b/uv.lock @@ -240,9 +240,9 @@ wheels = [ ] [[package]] -name = "oden-music-bot" +name = "oden" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "audioop-lts" }, { name = "colorthief" },