Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Javinator9889 committed Sep 25, 2019
1 parent d187e1b commit f1a7c4b
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 6 deletions.
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
72 changes: 72 additions & 0 deletions 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 <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()))
1 change: 1 addition & 0 deletions YouTubeMDBot/constants/__init__.py
Expand Up @@ -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 changes: 15 additions & 0 deletions YouTubeMDBot/constants/app_constants.py
Expand Up @@ -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}"
}
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
1 change: 1 addition & 0 deletions YouTubeMDBot/errors/__init__.py
Expand Up @@ -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
31 changes: 28 additions & 3 deletions YouTubeMDBot/metadata/MetadataIdentifier.py
Expand Up @@ -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
Expand All @@ -37,16 +38,33 @@ 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,
fingerprint=fingerprint.fingerprint(),
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
Expand All @@ -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
2 changes: 2 additions & 0 deletions YouTubeMDBot/requirements.txt
@@ -1,3 +1,5 @@
isodate
google-api-python-client
musicbrainzngs
ujson
youtube_dl
Expand Down
5 changes: 2 additions & 3 deletions YouTubeMDBot/tests/identifier.py
Expand Up @@ -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,
Expand All @@ -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()

Expand Down

0 comments on commit f1a7c4b

Please sign in to comment.