peg-this 4.0.0__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/audio.py +63 -16
- peg_this/features/batch.py +105 -79
- peg_this/features/convert.py +179 -95
- peg_this/features/crop.py +63 -34
- peg_this/features/inspect.py +71 -21
- peg_this/features/join.py +85 -31
- peg_this/features/subtitle.py +397 -0
- peg_this/features/trim.py +40 -10
- peg_this/peg_this.py +4 -1
- 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-4.0.0.dist-info → peg_this-4.1.0.dist-info}/WHEEL +1 -1
- peg_this-4.0.0.dist-info/METADATA +0 -164
- peg_this-4.0.0.dist-info/RECORD +0 -19
- {peg_this-4.0.0.dist-info → peg_this-4.1.0.dist-info}/entry_points.txt +0 -0
- {peg_this-4.0.0.dist-info → peg_this-4.1.0.dist-info}/licenses/LICENSE +0 -0
- {peg_this-4.0.0.dist-info → peg_this-4.1.0.dist-info}/top_level.txt +0 -0
peg_this/features/audio.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
from pathlib import Path
|
|
3
2
|
|
|
4
3
|
import ffmpeg
|
|
@@ -6,33 +5,81 @@ import questionary
|
|
|
6
5
|
from rich.console import Console
|
|
7
6
|
|
|
8
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
|
+
)
|
|
9
11
|
|
|
10
12
|
console = Console()
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
def extract_audio(file_path):
|
|
14
|
-
|
|
16
|
+
if not validate_input_file(file_path):
|
|
17
|
+
press_continue()
|
|
18
|
+
return
|
|
19
|
+
|
|
15
20
|
if not has_audio_stream(file_path):
|
|
16
21
|
console.print("[bold red]Error: No audio stream found in the file.[/bold red]")
|
|
17
|
-
|
|
22
|
+
press_continue()
|
|
18
23
|
return
|
|
19
24
|
|
|
20
|
-
audio_format = questionary.select(
|
|
21
|
-
|
|
25
|
+
audio_format = questionary.select(
|
|
26
|
+
"Select audio format:",
|
|
27
|
+
choices=["mp3", "flac", "wav"]
|
|
28
|
+
).ask()
|
|
29
|
+
if not audio_format:
|
|
30
|
+
return
|
|
22
31
|
|
|
23
32
|
output_file = f"{Path(file_path).stem}_audio.{audio_format}"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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()
|
|
29
55
|
|
|
30
56
|
|
|
31
57
|
def remove_audio(file_path):
|
|
32
|
-
|
|
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
|
+
|
|
33
67
|
output_file = f"{Path(file_path).stem}_no_audio{Path(file_path).suffix}"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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()
|
peg_this/features/batch.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import os
|
|
3
2
|
import logging
|
|
4
3
|
from pathlib import Path
|
|
@@ -9,36 +8,43 @@ from rich.console import Console
|
|
|
9
8
|
|
|
10
9
|
from peg_this.utils.ffmpeg_utils import run_command, has_audio_stream
|
|
11
10
|
from peg_this.utils.ui_utils import get_media_files
|
|
11
|
+
from peg_this.utils.validation import check_disk_space, press_continue
|
|
12
12
|
|
|
13
13
|
console = Console()
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def batch_convert():
|
|
17
|
-
"""Convert all media files in the directory to a specific format."""
|
|
18
17
|
media_files = get_media_files()
|
|
19
18
|
if not media_files:
|
|
20
19
|
console.print("[bold yellow]No media files found in the current directory.[/bold yellow]")
|
|
21
|
-
|
|
20
|
+
press_continue()
|
|
22
21
|
return
|
|
23
22
|
|
|
23
|
+
console.print(f"[dim]Found {len(media_files)} media file(s)[/dim]")
|
|
24
|
+
|
|
24
25
|
output_format = questionary.select(
|
|
25
26
|
"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"]
|
|
28
28
|
).ask()
|
|
29
|
-
if not output_format:
|
|
29
|
+
if not output_format:
|
|
30
|
+
return
|
|
30
31
|
|
|
31
32
|
quality_preset = None
|
|
32
33
|
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
|
|
33
34
|
quality_preset = questionary.select(
|
|
34
35
|
"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)"]
|
|
37
37
|
).ask()
|
|
38
|
-
if not quality_preset:
|
|
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
|
|
39
45
|
|
|
40
46
|
confirm = questionary.confirm(
|
|
41
|
-
f"This will convert {len(media_files)} file(s)
|
|
47
|
+
f"This will convert {len(media_files)} file(s) to .{output_format}. Continue?",
|
|
42
48
|
default=False
|
|
43
49
|
).ask()
|
|
44
50
|
|
|
@@ -48,78 +54,98 @@ def batch_convert():
|
|
|
48
54
|
|
|
49
55
|
success_count = 0
|
|
50
56
|
fail_count = 0
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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'
|
|
79
93
|
else:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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]")
|
|
100
139
|
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
|
|
114
140
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
117
145
|
|
|
118
|
-
|
|
119
|
-
|
|
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]")
|
|
122
148
|
|
|
123
149
|
console.rule("[bold green]Batch Conversion Complete[/bold green]")
|
|
124
|
-
console.print(f"Successful: {success_count} | Failed: {fail_count}")
|
|
125
|
-
|
|
150
|
+
console.print(f"Successful: {success_count} | Failed: {fail_count} | Skipped: {skipped_count}")
|
|
151
|
+
press_continue()
|