Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge branch 'milestone_2' into 'development'
Milestone #2

See merge request Javinator9889/YouTubeMDBot!4
  • Loading branch information
Javinator9889 committed Sep 26, 2019
2 parents 8c9fab7 + 285525d commit 245095e
Show file tree
Hide file tree
Showing 26 changed files with 619 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -102,3 +102,6 @@ venv.bak/

# mypy
.mypy_cache/

# keys folder
keys/
4 changes: 3 additions & 1 deletion .gitlab-ci.yml
Expand Up @@ -18,6 +18,9 @@ cache:

before_script:
- python -V # Print out python version for debugging
- apt update
- apt install -y libchromaprint-tools --install-recommends
- pip install -r YouTubeMDBot/requirements.txt

test:pylint:
script:
Expand All @@ -26,5 +29,4 @@ test:pylint:

test:
script:
- pip install -r YouTubeMDBot/requirements.txt
- python -m unittest $(pwd)/YouTubeMDBot/tests/*.py
8 changes: 0 additions & 8 deletions YouTubeMDBot/__init__.py
Expand Up @@ -13,11 +13,3 @@
#
# 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 .bot import PROGRAM_ARGS
from .bot import main

from .logging_utils import LoggingHandler
from .logging_utils import setup_logging

from .decorators import send_action
from .decorators import restricted
17 changes: 17 additions & 0 deletions YouTubeMDBot/api/__init__.py
@@ -0,0 +1,17 @@
# YouTubeMDBot
# Copyright (C) 2019 - Javinator9889
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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.youtube_api import YouTubeAPI
from ..api.youtube_api import YouTubeVideoData
106 changes: 106 additions & 0 deletions YouTubeMDBot/api/youtube_api.py
@@ -0,0 +1,106 @@
# YouTubeMDBot
# Copyright (C) 2019 - Javinator9889
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 isodate import parse_duration

from ..constants import YOUTUBE
from ..errors import EmptyBodyError


class YouTubeVideoData(object):
def __init__(self, data: dict, ignore_errors: bool = False):
if not data.get("items"):
raise EmptyBodyError("The data object has no items")
self.id: str = ""
self.title: str = ""
self.thumbnail: str = ""
self.artist: str = ""
self.duration: float = 0.0
self.views: int = 0
self.likes: int = 0
self.dislikes: int = 0
if len(data.get("items")) >= 1:
content = data.get("items")[0]
snippet = content.get("snippet")
details = content.get("contentDetails")
statistics = content.get("statistics")
if not snippet and not ignore_errors:
raise EmptyBodyError("No information available to requested video")
elif not snippet and ignore_errors:
snippet_available = False
else:
snippet_available = True
if not details and not ignore_errors:
raise EmptyBodyError("No video details available")
elif not details and ignore_errors:
details_available = False
else:
details_available = True
if not statistics and not ignore_errors:
raise EmptyBodyError("No statistics available")
elif not statistics and ignore_errors:
statistics_available = False
else:
statistics_available = True
c_id = content.get("id", "")
self.id = c_id.get("videoId", "") if isinstance(c_id, dict) else c_id
if snippet_available:
self.title = snippet["title"]
try:
self.thumbnail = snippet["thumbnails"]["maxres"]["url"]
except KeyError:
try:
self.thumbnail = snippet["thumbnails"]["high"]["url"]
except KeyError:
try:
self.thumbnail = snippet["thumbnails"]["medium"]["url"]
except KeyError:
self.thumbnail = snippet["thumbnails"]["default"]["url"]
self.artist = snippet["channelTitle"]
if details_available:
self.duration = parse_duration(details["duration"]).total_seconds()
if statistics_available:
self.views = int(statistics["viewCount"])
self.likes = int(statistics["likeCount"])
self.dislikes = int(statistics["dislikeCount"])


class YouTubeAPI(object):
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):
return self.__youtube.search().list(
q=term,
type="video",
part="id,snippet",
maxResults=1
).execute()

@staticmethod
def video_details(video_id: str) -> YouTubeVideoData:
try:
import ujson as json
except ImportError:
import json
from urllib.request import urlopen

api_url = YOUTUBE["endpoint"].format(video_id, YOUTUBE["key"])
data = urlopen(url=api_url)
return YouTubeVideoData(data=json.loads(data.read()))
18 changes: 18 additions & 0 deletions YouTubeMDBot/audio/__init__.py
@@ -0,0 +1,18 @@
# YouTubeMDBot
# Copyright (C) 2019 - Javinator9889
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 ..audio.ffmpeg import FFmpegOpener
from ..audio.ffmpeg import ffmpeg_available
from ..audio.fpcalc import FPCalc
49 changes: 49 additions & 0 deletions YouTubeMDBot/audio/ffmpeg.py
@@ -0,0 +1,49 @@
# YouTubeMDBot
# Copyright (C) 2019 - Javinator9889
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 io import BytesIO
from subprocess import PIPE
from subprocess import Popen


def ffmpeg_available() -> bool:
try:
proc = Popen(["ffmpeg", "-version"],
stdout=PIPE,
stderr=PIPE)
except OSError:
return False
else:
proc.wait()
return proc.returncode == 0


class FFmpegOpener(object):
def __init__(self, data: bytes):
io = BytesIO(data)
self.__ffmpeg_proc = Popen(["ffmpeg", "-i", "-", "-f", "s16le", "-"],
stdout=PIPE, stderr=PIPE, stdin=io)
self.__out = None
self.__err = None

def open(self) -> int:
self.__out, self.__err = self.__ffmpeg_proc.communicate()
return self.__ffmpeg_proc.returncode

def get_output(self) -> bytes:
return self.__out

def get_extra(self) -> bytes:
return self.__err
50 changes: 50 additions & 0 deletions YouTubeMDBot/audio/fpcalc.py
@@ -0,0 +1,50 @@
# YouTubeMDBot
# Copyright (C) 2019 - Javinator9889
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 re
from subprocess import PIPE
from subprocess import Popen

from ..constants import FPCALC


def is_fpcalc_available() -> bool:
try:
proc = Popen(["fpcalc", "-v"], stdout=PIPE, stderr=PIPE)
except OSError:
return False
else:
proc.wait()


class FPCalc(object):
def __init__(self, audio: bytes):
fpcalc = Popen(FPCALC, stdout=PIPE, stdin=PIPE)
out, _ = fpcalc.communicate(audio)
res = out.decode("utf-8")

duration_pattern = "[^=]\\d+\\n"
fingerprint_pattern = "[^=]*$"
duration = re.search(duration_pattern, res)
fingerprint = re.search(fingerprint_pattern, res)

self.__duration: int = int(duration.group(0))
self.__fp: str = str(fingerprint.group(0))

def duration(self) -> int:
return self.__duration

def fingerprint(self) -> str:
return self.__fp
4 changes: 2 additions & 2 deletions YouTubeMDBot/commands/StartHandler.py
Expand Up @@ -13,12 +13,12 @@
#
# 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 .. import LoggingHandler


class StartHandler(object):
def __init__(self):
self._user_data = {}

def start(self, bot, update):
self._user_data[]
pass
# TODO
5 changes: 4 additions & 1 deletion YouTubeMDBot/constants/__init__.py
Expand Up @@ -13,4 +13,7 @@
#
# 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 ..constants.app_constants import ydl_cli_options
from ..constants.app_constants import ACOUSTID_KEY
from ..constants.app_constants import FPCALC
from ..constants.app_constants import YDL_CLI_OPTIONS
from ..constants.app_constants import YOUTUBE
21 changes: 20 additions & 1 deletion YouTubeMDBot/constants/app_constants.py
Expand Up @@ -13,5 +13,24 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
ydl_cli_options = ["youtube-dl", "--format", "bestaudio[ext=m4a]", "--quiet", "--output",
import os

# YouTube DL options
YDL_CLI_OPTIONS = ["youtube-dl", "--format", "bestaudio[ext=m4a]", "--quiet", "--output",
"-"]

# FPCalc command
FPCALC = ["fpcalc", "-"]

# API keys
ACOUSTID_KEY = os.environ["ACOUSTID_KEY"]
YOUTUBE = {
"key": os.environ["YOUTUBE_KEY"],
"api": {
"name": "youtube",
"version": "v3"
},
"endpoint":
"https://www.googleapis.com/youtube/v3/videos?"
"part=id,snippet,contentDetails,statistics&id={0}&key={1}"
}
1 change: 0 additions & 1 deletion YouTubeMDBot/decorators/decorators.py
Expand Up @@ -48,7 +48,6 @@ def restricted(func):
def wrapped(update, context, *args, **kwargs):
user_id = update.effective_user.id
if user_id not in PROGRAM_ARGS["admin"]:
logging.warning("Unauthorized access denied for {}.".format(user_id))
return
return func(update, context, *args, **kwargs)
return wrapped
6 changes: 3 additions & 3 deletions YouTubeMDBot/downloader/youtube_downloader.py
Expand Up @@ -16,13 +16,13 @@
from io import BytesIO
from typing import Tuple

from ..constants.app_constants import ydl_cli_options
from ..constants.app_constants import YDL_CLI_OPTIONS


class YouTubeDownloader(object):
def __init__(self, url: str):
self.__url: str = url
self.__options: list = ydl_cli_options.copy()
self.__options: list = YDL_CLI_OPTIONS.copy()
self.__options.append(self.__url)

def download(self) -> Tuple[BytesIO, bytes]:
Expand All @@ -37,7 +37,7 @@ def download(self) -> Tuple[BytesIO, bytes]:
return BytesIO(stdout), stdout
else:
raise RuntimeError("youtube-dl downloader exception - more info: " +
str(stderr))
str(stderr.decode("utf-8")))

def get_url(self) -> str:
return self.__url
19 changes: 19 additions & 0 deletions YouTubeMDBot/errors/EmptyBodyError.py
@@ -0,0 +1,19 @@
# YouTubeMDBot
# Copyright (C) 2019 - Javinator9889
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.


class EmptyBodyError(Exception):
pass

0 comments on commit 245095e

Please sign in to comment.