Automated music library format conversion with cuesheet detection, tagging support and configurable regex to obtain tags from filenames. Configuration with ini-files to support multiple locations with multiple quality requirements.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123 lines
4.0 KiB

  1. CONFIG_FILE: str = "libconv.ini"
  2. CUE_SHEET_EXTENSION: str = ".cue"
  3. import os
  4. import shutil
  5. import logging
  6. import subprocess
  7. from configparser import ConfigParser
  8. from copy import deepcopy
  9. from pathlib import Path
  10. from typing import List, Optional, Tuple
  11. LOGGER = logging.getLogger(__name__)
  12. config = ConfigParser()
  13. config.read(CONFIG_FILE)
  14. source_folder: str = config.get("libconv", "source_folder")
  15. destination_folder: str = config.get("libconv", "destination_folder")
  16. source_file_extensions: List[str] = config.get("libconv", "source_file_extensions").split(",")
  17. destination_extension: str = config.get("libconv", "destination_extension")
  18. ffmpeg_options: str = config.get("libconv", "ffmpeg_options")
  19. ffmpeg_location: str = config.get("libconv", "ffmpeg_location")
  20. folder_icon_extensions: List[str] = config.get("libconv", "folder_icon_extensions").split(",")
  21. overwrite: bool = config.get("libconv", "overwrite_files") == "true"
  22. dry_run: bool = config.get("libconv", "dry_run") != "false"
  23. # ----------------------------------------------------------------- #
  24. # file and folder operations
  25. def copy(src: str, dst: str):
  26. if (dry_run):
  27. LOGGER.debug(f"copy {src} to {dst}")
  28. else:
  29. shutil.copyfile(src, dst, follow_symlinks=True)
  30. def mkdir(path: str):
  31. if (dry_run):
  32. LOGGER.debug(f"mkdir -r {path}")
  33. else:
  34. os.makedirs(path, exist_ok=True)
  35. def execute(command: str):
  36. if (dry_run):
  37. LOGGER.debug(f"execute: {command}")
  38. else:
  39. subprocess.run(command)
  40. # ----------------------------------------------------------------- #
  41. # cue sheet processing
  42. def cue_sheet_processor(path: str):
  43. dst_folder = prepare_destination(path)
  44. sheet = get_files_with_ext(path, [CUE_SHEET_EXTENSION])
  45. if (len(sheet) != 1):
  46. LOGGER.debug(f"ERR: Expected exactly one but {path} contains {len(sheet)} cue sheets")
  47. exit(-1)
  48. # ----------------------------------------------------------------- #
  49. # basic "folder contains audio files" processing
  50. def file_processor(path: str):
  51. dst_folder = prepare_destination(path)
  52. files = get_files_with_ext(path, source_file_extensions)
  53. for file in files:
  54. filename = get_filename(file, path)
  55. execute(f"{ffmpeg_location} -i {file} {ffmpeg_options} {dst_folder + filename}.{destination_extension}")
  56. # ----------------------------------------------------------------- #
  57. # Iteration over library folders and preparation
  58. def contains_extension(path: str, extensions: List[str]) -> bool:
  59. for root, dirs, files in os.walk(path):
  60. for file in files:
  61. for ext in extensions:
  62. if file.endswith(ext):
  63. return True
  64. return False
  65. def get_files_with_ext(path: str, extensions: List[str]) -> List[str]:
  66. files = []
  67. for root, dirs, files in os.walk(path):
  68. for file in files:
  69. for ext in extensions:
  70. if file.endswith(ext):
  71. files.append(os.path.join(root, file))
  72. return files
  73. def prepare_destination(path: str) -> str:
  74. images = get_files_with_ext(path, folder_icon_extensions)
  75. sub_path = path.replace(source_folder, "")
  76. new_path = destination_folder + sub_path
  77. mkdir(new_path)
  78. for img in images:
  79. img_name = img.replace(path, "")
  80. new_img_path = new_path + img_name
  81. copy(img, new_img_path)
  82. return new_path
  83. def get_filename(file: str, path: str) -> str:
  84. fname = file.replace(path, "")
  85. for ext in source_file_extensions:
  86. fname = fname.replace("." + ext, "")
  87. return fname
  88. def scan_folder(path: str):
  89. for root, dirs, files in os.walk(path):
  90. for directory in dirs:
  91. process_folder(os.path.join(root, directory))
  92. def process_folder(path: str):
  93. if contains_extension(path, [CUE_SHEET_EXTENSION]):
  94. cue_sheet_processor(path)
  95. elif contains_extension(path, source_file_extensions):
  96. file_processor(path)
  97. else:
  98. scan_folder(path)
  99. if __name__ == "__main__":
  100. process_folder(source_folder)