diff --git a/YouTubeMDBot/__init__.py b/YouTubeMDBot/__init__.py index e9bdb16..8a858f1 100644 --- a/YouTubeMDBot/__init__.py +++ b/YouTubeMDBot/__init__.py @@ -13,4 +13,3 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - 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 index 8a858f1..48f3467 100644 --- a/YouTubeMDBot/errors/__init__.py +++ b/YouTubeMDBot/errors/__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 ..errors.NoMatchError import NoMatchError diff --git a/YouTubeMDBot/metadata/MetadataIdentifier.py b/YouTubeMDBot/metadata/MetadataIdentifier.py index 04768c4..aaf9fa2 100644 --- a/YouTubeMDBot/metadata/MetadataIdentifier.py +++ b/YouTubeMDBot/metadata/MetadataIdentifier.py @@ -22,64 +22,48 @@ import json from ..audio import FPCalc +from ..utils import youtube_utils from ..constants import ACOUSTID_KEY +from ..downloader import YouTubeDownloader class MetadataIdentifier(object): - def __init__(self, audio: bytes): - self.__fingerprint = FPCalc(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) + 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._downloader = downloader def identify_audio(self) -> json: + fingerprint = FPCalc(self.audio) data: json = acoustid.lookup(apikey=ACOUSTID_KEY, - fingerprint=self.__fingerprint.fingerprint(), - duration=self.__fingerprint.duration(), + fingerprint=fingerprint.fingerprint(), + duration=fingerprint.duration(), meta="recordings releaseids") - self.__result = data - if data["status"] == "ok" and "results" in data: - result = data["results"][0] - score = result["score"] - recording = result["recordings"][0] - if recording.get("artists"): - names = [artist["name"] for artist in recording["artists"]] - artist_name = "; ".join(names) - else: - artist_name = None - title = recording.get("title") - release_id = recording["releases"][0]["id"] - recording_id = recording.get("id") - - self.__score = score - self.__title = title - self.__recording_id = recording_id - self.__release_id = release_id - self.__artist = artist_name - self.__cover = musicbrainzngs.get_image_front(release_id) + self.result = data + if "results" in data and data["status"] == "ok": + 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.cover = musicbrainzngs.get_image_front(self.release_id) + break + break + elif self._downloader: + id = youtube_utils.get_yt_video_id(self._downloader.get_url()) + return data - - def get_title(self) -> str: - return self.__title - - def get_score(self) -> float: - return self.__score - - def get_artist(self) -> str: - return self.__artist - - def get_recording_id(self) -> str: - return self.__recording_id - - def get_release_id(self) -> str: - return self.__release_id - - def get_cover(self) -> bytes: - return self.__cover - - def get_results(self) -> json: - return self.__result diff --git a/YouTubeMDBot/tests/identifier.py b/YouTubeMDBot/tests/identifier.py index 86b1f7d..03517af 100644 --- a/YouTubeMDBot/tests/identifier.py +++ b/YouTubeMDBot/tests/identifier.py @@ -1,11 +1,19 @@ +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) @@ -18,14 +26,65 @@ def test_identification(self): print("{0} by {1} - score: {2} / 1\n" "\thttps://musicbrainz.org/recording/{3}\n" "\thttps://musicbrainz.org/release/{4}\n\n" - .format(identifier.get_title(), identifier.get_artist(), - identifier.get_score(), - identifier.get_recording_id(), identifier.get_release_id())) + .format(identifier.title, identifier.artist, + identifier.score, + identifier.recording_id, identifier.release_id)) with open("cover.jpg", "wb") as cover: - cover.write(identifier.get_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) + identifier.identify_audio() + self.song_info[downloader.get_url()] = { + "title": identifier.title, + "artist": identifier.artist, + "score": identifier.score, + "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 + } + self.barrier() + 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