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..9fa9ec1 --- /dev/null +++ b/YouTubeMDBot/api/youtube_api.py @@ -0,0 +1,72 @@ +# 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 . +try: + import ujson as json +except ImportError: + import json +from urllib.request import urlopen + +import isodate +from googleapiclient.discovery import build + +from ..constants import YOUTUBE +from ..errors import EmptyBodyError + + +class YouTubeVideoData(object): + def __init__(self, data: dict): + if not data.get("items"): + raise EmptyBodyError("The data object has no items") + 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: + raise EmptyBodyError("No information available to requested video") + if not details: + raise EmptyBodyError("No video details available") + if not statistics: + raise EmptyBodyError("No statistics available") + self.title = snippet["title"] + self.thumbnail = snippet["thumbnails"]["maxres"]["url"] + self.artist = snippet["channelTitle"] + self.duration = isodate.parse_duration(details["duration"]).total_seconds() + self.view = int(statistics["viewCount"]) + self.like = int(statistics["likeCount"]) + self.dislike = int(statistics["dislikeCount"]) + + +class YouTubeAPI(object): + def __init__(self): + 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: + api_url = YOUTUBE["endpoint"].format(video_id, YOUTUBE["key"]) + print(api_url) + data = urlopen(url=api_url) + return YouTubeVideoData(data=json.loads(data.read())) diff --git a/YouTubeMDBot/constants/__init__.py b/YouTubeMDBot/constants/__init__.py index 31760cf..4b9bf4d 100644 --- a/YouTubeMDBot/constants/__init__.py +++ b/YouTubeMDBot/constants/__init__.py @@ -16,3 +16,4 @@ 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 56866d6..e05c7a6 100644 --- a/YouTubeMDBot/constants/app_constants.py +++ b/YouTubeMDBot/constants/app_constants.py @@ -15,7 +15,22 @@ # along with this program. If not, see . 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/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/__init__.py b/YouTubeMDBot/errors/__init__.py index 48f3467..9fc7566 100644 --- a/YouTubeMDBot/errors/__init__.py +++ b/YouTubeMDBot/errors/__init__.py @@ -13,4 +13,5 @@ # # 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 aaf9fa2..20bc56a 100644 --- a/YouTubeMDBot/metadata/MetadataIdentifier.py +++ b/YouTubeMDBot/metadata/MetadataIdentifier.py @@ -22,6 +22,7 @@ import json from ..audio import FPCalc +from ..api import YouTubeAPI from ..utils import youtube_utils from ..constants import ACOUSTID_KEY from ..downloader import YouTubeDownloader @@ -37,8 +38,23 @@ def __init__(self, audio: bytes, downloader: YouTubeDownloader = None): self.recording_id: str = "" self.score: float = 0.0 self.cover: bytes = bytes(0) + self.duration: int = 0 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, @@ -46,7 +62,9 @@ def identify_audio(self) -> json: duration=fingerprint.duration(), meta="recordings releaseids") self.result = data - if "results" in data and data["status"] == "ok": + print(len(data["results"])) + print(data["results"]) + if self._is_valid_result(data): for result in data["results"]: if "recordings" not in result: break @@ -60,10 +78,17 @@ def identify_audio(self) -> json: 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: - id = youtube_utils.get_yt_video_id(self._downloader.get_url()) - + 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() return data diff --git a/YouTubeMDBot/requirements.txt b/YouTubeMDBot/requirements.txt index 99a3959..d00dddf 100644 --- a/YouTubeMDBot/requirements.txt +++ b/YouTubeMDBot/requirements.txt @@ -1,3 +1,5 @@ +isodate +google-api-python-client musicbrainzngs ujson youtube_dl diff --git a/YouTubeMDBot/tests/identifier.py b/YouTubeMDBot/tests/identifier.py index 03517af..31af4f2 100644 --- a/YouTubeMDBot/tests/identifier.py +++ b/YouTubeMDBot/tests/identifier.py @@ -71,7 +71,7 @@ def find_metadata(self, downloader: YouTubeDownloader): f_dl_t = time() print("Downloaded {} - elapsed time: {:.1f}s".format(downloader.get_url(), f_dl_t - st_dl_t)) - identifier = MetadataIdentifier(audio=data) + identifier = MetadataIdentifier(audio=data, downloader=downloader) identifier.identify_audio() self.song_info[downloader.get_url()] = { "title": identifier.title, @@ -80,8 +80,7 @@ def find_metadata(self, downloader: YouTubeDownloader): "record_id": "https://musicbrainz.org/recording/{0}" .format(identifier.recording_id), "release_id": "https://musicbrainz.org/release/{0}" - .format(identifier.release_id), - "cover": identifier.cover + .format(identifier.release_id) } self.barrier()