Browse Source

add metadata extraction, fix logging and start dry run testing

master
Constantin Fürst 2 years ago
parent
commit
8aa2edf0c9
  1. 135
      libconv.py

135
libconv.py

@ -1,47 +1,53 @@
CONFIG_FILE: str = "libconv.ini"
CUE_SHEET_EXTENSION: str = ".cue" CUE_SHEET_EXTENSION: str = ".cue"
import os import os
import shutil import shutil
import logging import logging
import subprocess import subprocess
import sys
import re
from configparser import ConfigParser from configparser import ConfigParser
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
from ffcuesplitter.cuesplitter import FFCueSplitter
LOGGER = logging.getLogger(__name__)
CONFIG_FILE: str = sys.argv[1]
config = ConfigParser() config = ConfigParser()
config.read(CONFIG_FILE) config.read(CONFIG_FILE)
source_folder: str = config.get("libconv", "source_folder")
destination_folder: str = config.get("libconv", "destination_folder")
source_file_extensions: List[str] = config.get("libconv", "source_file_extensions").split(",")
destination_extension: str = config.get("libconv", "destination_extension")
ffmpeg_options: str = config.get("libconv", "ffmpeg_options")
ffmpeg_location: str = config.get("libconv", "ffmpeg_location")
folder_icon_extensions: List[str] = config.get("libconv", "folder_icon_extensions").split(",")
overwrite: bool = config.get("libconv", "overwrite_files") == "true"
dry_run: bool = config.get("libconv", "dry_run") != "false"
SRC_FOLDER: str = config.get("source", "folder")
SRC_FILE_EXTS: List[str] = config.get("source", "file_exts").split(",")
SRC_ICON_EXTS: List[str] = config.get("source", "icon_exts").split(",")
DST_FOLDER: str = config.get("destination", "folder")
DST_FILE_EXT: str = config.get("destination", "file_ext")
DST_OVERWRITE: bool = config.get("libconv", "overwrite") == "true"
DRY_RUN: bool = config.get("libconv", "dry_run") != "false"
DEDUCE_METADATA: bool = config.get("libconv", "deduce_metadata") == "true"
FFMPEG_LOCATION: str = config.get("ffmpeg", "location")
FFMPEG_OPTS: str = config.get("ffmpeg", "options")
logging.basicConfig(level=logging.INFO)
# ----------------------------------------------------------------- # # ----------------------------------------------------------------- #
# file and folder operations # file and folder operations
def copy(src: str, dst: str): def copy(src: str, dst: str):
if (dry_run):
LOGGER.debug(f"copy {src} to {dst}")
if (DRY_RUN):
logging.info(f"copy {src} to {dst}")
else: else:
shutil.copyfile(src, dst, follow_symlinks=True) shutil.copyfile(src, dst, follow_symlinks=True)
def mkdir(path: str): def mkdir(path: str):
if (dry_run):
LOGGER.debug(f"mkdir -r {path}")
if (DRY_RUN):
logging.info(f"mkdir -r {path}")
else: else:
os.makedirs(path, exist_ok=True) os.makedirs(path, exist_ok=True)
def execute(command: str): def execute(command: str):
if (dry_run):
LOGGER.debug(f"execute: {command}")
if (DRY_RUN):
logging.info(f"execute: {command}")
else: else:
subprocess.run(command) subprocess.run(command)
@ -53,45 +59,105 @@ def cue_sheet_processor(path: str):
sheet = get_files_with_ext(path, [CUE_SHEET_EXTENSION]) sheet = get_files_with_ext(path, [CUE_SHEET_EXTENSION])
if (len(sheet) != 1): if (len(sheet) != 1):
LOGGER.debug(f"ERR: Expected exactly one but {path} contains {len(sheet)} cue sheets")
exit(-1)
logging.error(f"Expected exactly one but {path} contains {len(sheet)} cue sheets")
return
data = FFCueSplitter(sheet[0], dry=DRY_RUN, outputdir=dst_folder, suffix=DST_FILE_EXT, overwrite=DST_OVERWRITE, ffmpeg_cmd=FFMPEG_LOCATION, ffmpeg_add_params=FFMPEG_OPTS)
data.open_cuefile()
args = data.ffmpeg_arguments()["arguments"]
dst_folder = prepare_destination(path)
for arg in args:
arg_end = arg.find("-y")
filename = arg[arg_end + 5:-1]
arg = arg[:arg_end]
execute(f"{arg} \"{dst_folder}{filename}\"")
# ----------------------------------------------------------------- # # ----------------------------------------------------------------- #
# basic "folder contains audio files" processing # basic "folder contains audio files" processing
def metadata_from_folder(path: str) -> str:
# this method has to be adapted to your individual folder structure
# if there is none then do nothing as by default this is not used
if not DEDUCE_METADATA:
return ""
path = os.path.normpath(path)
folders = path.split(os.sep)
logging.debug(f"deducing metadata from folders: {folders}")
album_name = re.sub(r'[ ]*\(.*?\)', '', folders[-1])
comment = folders[-1].replace(album_name, "").replace("(", "").replace(")", "").replace(" ","")
logging.debug(f"got metadata: album={album_name} comment={comment} artist={folders[-2]} genre={folders[-3]}")
return f"-metadata ALBUM=\"{album_name}\" -metadata COMMENT=\"{comment}\" -metadata ARTIST=\"{folders[-2]}\" -metadata GENRE=\"{folders[-3]}\""
def metadata_from_file(filename: str) -> str:
title=""
number=""
if re.match(r'[0-9]+[ ]*-[ ]*[.]*', filename):
logging.debug(f"file {filename} matched metadata regex #1")
number = re.search(r'[0-9]+[ ]*-[ ]*', filename).group()
title = filename.replace(number, "")
number = re.sub(r'[ ]*-[ ]*', '', number)
elif re.match(r'[0-9]+\.[ ]*[.]*', filename):
logging.info(f"file {filename} matched metadata regex #2")
number = re.search(r'[0-9]+\.[ ]*', filename).group()
title = filename.replace(number, "")
number = re.sub(r'\.[ ]*', "", number)
else:
logging.debug(f"file {filename} matched no metadata regex")
return f"-metadata TITLE=\"{title}\" -metadata TRACK=\"{number}\""
def file_processor(path: str): def file_processor(path: str):
dst_folder = prepare_destination(path) dst_folder = prepare_destination(path)
files = get_files_with_ext(path, source_file_extensions)
files = get_files_with_ext(path, SRC_FILE_EXTS)
album_metadata = metadata_from_folder(path)
for file in files: for file in files:
filename = get_filename(file, path) filename = get_filename(file, path)
execute(f"{ffmpeg_location} -i {file} {ffmpeg_options} {dst_folder + filename}.{destination_extension}")
file_metadata = metadata_from_file(filename[1:])
execute(f"\"{FFMPEG_LOCATION}\" -i \"{file}\" {FFMPEG_OPTS} {album_metadata} {file_metadata} \"{dst_folder + filename}.{DST_FILE_EXT}\"")
# ----------------------------------------------------------------- # # ----------------------------------------------------------------- #
# Iteration over library folders and preparation # Iteration over library folders and preparation
def contains_extension(path: str, extensions: List[str]) -> bool: def contains_extension(path: str, extensions: List[str]) -> bool:
logging.debug(f"searching for extensions {extensions} in {path}")
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
for file in files: for file in files:
for ext in extensions: for ext in extensions:
if file.endswith(ext): if file.endswith(ext):
logging.debug(f"file {file} matched extension '{ext}'")
return True return True
# needed to prevent recursive search
break
return False return False
def get_files_with_ext(path: str, extensions: List[str]) -> List[str]: def get_files_with_ext(path: str, extensions: List[str]) -> List[str]:
files = []
logging.debug(f"obtainint all files with extensions {extensions} from {path}")
ret_files = []
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
for file in files: for file in files:
for ext in extensions: for ext in extensions:
if file.endswith(ext): if file.endswith(ext):
files.append(os.path.join(root, file))
return files
logging.debug(f"file {file} matched extension '{ext}'")
ret_files.append(os.path.join(root, file))
# file is added now - no need to check the other extensions
break
# needed to prevent recursive search
break
return ret_files
def prepare_destination(path: str) -> str: def prepare_destination(path: str) -> str:
images = get_files_with_ext(path, folder_icon_extensions)
images = get_files_with_ext(path, SRC_ICON_EXTS)
sub_path = path.replace(source_folder, "")
new_path = destination_folder + sub_path
sub_path = path.replace(SRC_FOLDER, "")
new_path = DST_FOLDER + sub_path
mkdir(new_path) mkdir(new_path)
for img in images: for img in images:
@ -103,7 +169,7 @@ def prepare_destination(path: str) -> str:
def get_filename(file: str, path: str) -> str: def get_filename(file: str, path: str) -> str:
fname = file.replace(path, "") fname = file.replace(path, "")
for ext in source_file_extensions:
for ext in SRC_FILE_EXTS:
fname = fname.replace("." + ext, "") fname = fname.replace("." + ext, "")
return fname return fname
@ -111,14 +177,23 @@ def scan_folder(path: str):
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
for directory in dirs: for directory in dirs:
process_folder(os.path.join(root, directory)) process_folder(os.path.join(root, directory))
# needed to prevent recursive search
break
def process_folder(path: str): def process_folder(path: str):
logging.info(f"Visiting folder '{path}'")
if contains_extension(path, [CUE_SHEET_EXTENSION]): if contains_extension(path, [CUE_SHEET_EXTENSION]):
logging.info("found cuesheet")
cue_sheet_processor(path) cue_sheet_processor(path)
elif contains_extension(path, source_file_extensions):
elif contains_extension(path, SRC_FILE_EXTS):
logging.info("found audio files")
file_processor(path) file_processor(path)
else: else:
logging.info("scanning subfolders")
scan_folder(path) scan_folder(path)
if __name__ == "__main__": if __name__ == "__main__":
process_folder(source_folder)
logging.info(f"Using settings from {CONFIG_FILE}")
if DRY_RUN:
logging.info("Executing as DRY RUN")
process_folder(SRC_FOLDER)
Loading…
Cancel
Save