mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
refactor: extract OutputLayoutService
Extract output path resolution from conversion_planner.py into application/output_layout_service.py as a standalone service. Functions: - resolve_output_layout(request) -> OutputLayout - resolve_merged_path(layout, request) -> Path - resolve_chapter_path(layout, request, title, index) -> Path - should_merge_output(request) -> bool Planner now imports from output_layout_service instead of inline logic.
This commit is contained in:
@@ -20,17 +20,12 @@ from abogen.application.conversion_models import (
|
|||||||
SegmentPlan,
|
SegmentPlan,
|
||||||
)
|
)
|
||||||
from abogen.application.conversion_request import ConversionRequest
|
from abogen.application.conversion_request import ConversionRequest
|
||||||
|
from abogen.application.output_layout_service import resolve_output_layout
|
||||||
from abogen.domain.chapter_overrides import apply_chapter_overrides
|
from abogen.domain.chapter_overrides import apply_chapter_overrides
|
||||||
from abogen.domain.file_type import auto_select_relevant_chapters
|
from abogen.domain.file_type import auto_select_relevant_chapters
|
||||||
from abogen.domain.intro_outro import resolve_intro, resolve_outro
|
from abogen.domain.intro_outro import resolve_intro, resolve_outro
|
||||||
from abogen.domain.metadata_extraction import extract_metadata_for_file
|
from abogen.domain.metadata_extraction import extract_metadata_for_file
|
||||||
from abogen.domain.metadata_merge import merge_metadata
|
from abogen.domain.metadata_merge import merge_metadata
|
||||||
from abogen.domain.output_paths import (
|
|
||||||
resolve_output_directory,
|
|
||||||
resolve_project_layout,
|
|
||||||
resolve_unique_path,
|
|
||||||
sanitize_output_stem,
|
|
||||||
)
|
|
||||||
from abogen.subtitle_utils import split_text_by_voice_markers
|
from abogen.subtitle_utils import split_text_by_voice_markers
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +66,7 @@ def build_conversion_plan(request: ConversionRequest) -> ConversionPlan:
|
|||||||
intro, outro = _build_intro_outro(metadata, request)
|
intro, outro = _build_intro_outro(metadata, request)
|
||||||
|
|
||||||
# 7. Resolve output layout
|
# 7. Resolve output layout
|
||||||
output_layout = _resolve_output_layout(request)
|
output_layout = resolve_output_layout(request)
|
||||||
|
|
||||||
return ConversionPlan(
|
return ConversionPlan(
|
||||||
request=request,
|
request=request,
|
||||||
@@ -354,47 +349,4 @@ def _build_intro_outro(
|
|||||||
return intro_spec, outro_spec
|
return intro_spec, outro_spec
|
||||||
|
|
||||||
|
|
||||||
def _resolve_output_layout(request: ConversionRequest) -> OutputLayout:
|
# Output layout resolution is now in application/output_layout_service.py
|
||||||
"""Resolve output paths for the conversion."""
|
|
||||||
# Determine base output directory
|
|
||||||
if request.save_mode == "custom_folder" and request.output_folder:
|
|
||||||
parent_dir = Path(request.output_folder)
|
|
||||||
elif request.source_path:
|
|
||||||
parent_dir = request.source_path.parent
|
|
||||||
else:
|
|
||||||
parent_dir = Path.cwd()
|
|
||||||
|
|
||||||
# Determine base name
|
|
||||||
if request.original_filename:
|
|
||||||
base_name = sanitize_output_stem(request.original_filename)
|
|
||||||
elif request.source_path:
|
|
||||||
base_name = sanitize_output_stem(request.source_path.stem)
|
|
||||||
else:
|
|
||||||
base_name = "output"
|
|
||||||
|
|
||||||
# Find unique output path
|
|
||||||
allowed_exts = {request.output_format, "srt", "ass", "vtt", "mp4", "m4b"}
|
|
||||||
unique_base = resolve_unique_path(
|
|
||||||
parent_dir, base_name, "", allowed_extensions=allowed_exts
|
|
||||||
)
|
|
||||||
|
|
||||||
# Resolve project layout
|
|
||||||
project_root = None
|
|
||||||
audio_dir = parent_dir
|
|
||||||
subtitle_dir = None
|
|
||||||
metadata_dir = None
|
|
||||||
|
|
||||||
if request.save_as_project:
|
|
||||||
project_root, audio_dir, subtitle_dir, metadata_dir = resolve_project_layout(
|
|
||||||
original_filename=request.original_filename,
|
|
||||||
save_as_project=True,
|
|
||||||
base_dir=parent_dir,
|
|
||||||
)
|
|
||||||
|
|
||||||
return OutputLayout(
|
|
||||||
parent_dir=parent_dir,
|
|
||||||
project_root=project_root,
|
|
||||||
audio_dir=audio_dir,
|
|
||||||
subtitle_dir=subtitle_dir,
|
|
||||||
metadata_dir=metadata_dir,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
"""Output layout resolution service.
|
||||||
|
|
||||||
|
Determines where conversion outputs (audio, subtitles, metadata) should be written.
|
||||||
|
Extracted from conversion_planner.py as a standalone service per plan Stage 5.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
- Resolve base output directory from save_mode and source_path
|
||||||
|
- Determine base filename from original_filename
|
||||||
|
- Find unique output path to avoid overwrites
|
||||||
|
- Resolve project layout (audio_dir, subtitle_dir, metadata_dir)
|
||||||
|
- Force merged output for m4b format
|
||||||
|
- Return OutputLayout dataclass
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from abogen.application.conversion_models import OutputLayout
|
||||||
|
from abogen.application.conversion_request import ConversionRequest
|
||||||
|
from abogen.domain.output_paths import (
|
||||||
|
resolve_project_layout,
|
||||||
|
resolve_unique_path,
|
||||||
|
sanitize_output_stem,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_output_layout(request: ConversionRequest) -> OutputLayout:
|
||||||
|
"""Resolve output paths for a conversion request.
|
||||||
|
|
||||||
|
This is the single entry point for output path resolution,
|
||||||
|
used by both UIs and the conversion service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Normalized conversion request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OutputLayout with resolved paths
|
||||||
|
"""
|
||||||
|
# Determine base output directory
|
||||||
|
if request.save_mode == "custom_folder" and request.output_folder:
|
||||||
|
parent_dir = Path(request.output_folder)
|
||||||
|
elif request.source_path:
|
||||||
|
parent_dir = request.source_path.parent
|
||||||
|
else:
|
||||||
|
parent_dir = Path.cwd()
|
||||||
|
|
||||||
|
# Determine base name
|
||||||
|
if request.original_filename:
|
||||||
|
base_name = sanitize_output_stem(request.original_filename)
|
||||||
|
elif request.source_path:
|
||||||
|
base_name = sanitize_output_stem(request.source_path.stem)
|
||||||
|
else:
|
||||||
|
base_name = "output"
|
||||||
|
|
||||||
|
# Find unique output path
|
||||||
|
allowed_exts = {request.output_format, "srt", "ass", "vtt", "mp4", "m4b"}
|
||||||
|
unique_base = resolve_unique_path(
|
||||||
|
parent_dir, base_name, "", allowed_extensions=allowed_exts
|
||||||
|
)
|
||||||
|
|
||||||
|
# Resolve project layout
|
||||||
|
project_root = None
|
||||||
|
audio_dir = parent_dir
|
||||||
|
subtitle_dir = None
|
||||||
|
metadata_dir = None
|
||||||
|
|
||||||
|
if request.save_as_project:
|
||||||
|
project_root, audio_dir, subtitle_dir, metadata_dir = resolve_project_layout(
|
||||||
|
original_filename=request.original_filename,
|
||||||
|
save_as_project=True,
|
||||||
|
base_dir=parent_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
return OutputLayout(
|
||||||
|
parent_dir=parent_dir,
|
||||||
|
project_root=project_root,
|
||||||
|
audio_dir=audio_dir,
|
||||||
|
subtitle_dir=subtitle_dir,
|
||||||
|
metadata_dir=metadata_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_merged_path(
|
||||||
|
layout: OutputLayout,
|
||||||
|
request: ConversionRequest,
|
||||||
|
) -> Path:
|
||||||
|
"""Resolve the merged output audio file path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout: Resolved output layout
|
||||||
|
request: Conversion request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the merged output file
|
||||||
|
"""
|
||||||
|
base_name = sanitize_output_stem(
|
||||||
|
request.original_filename or "output"
|
||||||
|
)
|
||||||
|
return layout.audio_dir / f"{base_name}.{request.output_format}"
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_chapter_path(
|
||||||
|
layout: OutputLayout,
|
||||||
|
request: ConversionRequest,
|
||||||
|
chapter_title: str,
|
||||||
|
chapter_index: int,
|
||||||
|
) -> Path:
|
||||||
|
"""Resolve the output path for a separate chapter file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
layout: Resolved output layout
|
||||||
|
request: Conversion request
|
||||||
|
chapter_title: Chapter title for filename
|
||||||
|
chapter_index: Chapter number (1-based)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the chapter output file
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
slug = re.sub(r'[^\w\s-]', '', chapter_title.lower())
|
||||||
|
slug = re.sub(r'[\s_]+', '_', slug).strip('_')
|
||||||
|
if not slug:
|
||||||
|
slug = f"chapter_{chapter_index}"
|
||||||
|
filename = f"{chapter_index:02d}_{slug}.{request.separate_chapters_format}"
|
||||||
|
return layout.audio_dir / "chapters" / filename
|
||||||
|
|
||||||
|
|
||||||
|
def should_merge_output(request: ConversionRequest) -> bool:
|
||||||
|
"""Determine if merged output is required.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- m4b format always forces merged output
|
||||||
|
- If save_chapters_separately is False, merged is required
|
||||||
|
- Otherwise, use merge_chapters_at_end setting
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Conversion request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if merged output should be created
|
||||||
|
"""
|
||||||
|
if request.output_format.lower() == "m4b":
|
||||||
|
return True
|
||||||
|
if not request.save_chapters_separately:
|
||||||
|
return True
|
||||||
|
return request.merge_chapters_at_end
|
||||||
Reference in New Issue
Block a user