Add YAPF style + ignore, and format tracked Python files
This commit is contained in:
@@ -11,7 +11,7 @@ import subprocess as _subprocess
|
||||
import shutil as _shutil
|
||||
import re as _re
|
||||
|
||||
from config import resolve_output_dir
|
||||
from SYS.config import resolve_output_dir
|
||||
|
||||
from . import _shared as sh
|
||||
|
||||
@@ -62,7 +62,8 @@ except ImportError:
|
||||
def dedup_tags_by_namespace(tags: List[str]) -> List[str]:
|
||||
return tags
|
||||
|
||||
def merge_multiple_tag_lists(sources: List[List[str]], strategy: str = "first") -> List[str]:
|
||||
def merge_multiple_tag_lists(sources: List[List[str]],
|
||||
strategy: str = "first") -> List[str]:
|
||||
out: List[str] = []
|
||||
seen: set[str] = set()
|
||||
for src in sources:
|
||||
@@ -167,7 +168,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
cookiefile = None
|
||||
|
||||
fmts = list_formats(
|
||||
sample_url, no_playlist=False, playlist_items=None, cookiefile=cookiefile
|
||||
sample_url,
|
||||
no_playlist=False,
|
||||
playlist_items=None,
|
||||
cookiefile=cookiefile
|
||||
)
|
||||
if isinstance(fmts, list) and fmts:
|
||||
has_video = False
|
||||
@@ -263,7 +267,11 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
elif isinstance(url, list):
|
||||
source_url.extend(url)
|
||||
else:
|
||||
title = get_field(item, "title", "unknown") or get_field(item, "id", "unknown")
|
||||
title = get_field(item,
|
||||
"title",
|
||||
"unknown") or get_field(item,
|
||||
"id",
|
||||
"unknown")
|
||||
log(f"Warning: Could not locate file for item: {title}", file=sys.stderr)
|
||||
|
||||
if len(source_files) < 2:
|
||||
@@ -274,31 +282,45 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
file_types = set()
|
||||
for f in source_files:
|
||||
suffix = f.suffix.lower()
|
||||
if suffix in {".mp3", ".flac", ".wav", ".m4a", ".aac", ".ogg", ".opus", ".mka"}:
|
||||
if suffix in {".mp3",
|
||||
".flac",
|
||||
".wav",
|
||||
".m4a",
|
||||
".aac",
|
||||
".ogg",
|
||||
".opus",
|
||||
".mka"}:
|
||||
file_types.add("audio")
|
||||
elif suffix in {
|
||||
".mp4",
|
||||
".mkv",
|
||||
".webm",
|
||||
".mov",
|
||||
".avi",
|
||||
".flv",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
".ts",
|
||||
".m4v",
|
||||
".wmv",
|
||||
".mp4",
|
||||
".mkv",
|
||||
".webm",
|
||||
".mov",
|
||||
".avi",
|
||||
".flv",
|
||||
".mpg",
|
||||
".mpeg",
|
||||
".ts",
|
||||
".m4v",
|
||||
".wmv",
|
||||
}:
|
||||
file_types.add("video")
|
||||
elif suffix in {".pdf"}:
|
||||
file_types.add("pdf")
|
||||
elif suffix in {".txt", ".srt", ".vtt", ".md", ".log"}:
|
||||
elif suffix in {".txt",
|
||||
".srt",
|
||||
".vtt",
|
||||
".md",
|
||||
".log"}:
|
||||
file_types.add("text")
|
||||
else:
|
||||
file_types.add("other")
|
||||
|
||||
if len(file_types) > 1 and "other" not in file_types:
|
||||
log(f"Mixed file types detected: {', '.join(sorted(file_types))}", file=sys.stderr)
|
||||
log(
|
||||
f"Mixed file types detected: {', '.join(sorted(file_types))}",
|
||||
file=sys.stderr
|
||||
)
|
||||
log(f"Can only merge files of the same type", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
@@ -331,7 +353,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
except Exception:
|
||||
base_dir = first_file.parent
|
||||
output_path = (
|
||||
Path(base_dir) / f"{first_file.stem} (merged).{_ext_for_format(output_format)}"
|
||||
Path(base_dir) /
|
||||
f"{first_file.stem} (merged).{_ext_for_format(output_format)}"
|
||||
)
|
||||
|
||||
# Ensure output directory exists
|
||||
@@ -393,7 +416,8 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
# - .tag sidecars (if present)
|
||||
# Keep all unique plain tags, and keep the first value for namespaced tags.
|
||||
merged_tags = merge_multiple_tag_lists(
|
||||
source_item_tag_lists + ([source_tags] if source_tags else []), strategy="combine"
|
||||
source_item_tag_lists + ([source_tags] if source_tags else []),
|
||||
strategy="combine"
|
||||
)
|
||||
|
||||
# Ensure we always have a title tag (and make sure it's the chosen title)
|
||||
@@ -437,7 +461,10 @@ def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
tag_file.unlink()
|
||||
log(f"Deleted: {tag_file.name}", file=sys.stderr)
|
||||
except Exception as e:
|
||||
log(f"Warning: Could not delete {tag_file.name}: {e}", file=sys.stderr)
|
||||
log(
|
||||
f"Warning: Could not delete {tag_file.name}: {e}",
|
||||
file=sys.stderr
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -455,7 +482,10 @@ def _sanitize_name(text: str) -> str:
|
||||
"""Sanitize filename."""
|
||||
allowed = []
|
||||
for ch in text:
|
||||
allowed.append(ch if (ch.isalnum() or ch in {"-", "_", " ", "."}) else " ")
|
||||
allowed.append(ch if (ch.isalnum() or ch in {"-",
|
||||
"_",
|
||||
" ",
|
||||
"."}) else " ")
|
||||
return (" ".join("".join(allowed).split()) or "merged").strip()
|
||||
|
||||
|
||||
@@ -512,7 +542,10 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
]
|
||||
|
||||
probe_result = _subprocess.run(
|
||||
ffprobe_cmd, capture_output=True, text=True, timeout=10
|
||||
ffprobe_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if probe_result.returncode == 0 and probe_result.stdout.strip():
|
||||
try:
|
||||
@@ -528,7 +561,9 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
)
|
||||
duration_sec = 0
|
||||
except Exception as e:
|
||||
logger.warning(f"[merge-file] Could not get duration for {file_path.name}: {e}")
|
||||
logger.warning(
|
||||
f"[merge-file] Could not get duration for {file_path.name}: {e}"
|
||||
)
|
||||
duration_sec = 0
|
||||
|
||||
# Create chapter entry - use title: tag from metadata if available
|
||||
@@ -542,12 +577,15 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
if tags:
|
||||
# Look for title: tag
|
||||
for tag in tags:
|
||||
if isinstance(tag, str) and tag.lower().startswith("title:"):
|
||||
if isinstance(tag,
|
||||
str) and tag.lower().startswith("title:"):
|
||||
# Extract the title value after the colon
|
||||
title = tag.split(":", 1)[1].strip()
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(f"[merge-file] Could not read metadata for {file_path.name}: {e}")
|
||||
logger.debug(
|
||||
f"[merge-file] Could not read metadata for {file_path.name}: {e}"
|
||||
)
|
||||
pass # Fall back to filename
|
||||
|
||||
# Convert seconds to HH:MM:SS.mmm format
|
||||
@@ -626,7 +664,10 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
metadata_lines.append(f'title={chapter["title"]}')
|
||||
|
||||
metadata_file.write_text("\n".join(metadata_lines), encoding="utf-8")
|
||||
log(f"Created chapters metadata file with {len(chapters)} chapters", file=sys.stderr)
|
||||
log(
|
||||
f"Created chapters metadata file with {len(chapters)} chapters",
|
||||
file=sys.stderr
|
||||
)
|
||||
logger.info(f"[merge-file] Created {len(chapters)} chapters")
|
||||
|
||||
# Step 4: Build FFmpeg command to merge and embed chapters
|
||||
@@ -639,7 +680,8 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
# Audio codec selection for first input
|
||||
if output_format == "mp3":
|
||||
cmd.extend(["-c:a", "libmp3lame", "-q:a", "2"])
|
||||
elif output_format in {"m4a", "m4b"}:
|
||||
elif output_format in {"m4a",
|
||||
"m4b"}:
|
||||
# Use copy if possible (much faster), otherwise re-encode
|
||||
# Check if inputs are already AAC/M4A to avoid re-encoding
|
||||
# For now, default to copy if format matches, otherwise re-encode
|
||||
@@ -714,7 +756,10 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
if process.returncode != 0:
|
||||
log(f"FFmpeg error: {stderr}", file=sys.stderr)
|
||||
raise _subprocess.CalledProcessError(
|
||||
process.returncode, cmd, output=stdout, stderr=stderr
|
||||
process.returncode,
|
||||
cmd,
|
||||
output=stdout,
|
||||
stderr=stderr
|
||||
)
|
||||
|
||||
print_final_progress(output.name, int(total_duration_sec * 1000), 0)
|
||||
@@ -807,17 +852,24 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
logger.warning(f"[merge-file] Chapter embedding did not create output")
|
||||
logger.warning(
|
||||
f"[merge-file] Chapter embedding did not create output"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"[merge-file] Chapter embedding failed: {e}")
|
||||
log(
|
||||
f"Warning: Chapter embedding failed, using merge without chapters",
|
||||
file=sys.stderr,
|
||||
)
|
||||
elif output_format in {"m4a", "m4b"} or output.suffix.lower() in [".m4a", ".m4b", ".mp4"]:
|
||||
elif output_format in {"m4a",
|
||||
"m4b"} or output.suffix.lower() in [".m4a",
|
||||
".m4b",
|
||||
".mp4"]:
|
||||
# MP4/M4A format has native chapter support via iTunes metadata atoms
|
||||
log(f"Embedding chapters into MP4 container...", file=sys.stderr)
|
||||
logger.info(f"[merge-file] Adding chapters to M4A/MP4 file via iTunes metadata")
|
||||
logger.info(
|
||||
f"[merge-file] Adding chapters to M4A/MP4 file via iTunes metadata"
|
||||
)
|
||||
|
||||
temp_output = output.parent / f".temp_{output.stem}{output.suffix}"
|
||||
|
||||
@@ -864,7 +916,10 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
if output.exists():
|
||||
output.unlink()
|
||||
shutil.move(str(temp_output), str(output))
|
||||
log(f"✓ Chapters successfully embedded in MP4!", file=sys.stderr)
|
||||
log(
|
||||
f"✓ Chapters successfully embedded in MP4!",
|
||||
file=sys.stderr
|
||||
)
|
||||
logger.info(f"[merge-file] MP4 chapters embedded successfully")
|
||||
except Exception as e:
|
||||
logger.warning(f"[merge-file] Could not replace file: {e}")
|
||||
@@ -877,7 +932,9 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
logger.warning(f"[merge-file] MP4 chapter embedding did not create output")
|
||||
logger.warning(
|
||||
f"[merge-file] MP4 chapter embedding did not create output"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"[merge-file] MP4 chapter embedding failed: {e}")
|
||||
log(
|
||||
@@ -886,7 +943,9 @@ def _merge_audio(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
)
|
||||
else:
|
||||
# For other formats, chapters would require external tools
|
||||
logger.info(f"[merge-file] Format {output_format} does not have native chapter support")
|
||||
logger.info(
|
||||
f"[merge-file] Format {output_format} does not have native chapter support"
|
||||
)
|
||||
log(f"Note: For chapter support, use MKA or M4A format", file=sys.stderr)
|
||||
|
||||
# Clean up temp files
|
||||
@@ -944,7 +1003,16 @@ def _merge_video(files: List[Path], output: Path, output_format: str) -> bool:
|
||||
]
|
||||
)
|
||||
elif output_format == "mkv":
|
||||
cmd.extend(["-c:v", "libx265", "-preset", "fast", "-c:a", "aac", "-b:a", "192k"])
|
||||
cmd.extend(
|
||||
["-c:v",
|
||||
"libx265",
|
||||
"-preset",
|
||||
"fast",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"192k"]
|
||||
)
|
||||
else:
|
||||
cmd.extend(["-c", "copy"]) # Copy without re-encoding
|
||||
|
||||
@@ -994,7 +1062,10 @@ def _merge_text(files: List[Path], output: Path) -> bool:
|
||||
def _merge_pdf(files: List[Path], output: Path) -> bool:
|
||||
"""Merge PDF files."""
|
||||
if (not HAS_PYPDF) or (PdfWriter is None) or (PdfReader is None):
|
||||
log("pypdf is required for PDF merging. Install with: pip install pypdf", file=sys.stderr)
|
||||
log(
|
||||
"pypdf is required for PDF merging. Install with: pip install pypdf",
|
||||
file=sys.stderr
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -1022,16 +1093,21 @@ def _merge_pdf(files: List[Path], output: Path) -> bool:
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name="merge-file",
|
||||
summary="Merge multiple files into a single output file. Supports audio, video, PDF, and text merging with optional cleanup.",
|
||||
usage="merge-file [-delete] [-path <path>] [-format <auto|mka|m4a|m4b|mp3|aac|opus|mp4|mkv|pdf|txt>]",
|
||||
summary=
|
||||
"Merge multiple files into a single output file. Supports audio, video, PDF, and text merging with optional cleanup.",
|
||||
usage=
|
||||
"merge-file [-delete] [-path <path>] [-format <auto|mka|m4a|m4b|mp3|aac|opus|mp4|mkv|pdf|txt>]",
|
||||
arg=[
|
||||
CmdletArg(
|
||||
"-delete", type="flag", description="Delete source files after successful merge."
|
||||
"-delete",
|
||||
type="flag",
|
||||
description="Delete source files after successful merge."
|
||||
),
|
||||
SharedArgs.PATH,
|
||||
CmdletArg(
|
||||
"-format",
|
||||
description="Output format (auto/mka/m4a/m4b/mp3/aac/opus/mp4/mkv/pdf/txt). Default: auto-detect from first file.",
|
||||
description=
|
||||
"Output format (auto/mka/m4a/m4b/mp3/aac/opus/mp4/mkv/pdf/txt). Default: auto-detect from first file.",
|
||||
),
|
||||
],
|
||||
detail=[
|
||||
|
||||
Reference in New Issue
Block a user