#!/usr/bin/env python3
"""
Accept filenames (m3u playlist) from stdin, read media files for metadata, and add corresponding extended m3u headers.
Files must be accessible at the given path and are passed through unchanged in any case.
"""
import os
import sys
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
from pathlib import Path
from string import Template
from typing import Optional, Dict, Tuple, TextIO
from mutagen import File, FileType, StreamInfo, MutagenError
class TitleInfoTemplate(Template):
delimiter = "%"
def interpolate(self, tags: Dict[str, str]) -> str:
return self.safe_substitute(tags).replace("\r", " ").replace("\n", " ")
def _read_file(file_name: str) -> Tuple[Optional[float], Dict[str, str]]:
try:
file_meta: Optional[FileType] = File(Path(file_name), easy=True)
file_info: Optional[StreamInfo] = file_meta.info if file_meta is not None else None
except (MutagenError, OSError, ValueError, RuntimeError) as e:
print(f"{file_name}: {str(e)}", file=sys.stderr)
return None, {}
if file_meta is None or file_info is None:
print(f"{file_name}: can't parse audio file", file=sys.stderr)
return None, {}
duration: Optional[float] = getattr(file_info, "length", None)
tags: Dict[str, str] = {key: val[0] for key, val in {
str(k).lower(): list(filter(None, [str(_) for _ in v] if isinstance(v, list) else [str(v)]))
for k, v in file_meta.items()
}.items() if val}
return duration, tags
def _convert(in_stream: TextIO, out_stream: TextIO, template_str: str) -> None:
template = TitleInfoTemplate(template_str)
out_stream.write("#EXTM3U\r\n")
count = 0
for file_name in in_stream:
file_name = file_name.strip()
if not file_name or file_name.startswith("#"):
continue
duration, tags = _read_file(file_name)
info = template.interpolate(tags) if tags else None
out_stream.write(f"#EXTINF:"
f"{max(0, round(duration)) if duration is not None else -1},"
f"{info if info else os.path.splitext(os.path.basename(file_name))[0]}\r\n"
f"{file_name}\r\n")
count += 1
print(f"Processed {count} files", file=sys.stderr)
def _run(template: str) -> int:
_convert(sys.stdin, sys.stdout, template)
return 0
def main() -> int:
parser: ArgumentParser = ArgumentParser(description=__doc__, formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument("-c", metavar="PATH", type=Path,
default=None, help="change directory for relative filenames")
parser.add_argument("--template", metavar="STR", type=str,
default="%{artist} - %{title}", help="template to be interpolated with tags")
args = parser.parse_args()
if args.c is not None:
os.chdir(args.c)
return _run(args.template)
if __name__ == "__main__":
sys.exit(main())