"""
Plugin for providing a command to fetch YouTube video information from links.
"""
# Importing necessary libraries
import re
import logging
import asyncio
import aiohttp
import yt_dlp
import simplematrixbotlib as botlib
from youtube_title_parse import get_artist_title
LYRICIST_API_URL = "https://lyrist.vercel.app/api/{}/{}"
def seconds_to_minutes_seconds(seconds):
"""
Converts seconds to a string representation of minutes and seconds.
Args:
seconds (int): The number of seconds.
Returns:
str: A string representation of minutes and seconds in the format MM:SS.
"""
minutes = seconds // 60
seconds %= 60
return f"{minutes:02d}:{seconds:02d}"
async def fetch_lyrics(song, artist):
"""
Asynchronously fetches lyrics for a song from the Lyricist API.
Args:
song (str): The name of the song.
artist (str): The name of the artist.
Returns:
str: Lyrics of the song.
None if an error occurs during fetching.
"""
try:
async with aiohttp.ClientSession() as session:
url = LYRICIST_API_URL.format(artist, song)
logging.info(f"Fetching lyrics from: {url}")
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
if response.status == 200:
data = await response.json()
return data.get("lyrics")
else:
logging.warning(f"Lyrics API returned status {response.status}")
return None
except asyncio.TimeoutError:
logging.error("Timeout fetching lyrics")
return None
except Exception as e:
logging.error(f"Error fetching lyrics: {str(e)}")
return None
async def fetch_youtube_info(youtube_url):
"""
Asynchronously fetches information about a YouTube video using yt-dlp.
Args:
youtube_url (str): The URL of the YouTube video.
Returns:
str: A message containing information about the YouTube video.
None if an error occurs during fetching.
"""
try:
logging.info(f"Fetching YouTube info for: {youtube_url}")
# Configure yt-dlp options
ydl_opts = {
'quiet': True,
'no_warnings': True,
'extract_flat': False,
'skip_download': True,
}
# Run yt-dlp in thread pool to avoid blocking
loop = asyncio.get_event_loop()
def extract_info():
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
return ydl.extract_info(youtube_url, download=False)
info = await loop.run_in_executor(None, extract_info)
if not info:
logging.error("No info returned from yt-dlp")
return None
# Extract video information
title = info.get('title', 'Unknown Title')
description = info.get('description', 'No description available')
duration = info.get('duration', 0)
view_count = info.get('view_count', 0)
uploader = info.get('uploader', 'Unknown')
logging.info(f"Video title: {title}")
length = seconds_to_minutes_seconds(duration)
# Parse artist and song from title
artist, song = get_artist_title(title)
logging.info(f"Parsed artist: {artist}, song: {song}")
# Limit description length to avoid huge messages
if len(description) > 500:
description = description[:500] + "..."
description_with_breaks = description.replace('\n', '
')
# Build basic info message
info_message = f"""🎬🎝 Title: {title}
Length: {length} | Views: {view_count:,} | Uploader: {uploader}
⤵︎Description⤵︎
{description_with_breaks} """
# Try to fetch lyrics if artist and song were parsed
if artist and song:
logging.info("Attempting to fetch lyrics...")
lyrics = await fetch_lyrics(song, artist)
if lyrics:
lyrics = lyrics.replace('\n', "
")
# Limit lyrics length
if len(lyrics) > 3000:
lyrics = lyrics[:3000] + "
...(truncated)"
info_message += f"
🎵 Lyrics:
{lyrics} "
else:
logging.info("No lyrics found")
else:
logging.info("Could not parse artist/song from title, skipping lyrics")
return info_message
except Exception as e:
logging.error(f"Error fetching YouTube video information: {str(e)}", exc_info=True)
return None
async def handle_command(room, message, bot, prefix, config):
"""
Asynchronously handles the command to fetch YouTube video information.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
bot (MatrixBot): The Matrix bot instance.
prefix (str): The command prefix.
config (dict): The bot's configuration.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix)
# Check if message contains a YouTube link
if match.is_not_from_this_bot() and re.search(r'(youtube\.com/watch\?v=|youtu\.be/)', message.body):
logging.info(f"YouTube link detected in message: {message.body}")
# Match both youtube.com and youtu.be formats
video_id_match = re.search(r'(?:youtube\.com/watch\?v=|youtu\.be/)([a-zA-Z0-9_-]{11})', message.body)
if video_id_match:
video_id = video_id_match.group(1)
youtube_url = f"https://www.youtube.com/watch?v={video_id}"
logging.info(f"Fetching information for YouTube video ID: {video_id}")
retry_count = 2 # Reduced retries since yt-dlp is more reliable
while retry_count > 0:
info_message = await fetch_youtube_info(youtube_url)
if info_message:
await bot.api.send_markdown_message(room.room_id, info_message)
logging.info("Sent YouTube video information to the room")
break
else:
logging.warning(f"Failed to fetch info, retrying... ({retry_count-1} attempts left)")
retry_count -= 1
if retry_count > 0:
await asyncio.sleep(2) # wait for 2 seconds before retrying
else:
logging.error("Failed to fetch YouTube video information after all retries")
await bot.api.send_text_message(room.room_id, "Failed to fetch YouTube video information. The video may be unavailable or age-restricted.")
else:
logging.warning("Could not extract video ID from YouTube URL")