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