Skip to content

Commit

Permalink
Merge pull request #77 from freddy36/metadata
Browse files Browse the repository at this point in the history
add new metadata fields
  • Loading branch information
jo1gi authored Dec 22, 2023
2 parents c1f61e1 + 8d1a238 commit ffa1cf3
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 15 deletions.
51 changes: 44 additions & 7 deletions audiobookdl/output/metadata/id3.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
import re
import os
from datetime import date
from audiobookdl import logging, Chapter, AudiobookMetadata, Cover

from mutagen import File as MutagenFile
from mutagen.easyid3 import EasyID3
from mutagen.easyid3 import EasyID3, EasyID3KeyError
from mutagen.mp3 import MP3
from mutagen.id3 import ID3, APIC, CHAP, TIT2, CTOC, CTOCFlags, ID3NoHeaderError
from mutagen.id3 import ID3, APIC, CHAP, TIT2, CTOC, CTOCFlags, WCOM, ID3NoHeaderError

from typing import Sequence

EasyID3.RegisterTextKey("comment", "COMM")
EasyID3.RegisterTextKey("year", "TYER")
EasyID3.RegisterTextKey("originalreleaseyear", "TORY")
EasyID3.RegisterTXXXKey("isbn", "ISBN")

def commercialurl_get(id3, key):
urls = [frame.url for frame in id3.getall("WCOM")]
if urls:
return urls
else:
raise EasyID3KeyError(key)

def commercialurl_set(id3, key, value):
id3.delall("WCOM")
for v in value:
id3.add(WCOM(url=v))

def commercialurl_delete(id3, key):
id3.delall("WCOM")

EasyID3.RegisterKey("commercialurl", commercialurl_get, commercialurl_set, commercialurl_delete)

# Conversion table between metadata names and ID3 tags
ID3_CONVERT = {
"author": "artist",
"authors": "artist",
"series": "album",
"title": "title",
"narrator": "performer",
"publisher": "organization", # TPUB
"description": "comment", # COMM
"genres": "genre", # TCON
"scrape_url": "commercialurl", # WCOM
}

# List of file formats that use ID3 metadata
Expand All @@ -36,12 +62,23 @@ def add_id3_metadata(filepath: str, metadata: AudiobookMetadata):
"""Add ID3 metadata tags to the given audio file"""
audio = MP3(filepath, ID3=EasyID3)
# Adding tags
for key, value in metadata.all_properties(allow_duplicate_keys=False):
if key in ID3_CONVERT:
for key, value in metadata.all_properties(allow_duplicate_keys=None):
if key == "release_date":
value: date
audio["originaldate"] = value.strftime("%Y-%m-%d")
audio["year"] = audio["originaldate"]
elif key == "language":
audio["language"] = value.alpha_3
elif key == "narrators":
audio["composer"] = value
audio["performer"] = value
elif key == "series_order":
audio["tracknumber"] = str(value)
elif key in ID3_CONVERT:
audio[ID3_CONVERT[key]] = value
elif key in EasyID3.valid_keys.keys():
audio[key] = value
audio.save(v2_version=3)
audio.save(v2_version=4)


def embed_id3_cover(filepath: str, cover: Cover):
Expand Down
27 changes: 23 additions & 4 deletions audiobookdl/output/metadata/mp4.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import re
from datetime import date

from audiobookdl import logging, AudiobookMetadata, Cover
from mutagen.easymp4 import EasyMP4
from mutagen.easymp4 import EasyMP4, EasyMP4Tags
from mutagen.mp4 import MP4, MP4Cover, Chapter as MP4Chapter, MP4Chapters

MP4_EXTENSIONS = ["mp4","m4a","m4p","m4b","m4r","m4v"]

MP4_CONVERT = {
"author": "artist",
"authors": "artist",
"narrators": "narrator",
"publishers": "publisher",
"series": "album",
"title": "title",
"genres": "genre",
}

MP4_COVER_FORMATS = {
Expand All @@ -18,6 +22,12 @@
"png": MP4Cover.FORMAT_PNG,
}

EasyMP4Tags.RegisterTextKey("year", 'yrrc')
EasyMP4Tags.RegisterTextKey("narrator", '\xa9nrt')
EasyMP4Tags.RegisterTextKey("publisher", '\xa9pub')
EasyMP4Tags.RegisterTextKey("track", '\xa9trk')
EasyMP4Tags.RegisterFreeformKey("scrape_url", "URL")

def is_mp4_file(filepath: str) -> bool:
"""Returns true if `filepath` points to an id3 file"""
ext = re.search(r"(?<=(\.))\w+$", filepath)
Expand All @@ -27,9 +37,18 @@ def is_mp4_file(filepath: str) -> bool:
def add_mp4_metadata(filepath: str, metadata: AudiobookMetadata):
"""Add mp4 metadata tags to the given audio file"""
audio = EasyMP4(filepath)
for key, value in metadata.all_properties(allow_duplicate_keys=True):
for key, value in metadata.all_properties(allow_duplicate_keys=None):
# System defined metadata tags
if key in MP4_CONVERT:
if key == "release_date":
value: date
audio["date"] = value.strftime("%Y-%m-%d")
audio["year"] = str(value.year)
elif key == "language":
audio.tags.RegisterFreeformKey(key, key.capitalize())
audio["language"] = value.alpha_3
elif key == "series_order":
audio["track"] = str(value)
elif key in MP4_CONVERT:
audio[MP4_CONVERT[key]] = value
elif key in audio.Get.keys():
audio[key] = value
Expand Down
58 changes: 54 additions & 4 deletions audiobookdl/utils/audiobook.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from datetime import date
import requests
from typing import Dict, Generic, List, Optional, Union, Sequence, Tuple, TypeVar
import json
from attrs import define, Factory
import pycountry


@define
Expand Down Expand Up @@ -45,12 +47,17 @@ class AudiobookFile:
@define
class AudiobookMetadata:
title: str
scrape_url: Optional[str] = None
series: Optional[str] = None
series_order: Optional[int] = None
authors: List[str] = Factory(list)
narrators: List[str] = Factory(list)
language: Optional[str] = None
genres: List[str] = Factory(list)
language: Optional["pycountry.db.Language"] = None
description: Optional[str] = None
isbn: Optional[str] = None
publisher: Optional[str] = None
release_date: Optional[date] = None

def add_author(self, author: str):
"""Add author to metadata"""
Expand All @@ -60,28 +67,46 @@ def add_narrator(self, narrator: str):
"""Add narrator to metadata"""
self.narrators.append(narrator)

def add_genre(self, genre: str):
"""Add genre to metadata"""
self.genres.append(genre)

def add_authors(self, authors: Sequence[str]):
self.authors.extend(authors)

def add_narrators(self, narrators: Sequence[str]):
self.narrators.extend(narrators)

def add_genres(self, genres: Sequence[str]):
self.genres.extend(genres)

def all_properties(self, allow_duplicate_keys = False) -> List[Tuple[str, str]]:
result: List[Tuple[str, str]] = []
add = add_if_value_exists(self, result)
add("title")
add("scrape_url")
add("series")
add("series_order")
add("language")
add("description")
add("isbn")
if allow_duplicate_keys:
add("publisher")
add("release_date")
if allow_duplicate_keys == None: # return original lists
add("authors")
add("narrators")
add("genres")
elif allow_duplicate_keys == True: # return lists as multiple keys
for author in self.authors:
result.append(("author", author))
for narrator in self.narrators:
result.append(("narrator", narrator))
else:
for genre in self.genres:
result.append(("genre", genre))
else: # return lists concatenated into a string
result.append(("author", self.author))
result.append(("narrator", self.narrator))
result.append(("genre", self.genre))
return result

def all_properties_dict(self) -> Dict[str, str]:
Expand All @@ -100,6 +125,11 @@ def narrator(self) -> str:
"""All narrators concatenated into a single string"""
return "; ".join(self.narrators)

@property
def genre(self) -> str:
"""All genres concatenated into a single string"""
return "; ".join(self.genres)


def as_dict(self) -> dict:
"""
Expand All @@ -111,13 +141,25 @@ def as_dict(self) -> dict:
"title": self.title,
"authors": self.authors,
"narrators": self.narrators,
"genres": self.genres,
}
if self.scrape_url:
result["scrape_url"] = self.scrape_url
if self.series:
result["series"] = self.series
if self.series_order:
result["series_order"] = self.series_order
if self.language:
result["language"] = self.language
if self.description:
result["description"] = self.description
if self.isbn:
result["isbn"] = self.isbn
if self.publisher:
result["publisher"] = self.publisher
if self.release_date:
result["release_date"] = self.release_date

return result


Expand All @@ -127,7 +169,15 @@ def as_json(self) -> str:
:returns: Metadata as json
"""
return json.dumps(self.as_dict())
class AudiobookMetadataJSONEncoder(json.JSONEncoder):
def default(self, z):
if isinstance(z, date):
return str(z)
elif isinstance(z, pycountry.db.Data) and z.__class__.__name__ == "Language":
return z.alpha_3
else:
return super().default(z)
return json.dumps(self.as_dict(), cls=AudiobookMetadataJSONEncoder)


def add_if_value_exists(metadata: AudiobookMetadata, l: List[Tuple[str, str]]):
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies = [
"requests",
"rich",
"tomli",
"pycountry",
]
dynamic = ["version"]

Expand Down
1 change: 1 addition & 0 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mkShell {
appdirs
tomli
attrs
pycountry

# Test
pytest
Expand Down

0 comments on commit ffa1cf3

Please sign in to comment.