peg-this 3.0.2__py3-none-any.whl → 4.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- peg_this/features/__init__.py +0 -0
- peg_this/features/audio.py +85 -0
- peg_this/features/batch.py +151 -0
- peg_this/features/convert.py +309 -0
- peg_this/features/crop.py +207 -0
- peg_this/features/inspect.py +110 -0
- peg_this/features/join.py +137 -0
- peg_this/features/subtitle.py +397 -0
- peg_this/features/trim.py +56 -0
- peg_this/peg_this.py +53 -622
- peg_this/utils/__init__.py +0 -0
- peg_this/utils/ffmpeg_utils.py +129 -0
- peg_this/utils/ui_utils.py +52 -0
- peg_this/utils/validation.py +228 -0
- peg_this-4.1.0.dist-info/METADATA +283 -0
- peg_this-4.1.0.dist-info/RECORD +21 -0
- {peg_this-3.0.2.dist-info → peg_this-4.1.0.dist-info}/WHEEL +1 -1
- peg_this-3.0.2.dist-info/METADATA +0 -87
- peg_this-3.0.2.dist-info/RECORD +0 -8
- {peg_this-3.0.2.dist-info → peg_this-4.1.0.dist-info}/entry_points.txt +0 -0
- {peg_this-3.0.2.dist-info → peg_this-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {peg_this-3.0.2.dist-info → peg_this-4.1.0.dist-info}/top_level.txt +0 -0
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import ffmpeg
|
|
4
|
+
import questionary
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
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
|
+
)
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def extract_audio(file_path):
|
|
16
|
+
if not validate_input_file(file_path):
|
|
17
|
+
press_continue()
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
if not has_audio_stream(file_path):
|
|
21
|
+
console.print("[bold red]Error: No audio stream found in the file.[/bold red]")
|
|
22
|
+
press_continue()
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
audio_format = questionary.select(
|
|
26
|
+
"Select audio format:",
|
|
27
|
+
choices=["mp3", "flac", "wav"]
|
|
28
|
+
).ask()
|
|
29
|
+
if not audio_format:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
output_file = f"{Path(file_path).stem}_audio.{audio_format}"
|
|
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()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def remove_audio(file_path):
|
|
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
|
+
|
|
67
|
+
output_file = f"{Path(file_path).stem}_no_audio{Path(file_path).suffix}"
|
|
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()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import ffmpeg
|
|
6
|
+
import questionary
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from peg_this.utils.ffmpeg_utils import run_command, has_audio_stream
|
|
10
|
+
from peg_this.utils.ui_utils import get_media_files
|
|
11
|
+
from peg_this.utils.validation import check_disk_space, press_continue
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def batch_convert():
|
|
17
|
+
media_files = get_media_files()
|
|
18
|
+
if not media_files:
|
|
19
|
+
console.print("[bold yellow]No media files found in the current directory.[/bold yellow]")
|
|
20
|
+
press_continue()
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
console.print(f"[dim]Found {len(media_files)} media file(s)[/dim]")
|
|
24
|
+
|
|
25
|
+
output_format = questionary.select(
|
|
26
|
+
"Select output format for the batch conversion:",
|
|
27
|
+
choices=["mp4", "mkv", "mov", "avi", "webm", "mp3", "flac", "wav", "gif"]
|
|
28
|
+
).ask()
|
|
29
|
+
if not output_format:
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
quality_preset = None
|
|
33
|
+
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
|
|
34
|
+
quality_preset = questionary.select(
|
|
35
|
+
"Select quality preset:",
|
|
36
|
+
choices=["Same as source", "High (CRF 18)", "Medium (CRF 23)", "Low (CRF 28)"]
|
|
37
|
+
).ask()
|
|
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
|
|
45
|
+
|
|
46
|
+
confirm = questionary.confirm(
|
|
47
|
+
f"This will convert {len(media_files)} file(s) to .{output_format}. Continue?",
|
|
48
|
+
default=False
|
|
49
|
+
).ask()
|
|
50
|
+
|
|
51
|
+
if not confirm:
|
|
52
|
+
console.print("[bold yellow]Batch conversion cancelled.[/bold yellow]")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
success_count = 0
|
|
56
|
+
fail_count = 0
|
|
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'
|
|
93
|
+
else:
|
|
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]")
|
|
139
|
+
fail_count += 1
|
|
140
|
+
|
|
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
|
|
145
|
+
|
|
146
|
+
except KeyboardInterrupt:
|
|
147
|
+
console.print("\n[yellow]Batch conversion interrupted by user.[/yellow]")
|
|
148
|
+
|
|
149
|
+
console.rule("[bold green]Batch Conversion Complete[/bold green]")
|
|
150
|
+
console.print(f"Successful: {success_count} | Failed: {fail_count} | Skipped: {skipped_count}")
|
|
151
|
+
press_continue()
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import ffmpeg
|
|
5
|
+
import questionary
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from peg_this.utils.ffmpeg_utils import run_command, has_audio_stream
|
|
9
|
+
from peg_this.utils.validation import (
|
|
10
|
+
validate_input_file, check_output_file, validate_positive_integer, press_continue
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def convert_file(file_path):
|
|
17
|
+
if not validate_input_file(file_path):
|
|
18
|
+
press_continue()
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
is_gif = Path(file_path).suffix.lower() == '.gif'
|
|
22
|
+
has_audio = has_audio_stream(file_path)
|
|
23
|
+
|
|
24
|
+
output_format = questionary.select(
|
|
25
|
+
"Select the output format:",
|
|
26
|
+
choices=["mp4", "mkv", "mov", "avi", "webm", "mp3", "flac", "wav", "gif"]
|
|
27
|
+
).ask()
|
|
28
|
+
if not output_format:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
if (is_gif or not has_audio) and output_format in ["mp3", "flac", "wav"]:
|
|
32
|
+
console.print("[bold red]Error: Source has no audio to convert.[/bold red]")
|
|
33
|
+
press_continue()
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
output_file = f"{Path(file_path).stem}_converted.{output_format}"
|
|
37
|
+
action_result, final_output = check_output_file(output_file, "Output file")
|
|
38
|
+
|
|
39
|
+
if action_result == 'cancel':
|
|
40
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
41
|
+
press_continue()
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
input_stream = ffmpeg.input(file_path)
|
|
45
|
+
output_stream = None
|
|
46
|
+
kwargs = {}
|
|
47
|
+
|
|
48
|
+
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
|
|
49
|
+
quality = questionary.select(
|
|
50
|
+
"Select quality preset:",
|
|
51
|
+
choices=["Same as source", "High (CRF 18)", "Medium (CRF 23)", "Low (CRF 28)"]
|
|
52
|
+
).ask()
|
|
53
|
+
if not quality:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if quality == "Same as source":
|
|
57
|
+
kwargs['c'] = 'copy'
|
|
58
|
+
else:
|
|
59
|
+
crf = quality.split(" ")[-1][1:-1]
|
|
60
|
+
kwargs['c:v'] = 'libx264'
|
|
61
|
+
kwargs['crf'] = crf
|
|
62
|
+
kwargs['pix_fmt'] = 'yuv420p'
|
|
63
|
+
if has_audio:
|
|
64
|
+
kwargs['c:a'] = 'aac'
|
|
65
|
+
kwargs['b:a'] = '192k'
|
|
66
|
+
else:
|
|
67
|
+
kwargs['an'] = None
|
|
68
|
+
output_stream = input_stream.output(final_output, **kwargs)
|
|
69
|
+
|
|
70
|
+
elif output_format in ["mp3", "flac", "wav"]:
|
|
71
|
+
kwargs['vn'] = None
|
|
72
|
+
if output_format == 'mp3':
|
|
73
|
+
bitrate = questionary.select(
|
|
74
|
+
"Select audio bitrate:",
|
|
75
|
+
choices=["128k", "192k", "256k", "320k"]
|
|
76
|
+
).ask()
|
|
77
|
+
if not bitrate:
|
|
78
|
+
return
|
|
79
|
+
kwargs['c:a'] = 'libmp3lame'
|
|
80
|
+
kwargs['b:a'] = bitrate
|
|
81
|
+
else:
|
|
82
|
+
kwargs['c:a'] = output_format
|
|
83
|
+
output_stream = input_stream.output(final_output, **kwargs)
|
|
84
|
+
|
|
85
|
+
elif output_format == "gif":
|
|
86
|
+
fps = questionary.text("Enter frame rate (e.g., 15):", default="15").ask()
|
|
87
|
+
if not fps:
|
|
88
|
+
return
|
|
89
|
+
fps_val = validate_positive_integer(fps, "Frame rate")
|
|
90
|
+
if not fps_val:
|
|
91
|
+
press_continue()
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
scale = questionary.text("Enter width in pixels (e.g., 480):", default="480").ask()
|
|
95
|
+
if not scale:
|
|
96
|
+
return
|
|
97
|
+
scale_val = validate_positive_integer(scale, "Width")
|
|
98
|
+
if not scale_val:
|
|
99
|
+
press_continue()
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
palette_file = f"palette_{Path(file_path).stem}.png"
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
palette_gen_stream = input_stream.video.filter('fps', fps=fps_val).filter('scale', w=scale_val, h=-1, flags='lanczos').filter('palettegen')
|
|
106
|
+
run_command(palette_gen_stream.output(palette_file).overwrite_output(), "Generating color palette...")
|
|
107
|
+
|
|
108
|
+
if not os.path.exists(palette_file):
|
|
109
|
+
console.print("[bold red]Failed to generate color palette for GIF.[/bold red]")
|
|
110
|
+
press_continue()
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
palette_input = ffmpeg.input(palette_file)
|
|
114
|
+
video_stream = input_stream.video.filter('fps', fps=fps_val).filter('scale', w=scale_val, h=-1, flags='lanczos')
|
|
115
|
+
final_stream = ffmpeg.filter([video_stream, palette_input], 'paletteuse')
|
|
116
|
+
output_stream = final_stream.output(final_output)
|
|
117
|
+
|
|
118
|
+
finally:
|
|
119
|
+
if os.path.exists(palette_file):
|
|
120
|
+
os.remove(palette_file)
|
|
121
|
+
|
|
122
|
+
if output_stream:
|
|
123
|
+
if action_result == 'overwrite':
|
|
124
|
+
output_stream = output_stream.overwrite_output()
|
|
125
|
+
|
|
126
|
+
if run_command(output_stream, f"Converting to {output_format}...", show_progress=True):
|
|
127
|
+
console.print(f"[bold green]Successfully converted to {final_output}[/bold green]")
|
|
128
|
+
else:
|
|
129
|
+
console.print("[bold red]Conversion failed.[/bold red]")
|
|
130
|
+
|
|
131
|
+
press_continue()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def convert_image(file_path):
|
|
135
|
+
if not validate_input_file(file_path):
|
|
136
|
+
press_continue()
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
output_format = questionary.select(
|
|
140
|
+
"Select the output format:",
|
|
141
|
+
choices=["jpg", "png", "webp", "bmp", "tiff"]
|
|
142
|
+
).ask()
|
|
143
|
+
if not output_format:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
output_file = f"{Path(file_path).stem}_converted.{output_format}"
|
|
147
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
148
|
+
|
|
149
|
+
if action_result == 'cancel':
|
|
150
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
151
|
+
press_continue()
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
kwargs = {}
|
|
155
|
+
|
|
156
|
+
if output_format in ['jpg', 'webp']:
|
|
157
|
+
quality_preset = questionary.select(
|
|
158
|
+
"Select quality preset:",
|
|
159
|
+
choices=["High (95%)", "Medium (80%)", "Low (60%)"]
|
|
160
|
+
).ask()
|
|
161
|
+
if not quality_preset:
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
quality_map = {"High (95%)": "95", "Medium (80%)": "80", "Low (60%)": "60"}
|
|
165
|
+
quality = quality_map[quality_preset]
|
|
166
|
+
|
|
167
|
+
if output_format == 'jpg':
|
|
168
|
+
q_scale = int(31 - (int(quality) / 100.0) * 30)
|
|
169
|
+
kwargs['q:v'] = q_scale
|
|
170
|
+
elif output_format == 'webp':
|
|
171
|
+
kwargs['quality'] = quality
|
|
172
|
+
|
|
173
|
+
stream = ffmpeg.input(file_path).output(final_output, **kwargs)
|
|
174
|
+
|
|
175
|
+
if action_result == 'overwrite':
|
|
176
|
+
stream = stream.overwrite_output()
|
|
177
|
+
|
|
178
|
+
if run_command(stream, f"Converting to {output_format.upper()}..."):
|
|
179
|
+
console.print(f"[bold green]Successfully converted image to {final_output}[/bold green]")
|
|
180
|
+
else:
|
|
181
|
+
console.print("[bold red]Image conversion failed.[/bold red]")
|
|
182
|
+
|
|
183
|
+
press_continue()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def resize_image(file_path):
|
|
187
|
+
if not validate_input_file(file_path):
|
|
188
|
+
press_continue()
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
console.print("Enter new dimensions. Use [bold]-1[/bold] for one dimension to preserve aspect ratio.")
|
|
192
|
+
width = questionary.text("Enter new width (e.g., 1280 or -1):").ask()
|
|
193
|
+
if not width:
|
|
194
|
+
return
|
|
195
|
+
height = questionary.text("Enter new height (e.g., 720 or -1):").ask()
|
|
196
|
+
if not height:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
width_val = validate_positive_integer(width, "Width")
|
|
200
|
+
height_val = validate_positive_integer(height, "Height")
|
|
201
|
+
|
|
202
|
+
if width_val is None or height_val is None:
|
|
203
|
+
press_continue()
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if width_val == -1 and height_val == -1:
|
|
207
|
+
console.print("[bold red]Error: Width and Height cannot both be -1.[/bold red]")
|
|
208
|
+
press_continue()
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
output_file = f"{Path(file_path).stem}_resized{Path(file_path).suffix}"
|
|
212
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
213
|
+
|
|
214
|
+
if action_result == 'cancel':
|
|
215
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
216
|
+
press_continue()
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
stream = ffmpeg.input(file_path).filter('scale', w=width_val, h=height_val).output(final_output)
|
|
220
|
+
|
|
221
|
+
if action_result == 'overwrite':
|
|
222
|
+
stream = stream.overwrite_output()
|
|
223
|
+
|
|
224
|
+
if run_command(stream, "Resizing image..."):
|
|
225
|
+
console.print(f"[bold green]Successfully resized image to {final_output}[/bold green]")
|
|
226
|
+
else:
|
|
227
|
+
console.print("[bold red]Image resizing failed.[/bold red]")
|
|
228
|
+
|
|
229
|
+
press_continue()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def rotate_image(file_path):
|
|
233
|
+
if not validate_input_file(file_path):
|
|
234
|
+
press_continue()
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
rotation = questionary.select(
|
|
238
|
+
"Select rotation:",
|
|
239
|
+
choices=["90 degrees clockwise", "90 degrees counter-clockwise", "180 degrees"]
|
|
240
|
+
).ask()
|
|
241
|
+
if not rotation:
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
output_file = f"{Path(file_path).stem}_rotated{Path(file_path).suffix}"
|
|
245
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
246
|
+
|
|
247
|
+
if action_result == 'cancel':
|
|
248
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
249
|
+
press_continue()
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
stream = ffmpeg.input(file_path)
|
|
253
|
+
if rotation == "90 degrees clockwise":
|
|
254
|
+
stream = stream.filter('transpose', 1)
|
|
255
|
+
elif rotation == "90 degrees counter-clockwise":
|
|
256
|
+
stream = stream.filter('transpose', 2)
|
|
257
|
+
elif rotation == "180 degrees":
|
|
258
|
+
stream = stream.filter('transpose', 2).filter('transpose', 2)
|
|
259
|
+
|
|
260
|
+
output_stream = stream.output(final_output)
|
|
261
|
+
|
|
262
|
+
if action_result == 'overwrite':
|
|
263
|
+
output_stream = output_stream.overwrite_output()
|
|
264
|
+
|
|
265
|
+
if run_command(output_stream, "Rotating image..."):
|
|
266
|
+
console.print(f"[bold green]Successfully rotated image and saved to {final_output}[/bold green]")
|
|
267
|
+
else:
|
|
268
|
+
console.print("[bold red]Image rotation failed.[/bold red]")
|
|
269
|
+
|
|
270
|
+
press_continue()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def flip_image(file_path):
|
|
274
|
+
if not validate_input_file(file_path):
|
|
275
|
+
press_continue()
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
flip_direction = questionary.select(
|
|
279
|
+
"Select flip direction:",
|
|
280
|
+
choices=["Horizontal", "Vertical"]
|
|
281
|
+
).ask()
|
|
282
|
+
if not flip_direction:
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
output_file = f"{Path(file_path).stem}_flipped{Path(file_path).suffix}"
|
|
286
|
+
action_result, final_output = check_output_file(output_file, "Image file")
|
|
287
|
+
|
|
288
|
+
if action_result == 'cancel':
|
|
289
|
+
console.print("[yellow]Operation cancelled.[/yellow]")
|
|
290
|
+
press_continue()
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
stream = ffmpeg.input(file_path)
|
|
294
|
+
if flip_direction == "Horizontal":
|
|
295
|
+
stream = stream.filter('hflip')
|
|
296
|
+
else:
|
|
297
|
+
stream = stream.filter('vflip')
|
|
298
|
+
|
|
299
|
+
output_stream = stream.output(final_output)
|
|
300
|
+
|
|
301
|
+
if action_result == 'overwrite':
|
|
302
|
+
output_stream = output_stream.overwrite_output()
|
|
303
|
+
|
|
304
|
+
if run_command(output_stream, "Flipping image..."):
|
|
305
|
+
console.print(f"[bold green]Successfully flipped image and saved to {final_output}[/bold green]")
|
|
306
|
+
else:
|
|
307
|
+
console.print("[bold red]Image flipping failed.[/bold red]")
|
|
308
|
+
|
|
309
|
+
press_continue()
|