Refactor embedding metadata to media

This commit is contained in:
Ritiek Malhotra
2020-03-25 02:04:24 +05:30
parent d154b2be20
commit c9a804268d
13 changed files with 349 additions and 261 deletions

View File

@@ -1,2 +1,5 @@
from spotdl.metadata.metadata_base import MetadataBase
from spotdl.metadata.metadata_base import StreamsBase
from spotdl.metadata.provider_base import ProviderBase
from spotdl.metadata.provider_base import StreamsBase
from spotdl.metadata.embedder_base import EmbedderBase

View File

@@ -0,0 +1,88 @@
import os
from abc import ABC
from abc import abstractmethod
class EmbedderBase(ABC):
"""
The class must define the supported media file encoding
formats here using a static variable - such as:
>>> supported_formats = ("mp3", "opus", "flac")
"""
supported_formats = ()
@abstractmethod
def __init__(self):
"""
For every supported format, there must be a corresponding
method that applies metadata on this format.
Such as if mp3 is supported, there must exist a method named
`as_mp3` on this class that applies metadata on mp3 files.
"""
# self.targets = { fmt: eval(str("self.as_" + fmt))
# for fmt in self.supported_formats }
#
# TODO: The above code seems to fail for some reason
# I do not know.
self.targets = {}
for fmt in self.supported_formats:
# FIXME: Calling `eval` is dangerous here!
self.targets[fmt] = eval("self.as_" + fmt)
def get_encoding(self, path):
"""
This method must determine the encoding for a local
audio file. Such as "mp3", "wav", "m4a", etc.
"""
_, extension = os.path.splitext(path)
# Ignore the initial dot from file extension
return extension[1:]
def apply_metadata(self, path, metadata, encoding=None):
"""
This method must automatically detect the media encoding
format from file path and embed the corresponding metadata
on the given file by calling an appropriate submethod.
"""
if encoding is None:
encoding = self.get_encoding(path)
if encoding not in self.supported_formats:
raise TypeError(
'The input format ("{}") is not supported.'.format(
encoding,
))
embed_on_given_format = self.targets[encoding]
embed_on_given_format(path, metadata)
def as_mp3(self, path, metadata):
"""
Method for mp3 support. This method might be defined in
a subclass.
Other methods for additional supported formats must also
be declared here.
"""
raise NotImplementedError
def as_opus(self, path, metadata):
"""
Method for opus support. This method might be defined in
a subclass.
Other methods for additional supported formats must also
be declared here.
"""
raise NotImplementedError
def as_flac(self, path, metadata):
"""
Method for flac support. This method might be defined in
a subclass.
Other methods for additional supported formats must also
be declared here.
"""
raise NotImplementedError

View File

@@ -0,0 +1,2 @@
from spotdl.metadata.embedders.default_embedder import EmbedderDefault

View File

@@ -0,0 +1,173 @@
from mutagen.easyid3 import EasyID3
from mutagen.id3 import ID3, TORY, TYER, TPUB, APIC, USLT, COMM
from mutagen.mp4 import MP4, MP4Cover
from mutagen.flac import Picture, FLAC
import urllib.request
from spotdl.metadata import EmbedderBase
# Apple has specific tags - see mutagen docs -
# http://mutagen.readthedocs.io/en/latest/api/mp4.html
M4A_TAG_PRESET = {
"album": "\xa9alb",
"artist": "\xa9ART",
"date": "\xa9day",
"title": "\xa9nam",
"year": "\xa9day",
"originaldate": "purd",
"comment": "\xa9cmt",
"group": "\xa9grp",
"writer": "\xa9wrt",
"genre": "\xa9gen",
"tracknumber": "trkn",
"albumartist": "aART",
"discnumber": "disk",
"cpil": "cpil",
"albumart": "covr",
"copyright": "cprt",
"tempo": "tmpo",
"lyrics": "\xa9lyr",
"comment": "\xa9cmt",
}
TAG_PRESET = {}
for key in M4A_TAG_PRESET.keys():
TAG_PRESET[key] = key
class EmbedderDefault(EmbedderBase):
supported_formats = ("mp3", "opus", "flac")
def __init__(self):
super().__init__()
self._m4a_tag_preset = M4A_TAG_PRESET
self._tag_preset = TAG_PRESET
# self.provider = "spotify" if metadata["spotify_metadata"] else "youtube"
def as_mp3(self, path, metadata):
""" Embed metadata to MP3 files. """
# EasyID3 is fun to use ;)
# For supported easyid3 tags:
# https://github.com/quodlibet/mutagen/blob/master/mutagen/easyid3.py
# Check out somewhere at end of above linked file
audiofile = EasyID3(path)
self._embed_basic_metadata(audiofile, metadata, "mp3", preset=TAG_PRESET)
audiofile["media"] = metadata["type"]
audiofile["author"] = metadata["artists"][0]["name"]
audiofile["lyricist"] = metadata["artists"][0]["name"]
audiofile["arranger"] = metadata["artists"][0]["name"]
audiofile["performer"] = metadata["artists"][0]["name"]
provider = metadata["provider"]
audiofile["website"] = metadata["external_urls"][provider]
audiofile["length"] = str(metadata["duration"])
if metadata["publisher"]:
audiofile["encodedby"] = metadata["publisher"]
if metadata["external_ids"]["isrc"]:
audiofile["isrc"] = metadata["external_ids"]["isrc"]
audiofile.save(v2_version=3)
# For supported id3 tags:
# https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py
# Each class represents an id3 tag
audiofile = ID3(path)
if metadata["year"]:
audiofile["TORY"] = TORY(encoding=3, text=metadata["year"])
audiofile["TYER"] = TYER(encoding=3, text=metadata["year"])
if metadata["publisher"]:
audiofile["TPUB"] = TPUB(encoding=3, text=metadata["publisher"])
provider = metadata["provider"]
audiofile["COMM"] = COMM(
encoding=3, text=metadata["external_urls"][provider]
)
if metadata["lyrics"]:
audiofile["USLT"] = USLT(
encoding=3, desc=u"Lyrics", text=metadata["lyrics"]
)
try:
albumart = urllib.request.urlopen(metadata["album"]["images"][0]["url"])
audiofile["APIC"] = APIC(
encoding=3,
mime="image/jpeg",
type=3,
desc=u"Cover",
data=albumart.read(),
)
albumart.close()
except IndexError:
pass
audiofile.save(v2_version=3)
def as_opus(self, path):
""" Embed metadata to M4A files. """
audiofile = MP4(path)
self._embed_basic_metadata(audiofile, metadata, "opus", preset=M4A_TAG_PRESET)
if metadata["year"]:
audiofile[M4A_TAG_PRESET["year"]] = metadata["year"]
provider = metadata["provider"]
audiofile[M4A_TAG_PRESET["comment"]] = metadata["external_urls"][provider]
if metadata["lyrics"]:
audiofile[M4A_TAG_PRESET["lyrics"]] = metadata["lyrics"]
try:
albumart = urllib.request.urlopen(metadata["album"]["images"][0]["url"])
audiofile[M4A_TAG_PRESET["albumart"]] = [
MP4Cover(albumart.read(), imageformat=MP4Cover.FORMAT_JPEG)
]
albumart.close()
except IndexError:
pass
audiofile.save()
def as_flac(self, path, metadata):
audiofile = FLAC(path)
self._embed_basic_metadata(audiofile, metadata, "flac")
if metadata["year"]:
audiofile["year"] = metadata["year"]
provider = metadata["provider"]
audiofile["comment"] = metadata["external_urls"][provider]
if metadata["lyrics"]:
audiofile["lyrics"] = metadata["lyrics"]
image = Picture()
image.type = 3
image.desc = "Cover"
image.mime = "image/jpeg"
albumart = urllib.request.urlopen(metadata["album"]["images"][0]["url"])
image.data = albumart.read()
albumart.close()
audiofile.add_picture(image)
audiofile.save()
def _embed_basic_metadata(self, audiofile, metadata, encoding, preset=TAG_PRESET):
audiofile[preset["artist"]] = metadata["artists"][0]["name"]
if metadata["album"]["artists"][0]["name"]:
audiofile[preset["albumartist"]] = metadata["album"]["artists"][0]["name"]
if metadata["album"]["name"]:
audiofile[preset["album"]] = metadata["album"]["name"]
audiofile[preset["title"]] = metadata["name"]
if metadata["release_date"]:
audiofile[preset["date"]] = metadata["release_date"]
audiofile[preset["originaldate"]] = metadata["release_date"]
if metadata["genre"]:
audiofile[preset["genre"]] = metadata["genre"]
if metadata["copyright"]:
audiofile[preset["copyright"]] = metadata["copyright"]
if encoding == "flac":
audiofile[preset["discnumber"]] = str(metadata["disc_number"])
else:
audiofile[preset["discnumber"]] = [(metadata["disc_number"], 0)]
if encoding == "flac":
audiofile[preset["tracknumber"]] = str(metadata["track_number"])
else:
if preset["tracknumber"] == TAG_PRESET["tracknumber"]:
audiofile[preset["tracknumber"]] = "{}/{}".format(
metadata["track_number"], metadata["total_tracks"]
)
else:
audiofile[preset["tracknumber"]] = [
(metadata["track_number"], metadata["total_tracks"])
]

View File

@@ -34,7 +34,7 @@ class StreamsBase(ABC):
return self.all[-1]
class MetadataBase(ABC):
class ProviderBase(ABC):
def set_credentials(self, client_id, client_secret):
"""
This method may or not be used depending on

View File

@@ -1,2 +1,3 @@
from spotdl.metadata.providers.spotify import MetadataSpotify
from spotdl.metadata.providers.youtube import MetadataYouTube
from spotdl.metadata.providers.spotify import ProviderSpotify
from spotdl.metadata.providers.youtube import ProviderYouTube

View File

@@ -1,10 +1,10 @@
import spotipy
import spotipy.oauth2 as oauth2
from spotdl.metadata import MetadataBase
from spotdl.metadata import ProviderBase
class MetadataSpotify(MetadataBase):
class ProviderSpotify(ProviderBase):
def __init__(self, spotify=None):
self.spotify = spotify

View File

@@ -4,7 +4,7 @@ from bs4 import BeautifulSoup
import urllib.request
from spotdl.metadata import StreamsBase
from spotdl.metadata import MetadataBase
from spotdl.metadata import ProviderBase
BASE_URL = "https://www.youtube.com/results?sp=EgIQAQ%253D%253D&q={}"
@@ -90,7 +90,7 @@ class YouTubeStreams(StreamsBase):
return self.all[-1]
class MetadataYouTube(MetadataBase):
class ProviderYouTube(ProviderBase):
def from_query(self, query):
watch_urls = YouTubeSearch().search(query)
return self.from_url(watch_urls[0])