Skip to content

Commit

Permalink
Merge branch 'pylint' into 'development'
Browse files Browse the repository at this point in the history
Pylint

See merge request Javinator9889/YouTubeMDBot!5
  • Loading branch information
Javinator9889 committed Oct 17, 2019
2 parents 9b5880d + 5c6c861 commit 8621eea
Show file tree
Hide file tree
Showing 18 changed files with 333 additions and 138 deletions.
28 changes: 28 additions & 0 deletions YouTubeMDBot/__init__.py
Expand Up @@ -13,3 +13,31 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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 EmptyBodyError

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
47 changes: 42 additions & 5 deletions YouTubeMDBot/api/youtube_api.py
Expand Up @@ -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 = ""
Expand Down Expand Up @@ -77,15 +98,25 @@ 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

self.__youtube = build(serviceName=YOUTUBE["api"]["name"],
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",
Expand All @@ -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)
68 changes: 68 additions & 0 deletions YouTubeMDBot/audio/ffmpeg.py
Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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:
Expand Down
22 changes: 21 additions & 1 deletion YouTubeMDBot/audio/fpcalc.py
Expand Up @@ -21,6 +21,10 @@


def is_fpcalc_available() -> bool:
"""
Checks if ffmpeg is installed in the system.
:return: True if available, else False.
"""
try:
proc = Popen(["fpcalc", "-v"], stdout=PIPE, stderr=PIPE)
except OSError:
Expand All @@ -29,8 +33,16 @@ def is_fpcalc_available() -> bool:
proc.wait()


class FPCalc(object):
class FPCalc:
"""
Calculates audio fingerprint by passing the audio bytes.
It operates with pipes so no file is created.
"""
def __init__(self, audio: bytes):
"""
Creates the FPCalc object.
:param audio: the audio bytes.
"""
fpcalc = Popen(FPCALC, stdout=PIPE, stdin=PIPE)
out, _ = fpcalc.communicate(audio)
res = out.decode("utf-8")
Expand All @@ -44,7 +56,15 @@ def __init__(self, audio: bytes):
self.__fp: str = str(fingerprint.group(0))

def duration(self) -> int:
"""
Obtains the audio duration in seconds.
:return: duration in seconds.
"""
return self.__duration

def fingerprint(self) -> str:
"""
Obtains the audio fingerprint.
:return: fingerprint in seconds.
"""
return self.__fp
1 change: 1 addition & 0 deletions YouTubeMDBot/constants/__init__.py
Expand Up @@ -17,5 +17,6 @@
from ..constants.app_constants import FPCALC
from ..constants.app_constants import YDL_CLI_OPTIONS
from ..constants.app_constants import YOUTUBE
from ..constants.app_constants import PROGRAM_ARGS
from ..constants.app_constants import FFMPEG_OPENER
from ..constants.app_constants import FFMPEG_CONVERTER
2 changes: 2 additions & 0 deletions YouTubeMDBot/constants/app_constants.py
Expand Up @@ -14,7 +14,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import sys

PROGRAM_ARGS = sys.argv
# YouTube DL options
YDL_CLI_OPTIONS = ["youtube-dl", "--format", "bestaudio[ext=m4a]", "--quiet", "--output",
"-"]
Expand Down
5 changes: 1 addition & 4 deletions YouTubeMDBot/decorators/decorators.py
Expand Up @@ -15,10 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from functools import wraps

from .. import PROGRAM_ARGS


# logging = LoggingHandler()
from ..constants import PROGRAM_ARGS


def send_action(action):
Expand Down
17 changes: 16 additions & 1 deletion YouTubeMDBot/downloader/youtube_downloader.py
Expand Up @@ -19,13 +19,24 @@
from ..constants.app_constants import YDL_CLI_OPTIONS


class YouTubeDownloader(object):
class YouTubeDownloader:
"""
Download a YouTube video directly into memory.
"""
def __init__(self, url: str):
"""
Creates the YouTubeDownloader object. Call "download" for obtaining the video.
:param url: the video URL.
"""
self.__url: str = url
self.__options: list = YDL_CLI_OPTIONS.copy()
self.__options.append(self.__url)

def download(self) -> Tuple[BytesIO, bytes]:
"""
Downloads the YouTube video directly into memory by using pipes.
:return: a tuple with "BytesIO" and "bytes".
"""
import subprocess

proc = subprocess.Popen(self.__options,
Expand All @@ -40,4 +51,8 @@ def download(self) -> Tuple[BytesIO, bytes]:
str(stderr.decode("utf-8")))

def get_url(self) -> str:
"""
Obtains the video URL.
:return: str with the URL.
"""
return self.__url
4 changes: 4 additions & 0 deletions YouTubeMDBot/errors/EmptyBodyError.py
Expand Up @@ -16,4 +16,8 @@


class EmptyBodyError(Exception):
"""
Raises an exception when the body of the json data is empty (e.g.: there is no
video information)
"""
pass
19 changes: 0 additions & 19 deletions YouTubeMDBot/errors/InvalidCredentialsError.py

This file was deleted.

20 changes: 0 additions & 20 deletions YouTubeMDBot/errors/NoMatchError.py

This file was deleted.

0 comments on commit 8621eea

Please sign in to comment.