Compare commits
2 Commits
8ef6ccde08
...
a7f00f9e43
| Author | SHA1 | Date | |
|---|---|---|---|
| a7f00f9e43 | |||
| 08938c08ae |
163
.gitignore
vendored
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# Bot databases stored as .db
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
@ -15,6 +15,10 @@ Source Code:
|
|||||||
https://github.com/jtkick/base-discord-bot
|
https://github.com/jtkick/base-discord-bot
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# BOT PERMISSIONS
|
||||||
|
# 1729383718059856
|
||||||
|
|
||||||
PROJECT_VERSION = "0.1.0"
|
PROJECT_VERSION = "0.1.0"
|
||||||
|
|
||||||
# Standard imports
|
# Standard imports
|
||||||
@ -25,6 +29,7 @@ import sys
|
|||||||
# Third-part imports
|
# Third-part imports
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
from discord import app_commands
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
|
||||||
@ -72,6 +77,8 @@ def main():
|
|||||||
if filename.endswith('.py'):
|
if filename.endswith('.py'):
|
||||||
await client.load_extension(f'cogs.{filename[:-3]}')
|
await client.load_extension(f'cogs.{filename[:-3]}')
|
||||||
logger.info("Loaded %s cog", filename)
|
logger.info("Loaded %s cog", filename)
|
||||||
|
for server in client.guilds:
|
||||||
|
await client.tree.sync(guild=discord.Object(id=server.id))
|
||||||
|
|
||||||
client.run(TOKEN, log_handler=None)
|
client.run(TOKEN, log_handler=None)
|
||||||
|
|
||||||
|
|||||||
BIN
assets/dj.png
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.7 KiB |
BIN
assets/pause.png
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.8 KiB |
BIN
assets/play.png
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
assets/skip.png
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 7.0 KiB |
@ -3,6 +3,7 @@ import atexit
|
|||||||
import datetime
|
import datetime
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
from discord import app_commands
|
||||||
import enum
|
import enum
|
||||||
import random
|
import random
|
||||||
import asyncio
|
import asyncio
|
||||||
@ -20,16 +21,13 @@ import yt_dlp
|
|||||||
from yt_dlp import YoutubeDL
|
from yt_dlp import YoutubeDL
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import database
|
||||||
|
|
||||||
logger = logging.getLogger("music_player")
|
logger = logging.getLogger("music_player")
|
||||||
|
|
||||||
# Get API key for last.fm
|
# Get API key for last.fm
|
||||||
LASTFM_API_KEY = os.getenv("LASTFM_API_KEY")
|
LASTFM_API_KEY = os.getenv("LASTFM_API_KEY")
|
||||||
|
|
||||||
# TEMORARY LIST OF SONGS
|
|
||||||
songs = [
|
|
||||||
[REDACTED]
|
|
||||||
]
|
|
||||||
|
|
||||||
# Suppress noise about console usage from errors
|
# Suppress noise about console usage from errors
|
||||||
# yt_dlp.utils.bug_reports_message = lambda: ""
|
# yt_dlp.utils.bug_reports_message = lambda: ""
|
||||||
|
|
||||||
@ -86,15 +84,7 @@ class YTDLSource(discord.PCMVolumeTransformer):
|
|||||||
return self.__getattribute__(item)
|
return self.__getattribute__(item)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_source(
|
async def create_source(cls, ctx, search: str, *, download=False):
|
||||||
cls,
|
|
||||||
ctx,
|
|
||||||
search: str,
|
|
||||||
*,
|
|
||||||
download=False,
|
|
||||||
artist="",
|
|
||||||
song_title="",
|
|
||||||
):
|
|
||||||
loop = ctx.bot.loop if ctx else asyncio.get_event_loop()
|
loop = ctx.bot.loop if ctx else asyncio.get_event_loop()
|
||||||
|
|
||||||
# If we got a YouTube link, get the video title for the song search
|
# If we got a YouTube link, get the video title for the song search
|
||||||
@ -246,6 +236,24 @@ class MusicPlayer:
|
|||||||
logger.info("Updating 'Now Playing' message")
|
logger.info("Updating 'Now Playing' message")
|
||||||
await self.bot.wait_until_ready()
|
await self.bot.wait_until_ready()
|
||||||
async with self._guild_lock:
|
async with self._guild_lock:
|
||||||
|
# # Create new 'Now Playing' message
|
||||||
|
# if self._state is self.State.IDLE:
|
||||||
|
# embed = discord.Embed(
|
||||||
|
# title=f"◻️ Idle", color=discord.Color.light_gray()
|
||||||
|
# )
|
||||||
|
# elif self._state is self.State.PLAYING:
|
||||||
|
# embed = discord.Embed(
|
||||||
|
# title=f"▶️ Now Playing", color=discord.Color.blue()
|
||||||
|
# )
|
||||||
|
# elif self._state is self.State.PAUSED:
|
||||||
|
# embed = discord.Embed(
|
||||||
|
# title=f"⏸️ Paused", color=discord.Color.light_gray()
|
||||||
|
# )
|
||||||
|
# else:
|
||||||
|
# embed = discord.Embed(
|
||||||
|
# title="UNKNOWN STATE", color=discord.Color.red()
|
||||||
|
# )
|
||||||
|
|
||||||
# Create new 'Now Playing' message
|
# Create new 'Now Playing' message
|
||||||
if self._state is self.State.IDLE:
|
if self._state is self.State.IDLE:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
@ -253,28 +261,52 @@ class MusicPlayer:
|
|||||||
)
|
)
|
||||||
elif self._state is self.State.PLAYING:
|
elif self._state is self.State.PLAYING:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"▶️ Now Playing", color=discord.Color.blue()
|
title=f"'{self.current.song_title}' by {self.current.artist}",
|
||||||
|
url=self.current.web_url,
|
||||||
|
color=discord.Color.green()
|
||||||
)
|
)
|
||||||
elif self._state is self.State.PAUSED:
|
elif self._state is self.State.PAUSED:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"⏸️ Paused", color=discord.Color.light_gray()
|
title=f"'{self.current.song_title}' by {self.current.artist}",
|
||||||
|
url=self.current.web_url,
|
||||||
|
color=discord.Color.green()
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title="UNKNOWN STATE", color=discord.Color.red()
|
title="UNKNOWN STATE", color=discord.Color.red()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self._state is self.State.IDLE:
|
||||||
|
pass
|
||||||
|
elif self._state is self.State.PLAYING:
|
||||||
|
embed.set_author(
|
||||||
|
name="Now Playing",
|
||||||
|
icon_url="https://raw.githubusercontent.com/jtkick/base-discord-bot/refs/heads/develop/assets/play.png"
|
||||||
|
)
|
||||||
|
elif self._state is self.State.PAUSED:
|
||||||
|
embed.set_author(
|
||||||
|
name="Paused",
|
||||||
|
icon_url="https://raw.githubusercontent.com/jtkick/base-discord-bot/refs/heads/develop/assets/pause.png"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="UNKNOWN STATE", color=discord.Color.red()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Get and add the thumbnail
|
# Get and add the thumbnail
|
||||||
if self._state in [self.State.PLAYING, self.State.PAUSED]:
|
if self._state in [self.State.PLAYING, self.State.PAUSED]:
|
||||||
embed.set_thumbnail(url=self.current.thumbnail_url)
|
embed.set_thumbnail(url=self.current.thumbnail_url)
|
||||||
embed.add_field(
|
# embed.add_field(
|
||||||
name="",
|
# name="",
|
||||||
value=(
|
# value=(
|
||||||
f"[{self.current.song_title}]({self.current.web_url}) - "
|
# f"[{self.current.song_title}]({self.current.web_url}) - "
|
||||||
f"{self.current.artist}"
|
# f"{self.current.artist}"
|
||||||
),
|
# ),
|
||||||
inline=False,
|
# inline=False,
|
||||||
)
|
# )
|
||||||
|
|
||||||
# Add all upcoming songs
|
# Add all upcoming songs
|
||||||
# Possibly dangerous, but only obvious solution
|
# Possibly dangerous, but only obvious solution
|
||||||
@ -283,16 +315,19 @@ class MusicPlayer:
|
|||||||
value_str = ""
|
value_str = ""
|
||||||
for i, song in enumerate(queue):
|
for i, song in enumerate(queue):
|
||||||
value_str += (
|
value_str += (
|
||||||
f"{i+1}. [{song.song_title}]({song.web_url}) -"
|
f"{i+1}. ['{song.song_title}' by {song.artist}]({song.web_url})\n"
|
||||||
f" {song.artist}\n"
|
|
||||||
)
|
)
|
||||||
embed.add_field(name="Queue", value=value_str, inline=False)
|
embed.add_field(name="Queue", value=value_str, inline=False)
|
||||||
|
|
||||||
|
# Add 'DJ Mode' footer if on
|
||||||
|
if self.dj_mode:
|
||||||
|
embed.set_footer(text="DJ Mode", icon_url="https://raw.githubusercontent.com/jtkick/base-discord-bot/refs/heads/develop/assets/dj.png")
|
||||||
|
|
||||||
# Build controls
|
# Build controls
|
||||||
controls = discord.ui.View(timeout=None)
|
controls = discord.ui.View(timeout=None)
|
||||||
# Construct 'back' button
|
# Construct 'back' button
|
||||||
prev_button = discord.ui.Button(
|
prev_button = discord.ui.Button(
|
||||||
label="⏮️",
|
label="⏮",
|
||||||
style=discord.ButtonStyle.secondary,
|
style=discord.ButtonStyle.secondary,
|
||||||
custom_id="prev"
|
custom_id="prev"
|
||||||
)
|
)
|
||||||
@ -303,7 +338,7 @@ class MusicPlayer:
|
|||||||
|
|
||||||
# Construct 'play/pause' button
|
# Construct 'play/pause' button
|
||||||
play_button = discord.ui.Button(
|
play_button = discord.ui.Button(
|
||||||
label="▶️" if self._state is self.State.PAUSED else "⏸️",
|
label="⏵" if self._state is self.State.PAUSED else "⏸",
|
||||||
style=discord.ButtonStyle.secondary,
|
style=discord.ButtonStyle.secondary,
|
||||||
custom_id="playpause"
|
custom_id="playpause"
|
||||||
)
|
)
|
||||||
@ -316,7 +351,7 @@ class MusicPlayer:
|
|||||||
|
|
||||||
# Construct 'next' button
|
# Construct 'next' button
|
||||||
next_button = discord.ui.Button(
|
next_button = discord.ui.Button(
|
||||||
label="⏭️",
|
label="⏭",
|
||||||
style=discord.ButtonStyle.secondary,
|
style=discord.ButtonStyle.secondary,
|
||||||
custom_id="next"
|
custom_id="next"
|
||||||
)
|
)
|
||||||
@ -393,14 +428,24 @@ class MusicPlayer:
|
|||||||
"Queue is empty and DJ mode is on. Picking song at random"
|
"Queue is empty and DJ mode is on. Picking song at random"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
# TEST
|
||||||
|
user_ids = [m.id for m in self._channel.members]
|
||||||
|
channel_ids = [c.id for c in self._channel.guild.channels]
|
||||||
|
song = self.bot.db.get_next_song(users=user_ids, channels=channel_ids)
|
||||||
|
search = f"{song["artist"]} {song["title"]}"
|
||||||
|
|
||||||
source = await YTDLSource.create_source(
|
source = await YTDLSource.create_source(
|
||||||
None,
|
None,
|
||||||
random.choice(songs),
|
search,
|
||||||
download=True,
|
download=True,
|
||||||
)
|
)
|
||||||
if not source:
|
if not source:
|
||||||
raise RuntimeError("Could not get YouTube source.")
|
raise RuntimeError("Could not get YouTube source.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# Something's wrong, turn off DJ mode to prevent infinite
|
||||||
|
# loop
|
||||||
|
self.dj_mode = False
|
||||||
print(e)
|
print(e)
|
||||||
await self._channel.send("Failed to get YouTube source.")
|
await self._channel.send("Failed to get YouTube source.")
|
||||||
|
|
||||||
@ -494,6 +539,11 @@ class Music(commands.Cog):
|
|||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.players = {}
|
self.players = {}
|
||||||
|
|
||||||
|
# @commands.Cog.listener()
|
||||||
|
# async def on_ready(self):
|
||||||
|
# await self.bot.tree.sync()
|
||||||
|
# logger.info("Synced command tree")
|
||||||
|
|
||||||
async def cleanup(self, guild):
|
async def cleanup(self, guild):
|
||||||
try:
|
try:
|
||||||
await guild.voice_client.disconnect()
|
await guild.voice_client.disconnect()
|
||||||
@ -657,6 +707,10 @@ class Music(commands.Cog):
|
|||||||
await message.edit(embed=embed)
|
await message.edit(embed=embed)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
@app_commands.command(name="hello", description="says hello")
|
||||||
|
async def hello(self, interaction: discord.Interaction):
|
||||||
|
await interaction.response.send_message("hello");
|
||||||
|
|
||||||
@commands.command(
|
@commands.command(
|
||||||
name="djmode", aliases=["dj"], description="Turns DJ mode on or off."
|
name="djmode", aliases=["dj"], description="Turns DJ mode on or off."
|
||||||
)
|
)
|
||||||
|
|||||||
142
database.py
@ -1,10 +1,15 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import discord
|
import discord
|
||||||
|
import logging
|
||||||
|
import openai
|
||||||
|
import random
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from cogs import music_player
|
from cogs import music_player
|
||||||
|
|
||||||
|
logger = logging.getLogger("database")
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
def __init__(self, path: str):
|
def __init__(self, path: str):
|
||||||
self.path = path
|
self.path = path
|
||||||
@ -311,7 +316,7 @@ class Database:
|
|||||||
int: The row ID of the entered song. Used to update 'played' value.
|
int: The row ID of the entered song. Used to update 'played' value.
|
||||||
"""
|
"""
|
||||||
user_id = self._insert_user(source.requester.id) if source.requester else None
|
user_id = self._insert_user(source.requester.id) if source.requester else None
|
||||||
channel_id = self._insert_user(channel_id)
|
channel_id = self._insert_channel(channel_id)
|
||||||
# Insert the information
|
# Insert the information
|
||||||
with sqlite3.connect(self.path) as conn:
|
with sqlite3.connect(self.path) as conn:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
@ -409,3 +414,138 @@ class Database:
|
|||||||
if None in activity_stats:
|
if None in activity_stats:
|
||||||
del activity_stats[None]
|
del activity_stats[None]
|
||||||
return activity_stats
|
return activity_stats
|
||||||
|
|
||||||
|
def get_next_song(self, users: list[int], channels: list[int], limit: int = 100, cutoff: datetime = datetime.now() - timedelta(hours=1)):
|
||||||
|
|
||||||
|
print("users:", users)
|
||||||
|
print("channels:", channels)
|
||||||
|
|
||||||
|
# Convert user IDs to row IDs
|
||||||
|
with sqlite3.connect(self.path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
id
|
||||||
|
FROM
|
||||||
|
user
|
||||||
|
WHERE
|
||||||
|
discord_id IN (%s);""" % ",".join("?" for _ in users),
|
||||||
|
tuple(users))
|
||||||
|
user_ids = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
id
|
||||||
|
FROM
|
||||||
|
channel
|
||||||
|
WHERE
|
||||||
|
discord_id IN (%s);""" % ",".join("?" for _ in channels),
|
||||||
|
tuple(channels))
|
||||||
|
channel_ids = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Pull song plays from the given channels
|
||||||
|
logger.info("Getting past song plays")
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
song_title,
|
||||||
|
song_artist,
|
||||||
|
COUNT(*) AS count
|
||||||
|
FROM
|
||||||
|
song_play
|
||||||
|
WHERE
|
||||||
|
user_id IN (%s) AND
|
||||||
|
channel_id IN (%s) AND
|
||||||
|
finished = 1 AND
|
||||||
|
timestamp < ?
|
||||||
|
GROUP BY
|
||||||
|
song_title,
|
||||||
|
song_artist
|
||||||
|
ORDER BY
|
||||||
|
count DESC
|
||||||
|
LIMIT ?;
|
||||||
|
""" % (
|
||||||
|
",".join(str(id) for id in user_ids),
|
||||||
|
",".join(str(id) for id in channel_ids)
|
||||||
|
), (cutoff, limit))
|
||||||
|
old_song_plays = cursor.fetchall()
|
||||||
|
|
||||||
|
# Compile results into cleaner list of dicts
|
||||||
|
candidates = [{"title": t, "artist": a, "plays": p} for t, a, p in old_song_plays]
|
||||||
|
print("candidates:", candidates)
|
||||||
|
|
||||||
|
# Get recent song plays
|
||||||
|
logger.info("Getting recent song plays")
|
||||||
|
with sqlite3.connect(self.path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Get recent songs to avoid
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
song_title,
|
||||||
|
song_artist
|
||||||
|
FROM
|
||||||
|
song_play
|
||||||
|
WHERE
|
||||||
|
channel_id IN (%s) AND
|
||||||
|
timestamp >= ?
|
||||||
|
GROUP BY
|
||||||
|
song_title,
|
||||||
|
song_artist;
|
||||||
|
""" % (",".join(str(id) for id in channel_ids)), (cutoff, ))
|
||||||
|
recent_song_plays = cursor.fetchall()
|
||||||
|
print("recent:", recent_song_plays)
|
||||||
|
|
||||||
|
# Remove all songs that were recently played
|
||||||
|
def keep(song_play: dict[str, str, int]):
|
||||||
|
return not (song_play["title"], song_play["artist"]) in recent_song_plays
|
||||||
|
candidates = list(filter(keep, candidates))
|
||||||
|
print("filtered candidates:", candidates)
|
||||||
|
|
||||||
|
if len(candidates) > 0:
|
||||||
|
candidate = random.choice(candidates)
|
||||||
|
return {"title": candidate["title"], "artist": candidate["artist"]}
|
||||||
|
# If we have no songs left to play, get a recommendation from ChatGPT
|
||||||
|
else:
|
||||||
|
|
||||||
|
# Get last five or so completed song plays
|
||||||
|
with sqlite3.connect(self.path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Get recent songs to avoid
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
song_title,
|
||||||
|
song_artist
|
||||||
|
FROM
|
||||||
|
song_play
|
||||||
|
WHERE
|
||||||
|
channel_id IN (%s) AND
|
||||||
|
finished = 1
|
||||||
|
GROUP BY
|
||||||
|
song_title,
|
||||||
|
song_artist
|
||||||
|
ORDER BY
|
||||||
|
timestamp DESC
|
||||||
|
LIMIT 5;
|
||||||
|
""" % (",".join(str(id) for id in channel_ids)))
|
||||||
|
last_five = cursor.fetchall()
|
||||||
|
|
||||||
|
print("last five song plays:", last_five)
|
||||||
|
|
||||||
|
setup_prompt = "I'm going to give you a list of songs and artists "\
|
||||||
|
"formatted as a Python list of dicts where the "\
|
||||||
|
"song title is the 'title' key and the artist is "\
|
||||||
|
"the 'artist' key. I want you to return a song "\
|
||||||
|
"title and artist that you would recommend based "\
|
||||||
|
"on the given songs. You should give me only a bare text "\
|
||||||
|
"string formatted as a Python dict where the "\
|
||||||
|
"'title' key is the song title, and the 'artist' "\
|
||||||
|
"key is the song's artist. Don't add anything other "\
|
||||||
|
"than this dict."
|
||||||
|
user_prompt = []
|
||||||
|
completion = openai.OpenAI().chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": setup_prompt},
|
||||||
|
{"role": "user", "content": str(last_five)}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return eval(completion.choices[0].message.content)
|
||||||
@ -1,8 +1,7 @@
|
|||||||
discord
|
async_timeout==5.0.1
|
||||||
discord[voice]
|
discord.py==2.5.2
|
||||||
ffmpeg
|
openai==1.97.1
|
||||||
python-dotenv
|
python-dotenv==1.1.1
|
||||||
yt-dlp
|
Requests==2.32.4
|
||||||
async_timeout
|
validators==0.34.0
|
||||||
validators
|
yt_dlp @ git+https://github.com/yt-dlp/yt-dlp.git@2025.07.21
|
||||||
openai
|
|
||||||
|
|||||||