write metadata to file on m4b creation
This commit is contained in:
1 parent
be012e334b
commit
dc537635bb
1 file changed
+131
-42
@@ -15,8 +15,23 @@ GREEN = "\033[92m"
|
||||
YELLOW = "\033[93m"
|
||||
BLUE = "\033[94m"
|
||||
RESET = "\033[0m"
|
||||
TRACE = 5
|
||||
class CustomLogger(logging.Logger):
|
||||
def trace(self, msg: str, *args, **kwargs) -> None:
|
||||
if self.isEnabledFor(TRACE):
|
||||
self._log(TRACE, msg, args, **kwargs)
|
||||
|
||||
logging.setLoggerClass(CustomLogger)
|
||||
logger: CustomLogger = logging.getLogger(__name__) # type: ignore[assignment]
|
||||
LOG_LEVELS = {
|
||||
"TRACE": TRACE,
|
||||
"DEBUG": logging.DEBUG,
|
||||
"INFO": logging.INFO,
|
||||
"WARNING": logging.WARNING,
|
||||
"ERROR": logging.ERROR,
|
||||
"CRITICAL": logging.CRITICAL,
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
audio_extensions = {
|
||||
"*.m4b",
|
||||
"*.m4a",
|
||||
@@ -93,18 +108,25 @@ def get_duration_ms(media: Path) -> int:
|
||||
|
||||
|
||||
class Audiobook():
|
||||
def __init__(self, bookPath: Path) -> None:
|
||||
self.bookPath = bookPath
|
||||
def __init__(self, book_path: Path) -> None:
|
||||
self.metadata = None
|
||||
self.artist = None
|
||||
self.genre = None
|
||||
self.composer = None
|
||||
self.name = None
|
||||
self.comment = None
|
||||
self.bookPath = book_path
|
||||
self.files = [file for pattern in audio_extensions for file in
|
||||
bookPath.glob(pattern)]
|
||||
book_path.glob(pattern)]
|
||||
self.firstFile = self.get_first_file()
|
||||
self.size = sum(file.stat().st_size for pattern in audio_extensions for file in
|
||||
bookPath.glob(pattern))
|
||||
book_path.glob(pattern))
|
||||
self.fileConunt = len(self.files)
|
||||
self.singleFile = self.fileConunt > 1
|
||||
self.singleFile = self.fileConunt == 1
|
||||
# Metadata
|
||||
self.load_metadata()
|
||||
logger.info(f"Found {self.bookPath.name}, {self.fileConunt} files")
|
||||
self.changed = False
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
@@ -128,9 +150,10 @@ class Audiobook():
|
||||
self.composer = metadata["composer"]
|
||||
self.comment = metadata["comment"]
|
||||
self.genre = metadata["genre"]
|
||||
self.metadata= metadata
|
||||
logger.trace(f"Load metadata {self.artist}, {self.name}, {self.composer}, {self.comment}, {self.genre}")
|
||||
|
||||
def write_metadata(self, file, fields, values):
|
||||
metadata=dict.fromkeys(fields, "")
|
||||
for field,value in fields,values:
|
||||
cmd= (["kid3-cli", "-c" , "tag 2","-c", f"set {field} \"{value}\"", "-c" "save", str(file)])
|
||||
try:
|
||||
@@ -147,20 +170,21 @@ class Audiobook():
|
||||
return
|
||||
|
||||
def fix_composer(self):
|
||||
m = re.search(
|
||||
r"(?:read\s+by|spoken\s+by|narrated\s+by|gesprochen\s+von|gelesen\s+von)\s+([^\n.,;]+)",
|
||||
self.comment,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
if not m:
|
||||
logger.debug(f"No composer found in comment for {self.bookPath}")
|
||||
return
|
||||
self.changed=True
|
||||
self.comment = (self.comment[:m.start()] + self.comment[m.end():] ).strip(" .,;\n")
|
||||
self.comment = re.sub(r"\s+"," ",self.comment)
|
||||
if not self.composer:
|
||||
m = re.search(
|
||||
r"(?:read\s+by|spoken\s+by|narrated\s+by|gesprochen\s+von|gelesen\s+von)\s+([^\n.,;]+)",
|
||||
self.comment,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
if not m:
|
||||
logger.info(f"No composer found for {self.bookPath}")
|
||||
return
|
||||
logger.info(f"[Updated] composer found for {self.bookPath}")
|
||||
self.composer = m.group(1).strip()
|
||||
self.comment = (self.comment[:m.start()] + self.comment[m.end():] ).strip(" .,;\n")
|
||||
self.comment = re.sub(r"\s+"," ",self.comment)
|
||||
|
||||
|
||||
def fix_name(self):
|
||||
if not self.name:
|
||||
@@ -264,7 +288,12 @@ class Audiobook():
|
||||
metadata_file.open("w", encoding="utf-8") as mf,
|
||||
):
|
||||
mf.write(";FFMETADATA1\n")
|
||||
|
||||
|
||||
for key, value in self.metadata.items():
|
||||
mf.write(
|
||||
f"{ffmetadata_escape(key)}="
|
||||
f"{ffmetadata_escape(value)}\n"
|
||||
)
|
||||
for info in file_info:
|
||||
media = info["media"]
|
||||
chapters = info["chapters"]
|
||||
@@ -310,11 +339,12 @@ class Audiobook():
|
||||
"-i", str(concat_file),
|
||||
"-i", str(metadata_file),
|
||||
"-i", str(self.firstFile),
|
||||
|
||||
# audio from concat
|
||||
"-map", "0:a",
|
||||
# cover image from first media
|
||||
"-map", "2:v?",
|
||||
|
||||
"-map_metadata", "2",
|
||||
# Metadata and chapters from our generated FFMETADATA file
|
||||
"-map_metadata", "1",
|
||||
"-map_chapters", "1",
|
||||
|
||||
"-c:a", "aac",
|
||||
@@ -335,8 +365,8 @@ class Audiobook():
|
||||
|
||||
"-map", "0:a",
|
||||
"-map", "2:v?",
|
||||
|
||||
"-map_metadata", "2",
|
||||
# Metadata and chapters from our generated FFMETADATA file
|
||||
"-map_metadata", "1",
|
||||
"-map_chapters", "1",
|
||||
|
||||
"-c:a", "copy",
|
||||
@@ -375,6 +405,7 @@ class Audiobook():
|
||||
self.firstFile = Path(output_file_final)
|
||||
self.fileConunt = 1
|
||||
self.singleFile = True
|
||||
self.changed = False
|
||||
self.size = Path(output_file_final).stat().st_size
|
||||
logger.info(f"{GREEN}[SUCCESS]{RESET} - {self.bookPath.name} - delete media files")
|
||||
else:
|
||||
@@ -383,7 +414,11 @@ class Audiobook():
|
||||
|
||||
|
||||
class AudiobookLibrary:
|
||||
def __init__(self, libraryPath,logfilename: str="audiobook_library.log"):
|
||||
def __init__(self, libraryPath,dry_run,logfilename: str="audiobook_library.log"):
|
||||
|
||||
self.dry_run = dry_run
|
||||
logging.setLoggerClass(CustomLogger)
|
||||
logger: CustomLogger = logging.getLogger(__name__) # type: ignore[assignment]
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
@@ -401,7 +436,7 @@ class AudiobookLibrary:
|
||||
self._by_artist = defaultdict(list)
|
||||
self._by_composer = defaultdict(list)
|
||||
|
||||
self.loadAudiobooksFromDir()
|
||||
self.load_audiobooks_from_dir()
|
||||
self.size = sum( audiobook.size for audiobook in self.audiobooks ) # size in bytes
|
||||
|
||||
def add(self, book: Audiobook):
|
||||
@@ -415,7 +450,7 @@ class AudiobookLibrary:
|
||||
if book.composer:
|
||||
self._by_composer[book.composer].append(book)
|
||||
|
||||
def printName(self, books):
|
||||
def print_name(self, books):
|
||||
if not isinstance(books, list):
|
||||
books = [books]
|
||||
for book in books:
|
||||
@@ -459,7 +494,7 @@ class AudiobookLibrary:
|
||||
if book.artist and text in book.artist.lower()
|
||||
]
|
||||
|
||||
def loadAudiobooksFromDir(self) -> tuple[list[Any], int]:
|
||||
def load_audiobooks_from_dir(self):
|
||||
"""
|
||||
A book directory is any directory containing more than one
|
||||
audiobook file (.mp3, .m4a, .m4b)
|
||||
@@ -472,29 +507,47 @@ class AudiobookLibrary:
|
||||
for directory in dirs:
|
||||
self.add(Audiobook(directory))
|
||||
|
||||
def loadMetadata(self):
|
||||
def load_metadata(self):
|
||||
for audiobook in self.audiobooks:
|
||||
audiobook.load_metadata()
|
||||
|
||||
def fixComposers(self):
|
||||
def write_metadata(self):
|
||||
for audiobook in self.audiobooks:
|
||||
if audiobook.changed:
|
||||
logger.trace(f"Writing metadata: {audiobook.name}")
|
||||
if self.dry_run:
|
||||
continue
|
||||
audiobook.write_metadata()
|
||||
|
||||
def fix_composers(self):
|
||||
for book in self.audiobooks:
|
||||
book.fix_composer()
|
||||
|
||||
def fixNames(self):
|
||||
def fix_names(self):
|
||||
for book in self.audiobooks:
|
||||
book.fix_name()
|
||||
|
||||
def fixComments(self):
|
||||
def fix_comments(self):
|
||||
for book in self.audiobooks:
|
||||
if len(book.comment) < 40:
|
||||
if book.comment and len(book.comment) < 40:
|
||||
logger.info(f"Remove short comment: {book.comment} of {book.name}")
|
||||
book.comment = ""
|
||||
book.changed=True
|
||||
|
||||
def create_m4b_from_dir(self,path: Path="", name: str="",create_m4b_no_delete: bool=False) -> None:
|
||||
audiobooks = self.find_path_contains(path)
|
||||
audiobooks.extend(self.find_name_contains(name))
|
||||
audiobooks = []
|
||||
if path:
|
||||
audiobooks = self.find_path_contains(path)
|
||||
if name:
|
||||
audiobooks.extend(self.find_name_contains(name))
|
||||
if not name and not path:
|
||||
audiobooks = self.audiobooks
|
||||
for audiobook in audiobooks:
|
||||
self.printName(audiobook)
|
||||
if audiobook.singleFile:
|
||||
continue
|
||||
logger.debug(f"Crate m4b file for {audiobook.name}")
|
||||
if self.dry_run:
|
||||
continue
|
||||
audiobook.create_m4b_from_book( self.libraryPath,create_m4b_no_delete)
|
||||
|
||||
def ffmpeg_escape(path: Path) -> str:
|
||||
@@ -511,26 +564,62 @@ def main():
|
||||
type=Path,
|
||||
help="Library path"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--create-m4b-no-delete",
|
||||
action="store_true",
|
||||
help="create m4b without deleting"
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-ll",
|
||||
"--log-level",
|
||||
type=str.upper,
|
||||
default="INFO",
|
||||
choices=["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
help="Set log level",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-cmfn",
|
||||
"--create-m4b-filter-name",
|
||||
type=str,
|
||||
default="",
|
||||
help="Only create m4b from multiple files matching <name>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-cmfp",
|
||||
"--create-m4b-filter-path",
|
||||
type=str,
|
||||
default="",
|
||||
help="Only create m4b from multiple files matching <path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Dry run only",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
library_dir = args.library
|
||||
|
||||
library = AudiobookLibrary(library_dir)
|
||||
logging.basicConfig(
|
||||
level=LOG_LEVELS[args.log_level.upper()],
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
)
|
||||
library_dir = args.library.resolve()
|
||||
|
||||
library.fixComposers()
|
||||
library.fixNames()
|
||||
library.fixComments()
|
||||
library = AudiobookLibrary(library_dir,args.dry_run)
|
||||
|
||||
library.fix_composers()
|
||||
library.fix_names()
|
||||
library.fix_comments()
|
||||
library.create_m4b_from_dir(name=args.create_m4b_filter_name,
|
||||
path=args.create_m4b_filter_path,
|
||||
create_m4b_no_delete=args.create_m4b_no_delete)
|
||||
library.write_metadata()
|
||||
|
||||
#library.printName(library.get_by_artist("Tolkien"))
|
||||
#library.printName(library.find_artist_contains("Tolkien"))
|
||||
#library.printName(library.find_name_contains(""))
|
||||
#library.printName(library.get_by_path("/home/lucas/Nextcloud/Syncfolder/"))
|
||||
library.create_m4b_from_dir(name="Fellowship", create_m4b_no_delete=args.create_m4b_no_delete)
|
||||
print("Fin")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in new issue
Block a user