Commit f1a7c4b4 authored by Javinator9889's avatar Javinator9889 🎼

Fallback to MusicBrainz fingerprint recognition & YouTube API

Now there the program is able to perform requests to YouTube servers in order to obtain information about a video ID and do a search - there is still needed a testing for the search capability
parent d187e1ba
Pipeline #70 passed with stage
in 2 minutes and 20 seconds
# 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
# 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/>.
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()))
......@@ -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
......@@ -15,7 +15,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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}"
}
# 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
......@@ -13,4 +13,5 @@
#
# 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 ..errors.EmptyBodyError import EmptyBodyError
from ..errors.NoMatchError import NoMatchError
......@@ -22,6 +22,7 @@ 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
......@@ -37,8 +38,23 @@ class MetadataIdentifier(object):
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 @@ class MetadataIdentifier(object):
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 @@ class MetadataIdentifier(object):
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
isodate
google-api-python-client
musicbrainzngs
ujson
youtube_dl
......
......@@ -71,7 +71,7 @@ class IdentifierTest(unittest.TestCase):
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 @@ class IdentifierTest(unittest.TestCase):
"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()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment