peg-this 3.0.2__py3-none-any.whl → 3.0.6__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/features/__init__.py +0 -0
- peg_this/features/audio.py +38 -0
- peg_this/features/batch.py +125 -0
- peg_this/features/convert.py +93 -0
- peg_this/features/crop.py +106 -0
- peg_this/features/inspect.py +60 -0
- peg_this/features/join.py +83 -0
- peg_this/features/trim.py +26 -0
- peg_this/peg_this.py +13 -624
- peg_this/utils/__init__.py +0 -0
- peg_this/utils/ffmpeg_utils.py +134 -0
- peg_this/utils/ui_utils.py +43 -0
- {peg_this-3.0.2.dist-info → peg_this-3.0.6.dist-info}/METADATA +25 -2
- peg_this-3.0.6.dist-info/RECORD +19 -0
- peg_this-3.0.2.dist-info/RECORD +0 -8
- {peg_this-3.0.2.dist-info → peg_this-3.0.6.dist-info}/WHEEL +0 -0
- {peg_this-3.0.2.dist-info → peg_this-3.0.6.dist-info}/entry_points.txt +0 -0
- {peg_this-3.0.2.dist-info → peg_this-3.0.6.dist-info}/licenses/LICENSE +0 -0
- {peg_this-3.0.2.dist-info → peg_this-3.0.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
|
|
2
|
+
import subprocess
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import ffmpeg
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_ffmpeg_ffprobe():
|
|
14
|
+
"""
|
|
15
|
+
Checks if ffmpeg and ffprobe executables are available in the system's PATH.
|
|
16
|
+
ffmpeg-python requires this.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
# The library does this internally, but we can provide a clearer error message.
|
|
20
|
+
subprocess.check_call(['ffmpeg', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
21
|
+
subprocess.check_call(['ffprobe', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
22
|
+
except FileNotFoundError:
|
|
23
|
+
console.print("[bold red]Error: ffmpeg and ffprobe not found.[/bold red]")
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
console.print("You can install it using Chocolatey: [bold]choco install ffmpeg[/bold]")
|
|
26
|
+
console.print("Or Scoop: [bold]scoop install ffmpeg[/bold]")
|
|
27
|
+
elif sys.platform == "darwin":
|
|
28
|
+
console.print("You can install it using Homebrew: [bold]brew install ffmpeg[/bold]")
|
|
29
|
+
else:
|
|
30
|
+
console.print("You can install it using your system's package manager, e.g., [bold]sudo apt update && sudo apt install ffmpeg[/bold] on Debian/Ubuntu.")
|
|
31
|
+
console.print("Please ensure its location is in your system's PATH.")
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_command(stream_spec, description="Processing...", show_progress=False):
|
|
36
|
+
"""
|
|
37
|
+
Runs an ffmpeg command using ffmpeg-python.
|
|
38
|
+
- For simple commands, it runs directly.
|
|
39
|
+
- For commands with a progress bar, it generates the ffmpeg arguments,
|
|
40
|
+
runs them as a subprocess, and parses stderr to show progress,
|
|
41
|
+
mimicking the logic from the original script for accuracy.
|
|
42
|
+
"""
|
|
43
|
+
console.print(f"[bold cyan]{description}[/bold cyan]")
|
|
44
|
+
|
|
45
|
+
# Get the full command arguments from the ffmpeg-python stream object
|
|
46
|
+
args = stream_spec.get_args()
|
|
47
|
+
full_command = ['ffmpeg'] + args
|
|
48
|
+
logging.info(f"Executing command: {' '.join(full_command)}")
|
|
49
|
+
|
|
50
|
+
if not show_progress:
|
|
51
|
+
try:
|
|
52
|
+
# Use ffmpeg.run() for simple, non-progress tasks. It's cleaner.
|
|
53
|
+
out, err = ffmpeg.run(stream_spec, capture_stdout=True, capture_stderr=True, quiet=True)
|
|
54
|
+
logging.info("Command successful (no progress bar).")
|
|
55
|
+
return out.decode('utf-8')
|
|
56
|
+
except ffmpeg.Error as e:
|
|
57
|
+
error_message = e.stderr.decode('utf-8')
|
|
58
|
+
console.print("[bold red]An error occurred:[/bold red]")
|
|
59
|
+
console.print(error_message)
|
|
60
|
+
logging.error(f"ffmpeg error:{error_message}")
|
|
61
|
+
return None
|
|
62
|
+
else:
|
|
63
|
+
# For the progress bar, we must run ffmpeg as a subprocess and parse stderr.
|
|
64
|
+
duration = 0
|
|
65
|
+
try:
|
|
66
|
+
# Find the primary input file from the command arguments to probe it.
|
|
67
|
+
input_file_path = None
|
|
68
|
+
for i, arg in enumerate(full_command):
|
|
69
|
+
if arg == '-i' and i + 1 < len(full_command):
|
|
70
|
+
# This is a robust way to find the first input file.
|
|
71
|
+
input_file_path = full_command[i+1]
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
if input_file_path:
|
|
75
|
+
probe_info = ffmpeg.probe(input_file_path)
|
|
76
|
+
duration = float(probe_info['format']['duration'])
|
|
77
|
+
else:
|
|
78
|
+
logging.warning("Could not find input file in command to determine duration for progress bar.")
|
|
79
|
+
|
|
80
|
+
except (ffmpeg.Error, KeyError) as e:
|
|
81
|
+
console.print(f"[bold yellow]Warning: Could not determine video duration for progress bar.[/bold yellow]")
|
|
82
|
+
logging.warning(f"Could not probe for duration: {e}")
|
|
83
|
+
|
|
84
|
+
with Progress(
|
|
85
|
+
SpinnerColumn(),
|
|
86
|
+
TextColumn("[progress.description]{task.description}"),
|
|
87
|
+
BarColumn(),
|
|
88
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
89
|
+
console=console,
|
|
90
|
+
) as progress:
|
|
91
|
+
task = progress.add_task(description, total=100)
|
|
92
|
+
|
|
93
|
+
# Run the command as a subprocess to capture stderr in real-time
|
|
94
|
+
process = subprocess.Popen(
|
|
95
|
+
full_command,
|
|
96
|
+
stdout=subprocess.PIPE,
|
|
97
|
+
stderr=subprocess.PIPE,
|
|
98
|
+
universal_newlines=True,
|
|
99
|
+
encoding='utf-8'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
for line in process.stderr:
|
|
103
|
+
logging.debug(f"ffmpeg stderr: {line.strip()}")
|
|
104
|
+
if "time=" in line and duration > 0:
|
|
105
|
+
try:
|
|
106
|
+
time_str = line.split("time=")[1].split(" ")[0].strip()
|
|
107
|
+
h, m, s_parts = time_str.split(':')
|
|
108
|
+
s = float(s_parts)
|
|
109
|
+
elapsed_time = int(h) * 3600 + int(m) * 60 + s
|
|
110
|
+
percent_complete = (elapsed_time / duration) * 100
|
|
111
|
+
progress.update(task, completed=min(percent_complete, 100))
|
|
112
|
+
except Exception:
|
|
113
|
+
pass # Ignore any parsing errors
|
|
114
|
+
|
|
115
|
+
process.wait()
|
|
116
|
+
progress.update(task, completed=100)
|
|
117
|
+
|
|
118
|
+
if process.returncode != 0:
|
|
119
|
+
# The error was already logged line-by-line, but we can add a final message.
|
|
120
|
+
log_file = logging.getLogger().handlers[0].baseFilename
|
|
121
|
+
console.print(f"[bold red]An error occurred during processing. Check {log_file} for details.[/bold red]")
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
logging.info("Command successful (with progress bar).")
|
|
125
|
+
return "Success"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def has_audio_stream(file_path):
|
|
129
|
+
"""Check if the media file has an audio stream."""
|
|
130
|
+
try:
|
|
131
|
+
probe = ffmpeg.probe(file_path, select_streams='a')
|
|
132
|
+
return 'streams' in probe and len(probe['streams']) > 0
|
|
133
|
+
except ffmpeg.Error:
|
|
134
|
+
return False
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import questionary
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import tkinter as tk
|
|
10
|
+
from tkinter import filedialog
|
|
11
|
+
except ImportError:
|
|
12
|
+
tk = None
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_media_files():
|
|
18
|
+
"""Scan the current directory for media files."""
|
|
19
|
+
media_extensions = [".mkv", ".mp4", ".avi", ".mov", ".webm", ".flv", ".wmv", ".mp3", ".flac", ".wav", ".ogg", ".gif"]
|
|
20
|
+
files = [f for f in os.listdir('.') if os.path.isfile(f) and Path(f).suffix.lower() in media_extensions]
|
|
21
|
+
return files
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def select_media_file():
|
|
25
|
+
"""Display a menu to select a media file, or open a file picker."""
|
|
26
|
+
media_files = get_media_files()
|
|
27
|
+
if not media_files:
|
|
28
|
+
console.print("[bold yellow]No media files found in this directory.[/bold yellow]")
|
|
29
|
+
if tk and questionary.confirm("Would you like to select a file from another location?").ask():
|
|
30
|
+
root = tk.Tk()
|
|
31
|
+
root.withdraw()
|
|
32
|
+
file_path = filedialog.askopenfilename(
|
|
33
|
+
title="Select a media file",
|
|
34
|
+
filetypes=[("Media Files", "*.mkv *.mp4 *.avi *.mov *.webm *.flv *.wmv *.mp3 *.flac *.wav *.ogg *.gif"), ("All Files", "*.*")]
|
|
35
|
+
)
|
|
36
|
+
return file_path if file_path else None
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
choices = media_files + [questionary.Separator(), "Back"]
|
|
40
|
+
file = questionary.select("Select a media file to process:", choices=choices, use_indicator=True).ask()
|
|
41
|
+
|
|
42
|
+
# Return the absolute path to prevent "file not found" errors
|
|
43
|
+
return os.path.abspath(file) if file and file != "Back" else None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: peg_this
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.6
|
|
4
4
|
Summary: A powerful, intuitive command-line video editor suite, built on FFmpeg.
|
|
5
5
|
Author-email: Hariharen S S <thisishariharen@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/hariharen9/ffmpeg-this
|
|
@@ -26,6 +26,10 @@ Dynamic: license-file
|
|
|
26
26
|
|
|
27
27
|
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.
|
|
28
28
|
|
|
29
|
+
|
|
30
|
+
<img src="/assets/peg.gif" width="720">
|
|
31
|
+
|
|
32
|
+
|
|
29
33
|
## ✨ Features
|
|
30
34
|
|
|
31
35
|
- **Inspect Media Properties**: View detailed information about video and audio streams, including codecs, resolution, frame rate, bitrates, and more.
|
|
@@ -39,8 +43,27 @@ A powerful and user-friendly batch script for converting, manipulating, and insp
|
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
## 🚀 Usage
|
|
46
|
+
### Prerequisite: Install FFmpeg
|
|
47
|
+
|
|
48
|
+
> `peg_this` uses a library called `ffmpeg-python` which acts as a controller for the main FFmpeg program. It does not include FFmpeg itself. Therefore, you must have FFmpeg installed on your system and available in your terminal's PATH.
|
|
49
|
+
|
|
50
|
+
For **macOS** users, the easiest way to install it is with [Homebrew](https://brew.sh/):
|
|
51
|
+
```bash
|
|
52
|
+
brew install ffmpeg
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For **Windows** users, you can use a package manager like [Chocolatey](https://chocolatey.org/) or [Scoop](https://scoop.sh/):
|
|
56
|
+
```bash
|
|
57
|
+
# Using Chocolatey
|
|
58
|
+
choco install ffmpeg
|
|
59
|
+
|
|
60
|
+
# Using Scoop
|
|
61
|
+
scoop install ffmpeg
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For other systems, please see the official download page: **[ffmpeg.org/download.html](https://ffmpeg.org/download.html)**
|
|
42
65
|
|
|
43
|
-
There are three ways to use `peg_this`:
|
|
66
|
+
There are ***three*** ways to use `peg_this`:
|
|
44
67
|
|
|
45
68
|
### 1. Pip Install (Recommended)
|
|
46
69
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
peg_this/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
peg_this/peg_this.py,sha256=BqJutllqMHqE312WgPdnd3r9-msKKOfZp9ykV0ejUm8,3739
|
|
3
|
+
peg_this/features/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
peg_this/features/audio.py,sha256=O0_lRjzolualxWVXiSiEke5vOpPGwkz0FbMQy5brO58,1533
|
|
5
|
+
peg_this/features/batch.py,sha256=72yXjNfvg-SCxjtaacFzFudmZ8Yd7_rLpvJzbwB8UdA,5178
|
|
6
|
+
peg_this/features/convert.py,sha256=KLtmN6PGhqIEmY3j40Ur9xqC-rZREu8UBM9Ura1-20o,3865
|
|
7
|
+
peg_this/features/crop.py,sha256=bzlzfFM41OFX_3KEMNOAJu9XvM-6Mmf_AApJ1rN8oaU,3986
|
|
8
|
+
peg_this/features/inspect.py,sha256=xtlmedpsMYQ-vLsnDNzUF59TiZDFLGMyL_wtd8PgiHg,2535
|
|
9
|
+
peg_this/features/join.py,sha256=w_HUsuBv0g9nlOjVvdkeT9dDwxOEvdyvsYCwNocwl98,3414
|
|
10
|
+
peg_this/features/trim.py,sha256=2ZWSNpl4DzSCPt6q7FZloQc-Rga5cX8IW69MfzAZysE,863
|
|
11
|
+
peg_this/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
peg_this/utils/ffmpeg_utils.py,sha256=cxm5SyZYHw99dqaIiNZ9qhdltyvMwecs1FYHXjChwR8,5883
|
|
13
|
+
peg_this/utils/ui_utils.py,sha256=cntxp8m7en_irFNEK5TcrPotYVZr-hzF_QdcXUIBA2Y,1584
|
|
14
|
+
peg_this-3.0.6.dist-info/licenses/LICENSE,sha256=WL1MklYSco7KZvDjbf191tIKOxWQdekqda7dDJc6Wn8,1067
|
|
15
|
+
peg_this-3.0.6.dist-info/METADATA,sha256=_bWnDbXC0vGoPDImzXVd05AyITf8XZIuvJK-uK7K_3c,4310
|
|
16
|
+
peg_this-3.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
peg_this-3.0.6.dist-info/entry_points.txt,sha256=9GVTFuE1w_wgY-Tz3--wI5j52BAKrt4atphVD8ioHhQ,52
|
|
18
|
+
peg_this-3.0.6.dist-info/top_level.txt,sha256=kSS5jZg3KN2kJqYZwMvQnI4gvlFxsUNzIm3QJsbKFdc,9
|
|
19
|
+
peg_this-3.0.6.dist-info/RECORD,,
|
peg_this-3.0.2.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
peg_this/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
peg_this/peg_this.py,sha256=JSxUYO5zjg0URu6SG3I1AWio_yQ8t8wKW9yqP-pRbUY,30228
|
|
3
|
-
peg_this-3.0.2.dist-info/licenses/LICENSE,sha256=WL1MklYSco7KZvDjbf191tIKOxWQdekqda7dDJc6Wn8,1067
|
|
4
|
-
peg_this-3.0.2.dist-info/METADATA,sha256=suW_MI8jSHxZXEKKOsrry7QnXoHyVZ0s6ZFljc-RKy8,3527
|
|
5
|
-
peg_this-3.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
peg_this-3.0.2.dist-info/entry_points.txt,sha256=9GVTFuE1w_wgY-Tz3--wI5j52BAKrt4atphVD8ioHhQ,52
|
|
7
|
-
peg_this-3.0.2.dist-info/top_level.txt,sha256=kSS5jZg3KN2kJqYZwMvQnI4gvlFxsUNzIm3QJsbKFdc,9
|
|
8
|
-
peg_this-3.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|