peg-this 3.0.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.

Potentially problematic release.


This version of peg-this might be problematic. Click here for more details.

peg_this/__init__.py ADDED
File without changes
peg_this/peg_this.py ADDED
@@ -0,0 +1,520 @@
1
+ import os
2
+ import ffmpeg
3
+ import json
4
+ from pathlib import Path
5
+ import sys
6
+ import random
7
+ import logging
8
+ import tkinter as tk
9
+ from tkinter import filedialog, messagebox
10
+ from PIL import Image, ImageTk
11
+ import questionary
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
15
+
16
+ # Configure logging
17
+ log_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ffmpeg_log.txt")
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s - %(levelname)s - %(message)s',
21
+ handlers=[
22
+ logging.FileHandler(log_file),
23
+ ]
24
+ )
25
+
26
+ console = Console()
27
+
28
+
29
+ def run_command(stream, description="Processing...", show_progress=False):
30
+ """Runs a command using ffmpeg-python, with an optional progress bar."""
31
+ console.print(f"[bold cyan]{description}[/bold cyan]")
32
+
33
+ if not show_progress:
34
+ try:
35
+ out, err = ffmpeg.run(stream, capture_stdout=True, capture_stderr=True)
36
+ return out.decode('utf-8')
37
+ except ffmpeg.Error as e:
38
+ console.print("[bold red]An error occurred:[/bold red]")
39
+ console.print(e.stderr.decode('utf-8'))
40
+ return None
41
+ else:
42
+ with Progress(
43
+ SpinnerColumn(),
44
+ TextColumn("[progress.description]{task.description}"),
45
+ BarColumn(),
46
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
47
+ console=console,
48
+ ) as progress:
49
+ task = progress.add_task(description, total=100)
50
+ process = ffmpeg.run_async(stream, pipe_stdout=True, pipe_stderr=True)
51
+
52
+ # The following progress bar is a simulation, as ffmpeg-python does not directly expose progress.
53
+ # A more accurate progress bar would require parsing ffmpeg's stderr.
54
+ while process.poll() is None:
55
+ progress.update(task, advance=0.5)
56
+ # A more sophisticated implementation could read from process.stderr
57
+ # and parse the progress information.
58
+ import time
59
+ time.sleep(0.1)
60
+
61
+ progress.update(task, completed=100)
62
+ out, err = process.communicate()
63
+ if process.returncode != 0:
64
+ console.print(f"[bold red]An error occurred during processing.[/bold red]")
65
+ console.print(err.decode('utf-8'))
66
+ return None
67
+ return "Success"
68
+
69
+
70
+ def get_media_files():
71
+ """Scan the current directory for media files."""
72
+ media_extensions = [".mkv", ".mp4", ".avi", ".mov", ".webm", ".flv", ".wmv", ".mp3", ".flac", ".wav", ".ogg", ".gif"]
73
+ files = [f for f in os.listdir('.') if os.path.isfile(f) and Path(f).suffix.lower() in media_extensions]
74
+ return files
75
+
76
+
77
+ def select_media_file():
78
+ """Display a menu to select a media file, or open a file picker if none are found."""
79
+ media_files = get_media_files()
80
+ if not media_files:
81
+ console.print("[bold yellow]No media files found in this directory.[/bold yellow]")
82
+ if tk and questionary.confirm("Would you like to select a file from another location?").ask():
83
+ root = tk.Tk()
84
+ root.withdraw() # Hide the main window
85
+ file_path = filedialog.askopenfilename(
86
+ title="Select a media file",
87
+ filetypes=[
88
+ ("Media Files", "*.mkv *.mp4 *.avi *.mov *.webm *.flv *.wmv *.mp3 *.flac *.wav *.ogg *.gif"),
89
+ ("All Files", "*.*")
90
+ ]
91
+ )
92
+ return file_path
93
+ else:
94
+ return None
95
+
96
+ file = questionary.select(
97
+ "Select a media file to process:",
98
+ choices=media_files + [questionary.Separator(), "Back"],
99
+ use_indicator=True
100
+ ).ask()
101
+
102
+ return file if file != "Back" else None
103
+
104
+
105
+ def inspect_file(file_path):
106
+ """Show detailed information about the selected media file."""
107
+ try:
108
+ info = ffmpeg.probe(file_path)
109
+ except ffmpeg.Error as e:
110
+ console.print("[bold red]An error occurred while inspecting the file:[/bold red]")
111
+ console.print(e.stderr.decode('utf-8'))
112
+ return
113
+
114
+ format_info = info.get('format', {})
115
+
116
+ table = Table(title=f"File Information: {os.path.basename(file_path)}", show_header=True, header_style="bold magenta")
117
+ table.add_column("Property", style="dim")
118
+ table.add_column("Value")
119
+
120
+ size_bytes = int(format_info.get('size', 0))
121
+ size_mb = size_bytes / (1024 * 1024)
122
+ duration_sec = float(format_info.get('duration', 0))
123
+
124
+ table.add_row("Size", f"{size_mb:.2f} MB")
125
+ table.add_row("Duration", f"{duration_sec:.2f} seconds")
126
+ table.add_row("Format", format_info.get('format_long_name', 'N/A'))
127
+ table.add_row("Bitrate", f"{float(format_info.get('bit_rate', 0)) / 1000:.0f} kb/s")
128
+
129
+ console.print(table)
130
+
131
+ video_streams = [s for s in info.get('streams', []) if s.get('codec_type') == 'video']
132
+ if video_streams:
133
+ video_table = Table(title="Video Streams", show_header=True, header_style="bold cyan")
134
+ video_table.add_column("Stream")
135
+ video_table.add_column("Codec")
136
+ video_table.add_column("Resolution")
137
+ video_table.add_column("Frame Rate")
138
+ for s in video_streams:
139
+ video_table.add_row(
140
+ f"#{s.get('index')}",
141
+ s.get('codec_name'),
142
+ f"{s.get('width')}x{s.get('height')}",
143
+ s.get('r_frame_rate')
144
+ )
145
+ console.print(video_table)
146
+
147
+ audio_streams = [s for s in info.get('streams', []) if s.get('codec_type') == 'audio']
148
+ if audio_streams:
149
+ audio_table = Table(title="Audio Streams", show_header=True, header_style="bold green")
150
+ audio_table.add_column("Stream")
151
+ audio_table.add_column("Codec")
152
+ audio_table.add_column("Sample Rate")
153
+ audio_table.add_column("Channels")
154
+ for s in audio_streams:
155
+ audio_table.add_row(
156
+ f"#{s.get('index')}",
157
+ s.get('codec_name'),
158
+ f"{s.get('sample_rate')} Hz",
159
+ str(s.get('channels'))
160
+ )
161
+ console.print(audio_table)
162
+
163
+ questionary.press_any_key_to_continue().ask()
164
+
165
+
166
+ def trim_video(file_path):
167
+ """Cut a video by specifying start and end times."""
168
+ start_time = questionary.text("Enter start time (HH:MM:SS):").ask()
169
+ if not start_time: return
170
+ end_time = questionary.text("Enter end time (HH:MM:SS):").ask()
171
+ if not end_time: return
172
+
173
+ output_file = f"{Path(file_path).stem}_trimmed{Path(file_path).suffix}"
174
+ stream = ffmpeg.input(file_path, ss=start_time, to=end_time).output(output_file, c='copy')
175
+ run_command(stream, "Trimming video...", show_progress=True)
176
+ console.print(f"[bold green]Successfully trimmed to {output_file}[/bold green]")
177
+ questionary.press_any_key_to_continue().ask()
178
+
179
+ def extract_audio(file_path):
180
+ """Extract the audio track from a video file."""
181
+ audio_format = questionary.select(
182
+ "Select audio format:",
183
+ choices=[
184
+ questionary.Choice("MP3 (lossy)", {"codec": "libmp3lame", "ext": "mp3", "q": 2}),
185
+ questionary.Choice("FLAC (lossless)", {"codec": "flac", "ext": "flac"}),
186
+ questionary.Choice("WAV (uncompressed)", {"codec": "pcm_s16le", "ext": "wav"})
187
+ ],
188
+ use_indicator=True
189
+ ).ask()
190
+
191
+ if not audio_format: return
192
+
193
+ output_file = f"{Path(file_path).stem}_audio.{audio_format['ext']}"
194
+ stream = ffmpeg.input(file_path).output(output_file, vn=None, acodec=audio_format['codec'], **({'q:a': audio_format['q']} if 'q' in audio_format else {}))
195
+ run_command(stream, f"Extracting audio to {audio_format['ext'].upper()}...", show_progress=True)
196
+ console.print(f"[bold green]Successfully extracted audio to {output_file}[/bold green]")
197
+ questionary.press_any_key_to_continue().ask()
198
+
199
+ def remove_audio(file_path):
200
+ """Create a silent version of a video."""
201
+ output_file = f"{Path(file_path).stem}_no_audio{Path(file_path).suffix}"
202
+ stream = ffmpeg.input(file_path).output(output_file, vcodec='copy', an=None)
203
+ run_command(stream, "Removing audio track...", show_progress=True)
204
+ console.print(f"[bold green]Successfully removed audio, saved to {output_file}[/bold green]")
205
+ questionary.press_any_key_to_continue().ask()
206
+
207
+
208
+ def batch_convert():
209
+ """Convert all media files in the directory to a specific format."""
210
+ output_format = questionary.select(
211
+ "Select output format for the batch conversion:",
212
+ choices=["mp4", "mkv", "mov", "avi", "webm", "flv", "wmv", "mp3", "flac", "wav", "ogg", "m4a", "aac", "gif"],
213
+ use_indicator=True
214
+ ).ask()
215
+
216
+ if not output_format: return
217
+
218
+ quality = "copy"
219
+ if output_format in ["mp4", "webm", "avi", "wmv"]:
220
+ quality = questionary.select(
221
+ "Select quality preset:",
222
+ choices=["Same as source (lossless if possible)", "High Quality (CRF 18)", "Medium Quality (CRF 23)", "Low Quality (CRF 28)"],
223
+ use_indicator=True
224
+ ).ask()
225
+ if not quality: return
226
+
227
+ confirm = questionary.confirm(
228
+ f"This will attempt to convert ALL media files in the current directory to .{output_format}. Are you sure?",
229
+ default=False
230
+ ).ask()
231
+
232
+ if not confirm:
233
+ console.print("[bold yellow]Batch conversion cancelled.[/bold yellow]")
234
+ return
235
+
236
+ media_files = get_media_files()
237
+
238
+ for file in media_files:
239
+ is_gif = Path(file).suffix.lower() == '.gif'
240
+ has_audio = has_audio_stream(file)
241
+
242
+ if is_gif and output_format in ["mp3", "flac", "wav", "ogg", "m4a", "aac"]:
243
+ console.print(f"[bold yellow]Skipping {file}: Cannot convert a GIF to an audio format.[/bold yellow]")
244
+ continue
245
+
246
+ if not has_audio and output_format in ["mp3", "flac", "wav", "ogg", "m4a", "aac"]:
247
+ console.print(f"[bold yellow]Skipping {file}: No audio stream found.[/bold yellow]")
248
+ continue
249
+
250
+ output_file = f"{Path(file).stem}_batch.{output_format}"
251
+ stream = ffmpeg.input(file)
252
+
253
+ if output_format in ["mp4", "webm", "avi", "wmv"]:
254
+ if quality == "Same as source (lossless if possible)":
255
+ stream = stream.output(output_file, c='copy')
256
+ else:
257
+ crf = quality.split(" ")[-1][1:-1]
258
+ audio_kwargs = {'c:a': 'aac', 'b:a': '192k'} if has_audio else {'an': None}
259
+ stream = stream.output(output_file, **{'c:v': 'libx264', 'crf': crf, 'pix_fmt': 'yuv420p'}, **audio_kwargs)
260
+ elif output_format in ["mp3", "m4a", "aac"]:
261
+ stream = stream.output(output_file, vn=None, acodec='libmp3lame', **{'b:a': '192k'})
262
+ elif output_format in ["flac", "wav", "ogg"]:
263
+ stream = stream.output(output_file, vn=None, acodec=output_format)
264
+ elif output_format == "gif":
265
+ fps = "15"
266
+ scale = "480"
267
+ palette_file = f"palette_{Path(file).stem}.png"
268
+ palette_stream = ffmpeg.input(file).filter('fps', fps=fps).filter('scale', size=f"{scale}:-1", flags='lanczos').output(palette_file, y=None)
269
+ run_command(palette_stream, f"Generating color palette for {file}...")
270
+ stream = ffmpeg.input(file).overlay(ffmpeg.input(palette_file).filter('paletteuse')).output(output_file, y=None)
271
+
272
+
273
+ if run_command(stream, f"Converting {file}...", show_progress=True):
274
+ console.print(f" -> Saved as {output_file}")
275
+ else:
276
+ console.print(f"[bold red]Failed to convert {file}.[/bold red]")
277
+
278
+ if output_format == "gif" and os.path.exists(f"palette_{Path(file).stem}.png"):
279
+ os.remove(f"palette_{Path(file).stem}.png")
280
+
281
+ console.print("\n[bold green]Batch conversion finished.[/bold green]")
282
+ questionary.press_any_key_to_continue().ask()
283
+
284
+ def crop_video(file_path):
285
+ """Visually crop a video by selecting an area."""
286
+ logging.info(f"Starting crop_video for {file_path}")
287
+ if not tk:
288
+ logging.error("Cannot perform cropping: tkinter/Pillow is not installed.")
289
+ console.print("[bold red]Cannot perform cropping: tkinter/Pillow is not installed.[/bold red]")
290
+ return
291
+
292
+ try:
293
+ info = ffmpeg.probe(file_path)
294
+ duration = float(info['streams'][0].get('duration', '0'))
295
+ mid_point = duration / 2
296
+
297
+ preview_frame = "preview.jpg"
298
+ stream = ffmpeg.input(file_path, ss=mid_point).output(preview_frame, vframes=1, qv=2, y=None)
299
+ run_command(stream, "Extracting a frame for preview...")
300
+
301
+ if not os.path.exists(preview_frame):
302
+ logging.error(f"Could not extract preview frame. File not found: {preview_frame}")
303
+ console.print("[bold red]Could not extract a frame from the video.[/bold red]")
304
+ return
305
+ logging.info(f"Successfully extracted preview frame to {preview_frame}")
306
+
307
+ root = tk.Tk()
308
+ root.title("Crop Video - Drag to select area, close window to confirm")
309
+ root.attributes("-topmost", True)
310
+
311
+ img = Image.open(preview_frame)
312
+ img_tk = ImageTk.PhotoImage(img)
313
+
314
+ canvas = tk.Canvas(root, width=img.width, height=img.height, cursor="cross")
315
+ canvas.pack()
316
+ canvas.create_image(0, 0, anchor=tk.NW, image=img_tk)
317
+
318
+ rect_coords = {"x1": 0, "y1": 0, "x2": 0, "y2": 0}
319
+ rect_id = None
320
+
321
+ def on_press(event):
322
+ nonlocal rect_id
323
+ rect_coords['x1'] = event.x
324
+ rect_coords['y1'] = event.y
325
+ rect_id = canvas.create_rectangle(0, 0, 1, 1, outline='red', width=2)
326
+
327
+ def on_drag(event):
328
+ nonlocal rect_id
329
+ rect_coords['x2'] = event.x
330
+ rect_coords['y2'] = event.y
331
+ canvas.coords(rect_id, rect_coords['x1'], rect_coords['y1'], rect_coords['x2'], rect_coords['y2'])
332
+
333
+ def on_release(event):
334
+ pass
335
+
336
+ canvas.bind("<ButtonPress-1>", on_press)
337
+ canvas.bind("<B1-Motion>", on_drag)
338
+ canvas.bind("<ButtonRelease-1>", on_release)
339
+
340
+ messagebox.showinfo("Instructions", "Click and drag on the image to draw a cropping rectangle.\nClose this window when you are satisfied with the selection.", parent=root)
341
+
342
+ root.mainloop()
343
+
344
+ os.remove(preview_frame)
345
+
346
+ x1, y1, x2, y2 = rect_coords['x1'], rect_coords['y1'], rect_coords['x2'], rect_coords['y2']
347
+
348
+ crop_x = min(x1, x2)
349
+ crop_y = min(y1, y2)
350
+ crop_w = abs(x2 - x1)
351
+ crop_h = abs(y2 - y1)
352
+
353
+ if crop_w == 0 or crop_h == 0:
354
+ console.print("[bold yellow]Cropping cancelled as no area was selected.[/bold yellow]")
355
+ return
356
+
357
+ console.print(f"Selected crop area: [bold]width={crop_w} height={crop_h} at (x={crop_x}, y={crop_y})[/bold]")
358
+
359
+ output_file = f"{Path(file_path).stem}_cropped{Path(file_path).suffix}"
360
+ stream = ffmpeg.input(file_path).filter('crop', crop_w, crop_h, crop_x, crop_y).output(output_file, **{'c:a': 'copy'})
361
+ run_command(stream, "Applying crop to video...", show_progress=True)
362
+ console.print(f"[bold green]Successfully cropped video and saved to {output_file}[/bold green]")
363
+ questionary.press_any_key_to_continue().ask()
364
+ except Exception as e:
365
+ logging.exception(f"An error occurred in crop_video for file: {file_path}")
366
+ console.print(f"[bold red]An error occurred during the crop operation. Check {log_file} for details.[/bold red]")
367
+ questionary.press_any_key_to_continue().ask()
368
+
369
+ def has_audio_stream(file_path):
370
+ """Check if the media file has an audio stream."""
371
+ try:
372
+ info = ffmpeg.probe(file_path)
373
+ return any(s for s in info.get('streams', []) if s.get('codec_type') == 'audio')
374
+ except ffmpeg.Error as e:
375
+ logging.error(f"Error checking for audio stream in {file_path}: {e.stderr.decode('utf-8')}")
376
+ return False
377
+
378
+ def convert_file(file_path):
379
+ """Convert the file to a different format."""
380
+ is_gif = Path(file_path).suffix.lower() == '.gif'
381
+ has_audio = has_audio_stream(file_path)
382
+
383
+ output_format = questionary.select(
384
+ "Select the output format:",
385
+ choices=["mp4", "mkv", "mov", "avi", "webm", "flv", "wmv", "mp3", "flac", "wav", "ogg", "m4a", "aac", "gif"],
386
+ use_indicator=True
387
+ ).ask()
388
+
389
+ if not output_format:
390
+ return
391
+
392
+ if is_gif and output_format in ["mp3", "flac", "wav", "ogg", "m4a", "aac"]:
393
+ console.print("[bold red]Error: Cannot convert a GIF (no audio) to an audio format.[/bold red]")
394
+ questionary.press_any_key_to_continue().ask()
395
+ return
396
+
397
+ if not has_audio and output_format in ["mp3", "flac", "wav", "ogg", "m4a", "aac"]:
398
+ console.print("[bold red]Error: The source file has no audio stream to convert.[/bold red]")
399
+ questionary.press_any_key_to_continue().ask()
400
+ return
401
+
402
+ output_file = f"{Path(file_path).stem}_converted.{output_format}"
403
+ stream = ffmpeg.input(file_path)
404
+
405
+ if output_format in ["mp4", "webm", "avi", "wmv"]:
406
+ quality = questionary.select(
407
+ "Select quality preset:",
408
+ choices=["Same as source (lossless if possible)", "High Quality (CRF 18)", "Medium Quality (CRF 23)", "Low Quality (CRF 28)"],
409
+ use_indicator=True
410
+ ).ask()
411
+
412
+ if not quality: return
413
+
414
+ if quality == "Same as source (lossless if possible)":
415
+ stream = stream.output(output_file, c='copy')
416
+ else:
417
+ crf = quality.split(" ")[-1][1:-1]
418
+ audio_kwargs = {'c:a': 'aac', 'b:a': '192k'} if has_audio else {'an': None}
419
+ stream = stream.output(output_file, **{'c:v': 'libx264', 'crf': crf, 'pix_fmt': 'yuv420p'}, **audio_kwargs)
420
+
421
+ elif output_format in ["mp3", "m4a", "aac"]:
422
+ bitrate = questionary.select("Select audio bitrate:", choices=["128k", "192k", "256k", "320k"]).ask()
423
+ if not bitrate: return
424
+ stream = stream.output(output_file, vn=None, acodec='libmp3lame', **{'b:a': bitrate})
425
+
426
+ elif output_format in ["flac", "wav", "ogg"]:
427
+ stream = stream.output(output_file, vn=None, acodec=output_format)
428
+
429
+ elif output_format == "gif":
430
+ fps = questionary.text("Enter frame rate (e.g., 15):", default="15").ask()
431
+ if not fps: return
432
+ scale = questionary.text("Enter width in pixels (e.g., 480):", default="480").ask()
433
+ if not scale: return
434
+
435
+ palette_file = "palette.png"
436
+ palette_stream = ffmpeg.input(file_path).filter('fps', fps=fps).filter('scale', size=f"{scale}:-1", flags='lanczos').output(palette_file, y=None)
437
+ run_command(palette_stream, "Generating color palette...")
438
+ stream = ffmpeg.input(file_path).overlay(ffmpeg.input(palette_file).filter('paletteuse')).output(output_file, y=None)
439
+
440
+ if run_command(stream, f"Converting to {output_format}...", show_progress=True):
441
+ console.print(f"[bold green]Successfully converted to {output_file}[/bold green]")
442
+ else:
443
+ console.print("[bold red]Conversion failed. Please check the logs for more details.[/bold red]")
444
+
445
+ if output_format == "gif" and os.path.exists("palette.png"):
446
+ os.remove("palette.png")
447
+
448
+ questionary.press_any_key_to_continue().ask()
449
+
450
+
451
+ def action_menu(file_path):
452
+ """Display the menu of actions for a selected file."""
453
+ while True:
454
+ console.rule(f"[bold]Actions for: {file_path}[/bold]")
455
+ action = questionary.select(
456
+ "Choose an action:",
457
+ choices=[
458
+ "Inspect File Details",
459
+ "Convert",
460
+ "Trim Video",
461
+ "Crop Video",
462
+ "Extract Audio",
463
+ "Remove Audio",
464
+ questionary.Separator(),
465
+ "Back to File List"
466
+ ],
467
+ use_indicator=True
468
+ ).ask()
469
+
470
+ if action is None or action == "Back to File List":
471
+ break
472
+
473
+ actions = {
474
+ "Inspect File Details": inspect_file,
475
+ "Convert": convert_file,
476
+ "Trim Video": trim_video,
477
+ "Crop Video": crop_video,
478
+ "Extract Audio": extract_audio,
479
+ "Remove Audio": remove_audio,
480
+ }
481
+ actions[action](file_path)
482
+
483
+ def main_menu():
484
+ """Display the main menu."""
485
+ while True:
486
+ console.rule("[bold magenta]ffmPEG-this[/bold magenta]")
487
+ choice = questionary.select(
488
+ "What would you like to do?",
489
+ choices=[
490
+ "Select a Media File to Process",
491
+ "Batch Convert All Videos to a Format",
492
+ "Exit"
493
+ ],
494
+ use_indicator=True
495
+ ).ask()
496
+
497
+ if choice is None or choice == "Exit":
498
+ console.print("[bold]Goodbye![/bold]")
499
+ break
500
+ elif choice == "Select a Media File to Process":
501
+ selected_file = select_media_file()
502
+ if selected_file:
503
+ action_menu(selected_file)
504
+ elif choice == "Batch Convert All Videos to a Format":
505
+ batch_convert()
506
+
507
+
508
+ def main():
509
+ try:
510
+ main_menu()
511
+ except KeyboardInterrupt:
512
+ logging.info("Operation cancelled by user.")
513
+ console.print("\n[bold]Operation cancelled by user. Goodbye![/bold]")
514
+ except Exception as e:
515
+ logging.exception("An unexpected error occurred.")
516
+ console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
517
+ console.print(f"Details have been logged to {log_file}")
518
+
519
+ if __name__ == "__main__":
520
+ main()
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: peg_this
3
+ Version: 3.0.0
4
+ Summary: A powerful tool for converting, manipulating, and inspecting media files using FFmpeg.
5
+ Author-email: Hariharen S S <thisishariharen@gmail.com>
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: ffmpeg-python
13
+ Requires-Dist: questionary
14
+ Requires-Dist: rich
15
+ Requires-Dist: Pillow
16
+ Dynamic: license-file
17
+
18
+ # 🎬 ffmPEG-this
19
+
20
+ A powerful and user-friendly batch script for converting, manipulating, and inspecting media files using the power of FFmpeg. This script provides a simple command-line menu to perform common audio and video tasks without needing to memorize complex FFmpeg commands.
21
+
22
+ ## ✨ Features
23
+
24
+ - **Action-Oriented Menu:** Select a file, then choose from a list of available actions.
25
+ - **File Inspection:** View detailed information about a media file, including resolution, duration, size, and stream details.
26
+ - **Lossless Conversion:** Change a file's container format (e.g., **MKV to MP4, , MP4 to GIF** etc.) without re-encoding, preserving the original quality.
27
+ - **Lossy Conversion:** Re-encode video to reduce file size, with simple quality presets.
28
+ - **Video Trimming:** Cut a video by specifying a start and end time.
29
+ - **Audio Extraction:** Extract the audio from a video file into formats like MP3, FLAC, or WAV.
30
+ - **Audio Removal:** Create a silent version of a video by removing its audio track.
31
+ - **Batch Conversion:** Convert all video files in the directory to a specific format in one go.
32
+
33
+ ## 🚀 Usage
34
+
35
+ There are three ways to use `peg_this`:
36
+
37
+ ### 1. Pip Install (Recommended)
38
+
39
+ This is the easiest way to get started. This will install the tool and all its dependencies, including `ffmpeg`.
40
+
41
+ ```bash
42
+ pip install peg_this
43
+ ```
44
+
45
+ Once installed, you can run the tool from your terminal:
46
+
47
+ ```bash
48
+ peg_this
49
+ ```
50
+
51
+ ### 2. Download from Release
52
+
53
+ If you don't want to install the package, you can download a pre-built executable from the [Releases](https://github.com/hariharen9/ffmpeg-this/releases/latest) page.
54
+
55
+ 1. Download the executable for your operating system (Windows, macOS, or Linux).
56
+ 2. Place the downloaded file in a directory with your media files.
57
+ 3. Run the executable directly from your terminal or command prompt.
58
+
59
+ ### 3. Run from Source
60
+
61
+ If you want to run the script directly from the source code:
62
+
63
+ 1. **Clone the repository:**
64
+ ```bash
65
+ git clone https://github.com/hariharen9/ffmpeg-this.git
66
+ cd ffmpeg-this
67
+ ```
68
+ 2. **Install dependencies:**
69
+ ```bash
70
+ pip install -r requirements.txt
71
+ ```
72
+ 3. **Run the script:**
73
+ ```bash
74
+ python src/peg_this/peg_this.py
75
+ ```
76
+
77
+ ## 📄 License
78
+
79
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,8 @@
1
+ peg_this/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ peg_this/peg_this.py,sha256=aDGeEMBWxfr_jt0Fn0xdwVa6KzS9bcBvXoDfNQ2B7Ao,21508
3
+ peg_this-3.0.0.dist-info/licenses/LICENSE,sha256=WL1MklYSco7KZvDjbf191tIKOxWQdekqda7dDJc6Wn8,1067
4
+ peg_this-3.0.0.dist-info/METADATA,sha256=OMOLRyxRv5zs7xtbMZferx0RjWVs448zQrPemgepgZY,2902
5
+ peg_this-3.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ peg_this-3.0.0.dist-info/entry_points.txt,sha256=9GVTFuE1w_wgY-Tz3--wI5j52BAKrt4atphVD8ioHhQ,52
7
+ peg_this-3.0.0.dist-info/top_level.txt,sha256=kSS5jZg3KN2kJqYZwMvQnI4gvlFxsUNzIm3QJsbKFdc,9
8
+ peg_this-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ peg_this = peg_this.peg_this:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 hariharen9
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ peg_this