From 36ac4b78bc96862f44dc8c22abdea5e9492561b7 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 21 Jul 2019 23:31:53 +0200 Subject: [PATCH 1/7] Fix /cv typo --- royalnet/commands/cv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/royalnet/commands/cv.py b/royalnet/commands/cv.py index 73ae3911..cd4c37d2 100644 --- a/royalnet/commands/cv.py +++ b/royalnet/commands/cv.py @@ -94,7 +94,7 @@ class CvNH(NetworkHandler): except AttributeError: pass elif member.activity.type == discord.ActivityType.streaming: - message += f" | 📡 {member.activity.url})" + message += f" | 📡 {member.activity.url}" elif member.activity.type == discord.ActivityType.listening: if isinstance(member.activity, discord.Spotify): if member.activity.title == member.activity.album: From 463c33650bfdde6d8a9b8426c9885a7e8fa92273 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 30 Jul 2019 19:32:21 +0200 Subject: [PATCH 2/7] Reenable Videoinfo --- royalnet/royalgames.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/royalnet/royalgames.py b/royalnet/royalgames.py index 19632a27..81ae6b29 100644 --- a/royalnet/royalgames.py +++ b/royalnet/royalgames.py @@ -23,7 +23,7 @@ log.setLevel(logging.INFO) commands = [PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, SyncCommand, DiarioCommand, RageCommand, ReminderCommand, KvactiveCommand, KvCommand, KvrollCommand, SummonCommand, PlayCommand, SkipCommand, PlaymodeCommand, - VideochannelCommand, CvCommand, PauseCommand, QueueCommand, RoyalnetprofileCommand] + VideochannelCommand, CvCommand, PauseCommand, QueueCommand, RoyalnetprofileCommand, VideoinfoCommand] # noinspection PyUnreachableCode if __debug__: From a98b6c79fc8aff0297a51928687a2aafc9f85048 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 30 Jul 2019 20:34:32 +0200 Subject: [PATCH 3/7] Start something --- royalnet/audio/youtubedl.py | 141 ++++-------------------------------- royalnet/audio/ytdlinfo.py | 119 ++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 128 deletions(-) create mode 100644 royalnet/audio/ytdlinfo.py diff --git a/royalnet/audio/youtubedl.py b/royalnet/audio/youtubedl.py index bfce5976..2a25dd78 100644 --- a/royalnet/audio/youtubedl.py +++ b/royalnet/audio/youtubedl.py @@ -1,24 +1,19 @@ -import typing -import logging as _logging -import discord -import os -import dateparser -import datetime -from youtube_dl import YoutubeDL -from ..utils import ytdldateformat - -log = _logging.getLogger(__name__) - - -class DownloaderError(Exception): - pass - - -class InterruptDownload(DownloaderError): - """Raised from a progress_hook to interrupt the video download.""" +from .ytdlinfo import YtdlInfo class YtdlFile: + """Information about a youtube-dl downloaded file.""" + + def __init__(self, + url: str, + info: YtdlInfo = None, + filename: str): + ... + + + + +class OldYtdlFile: """A wrapper around a youtube_dl downloaded file.""" ytdl_args = { @@ -74,113 +69,3 @@ class YtdlFile: No checks are done when deleting, so it may try to delete a non-existing file and raise an exception or do some other weird stuff with weird filenames.""" os.remove(self.video_filename) - -class YtdlInfo: - """A wrapper around youtube_dl extracted info.""" - - def __init__(self, info: typing.Dict[str, typing.Any]): - """Create a YtdlInfo from the dict returned by the :py:func:`youtube_dl.YoutubeDL.extract_info` function. - - Warning: - Does not download the info, for that use :py:func:`royalnet.audio.YtdlInfo.create_from_url`.""" - self.id: typing.Optional[str] = info.get("id") - self.uploader: typing.Optional[str] = info.get("uploader") - self.uploader_id: typing.Optional[str] = info.get("uploader_id") - self.uploader_url: typing.Optional[str] = info.get("uploader_url") - self.channel_id: typing.Optional[str] = info.get("channel_id") - self.channel_url: typing.Optional[str] = info.get("channel_url") - self.upload_date: typing.Optional[datetime.datetime] = dateparser.parse(ytdldateformat(info.get("upload_date"))) - self.license: typing.Optional[str] = info.get("license") - self.creator: typing.Optional[...] = info.get("creator") - self.title: typing.Optional[str] = info.get("title") - self.alt_title: typing.Optional[...] = info.get("alt_title") - self.thumbnail: typing.Optional[str] = info.get("thumbnail") - self.description: typing.Optional[str] = info.get("description") - self.categories: typing.Optional[typing.List[str]] = info.get("categories") - self.tags: typing.Optional[typing.List[str]] = info.get("tags") - self.subtitles: typing.Optional[typing.Dict[str, typing.List[typing.Dict[str, str]]]] = info.get("subtitles") - self.automatic_captions: typing.Optional[dict] = info.get("automatic_captions") - self.duration: typing.Optional[datetime.timedelta] = datetime.timedelta(seconds=info.get("duration", 0)) - self.age_limit: typing.Optional[int] = info.get("age_limit") - self.annotations: typing.Optional[...] = info.get("annotations") - self.chapters: typing.Optional[...] = info.get("chapters") - self.webpage_url: typing.Optional[str] = info.get("webpage_url") - self.view_count: typing.Optional[int] = info.get("view_count") - self.like_count: typing.Optional[int] = info.get("like_count") - self.dislike_count: typing.Optional[int] = info.get("dislike_count") - self.average_rating: typing.Optional[...] = info.get("average_rating") - self.formats: typing.Optional[list] = info.get("formats") - self.is_live: typing.Optional[bool] = info.get("is_live") - self.start_time: typing.Optional[float] = info.get("start_time") - self.end_time: typing.Optional[float] = info.get("end_time") - self.series: typing.Optional[str] = info.get("series") - self.season_number: typing.Optional[int] = info.get("season_number") - self.episode_number: typing.Optional[int] = info.get("episode_number") - self.track: typing.Optional[...] = info.get("track") - self.artist: typing.Optional[...] = info.get("artist") - self.extractor: typing.Optional[str] = info.get("extractor") - self.webpage_url_basename: typing.Optional[str] = info.get("webpage_url_basename") - self.extractor_key: typing.Optional[str] = info.get("extractor_key") - self.playlist: typing.Optional[str] = info.get("playlist") - self.playlist_index: typing.Optional[int] = info.get("playlist_index") - self.thumbnails: typing.Optional[typing.List[typing.Dict[str, str]]] = info.get("thumbnails") - self.display_id: typing.Optional[str] = info.get("display_id") - self.requested_subtitles: typing.Optional[...] = info.get("requested_subtitles") - self.requested_formats: typing.Optional[tuple] = info.get("requested_formats") - self.format: typing.Optional[str] = info.get("format") - self.format_id: typing.Optional[str] = info.get("format_id") - self.width: typing.Optional[int] = info.get("width") - self.height: typing.Optional[int] = info.get("height") - self.resolution: typing.Optional[...] = info.get("resolution") - self.fps: typing.Optional[int] = info.get("fps") - self.vcodec: typing.Optional[str] = info.get("vcodec") - self.vbr: typing.Optional[int] = info.get("vbr") - self.stretched_ratio: typing.Optional[...] = info.get("stretched_ratio") - self.acodec: typing.Optional[str] = info.get("acodec") - self.abr: typing.Optional[int] = info.get("abr") - self.ext: typing.Optional[str] = info.get("ext") - - @staticmethod - def create_from_url(url, **ytdl_args) -> typing.List["YtdlInfo"]: - # So many redundant options! - ytdl = YoutubeDL({ - "logger": log, # Log messages to a logging.Logger instance. - "quiet": True, # Do not print messages to stdout. - "noplaylist": True, # Download single video instead of a playlist if in doubt. - "no_warnings": True, # Do not print out anything for warnings. - **ytdl_args - }) - first_info = ytdl.extract_info(url=url, download=False) - # If it is a playlist, create multiple videos! - if "entries" in first_info: - return [YtdlInfo(second_info) for second_info in first_info["entries"]] - return [YtdlInfo(first_info)] - - def download(self, outtmpl="%(title)s-%(id)s.%(ext)s", **ytdl_args) -> YtdlFile: - return YtdlFile(self, outtmpl, **ytdl_args) - - def to_discord_embed(self) -> discord.Embed: - embed = discord.Embed(title=self.title, - colour=discord.Colour(0xcc0000), - url=self.webpage_url) - embed.set_thumbnail( - url=self.thumbnail) - embed.set_author(name=self.uploader, url=self.uploader_url) - embed.set_footer(text="Source: youtube-dl", icon_url="https://i.imgur.com/TSvSRYn.png") - embed.add_field(name="Duration", value=str(self.duration), inline=True) - embed.add_field(name="Published on", value=self.upload_date.strftime("%d %b %Y"), inline=True) - return embed - - def __repr__(self): - if self.title: - return f"" - if self.webpage_url: - return f"" - return f"" - - def __str__(self): - if self.title: - return self.title - if self.webpage_url: - return self.webpage_url - return self.id diff --git a/royalnet/audio/ytdlinfo.py b/royalnet/audio/ytdlinfo.py new file mode 100644 index 00000000..6012e470 --- /dev/null +++ b/royalnet/audio/ytdlinfo.py @@ -0,0 +1,119 @@ +import typing +import datetime +import dateparser +import youtube_dl +import discord +import royalnet.utils as u + + +class YtdlInfo: + """A wrapper around youtube_dl extracted info.""" + + def __init__(self, info: typing.Dict[str, typing.Any]): + """Create a YtdlInfo from the dict returned by the :py:func:`youtube_dl.YoutubeDL.extract_info` function. + + Warning: + Does not download the info, for that use :py:func:`royalnet.audio.YtdlInfo.retrieve_for_url`.""" + self.id: typing.Optional[str] = info.get("id") + self.uploader: typing.Optional[str] = info.get("uploader") + self.uploader_id: typing.Optional[str] = info.get("uploader_id") + self.uploader_url: typing.Optional[str] = info.get("uploader_url") + self.channel_id: typing.Optional[str] = info.get("channel_id") + self.channel_url: typing.Optional[str] = info.get("channel_url") + self.upload_date: typing.Optional[datetime.datetime] = dateparser.parse(u.ytdldateformat(info.get("upload_date"))) + self.license: typing.Optional[str] = info.get("license") + self.creator: typing.Optional[...] = info.get("creator") + self.title: typing.Optional[str] = info.get("title") + self.alt_title: typing.Optional[...] = info.get("alt_title") + self.thumbnail: typing.Optional[str] = info.get("thumbnail") + self.description: typing.Optional[str] = info.get("description") + self.categories: typing.Optional[typing.List[str]] = info.get("categories") + self.tags: typing.Optional[typing.List[str]] = info.get("tags") + self.subtitles: typing.Optional[typing.Dict[str, typing.List[typing.Dict[str, str]]]] = info.get("subtitles") + self.automatic_captions: typing.Optional[dict] = info.get("automatic_captions") + self.duration: typing.Optional[datetime.timedelta] = datetime.timedelta(seconds=info.get("duration", 0)) + self.age_limit: typing.Optional[int] = info.get("age_limit") + self.annotations: typing.Optional[...] = info.get("annotations") + self.chapters: typing.Optional[...] = info.get("chapters") + self.webpage_url: typing.Optional[str] = info.get("webpage_url") + self.view_count: typing.Optional[int] = info.get("view_count") + self.like_count: typing.Optional[int] = info.get("like_count") + self.dislike_count: typing.Optional[int] = info.get("dislike_count") + self.average_rating: typing.Optional[...] = info.get("average_rating") + self.formats: typing.Optional[list] = info.get("formats") + self.is_live: typing.Optional[bool] = info.get("is_live") + self.start_time: typing.Optional[float] = info.get("start_time") + self.end_time: typing.Optional[float] = info.get("end_time") + self.series: typing.Optional[str] = info.get("series") + self.season_number: typing.Optional[int] = info.get("season_number") + self.episode_number: typing.Optional[int] = info.get("episode_number") + self.track: typing.Optional[...] = info.get("track") + self.artist: typing.Optional[...] = info.get("artist") + self.extractor: typing.Optional[str] = info.get("extractor") + self.webpage_url_basename: typing.Optional[str] = info.get("webpage_url_basename") + self.extractor_key: typing.Optional[str] = info.get("extractor_key") + self.playlist: typing.Optional[str] = info.get("playlist") + self.playlist_index: typing.Optional[int] = info.get("playlist_index") + self.thumbnails: typing.Optional[typing.List[typing.Dict[str, str]]] = info.get("thumbnails") + self.display_id: typing.Optional[str] = info.get("display_id") + self.requested_subtitles: typing.Optional[...] = info.get("requested_subtitles") + self.requested_formats: typing.Optional[tuple] = info.get("requested_formats") + self.format: typing.Optional[str] = info.get("format") + self.format_id: typing.Optional[str] = info.get("format_id") + self.width: typing.Optional[int] = info.get("width") + self.height: typing.Optional[int] = info.get("height") + self.resolution: typing.Optional[...] = info.get("resolution") + self.fps: typing.Optional[int] = info.get("fps") + self.vcodec: typing.Optional[str] = info.get("vcodec") + self.vbr: typing.Optional[int] = info.get("vbr") + self.stretched_ratio: typing.Optional[...] = info.get("stretched_ratio") + self.acodec: typing.Optional[str] = info.get("acodec") + self.abr: typing.Optional[int] = info.get("abr") + self.ext: typing.Optional[str] = info.get("ext") + + @staticmethod + def retrieve_for_url(url, **ytdl_args) -> typing.List["YtdlInfo"]: + """Fetch the info for an url through YoutubeDL. + + Returns: + A :py:class:`list` containing the infos for the requested videos.""" + # So many redundant options! + ytdl = youtube_dl.YoutubeDL({ + "quiet": True, # Do not print messages to stdout. + "noplaylist": True, # Download single video instead of a playlist if in doubt. + "no_warnings": True, # Do not print out anything for warnings. + **ytdl_args + }) + first_info = ytdl.extract_info(url=url, download=False) + # If it is a playlist, create multiple videos! + if "entries" in first_info: + return [YtdlInfo(second_info) for second_info in first_info["entries"]] + return [YtdlInfo(first_info)] + + def to_discord_embed(self) -> discord.Embed: + """Return this info as a :py:class:`discord.Embed`.""" + embed = discord.Embed(title=self.title, + colour=discord.Colour(0xcc0000), + url=self.webpage_url) + embed.set_thumbnail( + url=self.thumbnail) + embed.set_author(name=self.uploader, url=self.uploader_url) + embed.set_footer(text="Source: youtube-dl", icon_url="https://i.imgur.com/TSvSRYn.png") + embed.add_field(name="Duration", value=str(self.duration), inline=True) + embed.add_field(name="Published on", value=self.upload_date.strftime("%d %b %Y"), inline=True) + return embed + + def __repr__(self): + if self.title: + return f"" + if self.webpage_url: + return f"" + return f"" + + def __str__(self): + """Return the video name.""" + if self.title: + return self.title + if self.webpage_url: + return self.webpage_url + return self.id From 638e113edbe341e9ba941a2b43a3270d1aaa1e63 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 30 Jul 2019 22:34:11 +0200 Subject: [PATCH 4/7] More progress has been done --- royalnet/audio/__init__.py | 2 +- royalnet/audio/errors.py | 18 ++++++ royalnet/audio/royalpcmaudio.py | 72 ---------------------- royalnet/audio/royalpcmfile.py | 102 -------------------------------- royalnet/audio/youtubedl.py | 71 ---------------------- royalnet/audio/ytdldiscord.py | 2 + royalnet/audio/ytdlfile.py | 72 ++++++++++++++++++++++ royalnet/audio/ytdlinfo.py | 18 +++--- 8 files changed, 103 insertions(+), 254 deletions(-) create mode 100644 royalnet/audio/errors.py delete mode 100644 royalnet/audio/royalpcmaudio.py delete mode 100644 royalnet/audio/royalpcmfile.py delete mode 100644 royalnet/audio/youtubedl.py create mode 100644 royalnet/audio/ytdldiscord.py create mode 100644 royalnet/audio/ytdlfile.py diff --git a/royalnet/audio/__init__.py b/royalnet/audio/__init__.py index 96b3f35b..0fbf4e24 100644 --- a/royalnet/audio/__init__.py +++ b/royalnet/audio/__init__.py @@ -1,7 +1,7 @@ """Video and audio downloading related classes, mainly used for Discord voice bots.""" from .playmodes import PlayMode, Playlist, Pool -from .youtubedl import YtdlFile, YtdlInfo +from .ytdlfile import YtdlFile, YtdlInfo from .royalpcmfile import RoyalPCMFile from .royalpcmaudio import RoyalPCMAudio diff --git a/royalnet/audio/errors.py b/royalnet/audio/errors.py new file mode 100644 index 00000000..e6fddb89 --- /dev/null +++ b/royalnet/audio/errors.py @@ -0,0 +1,18 @@ +class YtdlError(Exception): + pass + + +class NotFoundError(YtdlError): + pass + + +class MultipleFilesError(YtdlError): + pass + + +class MissingInfoError(YtdlError): + pass + + +class AlreadyDownloadedError(YtdlError): + pass diff --git a/royalnet/audio/royalpcmaudio.py b/royalnet/audio/royalpcmaudio.py deleted file mode 100644 index 76ddc7e9..00000000 --- a/royalnet/audio/royalpcmaudio.py +++ /dev/null @@ -1,72 +0,0 @@ -from discord import AudioSource -from discord.opus import Encoder as OpusEncoder -import typing -from .royalpcmfile import RoyalPCMFile - - -class RoyalPCMAudio(AudioSource): - """A :py:class:`discord.AudioSource` that keeps data in a file instead of in memory.""" - - def __init__(self, rpf: "RoyalPCMFile"): - """Create a :py:class:`discord.audio.RoyalPCMAudio` from a :py:class:`royalnet.audio.RoyalPCMFile`. - - Warning: - Not recommended, use :py:func:`royalnet.audio.RoyalPCMAudio.create_from_url` or :py:func:`royalnet.audio.RoyalPCMAudio.create_from_ytsearch` instead.""" - self.rpf: "RoyalPCMFile" = rpf - self._file = open(self.rpf.audio_filename, "rb") - - @staticmethod - def create_from_url(url: str) -> typing.List["RoyalPCMAudio"]: - """Download a file with youtube_dl and create a list of RoyalPCMAudios. - - Parameters: - url: The url of the file to download. - - Returns: - A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video.""" - rpf_list = RoyalPCMFile.create_from_url(url) - return [RoyalPCMAudio(rpf) for rpf in rpf_list] - - @staticmethod - def create_from_ytsearch(search: str, amount: int = 1) -> typing.List["RoyalPCMAudio"]: - """Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of RoyalPCMAudios. - - Parameters: - search: The string to search on YouTube. - amount: The number of videos to download. - - Returns: - A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video.""" - rpf_list = RoyalPCMFile.create_from_ytsearch(search, amount) - return [RoyalPCMAudio(rpf) for rpf in rpf_list] - - def is_opus(self): - """This audio file isn't Opus-encoded, but PCM-encoded. - - Returns: - ``False``.""" - return False - - def read(self): - """Reads 20ms worth of audio. - - If the audio is complete, then returning an empty :py:class:`bytes`-like object to signal this is the way to do so.""" - data: bytes = self._file.read(OpusEncoder.FRAME_SIZE) - # If the file was externally closed, it means it was deleted - if self._file.closed: - return b"" - if len(data) != OpusEncoder.FRAME_SIZE: - # Close the file as soon as the playback is finished - self._file.close() - # Reopen the file, so it can be reused - self._file = open(self.rpf.audio_filename, "rb") - return b"" - return data - - def delete(self): - """Permanently delete the downloaded file.""" - self._file.close() - self.rpf.delete_audio_file() - - def __repr__(self): - return f"" diff --git a/royalnet/audio/royalpcmfile.py b/royalnet/audio/royalpcmfile.py deleted file mode 100644 index 2f7d4171..00000000 --- a/royalnet/audio/royalpcmfile.py +++ /dev/null @@ -1,102 +0,0 @@ -import logging -import ffmpeg -import os -import typing -import time -import datetime -from .youtubedl import YtdlFile, YtdlInfo -from ..error import FileTooBigError -from ..utils import fileformat - - -log = logging.getLogger(__name__) - - -class RoyalPCMFile(YtdlFile): - ytdl_args = { - "format": "bestaudio" # Fetch the best audio format available - } - - def __init__(self, info: "YtdlInfo", **ytdl_args): - # Preemptively initialize info to be able to generate the filename - self.info = info - # If the video is longer than 3 hours, don't download it - if self.info.duration >= datetime.timedelta(hours=3): - raise FileTooBigError("File is over 3 hours long") - # Set the time to generate the filename - self._time = time.time() - # Ensure the file doesn't already exist - if os.path.exists(self.ytdl_filename) or os.path.exists(self.audio_filename): - raise FileExistsError("Can't overwrite file") - # Overwrite the new ytdl_args - self.ytdl_args = {**self.ytdl_args, **ytdl_args} - log.info(f"Now downloading {info.webpage_url}") - super().__init__(info, outtmpl=self.ytdl_filename, **self.ytdl_args) - # Find the audio_filename with a regex (should be video.opus) - log.info(f"Converting {self.video_filename}...") - # Convert the video to pcm - try: - ffmpeg.input(f"./{self.video_filename}") \ - .output(self.audio_filename, format="s16le", ac=2, ar="48000") \ - .overwrite_output() \ - .run(quiet=True) - except ffmpeg.Error as exc: - log.error(f"FFmpeg error: {exc.stderr}") - raise - # Delete the video file - log.info(f"Deleting {self.video_filename}") - self.delete_video_file() - - def __repr__(self): - return f"" - - @staticmethod - def create_from_url(url: str, **ytdl_args) -> typing.List["RoyalPCMFile"]: - """Download a file with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`. - - Parameters: - url: The url of the file to download. - ytdl_args: Extra arguments to be passed to YoutubeDL while downloading. - - Returns: - A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video.""" - info_list = YtdlInfo.create_from_url(url) - return [RoyalPCMFile(info, **ytdl_args) for info in info_list] - - @staticmethod - def create_from_ytsearch(search: str, amount: int = 1, **ytdl_args) -> typing.List["RoyalPCMFile"]: - """Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`. - - Parameters: - search: The string to search on YouTube. - amount: The number of videos to download. - ytdl_args: Extra arguments to be passed to YoutubeDL while downloading. - - Returns: - A :py:class:`list` of RoyalPCMFiles, each corresponding to a downloaded video.""" - url = f"ytsearch{amount}:{search}" - info_list = YtdlInfo.create_from_url(url) - return [RoyalPCMFile(info, **ytdl_args) for info in info_list] - - @property - def ytdl_filename(self) -> str: - """ - Returns: - The name of the downloaded video file, as a :py:class:`str`. - - Warning: - It's going to be deleted as soon as the :py:func:`royalnet.audio.RoyalPCMFile.__init__` function has completed, so it's probably not going to be very useful... - """ - return f"./downloads/{fileformat(self.info.title)}-{fileformat(str(int(self._time)))}.ytdl" - - @property - def audio_filename(self) -> str: - """ - Returns: - The name of the downloaded and PCM-converted audio file.""" - return f"./downloads/{fileformat(self.info.title)}-{fileformat(str(int(self._time)))}.pcm" - - def delete_audio_file(self): - """Delete the PCM-converted audio file.""" - log.info(f"Deleting {self.audio_filename}") - os.remove(self.audio_filename) diff --git a/royalnet/audio/youtubedl.py b/royalnet/audio/youtubedl.py deleted file mode 100644 index 2a25dd78..00000000 --- a/royalnet/audio/youtubedl.py +++ /dev/null @@ -1,71 +0,0 @@ -from .ytdlinfo import YtdlInfo - - -class YtdlFile: - """Information about a youtube-dl downloaded file.""" - - def __init__(self, - url: str, - info: YtdlInfo = None, - filename: str): - ... - - - - -class OldYtdlFile: - """A wrapper around a youtube_dl downloaded file.""" - - ytdl_args = { - "logger": log, # Log messages to a logging.Logger instance. - "quiet": True, # Do not print messages to stdout. - "noplaylist": True, # Download single video instead of a playlist if in doubt. - "no_warnings": True, # Do not print out anything for warnings. - } - - def __init__(self, info: "YtdlInfo", outtmpl="%(title)s-%(id)s.%(ext)s", **ytdl_args): - self.info: "YtdlInfo" = info - self.video_filename: str - # Create a local args copy - ytdl_args["outtmpl"] = outtmpl - self.ytdl_args = {**self.ytdl_args, **ytdl_args} - # Create the ytdl - ytdl = YoutubeDL(ytdl_args) - # Find the file name - self.video_filename = ytdl.prepare_filename(self.info.__dict__) - # Download the file - ytdl.download([self.info.webpage_url]) - # Final checks - assert os.path.exists(self.video_filename) - - def __repr__(self): - return f"" - - @staticmethod - def create_from_url(url, outtmpl="%(title)s-%(id)s.%(ext)s", **ytdl_args) -> typing.List["YtdlFile"]: - """Download the videos at the specified url. - - Parameters: - url: The url to download the videos from. - outtmpl: The filename that the downloaded videos are going to have. The name can be formatted according to the `outtmpl documentation `_. - ytdl_args: Other arguments to be passed to the YoutubeDL object. - - Returns: - A :py:class:`list` of YtdlFiles.""" - info_list = YtdlInfo.create_from_url(url) - return [info.download(outtmpl, **ytdl_args) for info in info_list] - - def _stop_download(self): - """I have no clue of what this does, or why is it here. Possibly remove it? - - Raises: - InterruptDownload: ...uhhh, always?""" - raise InterruptDownload() - - def delete_video_file(self): - """Delete the file located at ``self.video_filename``. - - Note: - No checks are done when deleting, so it may try to delete a non-existing file and raise an exception or do some other weird stuff with weird filenames.""" - os.remove(self.video_filename) - diff --git a/royalnet/audio/ytdldiscord.py b/royalnet/audio/ytdldiscord.py new file mode 100644 index 00000000..2271322f --- /dev/null +++ b/royalnet/audio/ytdldiscord.py @@ -0,0 +1,2 @@ +import discord +from .ytdlfile import YtdlFile \ No newline at end of file diff --git a/royalnet/audio/ytdlfile.py b/royalnet/audio/ytdlfile.py new file mode 100644 index 00000000..88647a27 --- /dev/null +++ b/royalnet/audio/ytdlfile.py @@ -0,0 +1,72 @@ +import contextlib +import os +import typing +import youtube_dl +from .ytdlinfo import YtdlInfo +from .errors import NotFoundError, MultipleFilesError, MissingInfoError, AlreadyDownloadedError + + +class YtdlFile: + """Information about a youtube-dl downloaded file.""" + + _default_ytdl_args = { + "quiet": True, # Do not print messages to stdout. + "noplaylist": True, # Download single video instead of a playlist if in doubt. + "no_warnings": True, # Do not print out anything for warnings. + "outtmpl": "%(title)s-%(id)s.%(ext)s" # Use the default outtmpl. + } + + def __init__(self, + url: str, + info: YtdlInfo = None, + filename: str = None): + self.url: str = url + self.info: YtdlInfo = info + self.filename: str = filename + + def has_info(self) -> bool: + return self.info is not None + + def is_downloaded(self) -> bool: + return self.filename is not None and os.path.exists(self.filename) + + @contextlib.contextmanager + def open(self): + if not self.is_downloaded(): + raise FileNotFoundError("The file hasn't been downloaded yet.") + with open(self.filename, "r") as file: + yield file + + def update_info(self, **ytdl_args) -> None: + infos = YtdlInfo.retrieve_for_url(self.url, **ytdl_args) + if len(infos) == 0: + raise NotFoundError() + elif len(infos) > 1: + raise MultipleFilesError() + self.info = infos[0] + + def download_file(self, **ytdl_args) -> None: + if not self.has_info(): + raise MissingInfoError() + if self.is_downloaded(): + raise AlreadyDownloadedError() + with youtube_dl.YoutubeDL({**self._default_ytdl_args, **ytdl_args}) as ytdl: + ytdl.download([self.info.webpage_url]) + self.filename = ytdl.prepare_filename(self.info.__dict__) + + def delete_file(self): + """Delete the file located at ``self.filename``.""" + if not self.is_downloaded(): + raise FileNotFoundError("The file hasn't been downloaded yet.") + os.remove(self.filename) + self.filename = None + + @classmethod + def download_from_url(cls, url: str, **ytdl_args) -> typing.List["YtdlFile"]: + infos = YtdlInfo.retrieve_for_url(url, **ytdl_args) + files = [] + for info in infos: + file = YtdlFile(url=info.webpage_url, info=info) + file.download_file(**ytdl_args) + files.append(file) + return files diff --git a/royalnet/audio/ytdlinfo.py b/royalnet/audio/ytdlinfo.py index 6012e470..b89b22d4 100644 --- a/royalnet/audio/ytdlinfo.py +++ b/royalnet/audio/ytdlinfo.py @@ -9,6 +9,13 @@ import royalnet.utils as u class YtdlInfo: """A wrapper around youtube_dl extracted info.""" + _default_ytdl_args = { + "quiet": True, # Do not print messages to stdout. + "noplaylist": True, # Download single video instead of a playlist if in doubt. + "no_warnings": True, # Do not print out anything for warnings. + "outtmpl": "%(title)s-%(id)s.%(ext)s" # Use the default outtmpl. + } + def __init__(self, info: typing.Dict[str, typing.Any]): """Create a YtdlInfo from the dict returned by the :py:func:`youtube_dl.YoutubeDL.extract_info` function. @@ -71,19 +78,14 @@ class YtdlInfo: self.abr: typing.Optional[int] = info.get("abr") self.ext: typing.Optional[str] = info.get("ext") - @staticmethod - def retrieve_for_url(url, **ytdl_args) -> typing.List["YtdlInfo"]: + @classmethod + def retrieve_for_url(cls, url, **ytdl_args) -> typing.List["YtdlInfo"]: """Fetch the info for an url through YoutubeDL. Returns: A :py:class:`list` containing the infos for the requested videos.""" # So many redundant options! - ytdl = youtube_dl.YoutubeDL({ - "quiet": True, # Do not print messages to stdout. - "noplaylist": True, # Download single video instead of a playlist if in doubt. - "no_warnings": True, # Do not print out anything for warnings. - **ytdl_args - }) + ytdl = youtube_dl.YoutubeDL({**cls._default_ytdl_args, **ytdl_args}) first_info = ytdl.extract_info(url=url, download=False) # If it is a playlist, create multiple videos! if "entries" in first_info: From bb0507d79ea8a96c3de6e37b6e551156d4de806a Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 31 Jul 2019 01:49:53 +0200 Subject: [PATCH 5/7] New stuff --- royalnet/audio/fileaudiosource.py | 41 +++++++++++++++++++++++++++++++ royalnet/audio/ytdldiscord.py | 28 ++++++++++++++++++++- royalnet/audio/ytdlfile.py | 8 +++--- 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 royalnet/audio/fileaudiosource.py diff --git a/royalnet/audio/fileaudiosource.py b/royalnet/audio/fileaudiosource.py new file mode 100644 index 00000000..442b5885 --- /dev/null +++ b/royalnet/audio/fileaudiosource.py @@ -0,0 +1,41 @@ +import discord + + +class FileAudioSource(discord.AudioSource): + """A :py:class:`discord.AudioSource` that uses a :py:class:`io.BufferedIOBase` as an input instead of memory. + + The stream should be in the usual PCM encoding. + + Warning: + This AudioSource will consume (and close) the passed stream""" + + def __init__(self, file): + self.file = file + + def __repr__(self): + if self.file.seekable(): + return f"<{self.__class__.__name__} @{self.file.tell()}>" + else: + return f"<{self.__class__.__name__}>" + + def is_opus(self): + """This audio file isn't Opus-encoded, but PCM-encoded. + + Returns: + ``False``.""" + return False + + def read(self): + """Reads 20ms worth of audio. + + If the audio is complete, then returning an empty :py:class:`bytes`-like object to signal this is the way to do so.""" + data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE) + # If the stream is closed, it should stop playing immediatly + if self.file.closed: + return b"" + # If there is no more data to be streamed + if len(data) != discord.opus.Encoder.FRAME_SIZE: + # Close the file + self.file.close() + return b"" + return data diff --git a/royalnet/audio/ytdldiscord.py b/royalnet/audio/ytdldiscord.py index 2271322f..77010cf2 100644 --- a/royalnet/audio/ytdldiscord.py +++ b/royalnet/audio/ytdldiscord.py @@ -1,2 +1,28 @@ +import typing import discord -from .ytdlfile import YtdlFile \ No newline at end of file +import re +import ffmpeg +from .ytdlfile import YtdlFile +from .fileaudiosource import FileAudioSource + + +class YtdlDiscord: + def __init__(self, ytdl_file: YtdlFile): + self.ytdl_file: YtdlFile = ytdl_file + self.pcm_filename: typing.Optional[str] = None + + def convert_to_pcm(self) -> None: + if not self.ytdl_file.is_downloaded(): + raise FileNotFoundError("File hasn't been downloaded yet") + destination_filename = re.sub(r"\.[^.]+$", ".pcm", self.ytdl_file.filename) + ( + ffmpeg.input(self.ytdl_file.filename) + .output(destination_filename, format="s16le", ac=2, ar="48000") + .overwrite_output() + .run(quiet=True) + ) + self.pcm_filename = destination_filename + + def to_audiosource(self) -> discord.AudioSource: + stream = open(self.pcm_filename, "rb") + return FileAudioSource(stream) diff --git a/royalnet/audio/ytdlfile.py b/royalnet/audio/ytdlfile.py index 88647a27..8a9ae128 100644 --- a/royalnet/audio/ytdlfile.py +++ b/royalnet/audio/ytdlfile.py @@ -18,11 +18,11 @@ class YtdlFile: def __init__(self, url: str, - info: YtdlInfo = None, - filename: str = None): + info: typing.Optional[YtdlInfo] = None, + filename: typing.Optional[str] = None): self.url: str = url - self.info: YtdlInfo = info - self.filename: str = filename + self.info: typing.Optional[YtdlInfo] = info + self.filename: typing.Optional[str] = filename def has_info(self) -> bool: return self.info is not None From d570e09e900724ca8835b2a75ba4fb701e16c197 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 31 Jul 2019 02:10:27 +0200 Subject: [PATCH 6/7] ok im getting tired --- royalnet/audio/playmodes.py | 43 ++++++++++++++++++----------------- royalnet/audio/ytdldiscord.py | 20 ++++++++++++++++ royalnet/audio/ytdlfile.py | 10 ++++---- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/royalnet/audio/playmodes.py b/royalnet/audio/playmodes.py index d93ad243..712fb45a 100644 --- a/royalnet/audio/playmodes.py +++ b/royalnet/audio/playmodes.py @@ -1,7 +1,8 @@ import math import random import typing -from .royalpcmaudio import RoyalPCMAudio +from .ytdldiscord import YtdlDiscord +from .fileaudiosource import FileAudioSource class PlayMode: @@ -9,14 +10,14 @@ class PlayMode: def __init__(self): """Create a new PlayMode and initialize the generator inside.""" - self.now_playing: typing.Optional[RoyalPCMAudio] = None + self.now_playing: typing.Optional[YtdlDiscord] = None self.generator: typing.AsyncGenerator = self._generate_generator() - async def next(self) -> typing.Optional[RoyalPCMAudio]: - """Get the next :py:class:`royalnet.audio.RoyalPCMAudio` from the list and advance it. + async def next(self) -> typing.Optional[FileAudioSource]: + """Get the next :py:class:`royalnet.audio.FileAudioSource` from the list and advance it. Returns: - The next :py:class:`royalnet.audio.RoyalPCMAudio`.""" + The next :py:class:`royalnet.audio.FileAudioSource`.""" return await self.generator.__anext__() def videos_left(self) -> typing.Union[int, float]: @@ -27,30 +28,30 @@ class PlayMode: raise NotImplementedError() async def _generate_generator(self): - """Factory function for an async generator that changes the ``now_playing`` property either to a :py:class:`discord.audio.RoyalPCMAudio` or to ``None``, then yields the value it changed it to. + """Factory function for an async generator that changes the ``now_playing`` property either to a :py:class:`royalnet.audio.FileAudioSource` or to ``None``, then yields the value it changed it to. Yields: - The :py:class:`royalnet.audio.RoyalPCMAudio` to be played next.""" + The :py:class:`royalnet.audio.FileAudioSource` to be played next.""" raise NotImplementedError() # This is needed to make the coroutine an async generator # noinspection PyUnreachableCode yield NotImplemented - def add(self, item: RoyalPCMAudio) -> None: - """Add a new :py:class:`royalnet.audio.RoyalPCMAudio` to the PlayMode. + def add(self, item: YtdlDiscord) -> None: + """Add a new :py:class:`royalnet.audio.YtdlDiscord` to the PlayMode. Args: item: The item to add to the PlayMode.""" raise NotImplementedError() def delete(self) -> None: - """Delete all :py:class:`royalnet.audio.RoyalPCMAudio` contained inside this PlayMode.""" + """Delete all :py:class:`royalnet.audio.YtdlDiscord` contained inside this PlayMode.""" raise NotImplementedError() - def queue_preview(self) -> typing.List[RoyalPCMAudio]: + def queue_preview(self) -> typing.List[YtdlDiscord]: """Display all the videos in the PlayMode as a list, if possible. - To be used with `queue` commands, for example. + To be used with ``queue`` commands, for example. Raises: NotImplementedError: If a preview can't be generated. @@ -61,9 +62,9 @@ class PlayMode: class Playlist(PlayMode): - """A video list. :py:class:`royalnet.audio.RoyalPCMAudio` played are removed from the list.""" + """A video list. :py:class:`royalnet.audio.YtdlDiscord` played are removed from the list.""" - def __init__(self, starting_list: typing.List[RoyalPCMAudio] = None): + def __init__(self, starting_list: typing.List[YtdlDiscord] = None): """Create a new Playlist. Args: @@ -71,7 +72,7 @@ class Playlist(PlayMode): super().__init__() if starting_list is None: starting_list = [] - self.list: typing.List[RoyalPCMAudio] = starting_list + self.list: typing.List[YtdlDiscord] = starting_list def videos_left(self) -> typing.Union[int, float]: return len(self.list) @@ -97,14 +98,14 @@ class Playlist(PlayMode): while self.list: self.list.pop(0).delete() - def queue_preview(self) -> typing.List[RoyalPCMAudio]: + def queue_preview(self) -> typing.List[YtdlDiscord]: return self.list class Pool(PlayMode): """A :py:class:`royalnet.audio.RoyalPCMAudio` pool. :py:class:`royalnet.audio.RoyalPCMAudio` are selected in random order and are not repeated until every song has been played at least once.""" - def __init__(self, starting_pool: typing.List[RoyalPCMAudio] = None): + def __init__(self, starting_pool: typing.List[YtdlDiscord] = None): """Create a new Pool. Args: @@ -112,8 +113,8 @@ class Pool(PlayMode): super().__init__() if starting_pool is None: starting_pool = [] - self.pool: typing.List[RoyalPCMAudio] = starting_pool - self._pool_copy: typing.List[RoyalPCMAudio] = [] + self.pool: typing.List[YtdlDiscord] = starting_pool + self._pool_copy: typing.List[YtdlDiscord] = [] def videos_left(self) -> typing.Union[int, float]: return math.inf @@ -142,7 +143,7 @@ class Pool(PlayMode): self.pool = None self._pool_copy = None - def queue_preview(self) -> typing.List[RoyalPCMAudio]: + def queue_preview(self) -> typing.List[YtdlDiscord]: preview_pool = self.pool.copy() random.shuffle(preview_pool) - return preview_pool \ No newline at end of file + return preview_pool diff --git a/royalnet/audio/ytdldiscord.py b/royalnet/audio/ytdldiscord.py index 77010cf2..abf08f47 100644 --- a/royalnet/audio/ytdldiscord.py +++ b/royalnet/audio/ytdldiscord.py @@ -2,6 +2,7 @@ import typing import discord import re import ffmpeg +import os from .ytdlfile import YtdlFile from .fileaudiosource import FileAudioSource @@ -11,6 +12,9 @@ class YtdlDiscord: self.ytdl_file: YtdlFile = ytdl_file self.pcm_filename: typing.Optional[str] = None + def pcm_available(self): + return self.pcm_filename is not None and os.path.exists(self.pcm_filename) + def convert_to_pcm(self) -> None: if not self.ytdl_file.is_downloaded(): raise FileNotFoundError("File hasn't been downloaded yet") @@ -23,6 +27,22 @@ class YtdlDiscord: ) self.pcm_filename = destination_filename + def ready_up(self): + if not self.ytdl_file.has_info(): + self.ytdl_file.update_info() + if not self.ytdl_file.is_downloaded(): + self.ytdl_file.download_file() + if not self.pcm_available(): + self.convert_to_pcm() + def to_audiosource(self) -> discord.AudioSource: + if not self.pcm_available(): + raise FileNotFoundError("File hasn't been converted to PCM yet") stream = open(self.pcm_filename, "rb") return FileAudioSource(stream) + + def delete(self) -> None: + if self.pcm_available(): + os.remove(self.pcm_filename) + self.pcm_filename = None + self.ytdl_file.delete() diff --git a/royalnet/audio/ytdlfile.py b/royalnet/audio/ytdlfile.py index 8a9ae128..f19ab4f1 100644 --- a/royalnet/audio/ytdlfile.py +++ b/royalnet/audio/ytdlfile.py @@ -54,12 +54,10 @@ class YtdlFile: ytdl.download([self.info.webpage_url]) self.filename = ytdl.prepare_filename(self.info.__dict__) - def delete_file(self): - """Delete the file located at ``self.filename``.""" - if not self.is_downloaded(): - raise FileNotFoundError("The file hasn't been downloaded yet.") - os.remove(self.filename) - self.filename = None + def delete(self): + if self.is_downloaded(): + os.remove(self.filename) + self.filename = None @classmethod def download_from_url(cls, url: str, **ytdl_args) -> typing.List["YtdlFile"]: From a9967918ffc47408e237cf7fc99ab147a978596d Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Wed, 31 Jul 2019 16:18:19 +0200 Subject: [PATCH 7/7] rework is finally finished --- royalnet/audio/__init__.py | 11 ++++++----- royalnet/audio/fileaudiosource.py | 4 ++-- royalnet/audio/playmodes.py | 5 +++-- royalnet/audio/ytdldiscord.py | 32 +++++++++++++++++++++++++++++-- royalnet/bots/discord.py | 26 ++++++++++++------------- royalnet/commands/play.py | 20 ++++++++++++------- royalnet/commands/playmode.py | 8 +++----- royalnet/commands/queue.py | 4 ++-- royalnet/commands/videoinfo.py | 2 +- 9 files changed, 72 insertions(+), 40 deletions(-) diff --git a/royalnet/audio/__init__.py b/royalnet/audio/__init__.py index 0fbf4e24..ff7942f5 100644 --- a/royalnet/audio/__init__.py +++ b/royalnet/audio/__init__.py @@ -1,8 +1,9 @@ """Video and audio downloading related classes, mainly used for Discord voice bots.""" -from .playmodes import PlayMode, Playlist, Pool -from .ytdlfile import YtdlFile, YtdlInfo -from .royalpcmfile import RoyalPCMFile -from .royalpcmaudio import RoyalPCMAudio +from . import playmodes +from .ytdlinfo import YtdlInfo +from .ytdlfile import YtdlFile +from .fileaudiosource import FileAudioSource +from .ytdldiscord import YtdlDiscord -__all__ = ["PlayMode", "Playlist", "Pool", "YtdlFile", "YtdlInfo", "RoyalPCMFile", "RoyalPCMAudio"] +__all__ = ["playmodes", "YtdlInfo", "YtdlFile", "FileAudioSource", "YtdlDiscord"] diff --git a/royalnet/audio/fileaudiosource.py b/royalnet/audio/fileaudiosource.py index 442b5885..110b6a53 100644 --- a/royalnet/audio/fileaudiosource.py +++ b/royalnet/audio/fileaudiosource.py @@ -7,7 +7,7 @@ class FileAudioSource(discord.AudioSource): The stream should be in the usual PCM encoding. Warning: - This AudioSource will consume (and close) the passed stream""" + This AudioSource will consume (and close) the passed stream.""" def __init__(self, file): self.file = file @@ -29,10 +29,10 @@ class FileAudioSource(discord.AudioSource): """Reads 20ms worth of audio. If the audio is complete, then returning an empty :py:class:`bytes`-like object to signal this is the way to do so.""" - data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE) # If the stream is closed, it should stop playing immediatly if self.file.closed: return b"" + data: bytes = self.file.read(discord.opus.Encoder.FRAME_SIZE) # If there is no more data to be streamed if len(data) != discord.opus.Encoder.FRAME_SIZE: # Close the file diff --git a/royalnet/audio/playmodes.py b/royalnet/audio/playmodes.py index 712fb45a..fad8a667 100644 --- a/royalnet/audio/playmodes.py +++ b/royalnet/audio/playmodes.py @@ -83,9 +83,10 @@ class Playlist(PlayMode): next_video = self.list.pop(0) except IndexError: self.now_playing = None + yield None else: self.now_playing = next_video - yield self.now_playing + yield next_video.spawn_audiosource() if self.now_playing is not None: self.now_playing.delete() @@ -130,7 +131,7 @@ class Pool(PlayMode): while self._pool_copy: next_video = self._pool_copy.pop(0) self.now_playing = next_video - yield next_video + yield next_video.spawn_audiosource() def add(self, item) -> None: self.pool.append(item) diff --git a/royalnet/audio/ytdldiscord.py b/royalnet/audio/ytdldiscord.py index abf08f47..1b171b75 100644 --- a/royalnet/audio/ytdldiscord.py +++ b/royalnet/audio/ytdldiscord.py @@ -3,6 +3,7 @@ import discord import re import ffmpeg import os +from .ytdlinfo import YtdlInfo from .ytdlfile import YtdlFile from .fileaudiosource import FileAudioSource @@ -11,6 +12,7 @@ class YtdlDiscord: def __init__(self, ytdl_file: YtdlFile): self.ytdl_file: YtdlFile = ytdl_file self.pcm_filename: typing.Optional[str] = None + self._fas_spawned: typing.List[FileAudioSource] = [] def pcm_available(self): return self.pcm_filename is not None and os.path.exists(self.pcm_filename) @@ -35,14 +37,40 @@ class YtdlDiscord: if not self.pcm_available(): self.convert_to_pcm() - def to_audiosource(self) -> discord.AudioSource: + def spawn_audiosource(self) -> discord.AudioSource: if not self.pcm_available(): raise FileNotFoundError("File hasn't been converted to PCM yet") stream = open(self.pcm_filename, "rb") - return FileAudioSource(stream) + source = FileAudioSource(stream) + # FIXME: it's a intentional memory leak + self._fas_spawned.append(source) + return source def delete(self) -> None: if self.pcm_available(): + for source in self._fas_spawned: + if not source.file.closed: + source.file.close() os.remove(self.pcm_filename) self.pcm_filename = None self.ytdl_file.delete() + + @classmethod + def create_from_url(cls, url, **ytdl_args) -> typing.List["YtdlDiscord"]: + files = YtdlFile.download_from_url(url, **ytdl_args) + dfiles = [] + for file in files: + dfile = YtdlDiscord(file) + dfiles.append(dfile) + return dfiles + + @classmethod + def create_and_ready_from_url(cls, url, **ytdl_args) -> typing.List["YtdlDiscord"]: + dfiles = cls.create_from_url(url, **ytdl_args) + for dfile in dfiles: + dfile.ready_up() + return dfiles + + @property + def info(self) -> typing.Optional[YtdlInfo]: + return self.ytdl_file.info diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index b918a612..2c73b29d 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -8,7 +8,7 @@ from ..utils import asyncify, Call, Command, discord_escape from ..error import UnregisteredError, NoneFoundError, TooManyFoundError, InvalidConfigError, RoyalnetResponseError from ..network import RoyalnetConfig, Request, ResponseSuccess, ResponseError from ..database import DatabaseConfig -from ..audio import PlayMode, Playlist, RoyalPCMAudio +from ..audio import playmodes, YtdlDiscord log = _logging.getLogger(__name__) @@ -31,7 +31,7 @@ class DiscordBot(GenericBot): def _init_voice(self): """Initialize the variables needed for the connection to voice chat.""" log.debug(f"Creating music_data dict") - self.music_data: typing.Dict[discord.Guild, PlayMode] = {} + self.music_data: typing.Dict[discord.Guild, playmodes.PlayMode] = {} def _call_factory(self) -> typing.Type[Call]: log.debug(f"Creating DiscordCall") @@ -100,7 +100,7 @@ class DiscordBot(GenericBot): # Create a music_data entry, if it doesn't exist; default is a Playlist if not self.music_data.get(channel.guild): log.debug(f"Creating music_data for {channel.guild}") - self.music_data[channel.guild] = Playlist() + self.music_data[channel.guild] = playmodes.Playlist() @staticmethod async def on_message(message: discord.Message): @@ -216,12 +216,12 @@ class DiscordBot(GenericBot): await self.client.connect() # TODO: how to stop? - async def add_to_music_data(self, audio_sources: typing.List[RoyalPCMAudio], guild: discord.Guild): - """Add a file to the corresponding music_data object.""" + async def add_to_music_data(self, dfiles: typing.List[YtdlDiscord], guild: discord.Guild): + """Add a list of :py:class:`royalnet.audio.YtdlDiscord` to the corresponding music_data object.""" guild_music_data = self.music_data[guild] - for audio_source in audio_sources: - log.debug(f"Adding {audio_source} to music_data") - guild_music_data.add(audio_source) + for dfile in dfiles: + log.debug(f"Adding {dfile} to music_data") + guild_music_data.add(dfile) if guild_music_data.now_playing is None: await self.advance_music_data(guild) @@ -229,7 +229,7 @@ class DiscordBot(GenericBot): """Try to play the next song, while it exists. Otherwise, just return.""" guild_music_data = self.music_data[guild] voice_client = self.client.find_voice_client_by_guild(guild) - next_source: RoyalPCMAudio = await guild_music_data.next() + next_source: discord.AudioSource = await guild_music_data.next() await self.update_activity_with_source_title() if next_source is None: log.debug(f"Ending playback chain") @@ -252,16 +252,14 @@ class DiscordBot(GenericBot): log.debug(f"Updating current Activity: setting to None, as multiple guilds are using the bot") await self.client.change_presence(status=discord.Status.online) return - # FIXME: PyCharm faulty inspection? - # noinspection PyUnresolvedReferences - play_mode: PlayMode = list(self.music_data.items())[0][1] + play_mode: playmodes.PlayMode = self.music_data[list(self.music_data)[0]] now_playing = play_mode.now_playing if now_playing is None: # No songs are playing now log.debug(f"Updating current Activity: setting to None, as nothing is currently being played") await self.client.change_presence(status=discord.Status.online) return - log.debug(f"Updating current Activity: listening to {now_playing.rpf.info.title}") - await self.client.change_presence(activity=discord.Activity(name=now_playing.rpf.info.title, + log.debug(f"Updating current Activity: listening to {now_playing.info.title}") + await self.client.change_presence(activity=discord.Activity(name=now_playing.info.title, type=discord.ActivityType.listening), status=discord.Status.online) diff --git a/royalnet/commands/play.py b/royalnet/commands/play.py index 80c68167..7e302d3d 100644 --- a/royalnet/commands/play.py +++ b/royalnet/commands/play.py @@ -6,11 +6,17 @@ import pickle from ..utils import Command, Call, NetworkHandler, asyncify from ..network import Request, ResponseSuccess from ..error import TooManyFoundError, NoneFoundError -from ..audio import RoyalPCMAudio +from ..audio import YtdlDiscord if typing.TYPE_CHECKING: from ..bots import DiscordBot +ytdl_args = { + "format": "bestaudio", + "outtmpl": f"./downloads/%(autonumber)s_%(title)s.%(ext)s" +} + + class PlayNH(NetworkHandler): message_type = "music_play" @@ -32,16 +38,16 @@ class PlayNH(NetworkHandler): raise Exception("No music_data for this guild") # Start downloading if data["url"].startswith("http://") or data["url"].startswith("https://"): - audio_sources: typing.List[RoyalPCMAudio] = await asyncify(RoyalPCMAudio.create_from_url, data["url"]) + dfiles: typing.List[YtdlDiscord] = await asyncify(YtdlDiscord.create_and_ready_from_url, data["url"], **ytdl_args) else: - audio_sources = await asyncify(RoyalPCMAudio.create_from_ytsearch, data["url"]) - await bot.add_to_music_data(audio_sources, guild) + dfiles = await asyncify(YtdlDiscord.create_and_ready_from_url, f"ytsearch:{data['url']}", **ytdl_args) + await bot.add_to_music_data(dfiles, guild) # Create response dictionary response = { "videos": [{ - "title": source.rpf.info.title, - "discord_embed_pickle": str(pickle.dumps(source.rpf.info.to_discord_embed())) - } for source in audio_sources] + "title": dfile.info.title, + "discord_embed_pickle": str(pickle.dumps(dfile.info.to_discord_embed())) + } for dfile in dfiles] } return ResponseSuccess(response) diff --git a/royalnet/commands/playmode.py b/royalnet/commands/playmode.py index 0dbef442..10fb84d7 100644 --- a/royalnet/commands/playmode.py +++ b/royalnet/commands/playmode.py @@ -1,9 +1,8 @@ import typing -import asyncio from ..utils import Command, Call, NetworkHandler from ..network import Request, ResponseSuccess -from ..error import NoneFoundError, TooManyFoundError, CurrentlyDisabledError -from ..audio import Playlist, Pool +from ..error import NoneFoundError, TooManyFoundError +from ..audio.playmodes import Playlist, Pool if typing.TYPE_CHECKING: from ..bots import DiscordBot @@ -30,8 +29,7 @@ class PlaymodeNH(NetworkHandler): if data["mode_name"] == "playlist": bot.music_data[guild] = Playlist() elif data["mode_name"] == "pool": - # bot.music_data[guild] = Pool() - raise CurrentlyDisabledError("Bug: https://github.com/royal-games/royalnet/issues/61") + bot.music_data[guild] = Pool() else: raise ValueError("No such PlayMode") return ResponseSuccess() diff --git a/royalnet/commands/queue.py b/royalnet/commands/queue.py index f4fb1359..b2557604 100644 --- a/royalnet/commands/queue.py +++ b/royalnet/commands/queue.py @@ -37,8 +37,8 @@ class QueueNH(NetworkHandler): "type": playmode.__class__.__name__, "queue": { - "strings": [str(element.rpf.info) for element in queue], - "pickled_embeds": str(pickle.dumps([element.rpf.info.to_discord_embed() for element in queue])) + "strings": [str(dfile.info) for dfile in queue], + "pickled_embeds": str(pickle.dumps([dfile.info.to_discord_embed() for dfile in queue])) } }) diff --git a/royalnet/commands/videoinfo.py b/royalnet/commands/videoinfo.py index f92ac2bd..37929d7d 100644 --- a/royalnet/commands/videoinfo.py +++ b/royalnet/commands/videoinfo.py @@ -12,7 +12,7 @@ class VideoinfoCommand(Command): @classmethod async def common(cls, call: Call): url = call.args[0] - info_list = await asyncify(YtdlInfo.create_from_url, url) + info_list = await asyncify(YtdlInfo.retrieve_for_url, url) for info in info_list: info_dict = info.__dict__ message = f"🔍 Dati di [b]{info}[/b]:\n"