Skip to content

Commit

Permalink
merge upstream
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-je committed Nov 28, 2021
2 parents dab7c03 + cf1c80c commit e7fc47e
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 20 deletions.
1 change: 1 addition & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ RUN source venv/bin/activate && python3.8 -m pip install -r requirements.txt

# Copy necessary files
COPY *.py .
COPY *.ogg .

# Command to run
CMD source venv/bin/activate && python3.8 bot.py
99 changes: 80 additions & 19 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""
Serves a bot on Discord for playing music in voice chat, as well as some fun additions.
"""
# pylint: disable=import-error
# pylint: disable=missing-module-docstring
# pylint: disable=no-self-use
# pylint: disable=too-many-public-methods
# pylint: disable=too-many-locals
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-arguments


import os
Expand All @@ -15,6 +21,7 @@
import discord
import jokeapi
import youtubesearchpython
import pytube
import pafy_fixed.pafy_fixed as pafy


Expand Down Expand Up @@ -43,7 +50,7 @@ async def on_message(self, message):

async def on_error(
self, event_name, *args, **kwargs
): # pylint: disable=arguments-differ,no-self-use
): # pylint: disable=arguments-differ
"""
Notify user of error and log it
"""
Expand Down Expand Up @@ -83,14 +90,13 @@ class MusicBot:
The main bot functionality
"""

# pylint: disable=no-self-use
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods

COMMAND_PREFIX = "-"
REACTION_EMOJI = "👍"
DOCS_URL = "github.com/michael-je/the-lone-dancer"
DISCONNECT_TIMER_SECONDS = 600
TEXTWIDTH = 60
SYNTAX_LANGUAGE = "arm"
N_PLAYLIST_SHOW = 10

END_OF_QUEUE_MSG = ":sparkles: End of queue"

Expand All @@ -111,6 +117,7 @@ def __init__(self, guild, loop, dispatcher_user):
r"http[s]?://"
r"(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
)
self.playlist_regex = re.compile(r"\b(?:play)?list\b\=(\w+)")

# Boolean to control whether the after callback is called
self.after_callback_blocked = False
Expand Down Expand Up @@ -220,7 +227,7 @@ def __init__(self, guild, loop, dispatcher_user):
handler=self.joke,
)

def register_command( # pylint: disable=too-many-arguments
def register_command(
self,
command_name,
help_message: str,
Expand Down Expand Up @@ -260,10 +267,8 @@ def register_command( # pylint: disable=too-many-arguments
if guarded_by:

async def guarded_handler(*args):
logging.info("Aquiring lock")
async with guarded_by:
return await handler(*args)
logging.info("Releasing lock")

self.handlers[command_name] = guarded_handler
else:
Expand Down Expand Up @@ -341,7 +346,7 @@ def after_callback(self, _):
"""
if not self.after_callback_blocked:
self.loop.create_task(self.attempt_disconnect())
self.next_in_queue()
self.loop.create_task(self.next_in_queue())
else:
self.after_callback_blocked = False

Expand All @@ -358,7 +363,7 @@ def create_audio_source(self, audio_url):
"""Creates an audio sorce from an audio url"""
return discord.FFmpegPCMAudio(audio_url)

def next_in_queue(self):
async def next_in_queue(self):
"""
Switch to next song in queue
"""
Expand Down Expand Up @@ -386,10 +391,8 @@ def next_in_queue(self):
self.voice_client.play(audio_source, after=self.after_callback)
logging.info("Audio source started")

self.loop.create_task(
message.channel.send(
f":notes: Now Playing :notes:\n```\n{media.title}\n```"
)
await message.channel.send(
f":notes: Now Playing :notes:\n```\n{media.title}\n```"
)

async def create_or_get_voice_client(self, message):
Expand Down Expand Up @@ -457,6 +460,7 @@ async def attempt_disconnect(self):

if time.time() - self.last_played_time < self.DISCONNECT_TIMER_SECONDS:
return
logging.info("Disconnecting from voice chat due to inactivity")

self._stop()
await self.voice_client.disconnect()
Expand All @@ -474,6 +478,57 @@ async def notify_if_voice_client_is_missing(self, message):
return True
return False

async def playlist(self, message, command_content):
"""Play a playlist"""
logging.info("Fetching playlist for user %s", message.author)
playlist = pytube.Playlist(command_content)
await message.add_reaction(MusicBot.REACTION_EMOJI)
added = []
n_failed = 0
progress = 0
total = len(playlist)
status_fmt = "Fetching playlist... {}"
reply = await message.channel.send(status_fmt.format(""))
for video in playlist:
await reply.edit(content=status_fmt.format(f"{progress/total:.0%}"))
progress += 1
try:
media = pafy.new(video)
except KeyError as err:
logging.error(err)
n_failed += 1
continue
self.media_queue.put((media, message))
added.append(media)
if len(added) == 1 and not self.voice_client.is_playing():
await self.next_in_queue()
logging.info("%d items added to queue, %d failed", len(added), n_failed)

final_status = ""
final_status += f":clipboard: Added {len(added)} of "
final_status += f"{len(added)+n_failed} songs to queue :notes:\n"
final_status += f"```{self.SYNTAX_LANGUAGE}"
final_status += "\n"
for media in added[: self.N_PLAYLIST_SHOW]:
title = media.title
titlewidth = self.TEXTWIDTH - 10
if len(title) > titlewidth:
title = title[: titlewidth - 3] + "..."

duration_m = int(media.duration[:2]) * 60 + int(media.duration[3:5])
duration_s = int(media.duration[6:])
duration = f"({duration_m}:{duration_s:0>2})"
# Time: 5-6 char + () + buffer = 10
final_status += f"{title:<{titlewidth}}{duration:>10}"
final_status += "\n"
if len(added) >= self.N_PLAYLIST_SHOW:
final_status += "...\n"
final_status += "```"

logging.debug("final status message: \n%s", final_status)

await reply.edit(content=final_status)

async def play(self, message, command_content):
"""
Play URL or first search term from command_content in the author's voice channel
Expand All @@ -483,6 +538,7 @@ async def play(self, message, command_content):
return

if not command_content:
# No search term/url
if self.voice_client.is_paused():
await self.resume(message, command_content)
elif not self.voice_client.is_playing() and not self.media_queue.empty():
Expand All @@ -499,12 +555,18 @@ async def play(self, message, command_content):
)
return

if re.search(self.playlist_regex, command_content):
await self.playlist(message, command_content)
return

media = None
try:
if self.url_regex.match(command_content):
# url to video
logging.info("Fetching video metadata with pafy")
media = self.pafy_search(command_content)
else:
# search term
logging.info("Fetching search results with pafy")
search_result = self.youtube_search(command_content)
media = self.pafy_search(search_result["result"][0]["id"])
Expand All @@ -529,7 +591,7 @@ async def play(self, message, command_content):
)
else:
logging.info("Playing media")
self.next_in_queue()
await self.next_in_queue()

async def stop(self, message, _command_content):
"""
Expand Down Expand Up @@ -594,7 +656,7 @@ async def resume(self, message, _command_content):
self.voice_client.resume()
elif not self.voice_client.is_playing():
logging.info("Resuming for user %s (next_in_queue)", message.author)
self.next_in_queue()
await self.next_in_queue()
await message.add_reaction(MusicBot.REACTION_EMOJI)

async def skip(self, message, _command_content):
Expand All @@ -608,7 +670,7 @@ async def skip(self, message, _command_content):
await message.channel.send(MusicBot.END_OF_QUEUE_MSG)
self._stop()
else:
self.next_in_queue()
await self.next_in_queue()
await message.add_reaction(MusicBot.REACTION_EMOJI)

async def clear_queue(self, message, _command_content):
Expand Down Expand Up @@ -826,7 +888,6 @@ async def joke(self, message, command_content, joke_pause=3):
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)

# pylint: disable=invalid-name
token = os.getenv("DISCORD_TOKEN")
if token is None:
with open(".env", "r", encoding="utf-8") as env_file:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pafy==0.5.5
pycparser==2.20
PyNaCl==1.4.0
python-dotenv==0.13.0
pytube==11.0.1
rfc3986==1.5.0
simplejson==3.17.0
six==1.16.0
Expand Down
72 changes: 72 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#/bin/bash
# Set up the bot on the system as a container with auto-updates
# from GitHub and automatic container rebuilding and restarting.
# This script should be run interactively when setting up the bot
# on a new system.

WORKDIR=/var/cache/the-lone-dancer
CONF_DIR=/etc/the-lone-dancer
ENV_FILE=$CONF_DIR/.env
TOKEN=""

while [[ $# -gt 0 ]]
do
case $1 in
--token)
shift
TOKEN=$1
;;
--token-ask)
shift
echo -n "Paste your discord token here: "
read -s TOKEN
echo ""
;;
-h|--help)
echo "$0 [-h|--help] [--token TOKEN] [--token-ask]"
exit 0
;;
*)
;;
esac
done



if ! grep the-lone-dancer /etc/passwd >/dev/null
then
# Create user
useradd the-lone-dancer
loginctl enable-linger the-lone-dancer
fi

if [[ ! -d $WORKDIR ]]
then
# Create/clone git repo
git clone https://github.com/michael-je/the-lone-dancer.git $WORKDIR
chown the-lone-dancer:the-lone-dancer $WORKDIR
fi
cd $WORKDIR

if [[ ! -d $CONF_DIR ]]
then
# Create /etc/ directory
mkdir $CONF_DIR
fi

if [[ ! -f $ENV_FILE ]]
then
# Copy env-file
cp .env.example $ENV_FILE
sed -i "s/DISCORD_TOKEN.*/DISCORD_TOKEN=$TOKEN/" $ENV_FILE
fi

cp the-lone-dancer*.service /etc/systemd/system/
cp the-lone-dancer*.timer /etc/systemd/system/
chmod +x update.sh
cp update.sh /usr/local/bin/the-lone-dancer-update.sh
systemctl daemon-reload
systemctl enable the-lone-dancer.service
systemctl enable --now the-lone-dancer-update.timer
systemctl start the-lone-dancer-update.service
/usr/local/bin/the-lone-dancer-update.sh -f
12 changes: 12 additions & 0 deletions teardown.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#/bin/bash
# Remove the bot as set up by setup.sh if you don't want it on your system
# anymore.

systemctl disable --now the-lone-dancer-update.timer
systemctl disable --now the-lone-dancer-update.service
systemctl disable --now the-lone-dancer.service
rm /etc/systemd/system/the-lone-dancer*
systemctl daemon-reload
userdel --force the-lone-dancer
rm -r /etc/the-lone-dancer
rm -r /var/cache/the-lone-dancer
5 changes: 4 additions & 1 deletion test_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ async def test_play_fails_when_user_not_in_voice_channel(self):
await self.music_bot_.handle_message(play_message)

play_message.channel.send.assert_awaited_with(
":studio_microphone: default_author, please join a voice channel to start the :robot:"
":studio_microphone: default_author, please join a voice channel to start "
"the :robot:"
)

async def test_play_connects_deafaned(self):
Expand Down Expand Up @@ -194,6 +195,8 @@ async def test_second_play_command_queues_media(self):

self.music_bot_.voice_client.finish_audio_source()

await asyncio.sleep(0.1)

play_message2.channel.send.assert_called_with(
":notes: Now Playing :notes:\n```\nsong2\n```"
)
Expand Down
8 changes: 8 additions & 0 deletions the-lone-dancer-update.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Unit]
Description=The Lone Dancer updater
After=basic.target

[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/the-lone-dancer-update.sh
8 changes: 8 additions & 0 deletions the-lone-dancer-update.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Unit]
Description=Daily update of The Lone Dancer

[Timer]
OnCalendar=*-*-* 06:00:00

[Install]
WantedBy=timers.target
15 changes: 15 additions & 0 deletions the-lone-dancer.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Unit]
Description=The Lone Dancer
After=basic.target

[Service]
Type=simple
#Restart=always
#RemainAfterExit=yes
User=the-lone-dancer
ExecStart=/usr/bin/podman start --attach the-lone-dancer
#ExecReload=/usr/bin/podman restart the-lone-dancer
ExecStop=/usr/bin/podman stop the-lone-dancer

[Install]
WantedBy=multi-user.target
Loading

0 comments on commit e7fc47e

Please sign in to comment.