diff --git a/.gitignore b/.gitignore index 894a44c..7be2372 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ venv.bak/ # mypy .mypy_cache/ + +# keys folder +keys/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3ffca5e..e74df8f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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: @@ -26,5 +29,4 @@ test:pylint: test: script: - - pip install -r YouTubeMDBot/requirements.txt - python -m unittest $(pwd)/YouTubeMDBot/tests/*.py diff --git a/YouTubeMDBot/__init__.py b/YouTubeMDBot/__init__.py index d0c79d3..8a858f1 100644 --- a/YouTubeMDBot/__init__.py +++ b/YouTubeMDBot/__init__.py @@ -13,11 +13,3 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -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 diff --git a/YouTubeMDBot/api/__init__.py b/YouTubeMDBot/api/__init__.py new file mode 100644 index 0000000..4ba8496 --- /dev/null +++ b/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 . +from ..api.youtube_api import YouTubeAPI +from ..api.youtube_api import YouTubeVideoData diff --git a/YouTubeMDBot/api/youtube_api.py b/YouTubeMDBot/api/youtube_api.py new file mode 100644 index 0000000..1260023 --- /dev/null +++ b/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 . +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())) diff --git a/YouTubeMDBot/audio/__init__.py b/YouTubeMDBot/audio/__init__.py new file mode 100644 index 0000000..c6ce402 --- /dev/null +++ b/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 . +from ..audio.ffmpeg import FFmpegOpener +from ..audio.ffmpeg import ffmpeg_available +from ..audio.fpcalc import FPCalc diff --git a/YouTubeMDBot/audio/ffmpeg.py b/YouTubeMDBot/audio/ffmpeg.py new file mode 100644 index 0000000..ac8ee7b --- /dev/null +++ b/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 . +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 diff --git a/YouTubeMDBot/audio/fpcalc.py b/YouTubeMDBot/audio/fpcalc.py new file mode 100644 index 0000000..d4833fc --- /dev/null +++ b/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 . +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 diff --git a/YouTubeMDBot/commands/StartHandler.py b/YouTubeMDBot/commands/StartHandler.py index d209a1e..8d18c8e 100644 --- a/YouTubeMDBot/commands/StartHandler.py +++ b/YouTubeMDBot/commands/StartHandler.py @@ -13,7 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from .. import LoggingHandler class StartHandler(object): @@ -21,4 +20,5 @@ def __init__(self): self._user_data = {} def start(self, bot, update): - self._user_data[] + pass + # TODO diff --git a/YouTubeMDBot/constants/__init__.py b/YouTubeMDBot/constants/__init__.py index 1402386..4b9bf4d 100644 --- a/YouTubeMDBot/constants/__init__.py +++ b/YouTubeMDBot/constants/__init__.py @@ -13,4 +13,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -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 diff --git a/YouTubeMDBot/constants/app_constants.py b/YouTubeMDBot/constants/app_constants.py index e1a7cb1..e05c7a6 100644 --- a/YouTubeMDBot/constants/app_constants.py +++ b/YouTubeMDBot/constants/app_constants.py @@ -13,5 +13,24 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -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}" +} diff --git a/YouTubeMDBot/decorators/decorators.py b/YouTubeMDBot/decorators/decorators.py index b378e01..ab6f59e 100644 --- a/YouTubeMDBot/decorators/decorators.py +++ b/YouTubeMDBot/decorators/decorators.py @@ -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 diff --git a/YouTubeMDBot/downloader/youtube_downloader.py b/YouTubeMDBot/downloader/youtube_downloader.py index 8736255..6139205 100644 --- a/YouTubeMDBot/downloader/youtube_downloader.py +++ b/YouTubeMDBot/downloader/youtube_downloader.py @@ -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]: @@ -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 diff --git a/YouTubeMDBot/errors/EmptyBodyError.py b/YouTubeMDBot/errors/EmptyBodyError.py new file mode 100644 index 0000000..4706bda --- /dev/null +++ b/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 . + + +class EmptyBodyError(Exception): + pass diff --git a/YouTubeMDBot/errors/InvalidCredentialsError.py b/YouTubeMDBot/errors/InvalidCredentialsError.py new file mode 100644 index 0000000..9b3d8c3 --- /dev/null +++ b/YouTubeMDBot/errors/InvalidCredentialsError.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 . + + +class InvalidCredentialsError(Exception): + pass diff --git a/YouTubeMDBot/errors/NoMatchError.py b/YouTubeMDBot/errors/NoMatchError.py new file mode 100644 index 0000000..69c5c3f --- /dev/null +++ b/YouTubeMDBot/errors/NoMatchError.py @@ -0,0 +1,20 @@ +# 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 . + + +class NoMatchError(Exception): + """Raises an error when there is no match available""" + pass diff --git a/YouTubeMDBot/errors/__init__.py b/YouTubeMDBot/errors/__init__.py new file mode 100644 index 0000000..9fc7566 --- /dev/null +++ b/YouTubeMDBot/errors/__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 . +from ..errors.EmptyBodyError import EmptyBodyError +from ..errors.NoMatchError import NoMatchError diff --git a/YouTubeMDBot/metadata/MetadataIdentifier.py b/YouTubeMDBot/metadata/MetadataIdentifier.py index b473a23..69039cb 100644 --- a/YouTubeMDBot/metadata/MetadataIdentifier.py +++ b/YouTubeMDBot/metadata/MetadataIdentifier.py @@ -14,7 +14,83 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import acoustid +import musicbrainzngs + +try: + import ujson as json +except ImportError: + import json + +from ..audio import FPCalc +from ..api import YouTubeAPI +from ..utils import youtube_utils +from ..constants import ACOUSTID_KEY +from ..downloader import YouTubeDownloader class MetadataIdentifier(object): - def __init__(self, filename: str = None, audio: str = None): + def __init__(self, audio: bytes, downloader: YouTubeDownloader = None): + self.audio = audio + self.result: json = None + self.artist: str = "" + self.title: str = "" + self.release_id: str = "" + self.recording_id: str = "" + self.score: float = 0.0 + self.cover: bytes = bytes(0) + self.duration: int = 0 + self.youtube_data: bool = False + self.youtube_id: str = "" + self._downloader = downloader + + @staticmethod + def _is_valid_result(data: json) -> bool: + if "results" not in data: + return False + elif data["status"] != "ok": + return False + elif len(data["results"]) == 0: + return False + else: + if "recordings" not in data["results"][0]: + return False + else: + return True + + def identify_audio(self) -> json: + fingerprint = FPCalc(self.audio) + data: json = acoustid.lookup(apikey=ACOUSTID_KEY, + fingerprint=fingerprint.fingerprint(), + duration=fingerprint.duration(), + meta="recordings releaseids") + self.result = data + if self._is_valid_result(data): + for result in data["results"]: + if "recordings" not in result: + break + self.score = result["score"] + for recording in result["recordings"]: + if recording.get("artists"): + names = [artist["name"] for artist in recording["artists"]] + self.artist = "; ".join(names) + else: + self.artist = "Unknown" + self.title = recording["title"] + self.release_id = recording["releases"][0]["id"] + self.recording_id = recording["id"] + self.duration = recording["duration"] + self.cover = musicbrainzngs.get_image_front(self.release_id) + break + break + elif self._downloader: + from urllib.request import urlopen + + video_id = youtube_utils.get_yt_video_id(self._downloader.get_url()) + video_data = YouTubeAPI.video_details(video_id) + self.title = video_data.title + self.artist = video_data.artist + self.duration = video_data.duration + self.cover = urlopen(video_data.thumbnail).read() + self.youtube_id = video_data.id + self.youtube_data = True + return data diff --git a/YouTubeMDBot/metadata/__init__.py b/YouTubeMDBot/metadata/__init__.py index 8a858f1..3799511 100644 --- a/YouTubeMDBot/metadata/__init__.py +++ b/YouTubeMDBot/metadata/__init__.py @@ -13,3 +13,4 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from ..metadata.MetadataIdentifier import MetadataIdentifier diff --git a/YouTubeMDBot/requirements.txt b/YouTubeMDBot/requirements.txt index 9c3d398..d00dddf 100644 --- a/YouTubeMDBot/requirements.txt +++ b/YouTubeMDBot/requirements.txt @@ -1,3 +1,7 @@ +isodate +google-api-python-client +musicbrainzngs +ujson youtube_dl pyacoustid python-telegram-bot diff --git a/YouTubeMDBot/tests/identifier.py b/YouTubeMDBot/tests/identifier.py new file mode 100644 index 0000000..dec5b80 --- /dev/null +++ b/YouTubeMDBot/tests/identifier.py @@ -0,0 +1,94 @@ +import threading +import unittest +from pprint import pprint +from time import sleep +from time import time + +from YouTubeMDBot.downloader import YouTubeDownloader +from YouTubeMDBot.metadata import MetadataIdentifier + + +class IdentifierTest(unittest.TestCase): + lock = threading.Lock() + threads = 0 + max = 0 + song_info = {} + + def test_identification(self): + url = "https://www.youtube.com/watch?v=YQHsXMglC9A" + downloader = YouTubeDownloader(url=url) + audio, data = downloader.download() + with open("hello.m4a", "wb") as song: + song.write(data) + identifier = MetadataIdentifier(audio=data) + + results = identifier.identify_audio() + print("{0} by {1} - score: {2} / 1\n" + "\thttps://musicbrainz.org/recording/{3}\n" + "\thttps://musicbrainz.org/release/{4}\n\n" + .format(identifier.title, identifier.artist, + identifier.score, + identifier.recording_id, identifier.release_id)) + with open("cover.jpg", "wb") as cover: + cover.write(identifier.cover) + + pprint(results) + + def test_multiple_download_identification(self): + yt1 = YouTubeDownloader(url="https://www.youtube.com/watch?v=Inm-N5rLUSI") + yt2 = YouTubeDownloader(url="https://www.youtube.com/watch?v=-_ZwpOdXXcA") + yt3 = YouTubeDownloader(url="https://www.youtube.com/watch?v=WOGWZD5iT10") + yt4 = YouTubeDownloader(url="https://www.youtube.com/watch?v=GfKV9KaNJXc") + + t1 = threading.Thread(target=self.find_metadata, args=(yt1,)) + t2 = threading.Thread(target=self.find_metadata, args=(yt2,)) + t3 = threading.Thread(target=self.find_metadata, args=(yt3,)) + t4 = threading.Thread(target=self.find_metadata, args=(yt4,)) + + self.max = 4 + + t1.start() + t2.start() + t3.start() + t4.start() + + while self.threads < self.max: + sleep(1) + + pprint(self.song_info) + + def barrier(self): + with self.lock: + self.threads += 1 + + def getThreads(self): + with self.lock: + return self.threads + + def find_metadata(self, downloader: YouTubeDownloader): + st_dl_t = time() + _, data = downloader.download() + f_dl_t = time() + print("Downloaded {} - elapsed time: {:.1f}s".format(downloader.get_url(), + f_dl_t - st_dl_t)) + identifier = MetadataIdentifier(audio=data, downloader=downloader) + identifier.identify_audio() + self.song_info[downloader.get_url()] = { + "title": identifier.title, + "artist": identifier.artist + } + if not identifier.youtube_data: + self.song_info[downloader.get_url()]["score"] = identifier.score + self.song_info[downloader.get_url()]["record_id"] = \ + "https://musicbrainz.org/recording/{0}".format(identifier.recording_id) + self.song_info[downloader.get_url()]["release_id"] = \ + "https://musicbrainz.org/release/{0}".format(identifier.release_id) + else: + self.song_info[downloader.get_url()]["duration"] = identifier.duration + self.song_info[downloader.get_url()]["id"] = identifier.youtube_id + self.song_info[downloader.get_url()]["youtube_data"] = True + self.barrier() + + +if __name__ == '__main__': + unittest.main() diff --git a/YouTubeMDBot/tests/song_search.py b/YouTubeMDBot/tests/song_search.py new file mode 100644 index 0000000..c4afadb --- /dev/null +++ b/YouTubeMDBot/tests/song_search.py @@ -0,0 +1,25 @@ +import unittest + +from YouTubeMDBot.api import YouTubeAPI +from YouTubeMDBot.api import YouTubeVideoData + + +class TestSearch(unittest.TestCase): + def test_search(self): + s = YouTubeAPI() + search: dict = s.search(term="test") + data = YouTubeVideoData(data=search, ignore_errors=True) + print("Title: {0}\n" + "Artist: {1}\n" + "Thumbnail: {2}\n" + "Duration: {3}\n" + "Views: {4}\n" + "Likes: {5}\n" + "Dislikes: {6}\n" + "Id: {7}".format(data.title, data.artist, data.thumbnail, + data.duration, data.views, data.likes, + data.dislikes, data.id)) + + +if __name__ == '__main__': + unittest.main() diff --git a/YouTubeMDBot/utils/__init__.py b/YouTubeMDBot/utils/__init__.py new file mode 100644 index 0000000..d12bbb3 --- /dev/null +++ b/YouTubeMDBot/utils/__init__.py @@ -0,0 +1,16 @@ +# 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 . +from ..utils import youtube_utils diff --git a/YouTubeMDBot/utils/youtube_utils.py b/YouTubeMDBot/utils/youtube_utils.py new file mode 100644 index 0000000..7d66274 --- /dev/null +++ b/YouTubeMDBot/utils/youtube_utils.py @@ -0,0 +1,52 @@ +# 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 . + + +def get_yt_video_id(url: str) -> str: + # initial version: http://stackoverflow.com/a/7936523/617185 \ + # by Mikhail Kashkin(http://stackoverflow.com/users/85739/mikhail-kashkin) + """Returns Video_ID extracting from the given url of Youtube + + Examples of URLs: + Valid: + 'http://youtu.be/_lOT2p_FCvA', + 'www.youtube.com/watch?v=_lOT2p_FCvA&feature=feedu', + 'http://www.youtube.com/embed/_lOT2p_FCvA', + 'http://www.youtube.com/v/_lOT2p_FCvA?version=3&hl=en_US', + 'https://www.youtube.com/watch?v=rTHlyTphWP0&index=6&list=PLjeDyYvG6-40qawYNR4juzvSOg-ezZ2a6', + 'youtube.com/watch?v=_lOT2p_FCvA', + + Invalid: + 'youtu.be/watch?v=_lOT2p_FCvA', + """ + + from urllib.parse import urlparse + from urllib.parse import parse_qs + + if url.startswith(('youtu', 'www')): + url = 'http://' + url + + query = urlparse(url) + + if 'youtube' in query.hostname: + if query.path == '/watch': + return parse_qs(query.query)['v'][0] + elif query.path.startswith(('/embed/', '/v/')): + return query.path.split('/')[2] + elif 'youtu.be' in query.hostname: + return query.path[1:] + else: + raise ValueError diff --git a/cover.jpg b/cover.jpg new file mode 100644 index 0000000..169117d Binary files /dev/null and b/cover.jpg differ diff --git a/hello.m4a b/hello.m4a new file mode 100644 index 0000000..1126d7e Binary files /dev/null and b/hello.m4a differ