Skip to content

Commit f6f4a5d

Browse files
committed
refactor: consolidate input validation and improve error handling across features
- Extract common validation logic into new utils/validation.py module - Add validate_input_file() to check file existence and readability - Add check_output_file() to handle output file conflicts with user prompts - Add check_has_video_stream() to verify video stream presence - Add check_disk_space() to estimate and validate available disk space - Add press_continue() helper to replace repeated questionary calls - Update audio.py to use centralized validation and improved error handling - Update batch.py to use centralized validation and add disk space checks - Update convert.py to use centralized validation functions - Update crop.py to use centralized validation functions - Update inspect.py to use centralized validation functions - Update join.py to use centralized validation functions - Update subtitle.py to use centralized validation functions - Update trim.py to use centralized validation functions - Remove trailing whitespace and improve code formatting across all feature modules - Standardize questionary usage by removing use_indicator parameter - Improve user feedback with better error messages and operation status - Reduces code duplication and improves maintainability across the codebase
1 parent bee8ddf commit f6f4a5d

File tree

9 files changed

+851
-377
lines changed

9 files changed

+851
-377
lines changed

src/peg_this/features/audio.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,85 @@
1-
21
from pathlib import Path
32

43
import ffmpeg
54
import questionary
65
from rich.console import Console
76

87
from peg_this.utils.ffmpeg_utils import run_command, has_audio_stream
8+
from peg_this.utils.validation import (
9+
validate_input_file, check_output_file, check_has_video_stream, press_continue
10+
)
911

1012
console = Console()
1113

1214

1315
def extract_audio(file_path):
14-
"""Extract the audio track from a video file."""
16+
if not validate_input_file(file_path):
17+
press_continue()
18+
return
19+
1520
if not has_audio_stream(file_path):
1621
console.print("[bold red]Error: No audio stream found in the file.[/bold red]")
17-
questionary.press_any_key_to_continue().ask()
22+
press_continue()
1823
return
1924

20-
audio_format = questionary.select("Select audio format:", choices=["mp3", "flac", "wav"], use_indicator=True).ask()
21-
if not audio_format: return
25+
audio_format = questionary.select(
26+
"Select audio format:",
27+
choices=["mp3", "flac", "wav"]
28+
).ask()
29+
if not audio_format:
30+
return
2231

2332
output_file = f"{Path(file_path).stem}_audio.{audio_format}"
24-
stream = ffmpeg.input(file_path).output(output_file, vn=None, acodec='libmp3lame' if audio_format == 'mp3' else audio_format, y=None)
25-
26-
run_command(stream, f"Extracting audio to {audio_format.upper()}...", show_progress=True)
27-
console.print(f"[bold green]Successfully extracted audio to {output_file}[/bold green]")
28-
questionary.press_any_key_to_continue().ask()
33+
action_result, final_output = check_output_file(output_file, "Audio file")
34+
35+
if action_result == 'cancel':
36+
console.print("[yellow]Operation cancelled.[/yellow]")
37+
press_continue()
38+
return
39+
40+
stream = ffmpeg.input(file_path).output(
41+
final_output,
42+
vn=None,
43+
acodec='libmp3lame' if audio_format == 'mp3' else audio_format
44+
)
45+
46+
if action_result == 'overwrite':
47+
stream = stream.overwrite_output()
48+
49+
if run_command(stream, f"Extracting audio to {audio_format.upper()}...", show_progress=True):
50+
console.print(f"[bold green]Successfully extracted audio to {final_output}[/bold green]")
51+
else:
52+
console.print("[bold red]Failed to extract audio.[/bold red]")
53+
54+
press_continue()
2955

3056

3157
def remove_audio(file_path):
32-
"""Create a silent version of a video."""
58+
if not validate_input_file(file_path):
59+
press_continue()
60+
return
61+
62+
if not check_has_video_stream(file_path):
63+
console.print("[bold red]Error: No video stream found in the file.[/bold red]")
64+
press_continue()
65+
return
66+
3367
output_file = f"{Path(file_path).stem}_no_audio{Path(file_path).suffix}"
34-
stream = ffmpeg.input(file_path).output(output_file, vcodec='copy', an=None, y=None)
35-
36-
run_command(stream, "Removing audio track...", show_progress=True)
37-
console.print(f"[bold green]Successfully removed audio, saved to {output_file}[/bold green]")
38-
questionary.press_any_key_to_continue().ask()
68+
action_result, final_output = check_output_file(output_file, "Video file")
69+
70+
if action_result == 'cancel':
71+
console.print("[yellow]Operation cancelled.[/yellow]")
72+
press_continue()
73+
return
74+
75+
stream = ffmpeg.input(file_path).output(final_output, vcodec='copy', an=None)
76+
77+
if action_result == 'overwrite':
78+
stream = stream.overwrite_output()
79+
80+
if run_command(stream, "Removing audio track...", show_progress=True):
81+
console.print(f"[bold green]Successfully removed audio, saved to {final_output}[/bold green]")
82+
else:
83+
console.print("[bold red]Failed to remove audio.[/bold red]")
84+
85+
press_continue()

src/peg_this/features/batch.py

Lines changed: 105 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import os
32
import logging
43
from pathlib import Path
@@ -9,36 +8,43 @@
98

109
from peg_this.utils.ffmpeg_utils import run_command, has_audio_stream
1110
from peg_this.utils.ui_utils import get_media_files
11+
from peg_this.utils.validation import check_disk_space, press_continue
1212

1313
console = Console()
1414

1515

1616
def batch_convert():
17-
"""Convert all media files in the directory to a specific format."""
1817
media_files = get_media_files()
1918
if not media_files:
2019
console.print("[bold yellow]No media files found in the current directory.[/bold yellow]")
21-
questionary.press_any_key_to_continue().ask()
20+
press_continue()
2221
return
2322

23+
console.print(f"[dim]Found {len(media_files)} media file(s)[/dim]")
24+
2425
output_format = questionary.select(
2526
"Select output format for the batch conversion:",
26-
choices=["mp4", "mkv", "mov", "avi", "webm", "mp3", "flac", "wav", "gif"],
27-
use_indicator=True
27+
choices=["mp4", "mkv", "mov", "avi", "webm", "mp3", "flac", "wav", "gif"]
2828
).ask()
29-
if not output_format: return
29+
if not output_format:
30+
return
3031

3132
quality_preset = None
3233
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
3334
quality_preset = questionary.select(
3435
"Select quality preset:",
35-
choices=["Same as source", "High (CRF 18)", "Medium (CRF 23)", "Low (CRF 28)"],
36-
use_indicator=True
36+
choices=["Same as source", "High (CRF 18)", "Medium (CRF 23)", "Low (CRF 28)"]
3737
).ask()
38-
if not quality_preset: return
38+
if not quality_preset:
39+
return
40+
41+
# Estimate disk space needed
42+
total_size = sum(os.path.getsize(f) for f in media_files if os.path.exists(f))
43+
if total_size > 0 and not check_disk_space(media_files[0], multiplier=len(media_files)):
44+
return
3945

4046
confirm = questionary.confirm(
41-
f"This will convert {len(media_files)} file(s) in the current directory to .{output_format}. Continue?",
47+
f"This will convert {len(media_files)} file(s) to .{output_format}. Continue?",
4248
default=False
4349
).ask()
4450

@@ -48,78 +54,98 @@ def batch_convert():
4854

4955
success_count = 0
5056
fail_count = 0
51-
52-
for file in media_files:
53-
console.rule(f"Processing: {file}")
54-
file_path = os.path.abspath(file)
55-
is_gif = Path(file_path).suffix.lower() == '.gif'
56-
has_audio = has_audio_stream(file_path)
57-
58-
if (is_gif or not has_audio) and output_format in ["mp3", "flac", "wav"]:
59-
console.print(f"[bold yellow]Skipping {file}: Source has no audio to convert.[/bold yellow]")
60-
continue
61-
62-
output_file = f"{Path(file_path).stem}_batch.{output_format}"
63-
input_stream = ffmpeg.input(file_path)
64-
output_stream = None
65-
kwargs = {'y': None}
66-
67-
try:
68-
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
69-
if quality_preset == "Same as source":
70-
kwargs['c'] = 'copy'
71-
else:
72-
crf = quality_preset.split(" ")[-1][1:-1]
73-
kwargs['c:v'] = 'libx264'
74-
kwargs['crf'] = crf
75-
kwargs['pix_fmt'] = 'yuv420p'
76-
if has_audio:
77-
kwargs['c:a'] = 'aac'
78-
kwargs['b:a'] = '192k'
57+
skipped_count = 0
58+
59+
try:
60+
for i, file in enumerate(media_files):
61+
console.rule(f"[{i+1}/{len(media_files)}] Processing: {file}")
62+
file_path = os.path.abspath(file)
63+
64+
if not os.path.exists(file_path):
65+
console.print(f"[yellow]Skipping {file}: File not found.[/yellow]")
66+
skipped_count += 1
67+
continue
68+
69+
is_gif = Path(file_path).suffix.lower() == '.gif'
70+
has_audio = has_audio_stream(file_path)
71+
72+
if (is_gif or not has_audio) and output_format in ["mp3", "flac", "wav"]:
73+
console.print(f"[yellow]Skipping {file}: Source has no audio to convert.[/yellow]")
74+
skipped_count += 1
75+
continue
76+
77+
output_file = f"{Path(file_path).stem}_batch.{output_format}"
78+
79+
# Skip if output already exists
80+
if os.path.exists(output_file):
81+
console.print(f"[yellow]Skipping {file}: Output already exists ({output_file})[/yellow]")
82+
skipped_count += 1
83+
continue
84+
85+
input_stream = ffmpeg.input(file_path)
86+
output_stream = None
87+
kwargs = {}
88+
89+
try:
90+
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
91+
if quality_preset == "Same as source":
92+
kwargs['c'] = 'copy'
7993
else:
80-
kwargs['an'] = None
81-
output_stream = input_stream.output(output_file, **kwargs)
82-
83-
elif output_format in ["mp3", "flac", "wav"]:
84-
kwargs['vn'] = None
85-
kwargs['c:a'] = 'libmp3lame' if output_format == 'mp3' else output_format
86-
if output_format == 'mp3':
87-
kwargs['b:a'] = '192k' # Default bitrate for batch
88-
output_stream = input_stream.output(output_file, **kwargs)
89-
90-
elif output_format == "gif":
91-
fps = "15"
92-
scale = "480"
93-
palette_file = f"palette_{Path(file_path).stem}.png"
94-
95-
palette_gen_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos').filter('palettegen')
96-
run_command(palette_gen_stream.output(palette_file, y=None), f"Generating palette for {file}...")
97-
98-
if not os.path.exists(palette_file):
99-
console.print(f"[bold red]Failed to generate color palette for {file}.[/bold red]")
94+
crf = quality_preset.split(" ")[-1][1:-1]
95+
kwargs['c:v'] = 'libx264'
96+
kwargs['crf'] = crf
97+
kwargs['pix_fmt'] = 'yuv420p'
98+
if has_audio:
99+
kwargs['c:a'] = 'aac'
100+
kwargs['b:a'] = '192k'
101+
else:
102+
kwargs['an'] = None
103+
output_stream = input_stream.output(output_file, **kwargs)
104+
105+
elif output_format in ["mp3", "flac", "wav"]:
106+
kwargs['vn'] = None
107+
kwargs['c:a'] = 'libmp3lame' if output_format == 'mp3' else output_format
108+
if output_format == 'mp3':
109+
kwargs['b:a'] = '192k'
110+
output_stream = input_stream.output(output_file, **kwargs)
111+
112+
elif output_format == "gif":
113+
fps = 15
114+
scale = 480
115+
palette_file = f"palette_{Path(file_path).stem}.png"
116+
117+
try:
118+
palette_gen_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos').filter('palettegen')
119+
run_command(palette_gen_stream.output(palette_file).overwrite_output(), f"Generating palette for {file}...")
120+
121+
if not os.path.exists(palette_file):
122+
console.print(f"[red]Failed to generate color palette for {file}.[/red]")
123+
fail_count += 1
124+
continue
125+
126+
palette_input = ffmpeg.input(palette_file)
127+
video_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos')
128+
final_stream = ffmpeg.filter([video_stream, palette_input], 'paletteuse')
129+
output_stream = final_stream.output(output_file)
130+
finally:
131+
if os.path.exists(palette_file):
132+
os.remove(palette_file)
133+
134+
if output_stream and run_command(output_stream, f"Converting {file}...", show_progress=True):
135+
console.print(f" -> [green]Successfully converted to {output_file}[/green]")
136+
success_count += 1
137+
else:
138+
console.print(f" -> [red]Failed to convert {file}.[/red]")
100139
fail_count += 1
101-
continue
102-
103-
palette_input = ffmpeg.input(palette_file)
104-
video_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos')
105-
final_stream = ffmpeg.filter([video_stream, palette_input], 'paletteuse')
106-
output_stream = final_stream.output(output_file, y=None)
107-
108-
if output_stream and run_command(output_stream, f"Converting {file}...", show_progress=True):
109-
console.print(f" -> [bold green]Successfully converted to {output_file}[/bold green]")
110-
success_count += 1
111-
else:
112-
console.print(f" -> [bold red]Failed to convert {file}.[/bold red]")
113-
fail_count += 1
114140

115-
if output_format == "gif" and os.path.exists(f"palette_{Path(file_path).stem}.png"):
116-
os.remove(f"palette_{Path(file_path).stem}.png")
141+
except Exception as e:
142+
console.print(f"[red]Error processing {file}: {e}[/red]")
143+
logging.error(f"Batch convert error for file {file}: {e}")
144+
fail_count += 1
117145

118-
except Exception as e:
119-
console.print(f"[bold red]An unexpected error occurred while processing {file}: {e}[/bold red]")
120-
logging.error(f"Batch convert error for file {file}: {e}")
121-
fail_count += 1
146+
except KeyboardInterrupt:
147+
console.print("\n[yellow]Batch conversion interrupted by user.[/yellow]")
122148

123149
console.rule("[bold green]Batch Conversion Complete[/bold green]")
124-
console.print(f"Successful: {success_count} | Failed: {fail_count}")
125-
questionary.press_any_key_to_continue().ask()
150+
console.print(f"Successful: {success_count} | Failed: {fail_count} | Skipped: {skipped_count}")
151+
press_continue()

0 commit comments

Comments
 (0)