Compare commits
No commits in common. "a7f00f9e43196d90f9150aeb45f4f2323952c619" and "8ef6ccde085b7c90425745790d4170a13cb0fbcb" have entirely different histories.
a7f00f9e43
...
8ef6ccde08
163
.gitignore
vendored
@ -1,163 +0,0 @@
|
|||||||
# 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,10 +15,6 @@ 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
|
||||||
@ -29,7 +25,6 @@ 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
|
||||||
|
|
||||||
@ -77,8 +72,6 @@ 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.7 KiB After Width: | Height: | Size: 7.2 KiB |
BIN
assets/pause.png
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
assets/play.png
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.4 KiB |
BIN
assets/skip.png
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 6.6 KiB |
@ -3,7 +3,6 @@ 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
|
||||||
@ -21,13 +20,16 @@ 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: ""
|
||||||
|
|
||||||
@ -84,7 +86,15 @@ class YTDLSource(discord.PCMVolumeTransformer):
|
|||||||
return self.__getattribute__(item)
|
return self.__getattribute__(item)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_source(cls, ctx, search: str, *, download=False):
|
async def create_source(
|
||||||
|
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
|
||||||
@ -236,24 +246,6 @@ 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(
|
||||||
@ -261,52 +253,28 @@ class MusicPlayer:
|
|||||||
)
|
)
|
||||||
elif self._state is self.State.PLAYING:
|
elif self._state is self.State.PLAYING:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"'{self.current.song_title}' by {self.current.artist}",
|
title=f"▶️ Now Playing", color=discord.Color.blue()
|
||||||
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"'{self.current.song_title}' by {self.current.artist}",
|
title=f"⏸️ Paused", color=discord.Color.light_gray()
|
||||||
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
|
||||||
@ -315,19 +283,16 @@ 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}' by {song.artist}]({song.web_url})\n"
|
f"{i+1}. [{song.song_title}]({song.web_url}) -"
|
||||||
|
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"
|
||||||
)
|
)
|
||||||
@ -338,7 +303,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"
|
||||||
)
|
)
|
||||||
@ -351,7 +316,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"
|
||||||
)
|
)
|
||||||
@ -428,24 +393,14 @@ 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,
|
||||||
search,
|
random.choice(songs),
|
||||||
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.")
|
||||||
|
|
||||||
@ -539,11 +494,6 @@ 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()
|
||||||
@ -707,10 +657,6 @@ 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,15 +1,10 @@
|
|||||||
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
|
||||||
@ -316,7 +311,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_channel(channel_id)
|
channel_id = self._insert_user(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()
|
||||||
@ -414,138 +409,3 @@ 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,7 +1,8 @@
|
|||||||
async_timeout==5.0.1
|
discord
|
||||||
discord.py==2.5.2
|
discord[voice]
|
||||||
openai==1.97.1
|
ffmpeg
|
||||||
python-dotenv==1.1.1
|
python-dotenv
|
||||||
Requests==2.32.4
|
yt-dlp
|
||||||
validators==0.34.0
|
async_timeout
|
||||||
yt_dlp @ git+https://github.com/yt-dlp/yt-dlp.git@2025.07.21
|
validators
|
||||||
|
openai
|
||||||
|
|||||||