Major changes to music player and activity tracker. Adding dedicated database for tracking and DJing.
This commit is contained in:
parent
9cbcfc5b6b
commit
3a17d001e1
@ -4,25 +4,42 @@ import asyncio
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import database
|
||||
|
||||
# Create custom logging handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_formatter = logging.Formatter(
|
||||
"[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s")
|
||||
console_handler.setFormatter(console_formatter)
|
||||
|
||||
# Make sure all loggers use this handler
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# Get bot logger
|
||||
logger = logging.getLogger("boywife-bot")
|
||||
|
||||
# Load credentials
|
||||
load_dotenv()
|
||||
TOKEN = os.getenv('DISCORD_TOKEN')
|
||||
|
||||
# client = discord.Client()
|
||||
client = commands.Bot(command_prefix = '!', intents=discord.Intents.all())
|
||||
client = commands.Bot(
|
||||
command_prefix = '!', intents=discord.Intents.all(), log_hander=False)
|
||||
|
||||
# You need to import os for this method
|
||||
@client.event
|
||||
async def on_ready():
|
||||
print(f'{client.user} is now running')
|
||||
logger.info(f'{client.user} is now running')
|
||||
# Load cogs
|
||||
for filename in os.listdir('./cogs'):
|
||||
if filename.endswith('.py'):
|
||||
await client.load_extension(f'cogs.{filename[:-3]}')
|
||||
print(f'Loaded {filename} cog')
|
||||
logger.info(f'Loaded {filename} cog')
|
||||
|
||||
# player = music_player.setup(client)
|
||||
|
||||
client.run(TOKEN)
|
||||
client.run(TOKEN, log_handler=None)
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import datetime
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sqlite3
|
||||
import typing
|
||||
|
||||
import database
|
||||
|
||||
class Activities(commands.Cog):
|
||||
|
||||
@ -12,24 +16,8 @@ class Activities(commands.Cog):
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
# Initialize the databse
|
||||
self.db_path = pathlib.Path(os.getenv('DB_PATH', 'activities.db'))
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS user_activity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
before_activity_type TEXT,
|
||||
before_activity_name TEXT,
|
||||
after_activity_type TEXT,
|
||||
after_activity_name TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
self.logger = logging.getLogger("activities")
|
||||
self.db = database.Database("boywife_bot.db")
|
||||
|
||||
async def __local_check(self, ctx):
|
||||
"""A local check which applies to all commands in this cog."""
|
||||
@ -48,114 +36,39 @@ class Activities(commands.Cog):
|
||||
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
|
||||
traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr)
|
||||
|
||||
def log_activity(self,
|
||||
user_id: int,
|
||||
before_activity_type: str = None,
|
||||
before_activity_name: str = None,
|
||||
after_activity_type: str = None,
|
||||
after_activity_name: str = None):
|
||||
"""Log an activity into the activities database for stats."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
try:
|
||||
cursor.execute("""
|
||||
INSERT INTO user_activity (user_id, before_activity_type,
|
||||
before_activity_name, after_activity_type, after_activity_name)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (user_id, before_activity_type, before_activity_name,
|
||||
after_activity_type, after_activity_name))
|
||||
conn.commit()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_presence_update(self, before: discord.Member, after: discord.Member):
|
||||
# Ignore changes to any bots
|
||||
if after.bot:
|
||||
return
|
||||
if isinstance(before.activity, discord.Game):
|
||||
before_type = "game"
|
||||
before_name = before.activity.name
|
||||
elif isinstance(before.activity, discord.Spotify):
|
||||
before_type = "spotify"
|
||||
before_name = before.activity.artist
|
||||
elif isinstance(before.activity, discord.Activity):
|
||||
before_type = "activity"
|
||||
before_name = before.activity.name
|
||||
else:
|
||||
before_type = "other"
|
||||
before_name = "other"
|
||||
|
||||
if isinstance(after.activity, discord.Game):
|
||||
after_type = "game"
|
||||
after_name = after.activity.name
|
||||
elif isinstance(after.activity, discord.Spotify):
|
||||
after_type = "spotify"
|
||||
after_name = after.activity.artist
|
||||
elif isinstance(after.activity, discord.Activity):
|
||||
after_type = "activity"
|
||||
after_name = after.activity.name
|
||||
else:
|
||||
after_type = "other"
|
||||
after_name = "other"
|
||||
self.log_activity(
|
||||
user_id=before.id,
|
||||
before_activity_type=before_type,
|
||||
before_activity_name=before_name,
|
||||
after_activity_type=after_type,
|
||||
after_activity_name=after_name
|
||||
)
|
||||
async def on_presence_update(
|
||||
self,
|
||||
before: discord.Member,
|
||||
after: discord.Member):
|
||||
# Log the activity or status change
|
||||
self.logger.info("Received activity change; logging")
|
||||
self.db.insert_activity_change(before, after)
|
||||
|
||||
@commands.command(name='nerd', aliases=['nerdscale'],
|
||||
description="Find how nerdy a user is.")
|
||||
async def nerd_(self, ctx, member: discord.Member = None):
|
||||
async def nerd_(self, ctx, member: typing.Union[discord.Member, int, None]):
|
||||
"""Checks the play history of all users in the Discord server, and
|
||||
based on how many hours they have in League of Legends compared to
|
||||
other users, declare how nerdy they are."""
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Start by getting all unique user IDs
|
||||
if member:
|
||||
user_ids = [member.id]
|
||||
# If member is not defined, find the user with the most time
|
||||
if not member:
|
||||
members = [member.id for member in ctx.guild.members]
|
||||
else:
|
||||
user_ids = []
|
||||
cursor.execute("SELECT DISTINCT user_id FROM user_activity")
|
||||
user_ids = [int(r[0]) for r in cursor.fetchall()]
|
||||
if isinstance(member, discord.Member):
|
||||
members = [member.id]
|
||||
else:
|
||||
members = [member]
|
||||
|
||||
# For each user, compute timedelta for time playing League
|
||||
# Get League stats for every member in guild
|
||||
league_stats = {}
|
||||
for user_id in user_ids:
|
||||
# Get all entries that mention league
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
before_activity_name,
|
||||
after_activity_name,
|
||||
timestamp
|
||||
FROM
|
||||
user_activity
|
||||
WHERE
|
||||
user_id = (?) AND
|
||||
timestamp > datetime('now', '-1 month')
|
||||
""", (user_id,))
|
||||
league_activities = cursor.fetchall()
|
||||
|
||||
# Sort by timestamp
|
||||
league_activities = sorted(league_activities, key=lambda x: x[2])
|
||||
|
||||
# Compare each status change to find each time playing League
|
||||
total_time = datetime.timedelta()
|
||||
for first, second in zip(league_activities, league_activities[1:]):
|
||||
if "league of legends" in first[1].lower().strip() and \
|
||||
"league of legends" in second[0].lower().strip():
|
||||
total_time += \
|
||||
datetime.datetime.fromisoformat(second[2]) - \
|
||||
datetime.datetime.fromisoformat(first[2])
|
||||
|
||||
# Save total time in League
|
||||
league_stats[user_id] = total_time
|
||||
for m in members:
|
||||
league_stats[m] = datetime.timedelta()
|
||||
stats = self.db.get_activity_stats(m)
|
||||
sus = True if stats == {} else False
|
||||
for key, value in stats.items():
|
||||
if 'leagueoflegends' in key.lower().strip().replace(' ', ''):
|
||||
league_stats[m] += value
|
||||
|
||||
# Sort all users by time in League
|
||||
league_stats = dict(sorted(
|
||||
@ -163,11 +76,6 @@ class Activities(commands.Cog):
|
||||
reverse=True
|
||||
))
|
||||
|
||||
# Print all stats for testing
|
||||
print("League Stats:")
|
||||
for user_id, time in league_stats.items():
|
||||
print(user_id, ":", str(time))
|
||||
|
||||
# Get top user
|
||||
user_id, time = next(iter(league_stats.items()))
|
||||
time_val = None
|
||||
@ -184,9 +92,9 @@ class Activities(commands.Cog):
|
||||
if member:
|
||||
if time_val != 0:
|
||||
descriptor = ""
|
||||
if time_units == "hours":
|
||||
if time_units in ["hour", "hours"]:
|
||||
descriptor = "a massive fucking nerd"
|
||||
elif time_units == "hour":
|
||||
elif time_units in ["minutes", "minute"]:
|
||||
descriptor = "a huge nerd"
|
||||
else:
|
||||
descriptor = "a nerd"
|
||||
@ -194,8 +102,12 @@ class Activities(commands.Cog):
|
||||
f"{time_units} in the past month, making them "\
|
||||
f"{descriptor}."
|
||||
else:
|
||||
response = f"<@{user_id}> doesn't have any time in League. "\
|
||||
f"They're not a nerd."
|
||||
if sus:
|
||||
response = f"<@{user_id}> doesn't have any activities at "\
|
||||
f"all. They're definitely hiding something."
|
||||
else:
|
||||
response = f"<@{user_id}> doesn't have any time in "\
|
||||
f"League. They're not a nerd."
|
||||
else:
|
||||
response = (
|
||||
f"<@{user_id}> has played League for {time_val} {time_units} "\
|
||||
|
||||
@ -15,10 +15,78 @@ from async_timeout import timeout
|
||||
from functools import partial
|
||||
import yt_dlp
|
||||
from yt_dlp import YoutubeDL
|
||||
import logging
|
||||
|
||||
import database
|
||||
|
||||
logger = logging.getLogger("music_player")
|
||||
|
||||
# Get API key for last.fm
|
||||
LASTFM_API_KEY = os.getenv('LASTFM_API_KEY')
|
||||
|
||||
# TEMORARY LIST OF SONGS
|
||||
songs = [
|
||||
"I Love It - Icona Pop",
|
||||
"Vanished - Crystal Castles",
|
||||
"We Like To Party - Vengaboys",
|
||||
"Gimme! Gimme! Gimme! - ABBA",
|
||||
"Dancing Queen - ABBA",
|
||||
"I Wanna Dance With Somebody - Whitney Houston",
|
||||
"Dance in the Dark - Lady Gaga",
|
||||
"Telephone - Lady Gaga",
|
||||
"Just Dance - Lady Gaga",
|
||||
"Rewind - Charli xcx",
|
||||
"Nasty - Tinashe",
|
||||
"Rush - Troy Sivan",
|
||||
"360 - Charli xcx",
|
||||
"Talk talk - Charli xcx",
|
||||
"Von dutch - Charli xcx",
|
||||
"365 - Charli xcx",
|
||||
"HOT TO GO! - Chappell Roan",
|
||||
"Super Graphic Ultra Modern Girl - Chappell Roan",
|
||||
"Womanizer - Britney Spears",
|
||||
"Red Wind Supernova - Chappell Roan",
|
||||
"Toxic - Britney Spears",
|
||||
"We Found Love - Rihanna",
|
||||
"212 - Azealia Banks",
|
||||
"Bad Romance - Lady Gaga",
|
||||
"Girl, so confusing - Charli xcx",
|
||||
"Alter Ego - Doechii",
|
||||
"Break Free - Ariana Grande",
|
||||
"Raingurl - Yaeji",
|
||||
"Thot Shit - Megan Thee Stallion",
|
||||
"BREAK MY SOUL - Beyonce",
|
||||
"She Wolf - Shakira",
|
||||
"Your Love Is My Drug - Ke$ha",
|
||||
"365 featuring shygirl - Charli xcx",
|
||||
"Applause - Lady Gaga",
|
||||
"Lay All Your Love On Me - ABBA",
|
||||
"Apple - Charli xcx",
|
||||
"Pump It Up - Endor",
|
||||
"Everytime We Touch - Cascada",
|
||||
"Fantasy - Mariah Carey",
|
||||
"Water - Tyla",
|
||||
"Be The One - Eli Brown",
|
||||
"3 - Britney Spears",
|
||||
"Guess featuring billie ellish - Charli xcx",
|
||||
"Bunny Is A Rider - Doss Remix - Caroline Polachek",
|
||||
"Do You Miss Me? - PinkPantheress",
|
||||
"Perfect (Exceeder) - Mason",
|
||||
"Better Off Alone (Laidback Luke Remix) - Alice DJ",
|
||||
"Beauty And A Beat - Justin Bieber",
|
||||
"Girl, so confusing - Charli xcx",
|
||||
"Got Me Started - Troy Sivan",
|
||||
"Gimme More - Britney Spears",
|
||||
"Around the World - Daft Punk",
|
||||
"Harder, Better, Faster, Stronger - Daft Punk",
|
||||
"Sweet Dreams - Eurythmics",
|
||||
"Dancing Elephants - DJ Minx Remix - Rochelle Jordan",
|
||||
"MADELINE - INJI",
|
||||
"Baddy On The Floor - Jamix xx",
|
||||
"SWEET HONEY BUCKIIN' - Beyonce",
|
||||
"Boots & Boys - Ke$ha"
|
||||
]
|
||||
|
||||
# Suppress noise about console usage from errors
|
||||
yt_dlp.utils.bug_reports_message = lambda: ''
|
||||
|
||||
@ -62,12 +130,16 @@ class YTDLSource(discord.PCMVolumeTransformer):
|
||||
super().__init__(source)
|
||||
self.requester = requester
|
||||
|
||||
# YouTube Metadata
|
||||
self.title = data.get('title')
|
||||
self.web_url = data.get('webpage_url')
|
||||
self.thumbnail_url = data.get('thumbnail')
|
||||
self.duration = data.get('duration')
|
||||
|
||||
self.artist = ''
|
||||
self.song_title = ''
|
||||
# Song metadata
|
||||
self.search_term = ""
|
||||
self.artist = ""
|
||||
self.song_title = ""
|
||||
|
||||
# YTDL info dicts (data) have other useful information you might want
|
||||
# https://github.com/rg3/youtube-dl/blob/master/README.md
|
||||
@ -82,23 +154,33 @@ class YTDLSource(discord.PCMVolumeTransformer):
|
||||
async def create_source(cls, ctx, search: str, *, loop, download=False, silent=False, artist='Unknown', song_title='Unknown'):
|
||||
loop = loop or asyncio.get_event_loop()
|
||||
|
||||
# Find out which song the user wants
|
||||
if not validators.url(search):
|
||||
# Fetch song metadata
|
||||
url = f'http://ws.audioscrobbler.com/2.0/?method=track.search&track={search}&api_key={LASTFM_API_KEY}&format=json'
|
||||
# If we got a YouTube link, get the video title for the song search
|
||||
if validators.url(search):
|
||||
with YoutubeDL() as ydl:
|
||||
info = ydl.extract_info(search, download=False)
|
||||
search_term = info.get('title', '')
|
||||
else:
|
||||
search_term = search
|
||||
|
||||
# Get song metadata
|
||||
logger.info(f"Searching LastFM for: '{search_term}'")
|
||||
url = f"http://ws.audioscrobbler.com/2.0/?method=track.search&"\
|
||||
f"track={search_term}&api_key={LASTFM_API_KEY}&format=json"
|
||||
response = requests.get(url)
|
||||
lastfm_data = response.json()
|
||||
|
||||
# Let's get the first result, if any
|
||||
if lastfm_data['results']['trackmatches']['track']:
|
||||
track = lastfm_data['results']['trackmatches']['track'][0]
|
||||
search = track['artist'] + ' ' + track['name'] + ' album version'
|
||||
artist = track['artist']
|
||||
song_title = track['name']
|
||||
else:
|
||||
return
|
||||
|
||||
# Get source
|
||||
# Adjust search term if we didn't get a URL
|
||||
if not validators.url(search):
|
||||
search = f"{song_title} {artist} official audio"
|
||||
|
||||
# Get YouTube video source
|
||||
logger.info(f"Getting YouTube video: {search}")
|
||||
to_run = partial(ytdl.extract_info, url=search, download=download)
|
||||
data = await loop.run_in_executor(None, to_run)
|
||||
|
||||
@ -114,14 +196,12 @@ class YTDLSource(discord.PCMVolumeTransformer):
|
||||
return {'webpage_url': data['webpage_url'], 'requester': ctx.author, 'title': data['title']}
|
||||
|
||||
ffmpeg_source = cls(discord.FFmpegPCMAudio(source), data=data, requester=ctx.author)
|
||||
# TODO: ADD THESE TO THE CONSTRUCTOR
|
||||
ffmpeg_source.search_term = search_term
|
||||
ffmpeg_source.artist = artist
|
||||
ffmpeg_source.song_title = song_title
|
||||
ffmpeg_source.filename = source
|
||||
|
||||
if not silent:
|
||||
embed = discord.Embed(title="Queued", description=f"[{ffmpeg_source.song_title} by {ffmpeg_source.artist}]({data['webpage_url']})", color=discord.Color.green())
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
return ffmpeg_source
|
||||
|
||||
@classmethod
|
||||
@ -144,7 +224,11 @@ class MusicPlayer:
|
||||
When the bot disconnects from the Voice it's instance will be destroyed.
|
||||
"""
|
||||
|
||||
__slots__ = ('bot', '_guild', '_channel', '_cog', 'queue', 'next', 'current', 'np', 'volume')
|
||||
__slots__ = ('bot', '_guild', '_channel', '_cog', 'queue', 'next', 'current', 'np', 'volume', 'dj_mode')
|
||||
|
||||
# Each player is assiciated with a guild, so create a lock for when we do
|
||||
# volatile things in the server like delete previous messages
|
||||
guild_lock_ = asyncio.Lock()
|
||||
|
||||
def __init__(self, ctx):
|
||||
self.bot = ctx.bot
|
||||
@ -158,6 +242,7 @@ class MusicPlayer:
|
||||
self.np = None # Now playing message
|
||||
self.volume = .5
|
||||
self.current = None
|
||||
self.dj_mode = False
|
||||
|
||||
ctx.bot.loop.create_task(self.player_loop())
|
||||
|
||||
@ -168,12 +253,33 @@ class MusicPlayer:
|
||||
while not self.bot.is_closed():
|
||||
self.next.clear()
|
||||
|
||||
# Always get a song if there's one in the queue
|
||||
if self.queue.qsize() > 0 or self.dj_mode is False:
|
||||
logger.info("Getting song from play queue")
|
||||
try:
|
||||
# Wait for the next song. If we timeout cancel the player and disconnect...
|
||||
# Wait for the next song. If we timeout cancel the player
|
||||
# and disconnect...
|
||||
async with timeout(300): # 5 minutes...
|
||||
source = await self.queue.get()
|
||||
except asyncio.TimeoutError:
|
||||
return self.destroy(self._guild)
|
||||
# Otherwise we're in DJ mode and a user hasn't requested one, so
|
||||
# pick a song at random and create a source for it
|
||||
else:
|
||||
logger.info(
|
||||
"Queue is empty and DJ mode is on. Picking song at random")
|
||||
source = await YTDLSource.create_source(
|
||||
None, random.choice(songs),
|
||||
loop=self.bot.loop,
|
||||
download=True
|
||||
)
|
||||
|
||||
# For the time being, we're going to use 'None' to signal to the
|
||||
# player that is should go back around and check for a song again,
|
||||
# mainly because DJ mode was switched on and it should pick a song
|
||||
# at random this time
|
||||
if source is None:
|
||||
continue
|
||||
|
||||
if not isinstance(source, YTDLSource):
|
||||
# Source was probably a stream (not downloaded)
|
||||
@ -188,14 +294,16 @@ class MusicPlayer:
|
||||
source.volume = self.volume
|
||||
self.current = source
|
||||
|
||||
logger.info(f"Playing '{source.song_title}' by '{source.artist}'")
|
||||
self._guild.voice_client.play(source, after=lambda _: self.bot.loop.call_soon_threadsafe(self.next.set))
|
||||
|
||||
logger.info("Updating presense and 'now playing' message")
|
||||
await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.custom, name="custom", state=f"🎵 {source.song_title} by {source.artist}"))
|
||||
embed = discord.Embed(title="Now playing", description=f"[{source.song_title} by {source.artist}]({source.web_url})", color=discord.Color.green())
|
||||
self.np = await self._channel.send(embed=embed)
|
||||
await self.update_now_playing_message(repost=True)
|
||||
|
||||
logger.info("Waiting for song to finish")
|
||||
await self.next.wait()
|
||||
|
||||
await self.bot.change_presence(status=None)
|
||||
if os.path.exists(source.filename):
|
||||
os.remove(source.filename)
|
||||
|
||||
@ -206,6 +314,52 @@ class MusicPlayer:
|
||||
pass
|
||||
self.current = None
|
||||
|
||||
# Update bot statuses to match no song playing
|
||||
await self.bot.change_presence(status=None)
|
||||
await self.update_now_playing_message(repost=False)
|
||||
|
||||
async def update_now_playing_message(self, repost=False, emoji='▶️'):
|
||||
await self.bot.wait_until_ready()
|
||||
|
||||
# Create and post new 'Now Playing' message
|
||||
embed = discord.Embed(
|
||||
title=f"{emoji} Now Playing", color=discord.Color.green())
|
||||
|
||||
# Get and add the thumbnail
|
||||
if self.current:
|
||||
embed.set_thumbnail(url=self.current.thumbnail_url)
|
||||
embed.add_field(
|
||||
name="",
|
||||
value=f"[{self.current.song_title}]({self.current.web_url}) - "
|
||||
f"{self.current.artist}",
|
||||
inline=False
|
||||
)
|
||||
|
||||
# Add all upcoming songs
|
||||
# Possibly dangerous, but only obvious solution
|
||||
queue = [s for s in self.queue._queue if s is not None]
|
||||
if self.queue.qsize() > 0:
|
||||
value_str = ""
|
||||
for i, song in enumerate(queue):
|
||||
value_str += f"{i+1}. [{song.song_title}]({song.web_url}) - "\
|
||||
f"{song.artist}\n"
|
||||
embed.add_field(name="Queue", value=value_str, inline=False)
|
||||
|
||||
# There's a chance we'll delete a message another coroutine is using, so
|
||||
# lock the messages
|
||||
async with self. guild_lock_:
|
||||
async for message in self._channel.history(limit=50):
|
||||
if len(message.embeds) == 1 and message.embeds[0].title and "Now Playing" in message.embeds[0].title:
|
||||
if repost:
|
||||
await message.delete()
|
||||
else:
|
||||
await message.edit(embed=embed)
|
||||
break
|
||||
|
||||
if repost:
|
||||
self.np = await self._channel.send(embed=embed)
|
||||
|
||||
|
||||
def destroy(self, guild):
|
||||
"""Disconnect and cleanup the player."""
|
||||
return self.bot.loop.create_task(self._cog.cleanup(guild))
|
||||
@ -219,23 +373,27 @@ class Music(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.players = {}
|
||||
self.last_tag_play_time = datetime.datetime.now() - datetime.timedelta(minutes=30)
|
||||
self.last_tag_play_time = datetime.datetime.now()
|
||||
|
||||
def update_cache():
|
||||
with yt_dlp.YoutubeDL({'quiet': True}) as ydl:
|
||||
self.boywife_tracks = ydl.extract_info('https://soundcloud.com/djboywife', download=False)['entries']
|
||||
# Get a reference to the database
|
||||
# TODO: MAKE THIS INJECTED
|
||||
self.db = database.Database("boywife_bot.db")
|
||||
|
||||
with open('soundcloud-cache', 'w') as f:
|
||||
f.write(str(self.boywife_tracks))
|
||||
# pickle.dump(self.boywife_tracks, f)
|
||||
# def update_cache():
|
||||
# with yt_dlp.YoutubeDL({'quiet': True}) as ydl:
|
||||
# self.boywife_tracks = ydl.extract_info('https://soundcloud.com/djboywife', download=False)['entries']
|
||||
|
||||
if os.path.exists('soundcloud-cache'):
|
||||
with open('soundcloud-cache', 'r') as f:
|
||||
exec(f'self.boywife_tracks = {f.read()}')
|
||||
# self.boywife_tracks = pickle.load(f)
|
||||
threading.Thread(target=update_cache).start()
|
||||
else:
|
||||
update_cache()
|
||||
# with open('soundcloud-cache', 'w') as f:
|
||||
# f.write(str(self.boywife_tracks))
|
||||
# # pickle.dump(self.boywife_tracks, f)
|
||||
|
||||
# if os.path.exists('soundcloud-cache'):
|
||||
# with open('soundcloud-cache', 'r') as f:
|
||||
# exec(f'self.boywife_tracks = {f.read()}')
|
||||
# # self.boywife_tracks = pickle.load(f)
|
||||
# threading.Thread(target=update_cache).start()
|
||||
# else:
|
||||
# update_cache()
|
||||
|
||||
|
||||
async def cleanup(self, guild):
|
||||
@ -311,9 +469,7 @@ class Music(commands.Cog):
|
||||
await channel.connect()
|
||||
except asyncio.TimeoutError:
|
||||
raise VoiceConnectionError(f'Connecting to channel: <{channel}> timed out.')
|
||||
if (random.randint(0, 1) == 0):
|
||||
await ctx.message.add_reaction('👍')
|
||||
# await ctx.send(f'**Joined `{channel}`**')
|
||||
# await ctx.message.add_reaction('👍')
|
||||
|
||||
@commands.command(name='play', aliases=['sing','p'], description="streams music")
|
||||
async def play_(self, ctx, *, search: str = None):
|
||||
@ -325,29 +481,59 @@ class Music(commands.Cog):
|
||||
search: str [Required]
|
||||
The song to search and retrieve using YTDL. This could be a simple search, an ID or URL.
|
||||
"""
|
||||
# Ensure we're connected to the proper voice channel
|
||||
vc = ctx.voice_client
|
||||
|
||||
if not vc:
|
||||
await ctx.invoke(self.connect_)
|
||||
|
||||
player = self.get_player(ctx)
|
||||
# Send message to say we're working on it
|
||||
embed = discord.Embed(
|
||||
title=f"🔎 Searching for:",
|
||||
description=f"{search}",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
message = await ctx.channel.send(embed=embed)
|
||||
|
||||
# Play tag every 30 minutes
|
||||
if datetime.datetime.now() - self.last_tag_play_time > datetime.timedelta(minutes=30):
|
||||
self.last_tag_play_time = datetime.datetime.now()
|
||||
source = await YTDLSource.create_source(ctx, 'https://soundcloud.com/djboywife/youre-listening-to-dj-boywife', loop=self.bot.loop,
|
||||
download=True, silent=True, artist='DJ Boywife', song_title="Tag")
|
||||
await player.queue.put(source)
|
||||
|
||||
# If no song is given, pick a soundcloud track at random
|
||||
if not search:
|
||||
track = random.choice(self.boywife_tracks)
|
||||
source = await YTDLSource.create_source(ctx, track['formats'][0]['url'], loop=self.bot.loop,
|
||||
download=True, silent=True, artist=track['uploader'], song_title=track['title'])
|
||||
else:
|
||||
# Create source
|
||||
try:
|
||||
source = await YTDLSource.create_source(ctx, search, loop=self.bot.loop, download=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Update previous message to show found song and video
|
||||
embed = discord.Embed(
|
||||
title=f"Queued",
|
||||
description=f"[{source.song_title}]({source.web_url}) - {source.artist}",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
embed.set_thumbnail(url=source.thumbnail_url)
|
||||
await message.edit(embed=embed)
|
||||
|
||||
# Add song to the corresponding player object
|
||||
player = self.get_player(ctx)
|
||||
await player.queue.put(source)
|
||||
await player.update_now_playing_message()
|
||||
|
||||
@commands.command(name="djmode", aliases=['dj'], description="Turns DJ mode on or off.")
|
||||
async def djmode_(self, ctx, *, mode: str = "on"):
|
||||
"""Turns DJ mode on or off. When on, the bot will play songs
|
||||
automatically."""
|
||||
# Get desired mode
|
||||
mode = mode.lower().strip()
|
||||
if mode in ("true", "t", "yes", "y", "on"):
|
||||
mode = True
|
||||
elif mode in ("false", "f", "no", "n", "off"):
|
||||
mode = False
|
||||
else:
|
||||
return
|
||||
|
||||
# Switch to desired mode
|
||||
player = self.get_player(ctx)
|
||||
player.dj_mode = mode
|
||||
|
||||
# Break player out of waiting on queue so it can pick a song at random
|
||||
if player.dj_mode:
|
||||
await player.queue.put(None)
|
||||
|
||||
@commands.command(name='pause', description="pauses music")
|
||||
async def pause_(self, ctx):
|
||||
@ -361,7 +547,10 @@ class Music(commands.Cog):
|
||||
return
|
||||
|
||||
vc.pause()
|
||||
await ctx.send("Paused ⏸️")
|
||||
|
||||
# Update the 'Now Playing' message to reflect its paused
|
||||
player = self.get_player(ctx)
|
||||
await player.update_now_playing_message(emoji='⏸️')
|
||||
|
||||
@commands.command(name='resume', description="resumes music")
|
||||
async def resume_(self, ctx):
|
||||
@ -375,7 +564,10 @@ class Music(commands.Cog):
|
||||
return
|
||||
|
||||
vc.resume()
|
||||
await ctx.send("Resuming ⏯️")
|
||||
|
||||
# Update the 'Now Playing' message to reflect its resumed
|
||||
player = self.get_player(ctx)
|
||||
await player.update_now_playing_message()
|
||||
|
||||
@commands.command(name='skip', description="skips to next song in queue")
|
||||
async def skip_(self, ctx):
|
||||
@ -393,7 +585,7 @@ class Music(commands.Cog):
|
||||
|
||||
vc.stop()
|
||||
|
||||
@commands.command(name='remove', aliases=['rm', 'rem'], description="removes specified song from queue")
|
||||
@commands.command(name='remove', aliases=['rm'], description="removes specified song from queue")
|
||||
async def remove_(self, ctx, pos : int=None):
|
||||
"""Removes specified song from queue"""
|
||||
|
||||
@ -430,69 +622,6 @@ class Music(commands.Cog):
|
||||
player.queue._queue.clear()
|
||||
await ctx.send('**Cleared**')
|
||||
|
||||
@commands.command(name='queue', aliases=['q', 'playlist', 'que'], description="shows the queue")
|
||||
async def queue_info(self, ctx):
|
||||
"""Retrieve a basic queue of upcoming songs."""
|
||||
vc = ctx.voice_client
|
||||
|
||||
if not vc or not vc.is_connected():
|
||||
embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green())
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
player = self.get_player(ctx)
|
||||
if player.queue.empty():
|
||||
embed = discord.Embed(title="", description="The queue is empty", color=discord.Color.green())
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
seconds = vc.source.duration % (24 * 3600)
|
||||
hour = seconds // 3600
|
||||
seconds %= 3600
|
||||
minutes = seconds // 60
|
||||
seconds %= 60
|
||||
if hour > 0:
|
||||
duration = "%dh %02dm %02ds" % (hour, minutes, seconds)
|
||||
else:
|
||||
duration = "%02dm %02ds" % (minutes, seconds)
|
||||
|
||||
# Grabs the songs in the queue...
|
||||
upcoming = list(itertools.islice(player.queue._queue, 0, int(len(player.queue._queue))))
|
||||
# fmt = '\n'.join(f"`{(upcoming.index(_)) + 1}.` [{_['title']}]({_['webpage_url']}) | ` {duration} Requested by: {_['requester']}`\n" for _ in upcoming)
|
||||
fmt = '\n'.join(f"`{(upcoming.index(_)) + 1}.` {_['title']} | ` {duration} Requested by: {_['requester']}`\n" for _ in upcoming)
|
||||
fmt = f"\n__Now Playing__:\n[{vc.source.title}]({vc.source.web_url})\n\n__Up Next:__\n" + fmt
|
||||
embed = discord.Embed(title=f'Queue for {ctx.guild.name}', description=fmt, color=discord.Color.green())
|
||||
# embed.set_footer(text=f"{ctx.author.display_name}", icon_url=ctx.author.avatar_url)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name='np', aliases=['song', 'current', 'currentsong', 'playing'], description="shows the current playing song")
|
||||
async def now_playing_(self, ctx):
|
||||
"""Display information about the currently playing song."""
|
||||
vc = ctx.voice_client
|
||||
|
||||
if not vc or not vc.is_connected():
|
||||
embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green())
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
player = self.get_player(ctx)
|
||||
if not player.current:
|
||||
embed = discord.Embed(title="", description="I am currently not playing anything", color=discord.Color.green())
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
seconds = vc.source.duration % (24 * 3600)
|
||||
hour = seconds // 3600
|
||||
seconds %= 3600
|
||||
minutes = seconds // 60
|
||||
seconds %= 60
|
||||
if hour > 0:
|
||||
duration = "%dh %02dm %02ds" % (hour, minutes, seconds)
|
||||
else:
|
||||
duration = "%02dm %02ds" % (minutes, seconds)
|
||||
|
||||
embed = discord.Embed(title="", description=f"[{vc.source.title}]({vc.source.web_url}) [{vc.source.requester.mention}] | `{duration}`", color=discord.Color.green())
|
||||
#embed.set_author(icon_url=self.bot.user.avatar_url, name=f"Now Playing 🎶")
|
||||
embed.set_author(name=f"Now Playing 🎶")
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name='volume', aliases=['vol', 'v'], description="changes Kermit's volume")
|
||||
async def change_volume(self, ctx, *, vol: float=None):
|
||||
"""Change the player volume.
|
||||
@ -536,9 +665,7 @@ class Music(commands.Cog):
|
||||
embed = discord.Embed(title="", description="I'm not connected to a voice channel", color=discord.Color.green())
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
if (random.randint(0, 1) == 0):
|
||||
await ctx.message.add_reaction('👋')
|
||||
#await ctx.send('**Successfully disconnected**')
|
||||
|
||||
await self.cleanup(ctx.guild)
|
||||
|
||||
|
||||
443
database.py
Normal file
443
database.py
Normal file
@ -0,0 +1,443 @@
|
||||
from datetime import datetime, timedelta
|
||||
import discord
|
||||
import sqlite3
|
||||
import typing
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from cogs import music_player
|
||||
|
||||
class Database:
|
||||
def __init__(self, path: str = "boywife_bot.db"):
|
||||
self.path = path
|
||||
self._ensure_db()
|
||||
|
||||
# # TEMP: THIS IS FOR MIGRATING THE PREVIOUS DATABASE SCHEMA
|
||||
# with sqlite3.connect("ingest.db") as conn:
|
||||
# cursor = conn.cursor()
|
||||
# cursor.execute("""
|
||||
# SELECT
|
||||
# user_id,
|
||||
# before_activity_type,
|
||||
# before_activity_name,
|
||||
# after_activity_type,
|
||||
# after_activity_name,
|
||||
# timestamp
|
||||
# FROM
|
||||
# user_activity
|
||||
# """)
|
||||
# activities = cursor.fetchall()
|
||||
|
||||
# for activity in tqdm(activities):
|
||||
|
||||
# # Convert activity type
|
||||
# if activity[1] == "activity":
|
||||
# new_before_type = "playing"
|
||||
# elif activity[1] == "spotify":
|
||||
# new_before_type = "listening"
|
||||
# elif activity[1] == "game":
|
||||
# new_before_type = "playing"
|
||||
# else:
|
||||
# new_before_type = None
|
||||
# if activity[3] == "activity":
|
||||
# new_after_type = "playing"
|
||||
# elif activity[3] == "spotify":
|
||||
# new_after_type = "listening"
|
||||
# elif activity[3] == "game":
|
||||
# new_after_type = "playing"
|
||||
# else:
|
||||
# new_after_type = None
|
||||
|
||||
# new_before_name = None if activity[2] == "other" else activity[2]
|
||||
# new_after_name = None if activity[4] == "other" else activity[4]
|
||||
|
||||
# user_id = self._insert_user(activity[0])
|
||||
|
||||
# with sqlite3.connect(self.path) as conn:
|
||||
# cursor = conn.cursor()
|
||||
# cursor.execute("""
|
||||
# INSERT INTO activity_change (
|
||||
# user_id,
|
||||
# before_activity_type,
|
||||
# before_activity_name,
|
||||
# before_activity_status,
|
||||
# after_activity_type,
|
||||
# after_activity_name,
|
||||
# after_activity_status,
|
||||
# timestamp
|
||||
# ) VALUES (
|
||||
# ?, ?, ?, ?, ?, ?, ?, ?
|
||||
# )
|
||||
# """, (
|
||||
# user_id,
|
||||
# new_before_type,
|
||||
# new_before_name,
|
||||
# "unknown",
|
||||
# new_after_type,
|
||||
# new_after_name,
|
||||
# "unknown",
|
||||
# datetime.datetime.fromisoformat(activity[5])
|
||||
# ))
|
||||
|
||||
def _ensure_db(self):
|
||||
with sqlite3.connect(self.path) as conn:
|
||||
|
||||
# Table for keeping track of servers
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS server (
|
||||
id INTEGER PRIMARY KEY,
|
||||
discord_id INTEGER NOT NULL UNIQUE
|
||||
)
|
||||
""")
|
||||
|
||||
# Table for keeping track of channels
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS channel (
|
||||
id INTEGER PRIMARY KEY,
|
||||
discord_id INTEGER NOT NULL UNIQUE
|
||||
)
|
||||
""")
|
||||
|
||||
# Table for keeping track of users
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id INTEGER PRIMARY KEY,
|
||||
discord_id INTEGER NOT NULL UNIQUE
|
||||
)
|
||||
""")
|
||||
|
||||
# Create the activity table
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS activity_change (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
before_activity_type TEXT,
|
||||
before_activity_name TEXT,
|
||||
before_activity_status TEXT NOT NULL,
|
||||
after_activity_type TEXT,
|
||||
after_activity_name TEXT,
|
||||
after_activity_status TEXT NOT NULL,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Create the song request table
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS song_request (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
channel_id INTEGER NOT NULL,
|
||||
search_term TEXT NOT NULL,
|
||||
song_title TEXT,
|
||||
song_artist TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
# Table for songs that actually get played
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS song_play (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
channel_id INTEGER NOT NULL,
|
||||
search_term TEXT NOT NULL,
|
||||
song_title TEXT,
|
||||
song_artist TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
def _insert_server(self, discord_id: int = None) -> int:
|
||||
"""Inserts Discord server ID into the 'server' table.
|
||||
|
||||
This method takes an ID for a server used in Discord, and inserts it
|
||||
into the database. It ignores the case where the server ID is already
|
||||
present. It then returns the row ID regardless.
|
||||
|
||||
Args:
|
||||
discord_id (int): The ID used to identify the server in Discord.
|
||||
|
||||
Returns:
|
||||
int: The ID of the server in the server table.
|
||||
|
||||
Examples:
|
||||
>>> db = Database("path.db")
|
||||
>>> db._insert_server(850610922256442889)
|
||||
12
|
||||
"""
|
||||
with sqlite3.connect(self.path) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Insert it; ignoring already exists error
|
||||
cursor.execute("""
|
||||
INSERT INTO server (discord_id)
|
||||
VALUES (?)
|
||||
ON CONFLICT(discord_id) DO NOTHING
|
||||
RETURNING id;
|
||||
""", (discord_id,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
row_id = row[0]
|
||||
else:
|
||||
# Get row ID if it already exists and wasn't inserted
|
||||
cursor.execute("""
|
||||
SELECT id FROM server WHERE discord_id = ?
|
||||
""", (discord_id,))
|
||||
row_id = cursor.fetchone()[0]
|
||||
return row_id
|
||||
|
||||
def _insert_channel(self, discord_id: int = None) -> int:
|
||||
"""Inserts Discord channel ID into the 'channel' table.
|
||||
|
||||
This method takes an ID for a channel used in Discord, and inserts it
|
||||
into the database. It ignores the case where the channel ID is already
|
||||
present. It then returns the row ID regardless.
|
||||
|
||||
Args:
|
||||
discord_id (int): The ID used to identify the channel in Discord.
|
||||
|
||||
Returns:
|
||||
int: The ID of the channel in the channel table.
|
||||
|
||||
Examples:
|
||||
>>> db = Database("path.db")
|
||||
>>> db._insert_channel(8506109222564428891)
|
||||
12
|
||||
"""
|
||||
with sqlite3.connect(self.path) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Insert it; ignoring already exists error
|
||||
cursor.execute("""
|
||||
INSERT INTO channel (discord_id)
|
||||
VALUES (?)
|
||||
ON CONFLICT(discord_id) DO NOTHING
|
||||
RETURNING id;
|
||||
""", (discord_id,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
row_id = row[0]
|
||||
else:
|
||||
# Get row ID if it already exists and wasn't inserted
|
||||
cursor.execute("""
|
||||
SELECT id FROM channel WHERE discord_id = ?
|
||||
""", (discord_id,))
|
||||
row_id = cursor.fetchone()[0]
|
||||
return row_id
|
||||
|
||||
def _insert_user(self, discord_id: int = None) -> int:
|
||||
"""Inserts Discord user ID into the 'user' table.
|
||||
|
||||
This method takes an ID for a user used in Discord, and inserts it
|
||||
into the database. It ignores the case where the user ID is already
|
||||
present. It then returns the row ID regardless.
|
||||
|
||||
Args:
|
||||
discord_id (int): The ID used to identify the user in Discord.
|
||||
|
||||
Returns:
|
||||
int: The ID of the user in the user table.
|
||||
|
||||
Examples:
|
||||
>>> db = Database("path.db")
|
||||
>>> db._insert_user(850610922256442889)
|
||||
12
|
||||
"""
|
||||
with sqlite3.connect(self.path) as conn:
|
||||
cursor = conn.cursor()
|
||||
# Insert it; ignoring already exists error
|
||||
cursor.execute("""
|
||||
INSERT INTO user (discord_id)
|
||||
VALUES (?)
|
||||
ON CONFLICT(discord_id) DO NOTHING
|
||||
RETURNING id;
|
||||
""", (discord_id,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
row_id = row[0]
|
||||
else:
|
||||
# Get row ID if it already exists and wasn't inserted
|
||||
cursor.execute("""
|
||||
SELECT id FROM user WHERE discord_id = ?
|
||||
""", (discord_id,))
|
||||
row_id = cursor.fetchone()[0]
|
||||
return row_id
|
||||
|
||||
def insert_activity_change(
|
||||
self,
|
||||
before: discord.Member,
|
||||
after: discord.Member):
|
||||
"""Inserts an activity change into the database.
|
||||
|
||||
This method takes two discord.Memeber objects, and records the change
|
||||
in activity into the 'activity_change' table.
|
||||
|
||||
Args:
|
||||
before (discord.Member): The previous user status.
|
||||
after (discord.Member): The current user status.
|
||||
|
||||
Raises:
|
||||
ValueError: If the before and after activity do not refer to the
|
||||
same user.
|
||||
|
||||
Examples:
|
||||
>>> @commands.Cog.listener()
|
||||
>>> async def on_presence_update(
|
||||
... self,
|
||||
... before: discord.Member,
|
||||
... after: discord.Member):
|
||||
... db = Database("path.db")
|
||||
... db.insert_activity_change(before, after)
|
||||
>>>
|
||||
"""
|
||||
# Ensure the users are the same
|
||||
if before.id != after.id:
|
||||
raise ValueError("User IDs do not match.")
|
||||
user_id = self._insert_user(before.id)
|
||||
# Get activities if they exist
|
||||
before_type = before.activity.type.name if before.activity else None
|
||||
before_name = before.activity.name if before.activity else None
|
||||
after_type = after.activity.type.name if after.activity else None
|
||||
after_name = after.activity.name if after.activity else None
|
||||
# Insert the activity change
|
||||
with sqlite3.connect(self.path) as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO activity_change (
|
||||
user_id,
|
||||
before_activity_type,
|
||||
before_activity_name,
|
||||
before_activity_status,
|
||||
after_activity_type,
|
||||
after_activity_name,
|
||||
after_activity_status
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
""", (
|
||||
user_id,
|
||||
before_type,
|
||||
before_name,
|
||||
before.status.name,
|
||||
after_type,
|
||||
after_name,
|
||||
after.status.name
|
||||
))
|
||||
|
||||
def insert_song_request(
|
||||
self,
|
||||
message: discord.Message,
|
||||
source: music_player.YTDLSource):
|
||||
"""Inserts a song request into the database.
|
||||
|
||||
This method takes a message and its derived music source and inserts
|
||||
the relevant information into the 'song_request' table.
|
||||
|
||||
Args:
|
||||
message (discord.Message): The Discord message requesting the song.
|
||||
source (music_player.YTDLSource): The audio source.
|
||||
"""
|
||||
# Insert the information
|
||||
with sqlite3.connect(self.path) as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO song_request (
|
||||
user_id,
|
||||
channel_id,
|
||||
search_term,
|
||||
song_title,
|
||||
song_artist
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?
|
||||
)
|
||||
""", (
|
||||
self._insert_user(message.author.id),
|
||||
self._insert_channel(message.channel.id),
|
||||
source.search_term,
|
||||
source.song_title,
|
||||
source.artist
|
||||
))
|
||||
|
||||
def insert_song_play(
|
||||
self,
|
||||
channel_id: int,
|
||||
source: music_player.YTDLSource):
|
||||
"""Inserts a song play into the database.
|
||||
|
||||
This method takes a channel and the song being played and inserts the
|
||||
relevant information into the 'song_play' table.
|
||||
|
||||
Args:
|
||||
channel (int): The Discord channel the song is being played in.
|
||||
source (music_player.YTDLSource): The audio source.
|
||||
"""
|
||||
user_id = self._insert_user(source.requester.id)
|
||||
channel_id = self._insert_user(channel.id)
|
||||
# Insert the information
|
||||
with sqlite3.connect(self.path) as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO song_play (
|
||||
user_id,
|
||||
channel_id,
|
||||
search_term,
|
||||
song_title,
|
||||
song_artist
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?
|
||||
)
|
||||
""", (
|
||||
user_id,
|
||||
channel_id,
|
||||
source.search_term,
|
||||
source.song_title,
|
||||
source.artist
|
||||
))
|
||||
|
||||
def get_activity_stats(
|
||||
self,
|
||||
member: typing.Union[discord.Member, int],
|
||||
start: datetime = datetime.now() - timedelta(days=30)
|
||||
)-> dict[str, timedelta]:
|
||||
"""Gets stats on the activities of the given member.
|
||||
|
||||
This method searches the database for activity changes by the given
|
||||
user and computes the amount of time spent in each activity.
|
||||
|
||||
Args:
|
||||
member (discord.Member): The Discord member to get stats for.
|
||||
start (datetime): The earliest activity change to get.
|
||||
|
||||
Returns:
|
||||
dict[str, timedelta]: A dictionary of activity names and
|
||||
seconds in each.
|
||||
"""
|
||||
# Get member Discord ID and convert to DB ID
|
||||
member_id = member.id if isinstance(member, discord.Member) else member
|
||||
member_id = self._insert_user(member_id)
|
||||
# Pull all activities for this user
|
||||
with sqlite3.connect(self.path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
before_activity_name,
|
||||
after_activity_name,
|
||||
timestamp
|
||||
FROM
|
||||
activity_change
|
||||
WHERE
|
||||
user_id = (?) AND
|
||||
timestamp > (?)
|
||||
""", (member_id, start))
|
||||
activities = cursor.fetchall()
|
||||
# Collect activities
|
||||
activity_stats = {}
|
||||
for first, second in zip(activities, activities[1:]):
|
||||
if first[1] == second[0]:
|
||||
activity_name = first[1]
|
||||
activity_time = \
|
||||
datetime.fromisoformat(second[2]) - \
|
||||
datetime.fromisoformat(first[2])
|
||||
if activity_name in activity_stats:
|
||||
activity_stats[activity_name] += activity_time
|
||||
else:
|
||||
activity_stats[activity_name] = activity_time
|
||||
if None in activity_stats:
|
||||
del activity_stats[None]
|
||||
return activity_stats
|
||||
Loading…
x
Reference in New Issue
Block a user