diff --git a/YouTubeMDBot/__init__.py b/YouTubeMDBot/__init__.py index 8a858f1..a7f5f48 100755 --- a/YouTubeMDBot/__init__.py +++ b/YouTubeMDBot/__init__.py @@ -13,3 +13,34 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from .api import YouTubeAPI +from .api import YouTubeVideoData + +from .audio import FPCalc +from .audio import FFmpegOGG +from .audio import FFmpegMP3 +from .audio import FFmpegOpener +from .audio import ffmpeg_available + +from .commands import StartHandler + +from .constants import * + +from .decorators import restricted +from .decorators import send_action + +from .downloader import YouTubeDownloader + +from .errors import ProcessorError +from .errors import EmptyBodyError +from .errors import NoMatchError + +from .logging_utils import LoggingHandler +from .logging_utils import setup_logging + +from .metadata import AudioMetadata +from .metadata import MetadataIdentifier +from .metadata import YouTubeMetadataIdentifier + +from .utils import get_yt_video_id +from .utils import timeout diff --git a/YouTubeMDBot/api/youtube_api.py b/YouTubeMDBot/api/youtube_api.py index 1260023..d8ff82f 100755 --- a/YouTubeMDBot/api/youtube_api.py +++ b/YouTubeMDBot/api/youtube_api.py @@ -19,8 +19,29 @@ from ..errors import EmptyBodyError -class YouTubeVideoData(object): +class YouTubeVideoData: + """ + Obtains YouTube video data and wraps it inside this class. All fields are direct + access available, so it is possible to access them directly: + - title + - id + - thumbnail + - artist + - duration + - views + - likes + - dislikes + """ + def __init__(self, data: dict, ignore_errors: bool = False): + """ + By passing a dict with the YouTube data (YouTube API v3), generate and obtain + the information available from the result. + :param data: a dictionary with the information obtained from YouTube API. + :param ignore_errors: whether to ignore or not errors (do not raise exceptions). + :raises EmptyBodyError when there is no information available and ignored + errors is False. + """ if not data.get("items"): raise EmptyBodyError("The data object has no items") self.id: str = "" @@ -77,7 +98,12 @@ def __init__(self, data: dict, ignore_errors: bool = False): self.dislikes = int(statistics["dislikeCount"]) -class YouTubeAPI(object): +class YouTubeAPI: + """ + Wrapper for the YouTube API data. Allows the developer searching for videos and, + with a given video ID, obtain its data. + """ + def __init__(self): from googleapiclient.discovery import build @@ -85,7 +111,12 @@ def __init__(self): version=YOUTUBE["api"]["version"], developerKey=YOUTUBE["key"]) - def search(self, term: str): + def search(self, term: str) -> dict: + """ + Searchs for a video with the specified term. + :param term: the search term. + :return: dict with YouTube data - can be wrapped inside "YouTubeVideoData" class. + """ return self.__youtube.search().list( q=term, type="video", @@ -95,12 +126,18 @@ def search(self, term: str): @staticmethod def video_details(video_id: str) -> YouTubeVideoData: + """ + Generates a "YouTubeVideoData" object wrapper with the video ID information. + :param video_id: YouTube video ID. + :return: YouTubeVideoData object with the available metadata. + """ try: import ujson as json except ImportError: import json from urllib.request import urlopen - api_url = YOUTUBE["endpoint"].format(video_id, YOUTUBE["key"]) + youtube_information = YOUTUBE.copy() + api_url = youtube_information["endpoint"].format(video_id, YOUTUBE["key"]) data = urlopen(url=api_url) - return YouTubeVideoData(data=json.loads(data.read())) + return YouTubeVideoData(data=json.loads(data.read()), ignore_errors=True) diff --git a/YouTubeMDBot/audio/ffmpeg.py b/YouTubeMDBot/audio/ffmpeg.py index 53d199f..f94c5ae 100755 --- a/YouTubeMDBot/audio/ffmpeg.py +++ b/YouTubeMDBot/audio/ffmpeg.py @@ -24,6 +24,10 @@ def ffmpeg_available() -> bool: + """ + Checks if "ffmpeg" is installed or not. + :return: True if installed, else False + """ try: proc = Popen(["ffmpeg", "-version"], stdout=PIPE, @@ -36,46 +40,107 @@ def ffmpeg_available() -> bool: class FFmpeg(ABC): + """ + Base abstract class for the FFmpeg operators. All classes that works with FFmpeg + must inherit from this class in order to maintain readability and code optimization. + + Allows execution of the ffmpeg command by using the subprocess module. Everything + is working with PIPEs, so there is no directly discs operations (everything is + loaded and working with RAM). + """ + def __init__(self, data: bytes, command: List[str] = None): + """ + Creates the class by passing the data which will be processed and the command ( + by default, None). + :param data: audio data that will be processed. + :param command: the ffmpeg command. + """ self._data = data self.__command = command self.__out = None self.__err = None def process(self) -> int: + """ + Runs the ffmpeg command in a separate process and pipes both stdout and stderr. + :return: the return code of the operation ('0' if everything is OK, > 0 if not). + """ proc = Popen(self.__command, stdout=PIPE, stderr=PIPE, stdin=PIPE) self.__out, self.__err = proc.communicate(self._data) return proc.returncode def get_command(self) -> List[str]: + """ + Get the command for editing. + :return: List[str] with the command - as this is a pointer, all editions done + to the list are directly changing the self object. + """ return self.__command def set_command(self, command: List[str]): + """ + Sets the new list, overriding every old implementation. + :param command: the new command. + """ self.__command = command def get_output(self) -> bytes: + """ + Gets the stdout of the process. + :return: bytes with the command output. + """ return self.__out def get_extra(self) -> bytes: + """ + Gets the stderr of the process. + :return: bytes with extra information. + """ return self.__err class FFmpegOpener(FFmpeg): + """ + Opens and produces and audio in PWM mode. + """ + def __init__(self, data: bytes): super().__init__(data=data, command=FFMPEG_OPENER.copy()) class FFmpegExporter(FFmpeg): + """ + Base class for the exporter options available in ffmpeg. + All classes that are developed for converting audio files must inherit from this + class and implement the "convert" method. + """ + def __init__(self, data: bytes, bitrate: str = None): + """ + Generates a new instance of the class. + :param data: the audio data. + :param bitrate: the new bitrate of the audio data, or None for keeping its + default. + """ super().__init__(data=data, command=FFMPEG_CONVERTER.copy()) self._bitrate = bitrate @abstractmethod def convert(self) -> int: + """ + Converts the audio to the desired format. + :return: the operation result code. + :raises NotImplementedError when trying to access this method directly on super + class. + """ raise NotImplementedError class FFmpegMP3(FFmpegExporter): + """ + Exports audio data to MP3 format. + """ def convert(self) -> int: command = super().get_command() if self._bitrate: @@ -90,6 +155,9 @@ def convert(self) -> int: class FFmpegOGG(FFmpegExporter): + """ + Exports audio data to OGG format. + """ def convert(self) -> int: command = super().get_command() if self._bitrate: diff --git a/YouTubeMDBot/utils/__init__.py b/YouTubeMDBot/utils/__init__.py index 1315c64..d94ea00 100755 --- a/YouTubeMDBot/utils/__init__.py +++ b/YouTubeMDBot/utils/__init__.py @@ -13,5 +13,5 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from ..utils import youtube_utils +from ..utils.youtube_utils import get_yt_video_id from ..utils.timeout import timeout