peg-this 3.0.6__tar.gz → 4.0.0__tar.gz
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-3.0.6/src/peg_this.egg-info → peg_this-4.0.0}/PKG-INFO +66 -12
- {peg_this-3.0.6 → peg_this-4.0.0}/README.md +60 -11
- {peg_this-3.0.6 → peg_this-4.0.0}/pyproject.toml +7 -2
- peg_this-4.0.0/src/peg_this/features/convert.py +225 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/crop.py +72 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/peg_this.py +42 -3
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/utils/ffmpeg_utils.py +8 -13
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/utils/ui_utils.py +11 -2
- {peg_this-3.0.6 → peg_this-4.0.0/src/peg_this.egg-info}/PKG-INFO +66 -12
- peg_this-3.0.6/src/peg_this/features/convert.py +0 -93
- {peg_this-3.0.6 → peg_this-4.0.0}/LICENSE +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/setup.cfg +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/__init__.py +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/__init__.py +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/audio.py +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/batch.py +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/inspect.py +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/join.py +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/trim.py +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/utils/__init__.py +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/SOURCES.txt +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/dependency_links.txt +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/entry_points.txt +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/requires.txt +0 -0
- {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: peg_this
|
|
3
|
-
Version:
|
|
4
|
-
Summary: A powerful
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: A powerful and intuitive command-line video editor, built on FFmpeg.
|
|
5
5
|
Author-email: Hariharen S S <thisishariharen@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/hariharen9/ffmpeg-this
|
|
7
|
+
Project-URL: Documentation, https://github.com/hariharen9/ffmpeg-this/blob/main/README.md
|
|
8
|
+
Project-URL: Funding, https://www.buymeacoffee.com/hariharen
|
|
9
|
+
Project-URL: Say Thanks!, https://saythanks.io/to/thisishariharen
|
|
10
|
+
Project-URL: Social, https://twitter.com/thisishariharen
|
|
7
11
|
Project-URL: Bug Tracker, https://github.com/hariharen9/ffmpeg-this/issues
|
|
8
12
|
Project-URL: Releases, https://github.com/hariharen9/ffmpeg-this/releases
|
|
13
|
+
Project-URL: Sponsor, https://github.com/sponsors/hariharen9
|
|
9
14
|
Classifier: Programming Language :: Python :: 3
|
|
10
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
16
|
Classifier: Operating System :: OS Independent
|
|
@@ -22,12 +27,29 @@ Dynamic: license-file
|
|
|
22
27
|
|
|
23
28
|
# 🎬 ffmPEG-this
|
|
24
29
|
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://pypi.org/project/peg-this/">
|
|
32
|
+
<img src="https://img.shields.io/pypi/v/peg_this?color=blue&label=version" alt="PyPI Version">
|
|
33
|
+
</a>
|
|
34
|
+
<a href="https://pypi.org/project/peg-this/">
|
|
35
|
+
<img src="https://img.shields.io/pypi/pyversions/peg_this.svg" alt="PyPI Python Versions">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://github.com/hariharen9/ffmpeg-this/blob/main/LICENSE">
|
|
38
|
+
<img src="https://img.shields.io/github/license/hariharen9/ffmpeg-this" alt="License">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://pepy.tech/project/peg-this">
|
|
41
|
+
<img src="https://static.pepy.tech/badge/peg-this" alt="Downloads">
|
|
42
|
+
</a>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
25
45
|
> Your Video editor within CLI 🚀
|
|
26
46
|
|
|
27
47
|
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
48
|
|
|
29
49
|
|
|
30
|
-
<
|
|
50
|
+
<p align="center">
|
|
51
|
+
<img src="/assets/peg.gif" width="720">
|
|
52
|
+
</p>
|
|
31
53
|
|
|
32
54
|
|
|
33
55
|
## ✨ Features
|
|
@@ -40,11 +62,13 @@ A powerful and user-friendly batch script for converting, manipulating, and insp
|
|
|
40
62
|
- **Extract Audio**: Rip the audio track from any video file into MP3, FLAC, or WAV.
|
|
41
63
|
- **Remove Audio**: Create a silent version of your video by stripping out all audio streams.
|
|
42
64
|
- **Batch Conversion**: Convert all media files in the current directory to a specified format in one go.
|
|
65
|
+
- **CLI Interface**: A user-friendly command-line interface that makes it easy to perform common tasks and navigate the tool's features.
|
|
43
66
|
|
|
44
67
|
|
|
45
68
|
## 🚀 Usage
|
|
46
69
|
### Prerequisite: Install FFmpeg
|
|
47
70
|
|
|
71
|
+
> [NOTE]
|
|
48
72
|
> `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
73
|
|
|
50
74
|
For **macOS** users, the easiest way to install it is with [Homebrew](https://brew.sh/):
|
|
@@ -63,11 +87,10 @@ scoop install ffmpeg
|
|
|
63
87
|
|
|
64
88
|
For other systems, please see the official download page: **[ffmpeg.org/download.html](https://ffmpeg.org/download.html)**
|
|
65
89
|
|
|
66
|
-
There are
|
|
90
|
+
There are three ways to use `peg_this`:
|
|
67
91
|
|
|
68
92
|
### 1. Pip Install (Recommended)
|
|
69
|
-
|
|
70
|
-
This is the easiest way to get started. This will install the tool and all its dependencies, including `ffmpeg`.
|
|
93
|
+
This is the easiest way to get started. This will install the tool and all its dependencies.
|
|
71
94
|
|
|
72
95
|
```bash
|
|
73
96
|
pip install peg_this
|
|
@@ -80,15 +103,13 @@ peg_this
|
|
|
80
103
|
```
|
|
81
104
|
|
|
82
105
|
### 2. Download from Release
|
|
83
|
-
|
|
84
|
-
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.
|
|
106
|
+
If you prefer not to install the package, you can download a pre-built executable from the [Releases](https://github.com/hariharen9/ffmpeg-this/releases/latest) page.
|
|
85
107
|
|
|
86
108
|
1. Download the executable for your operating system (Windows, macOS, or Linux).
|
|
87
|
-
2. Place
|
|
88
|
-
3. Run the executable directly from your terminal
|
|
109
|
+
2. Place it in a directory with your media files.
|
|
110
|
+
3. Run the executable directly from your terminal.
|
|
89
111
|
|
|
90
112
|
### 3. Run from Source
|
|
91
|
-
|
|
92
113
|
If you want to run the script directly from the source code:
|
|
93
114
|
|
|
94
115
|
1. **Clone the repository:**
|
|
@@ -102,9 +123,42 @@ If you want to run the script directly from the source code:
|
|
|
102
123
|
```
|
|
103
124
|
3. **Run the script:**
|
|
104
125
|
```bash
|
|
105
|
-
python src
|
|
126
|
+
python -m src.peg_this.peg_this
|
|
106
127
|
```
|
|
107
128
|
|
|
129
|
+
## 📈 Star History
|
|
130
|
+
|
|
131
|
+
<p align="center">
|
|
132
|
+
<a href="https://star-history.com/#hariharen9/ffmpeg-this&Date">
|
|
133
|
+
<img src="https://api.star-history.com/svg?repos=hariharen9/ffmpeg-this&type=Date" alt="Star History Chart">
|
|
134
|
+
</a>
|
|
135
|
+
</p>
|
|
136
|
+
|
|
137
|
+
## ✨ Sponsor
|
|
138
|
+
|
|
139
|
+
<p align="center">
|
|
140
|
+
<a href="https://github.com/sponsors/hariharen9">
|
|
141
|
+
<img src="https://img.shields.io/github/sponsors/hariharen9?style=for-the-badge&logo=github&color=white" alt="GitHub Sponsors">
|
|
142
|
+
</a>
|
|
143
|
+
<a href="https://www.buymeacoffee.com/hariharen">
|
|
144
|
+
<img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me a Coffee">
|
|
145
|
+
</a>
|
|
146
|
+
</p>
|
|
147
|
+
|
|
148
|
+
## 👥 Contributors
|
|
149
|
+
|
|
150
|
+
<a href="https://github.com/hariharen9/ffmpeg-this/graphs/contributors">
|
|
151
|
+
<img src="https://contrib.rocks/image?repo=hariharen9/ffmpeg-this" />
|
|
152
|
+
</a>
|
|
153
|
+
|
|
154
|
+
## 🤝 Contributing
|
|
155
|
+
|
|
156
|
+
Contributions are welcome! Please see the [Contributing Guidelines](CONTRIBUTING.md) for more information.
|
|
157
|
+
|
|
108
158
|
## 📄 License
|
|
109
159
|
|
|
110
160
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
161
|
+
|
|
162
|
+
<p align="center">
|
|
163
|
+
<h2>Made with ❤️ by <a href="https://hariharen.site">Hariharen</a></h2>
|
|
164
|
+
</p>
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
# 🎬 ffmPEG-this
|
|
2
2
|
|
|
3
|
+
<p align="center">
|
|
4
|
+
<a href="https://pypi.org/project/peg-this/">
|
|
5
|
+
<img src="https://img.shields.io/pypi/v/peg_this?color=blue&label=version" alt="PyPI Version">
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://pypi.org/project/peg-this/">
|
|
8
|
+
<img src="https://img.shields.io/pypi/pyversions/peg_this.svg" alt="PyPI Python Versions">
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://github.com/hariharen9/ffmpeg-this/blob/main/LICENSE">
|
|
11
|
+
<img src="https://img.shields.io/github/license/hariharen9/ffmpeg-this" alt="License">
|
|
12
|
+
</a>
|
|
13
|
+
<a href="https://pepy.tech/project/peg-this">
|
|
14
|
+
<img src="https://static.pepy.tech/badge/peg-this" alt="Downloads">
|
|
15
|
+
</a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
3
18
|
> Your Video editor within CLI 🚀
|
|
4
19
|
|
|
5
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.
|
|
6
21
|
|
|
7
22
|
|
|
8
|
-
<
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="/assets/peg.gif" width="720">
|
|
25
|
+
</p>
|
|
9
26
|
|
|
10
27
|
|
|
11
28
|
## ✨ Features
|
|
@@ -18,11 +35,13 @@ A powerful and user-friendly batch script for converting, manipulating, and insp
|
|
|
18
35
|
- **Extract Audio**: Rip the audio track from any video file into MP3, FLAC, or WAV.
|
|
19
36
|
- **Remove Audio**: Create a silent version of your video by stripping out all audio streams.
|
|
20
37
|
- **Batch Conversion**: Convert all media files in the current directory to a specified format in one go.
|
|
38
|
+
- **CLI Interface**: A user-friendly command-line interface that makes it easy to perform common tasks and navigate the tool's features.
|
|
21
39
|
|
|
22
40
|
|
|
23
41
|
## 🚀 Usage
|
|
24
42
|
### Prerequisite: Install FFmpeg
|
|
25
43
|
|
|
44
|
+
> [NOTE]
|
|
26
45
|
> `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.
|
|
27
46
|
|
|
28
47
|
For **macOS** users, the easiest way to install it is with [Homebrew](https://brew.sh/):
|
|
@@ -41,11 +60,10 @@ scoop install ffmpeg
|
|
|
41
60
|
|
|
42
61
|
For other systems, please see the official download page: **[ffmpeg.org/download.html](https://ffmpeg.org/download.html)**
|
|
43
62
|
|
|
44
|
-
There are
|
|
63
|
+
There are three ways to use `peg_this`:
|
|
45
64
|
|
|
46
65
|
### 1. Pip Install (Recommended)
|
|
47
|
-
|
|
48
|
-
This is the easiest way to get started. This will install the tool and all its dependencies, including `ffmpeg`.
|
|
66
|
+
This is the easiest way to get started. This will install the tool and all its dependencies.
|
|
49
67
|
|
|
50
68
|
```bash
|
|
51
69
|
pip install peg_this
|
|
@@ -58,15 +76,13 @@ peg_this
|
|
|
58
76
|
```
|
|
59
77
|
|
|
60
78
|
### 2. Download from Release
|
|
61
|
-
|
|
62
|
-
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.
|
|
79
|
+
If you prefer not to install the package, you can download a pre-built executable from the [Releases](https://github.com/hariharen9/ffmpeg-this/releases/latest) page.
|
|
63
80
|
|
|
64
81
|
1. Download the executable for your operating system (Windows, macOS, or Linux).
|
|
65
|
-
2. Place
|
|
66
|
-
3. Run the executable directly from your terminal
|
|
82
|
+
2. Place it in a directory with your media files.
|
|
83
|
+
3. Run the executable directly from your terminal.
|
|
67
84
|
|
|
68
85
|
### 3. Run from Source
|
|
69
|
-
|
|
70
86
|
If you want to run the script directly from the source code:
|
|
71
87
|
|
|
72
88
|
1. **Clone the repository:**
|
|
@@ -80,9 +96,42 @@ If you want to run the script directly from the source code:
|
|
|
80
96
|
```
|
|
81
97
|
3. **Run the script:**
|
|
82
98
|
```bash
|
|
83
|
-
python src
|
|
99
|
+
python -m src.peg_this.peg_this
|
|
84
100
|
```
|
|
85
101
|
|
|
102
|
+
## 📈 Star History
|
|
103
|
+
|
|
104
|
+
<p align="center">
|
|
105
|
+
<a href="https://star-history.com/#hariharen9/ffmpeg-this&Date">
|
|
106
|
+
<img src="https://api.star-history.com/svg?repos=hariharen9/ffmpeg-this&type=Date" alt="Star History Chart">
|
|
107
|
+
</a>
|
|
108
|
+
</p>
|
|
109
|
+
|
|
110
|
+
## ✨ Sponsor
|
|
111
|
+
|
|
112
|
+
<p align="center">
|
|
113
|
+
<a href="https://github.com/sponsors/hariharen9">
|
|
114
|
+
<img src="https://img.shields.io/github/sponsors/hariharen9?style=for-the-badge&logo=github&color=white" alt="GitHub Sponsors">
|
|
115
|
+
</a>
|
|
116
|
+
<a href="https://www.buymeacoffee.com/hariharen">
|
|
117
|
+
<img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me a Coffee">
|
|
118
|
+
</a>
|
|
119
|
+
</p>
|
|
120
|
+
|
|
121
|
+
## 👥 Contributors
|
|
122
|
+
|
|
123
|
+
<a href="https://github.com/hariharen9/ffmpeg-this/graphs/contributors">
|
|
124
|
+
<img src="https://contrib.rocks/image?repo=hariharen9/ffmpeg-this" />
|
|
125
|
+
</a>
|
|
126
|
+
|
|
127
|
+
## 🤝 Contributing
|
|
128
|
+
|
|
129
|
+
Contributions are welcome! Please see the [Contributing Guidelines](CONTRIBUTING.md) for more information.
|
|
130
|
+
|
|
86
131
|
## 📄 License
|
|
87
132
|
|
|
88
|
-
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
133
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
134
|
+
|
|
135
|
+
<p align="center">
|
|
136
|
+
<h2>Made with ❤️ by <a href="https://hariharen.site">Hariharen</a></h2>
|
|
137
|
+
</p>
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "peg_this"
|
|
7
|
-
version = "
|
|
7
|
+
version = "4.0.0"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Hariharen S S", email="thisishariharen@gmail.com" },
|
|
10
10
|
]
|
|
11
|
-
description = "A powerful
|
|
11
|
+
description = "A powerful and intuitive command-line video editor, built on FFmpeg."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.8"
|
|
14
14
|
classifiers = [
|
|
@@ -27,8 +27,13 @@ dependencies = [
|
|
|
27
27
|
|
|
28
28
|
[project.urls]
|
|
29
29
|
"Homepage" = "https://github.com/hariharen9/ffmpeg-this"
|
|
30
|
+
"Documentation" = "https://github.com/hariharen9/ffmpeg-this/blob/main/README.md"
|
|
31
|
+
"Funding" = "https://www.buymeacoffee.com/hariharen"
|
|
32
|
+
"Say Thanks!" = "https://saythanks.io/to/thisishariharen"
|
|
33
|
+
"Social" = "https://twitter.com/thisishariharen"
|
|
30
34
|
"Bug Tracker" = "https://github.com/hariharen9/ffmpeg-this/issues"
|
|
31
35
|
"Releases" = "https://github.com/hariharen9/ffmpeg-this/releases"
|
|
36
|
+
"Sponsor" = "https://github.com/sponsors/hariharen9"
|
|
32
37
|
|
|
33
38
|
|
|
34
39
|
[project.scripts]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
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
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def convert_file(file_path):
|
|
15
|
+
"""Convert the file to a different format."""
|
|
16
|
+
is_gif = Path(file_path).suffix.lower() == '.gif'
|
|
17
|
+
has_audio = has_audio_stream(file_path)
|
|
18
|
+
|
|
19
|
+
output_format = questionary.select("Select the output format:", choices=["mp4", "mkv", "mov", "avi", "webm", "mp3", "flac", "wav", "gif"], use_indicator=True).ask()
|
|
20
|
+
if not output_format: return
|
|
21
|
+
|
|
22
|
+
if (is_gif or not has_audio) and output_format in ["mp3", "flac", "wav"]:
|
|
23
|
+
console.print("[bold red]Error: Source has no audio to convert.[/bold red]")
|
|
24
|
+
questionary.press_any_key_to_continue().ask()
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
output_file = f"{Path(file_path).stem}_converted.{output_format}"
|
|
28
|
+
|
|
29
|
+
input_stream = ffmpeg.input(file_path)
|
|
30
|
+
output_stream = None
|
|
31
|
+
kwargs = {'y': None}
|
|
32
|
+
|
|
33
|
+
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
|
|
34
|
+
quality = questionary.select("Select quality preset:", choices=["Same as source", "High (CRF 18)", "Medium (CRF 23)", "Low (CRF 28)"], use_indicator=True).ask()
|
|
35
|
+
if not quality: return
|
|
36
|
+
|
|
37
|
+
if quality == "Same as source":
|
|
38
|
+
kwargs['c'] = 'copy'
|
|
39
|
+
else:
|
|
40
|
+
crf = quality.split(" ")[-1][1:-1]
|
|
41
|
+
kwargs['c:v'] = 'libx264'
|
|
42
|
+
kwargs['crf'] = crf
|
|
43
|
+
kwargs['pix_fmt'] = 'yuv420p'
|
|
44
|
+
if has_audio:
|
|
45
|
+
kwargs['c:a'] = 'aac'
|
|
46
|
+
kwargs['b:a'] = '192k'
|
|
47
|
+
else:
|
|
48
|
+
kwargs['an'] = None
|
|
49
|
+
output_stream = input_stream.output(output_file, **kwargs)
|
|
50
|
+
|
|
51
|
+
elif output_format in ["mp3", "flac", "wav"]:
|
|
52
|
+
kwargs['vn'] = None
|
|
53
|
+
if output_format == 'mp3':
|
|
54
|
+
bitrate = questionary.select("Select audio bitrate:", choices=["128k", "192k", "256k", "320k"]).ask()
|
|
55
|
+
if not bitrate: return
|
|
56
|
+
kwargs['c:a'] = 'libmp3lame'
|
|
57
|
+
kwargs['b:a'] = bitrate
|
|
58
|
+
else:
|
|
59
|
+
kwargs['c:a'] = output_format
|
|
60
|
+
output_stream = input_stream.output(output_file, **kwargs)
|
|
61
|
+
|
|
62
|
+
elif output_format == "gif":
|
|
63
|
+
fps = questionary.text("Enter frame rate (e.g., 15):", default="15").ask()
|
|
64
|
+
if not fps: return
|
|
65
|
+
scale = questionary.text("Enter width in pixels (e.g., 480):", default="480").ask()
|
|
66
|
+
if not scale: return
|
|
67
|
+
|
|
68
|
+
palette_file = f"palette_{Path(file_path).stem}.png"
|
|
69
|
+
|
|
70
|
+
# Correctly chain filters for palette generation using explicit w/h arguments
|
|
71
|
+
palette_gen_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos').filter('palettegen')
|
|
72
|
+
run_command(palette_gen_stream.output(palette_file, y=None), "Generating color palette...")
|
|
73
|
+
|
|
74
|
+
if not os.path.exists(palette_file):
|
|
75
|
+
console.print("[bold red]Failed to generate color palette for GIF.[/bold red]")
|
|
76
|
+
questionary.press_any_key_to_continue().ask()
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
palette_input = ffmpeg.input(palette_file)
|
|
80
|
+
video_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos')
|
|
81
|
+
|
|
82
|
+
final_stream = ffmpeg.filter([video_stream, palette_input], 'paletteuse')
|
|
83
|
+
output_stream = final_stream.output(output_file, y=None)
|
|
84
|
+
|
|
85
|
+
if output_stream and run_command(output_stream, f"Converting to {output_format}...", show_progress=True):
|
|
86
|
+
console.print(f"[bold green]Successfully converted to {output_file}[/bold green]")
|
|
87
|
+
else:
|
|
88
|
+
console.print("[bold red]Conversion failed.[/bold red]")
|
|
89
|
+
|
|
90
|
+
if output_format == "gif" and os.path.exists(f"palette_{Path(file_path).stem}.png"):
|
|
91
|
+
os.remove(f"palette_{Path(file_path).stem}.png")
|
|
92
|
+
|
|
93
|
+
questionary.press_any_key_to_continue().ask()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def convert_image(file_path):
|
|
97
|
+
"""Convert an image to a different format."""
|
|
98
|
+
output_format = questionary.select(
|
|
99
|
+
"Select the output format:",
|
|
100
|
+
choices=["jpg", "png", "webp", "bmp", "tiff"],
|
|
101
|
+
use_indicator=True
|
|
102
|
+
).ask()
|
|
103
|
+
if not output_format: return
|
|
104
|
+
|
|
105
|
+
output_file = f"{Path(file_path).stem}_converted.{output_format}"
|
|
106
|
+
kwargs = {'y': None}
|
|
107
|
+
|
|
108
|
+
# For JPG and WEBP, allow quality selection
|
|
109
|
+
if output_format in ['jpg', 'webp']:
|
|
110
|
+
quality_preset = questionary.select(
|
|
111
|
+
"Select quality preset:",
|
|
112
|
+
choices=["High (95%)", "Medium (80%)", "Low (60%)"],
|
|
113
|
+
use_indicator=True
|
|
114
|
+
).ask()
|
|
115
|
+
if not quality_preset: return
|
|
116
|
+
|
|
117
|
+
quality_map = {"High (95%)": "95", "Medium (80%)": "80", "Low (60%)": "60"}
|
|
118
|
+
quality = quality_map[quality_preset]
|
|
119
|
+
|
|
120
|
+
if output_format == 'jpg':
|
|
121
|
+
q_scale = int(31 - (int(quality) / 100.0) * 30)
|
|
122
|
+
kwargs['q:v'] = q_scale
|
|
123
|
+
elif output_format == 'webp':
|
|
124
|
+
kwargs['quality'] = quality
|
|
125
|
+
|
|
126
|
+
stream = ffmpeg.input(file_path).output(output_file, **kwargs)
|
|
127
|
+
|
|
128
|
+
if run_command(stream, f"Converting to {output_format.upper()}..."):
|
|
129
|
+
console.print(f"[bold green]Successfully converted image to {output_file}[/bold green]")
|
|
130
|
+
else:
|
|
131
|
+
console.print("[bold red]Image conversion failed.[/bold red]")
|
|
132
|
+
|
|
133
|
+
questionary.press_any_key_to_continue().ask()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def resize_image(file_path):
|
|
137
|
+
"""Resize an image to new dimensions."""
|
|
138
|
+
console.print("Enter new dimensions. Use [bold]-1[/bold] for one dimension to preserve aspect ratio.")
|
|
139
|
+
width = questionary.text("Enter new width (e.g., 1280 or -1):").ask()
|
|
140
|
+
if not width: return
|
|
141
|
+
height = questionary.text("Enter new height (e.g., 720 or -1):").ask()
|
|
142
|
+
if not height: return
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
if int(width) == -1 and int(height) == -1:
|
|
146
|
+
console.print("[bold red]Error: Width and Height cannot both be -1.[/bold red]")
|
|
147
|
+
questionary.press_any_key_to_continue().ask()
|
|
148
|
+
return
|
|
149
|
+
except ValueError:
|
|
150
|
+
console.print("[bold red]Error: Invalid dimensions. Please enter numbers.[/bold red]")
|
|
151
|
+
questionary.press_any_key_to_continue().ask()
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
output_file = f"{Path(file_path).stem}_resized{Path(file_path).suffix}"
|
|
155
|
+
|
|
156
|
+
stream = ffmpeg.input(file_path).filter('scale', w=width, h=height).output(output_file, y=None)
|
|
157
|
+
|
|
158
|
+
if run_command(stream, "Resizing image..."):
|
|
159
|
+
console.print(f"[bold green]Successfully resized image to {output_file}[/bold green]")
|
|
160
|
+
else:
|
|
161
|
+
console.print("[bold red]Image resizing failed.[/bold red]")
|
|
162
|
+
|
|
163
|
+
questionary.press_any_key_to_continue().ask()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def rotate_image(file_path):
|
|
167
|
+
"""Rotate an image."""
|
|
168
|
+
rotation = questionary.select(
|
|
169
|
+
"Select rotation:",
|
|
170
|
+
choices=[
|
|
171
|
+
"90 degrees clockwise",
|
|
172
|
+
"90 degrees counter-clockwise",
|
|
173
|
+
"180 degrees"
|
|
174
|
+
],
|
|
175
|
+
use_indicator=True
|
|
176
|
+
).ask()
|
|
177
|
+
if not rotation: return
|
|
178
|
+
|
|
179
|
+
output_file = f"{Path(file_path).stem}_rotated{Path(file_path).suffix}"
|
|
180
|
+
|
|
181
|
+
stream = ffmpeg.input(file_path)
|
|
182
|
+
if rotation == "90 degrees clockwise":
|
|
183
|
+
stream = stream.filter('transpose', 1)
|
|
184
|
+
elif rotation == "90 degrees counter-clockwise":
|
|
185
|
+
stream = stream.filter('transpose', 2)
|
|
186
|
+
elif rotation == "180 degrees":
|
|
187
|
+
# Apply 90-degree rotation twice for 180 degrees
|
|
188
|
+
stream = stream.filter('transpose', 2).filter('transpose', 2)
|
|
189
|
+
|
|
190
|
+
output_stream = stream.output(output_file, y=None)
|
|
191
|
+
|
|
192
|
+
if run_command(output_stream, "Rotating image..."):
|
|
193
|
+
console.print(f"[bold green]Successfully rotated image and saved to {output_file}[/bold green]")
|
|
194
|
+
else:
|
|
195
|
+
console.print("[bold red]Image rotation failed.[/bold red]")
|
|
196
|
+
|
|
197
|
+
questionary.press_any_key_to_continue().ask()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def flip_image(file_path):
|
|
201
|
+
"""Flip an image horizontally or vertically."""
|
|
202
|
+
flip_direction = questionary.select(
|
|
203
|
+
"Select flip direction:",
|
|
204
|
+
choices=["Horizontal", "Vertical"],
|
|
205
|
+
use_indicator=True
|
|
206
|
+
).ask()
|
|
207
|
+
if not flip_direction: return
|
|
208
|
+
|
|
209
|
+
output_file = f"{Path(file_path).stem}_flipped{Path(file_path).suffix}"
|
|
210
|
+
|
|
211
|
+
stream = ffmpeg.input(file_path)
|
|
212
|
+
if flip_direction == "Horizontal":
|
|
213
|
+
stream = stream.filter('hflip')
|
|
214
|
+
else:
|
|
215
|
+
stream = stream.filter('vflip')
|
|
216
|
+
|
|
217
|
+
output_stream = stream.output(output_file, y=None)
|
|
218
|
+
|
|
219
|
+
if run_command(output_stream, "Flipping image..."):
|
|
220
|
+
console.print(f"[bold green]Successfully flipped image and saved to {output_file}[/bold green]")
|
|
221
|
+
else:
|
|
222
|
+
console.print("[bold red]Image flipping failed.[/bold red]")
|
|
223
|
+
|
|
224
|
+
questionary.press_any_key_to_continue().ask()
|
|
225
|
+
|
|
@@ -104,3 +104,75 @@ def crop_video(file_path):
|
|
|
104
104
|
if os.path.exists(preview_frame):
|
|
105
105
|
os.remove(preview_frame)
|
|
106
106
|
questionary.press_any_key_to_continue().ask()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def crop_image(file_path):
|
|
110
|
+
"""Visually crop an image by selecting an area."""
|
|
111
|
+
if not tk:
|
|
112
|
+
console.print("[bold red]Cannot perform visual cropping: tkinter & Pillow are not installed.[/bold red]")
|
|
113
|
+
console.print("Please install them with: [bold]pip install tk Pillow[/bold]")
|
|
114
|
+
questionary.press_any_key_to_continue().ask()
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# --- Tkinter GUI for Cropping ---
|
|
119
|
+
root = tk.Tk()
|
|
120
|
+
root.title("Crop Image - Drag to select area, close window to confirm")
|
|
121
|
+
root.attributes("-topmost", True)
|
|
122
|
+
|
|
123
|
+
img = Image.open(file_path)
|
|
124
|
+
|
|
125
|
+
max_width = root.winfo_screenwidth() - 100
|
|
126
|
+
max_height = root.winfo_screenheight() - 100
|
|
127
|
+
img.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
|
128
|
+
|
|
129
|
+
img_tk = ImageTk.PhotoImage(img)
|
|
130
|
+
|
|
131
|
+
canvas = tk.Canvas(root, width=img.width, height=img.height, cursor="cross")
|
|
132
|
+
canvas.pack()
|
|
133
|
+
canvas.create_image(0, 0, anchor=tk.NW, image=img_tk)
|
|
134
|
+
|
|
135
|
+
rect_coords = {"x1": 0, "y1": 0, "x2": 0, "y2": 0}
|
|
136
|
+
rect_id = None
|
|
137
|
+
|
|
138
|
+
def on_press(event):
|
|
139
|
+
nonlocal rect_id
|
|
140
|
+
rect_coords['x1'], rect_coords['y1'] = event.x, event.y
|
|
141
|
+
rect_id = canvas.create_rectangle(0, 0, 1, 1, outline='red', width=2)
|
|
142
|
+
|
|
143
|
+
def on_drag(event):
|
|
144
|
+
rect_coords['x2'], rect_coords['y2'] = event.x, event.y
|
|
145
|
+
canvas.coords(rect_id, rect_coords['x1'], rect_coords['y1'], rect_coords['x2'], rect_coords['y2'])
|
|
146
|
+
|
|
147
|
+
canvas.bind("<ButtonPress-1>", on_press)
|
|
148
|
+
canvas.bind("<B1-Motion>", on_drag)
|
|
149
|
+
|
|
150
|
+
messagebox.showinfo("Instructions", "Click and drag to draw a cropping rectangle.\nClose this window when you are done.", parent=root)
|
|
151
|
+
root.mainloop()
|
|
152
|
+
|
|
153
|
+
# --- Cropping Logic ---
|
|
154
|
+
crop_w = abs(rect_coords['x2'] - rect_coords['x1'])
|
|
155
|
+
crop_h = abs(rect_coords['y2'] - rect_coords['y1'])
|
|
156
|
+
crop_x = min(rect_coords['x1'], rect_coords['x2'])
|
|
157
|
+
crop_y = min(rect_coords['y1'], rect_coords['y2'])
|
|
158
|
+
|
|
159
|
+
if crop_w < 2 or crop_h < 2:
|
|
160
|
+
console.print("[bold yellow]Cropping cancelled as no valid area was selected.[/bold yellow]")
|
|
161
|
+
questionary.press_any_key_to_continue().ask()
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
console.print(f"Selected crop area: [bold]width={crop_w} height={crop_h} at (x={crop_x}, y={crop_y})[/bold]")
|
|
165
|
+
|
|
166
|
+
output_file = f"{Path(file_path).stem}_cropped{Path(file_path).suffix}"
|
|
167
|
+
|
|
168
|
+
stream = ffmpeg.input(file_path).filter('crop', w=crop_w, h=crop_h, x=crop_x, y=crop_y).output(output_file, y=None)
|
|
169
|
+
|
|
170
|
+
if run_command(stream, "Applying crop to image..."):
|
|
171
|
+
console.print(f"[bold green]Successfully cropped image and saved to {output_file}[/bold green]")
|
|
172
|
+
else:
|
|
173
|
+
console.print("[bold red]Image cropping failed.[/bold red]")
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
console.print(f"[bold red]An error occurred during cropping: {e}[/bold red]")
|
|
177
|
+
finally:
|
|
178
|
+
questionary.press_any_key_to_continue().ask()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
import logging
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
|
5
6
|
|
|
6
7
|
import questionary
|
|
@@ -8,8 +9,8 @@ from rich.console import Console
|
|
|
8
9
|
|
|
9
10
|
from peg_this.features.audio import extract_audio, remove_audio
|
|
10
11
|
from peg_this.features.batch import batch_convert
|
|
11
|
-
from peg_this.features.convert import convert_file
|
|
12
|
-
from peg_this.features.crop import crop_video
|
|
12
|
+
from peg_this.features.convert import convert_file, convert_image, resize_image, rotate_image, flip_image
|
|
13
|
+
from peg_this.features.crop import crop_video, crop_image
|
|
13
14
|
from peg_this.features.inspect import inspect_file
|
|
14
15
|
from peg_this.features.join import join_videos
|
|
15
16
|
from peg_this.features.trim import trim_video
|
|
@@ -29,9 +30,44 @@ logging.basicConfig(
|
|
|
29
30
|
|
|
30
31
|
# Initialize Rich Console
|
|
31
32
|
console = Console()
|
|
33
|
+
IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"]
|
|
32
34
|
# --- End Global Configuration ---
|
|
33
35
|
|
|
34
36
|
|
|
37
|
+
def image_action_menu(file_path):
|
|
38
|
+
"""Display the menu of actions for a selected image file."""
|
|
39
|
+
while True:
|
|
40
|
+
console.rule(f"[bold]Actions for Image: {os.path.basename(file_path)}[/bold]")
|
|
41
|
+
action = questionary.select(
|
|
42
|
+
"Choose an action:",
|
|
43
|
+
choices=[
|
|
44
|
+
"Inspect File Details",
|
|
45
|
+
"Convert Format",
|
|
46
|
+
"Resize",
|
|
47
|
+
"Rotate",
|
|
48
|
+
"Flip",
|
|
49
|
+
"Crop (Visual)",
|
|
50
|
+
questionary.Separator(),
|
|
51
|
+
"Back to File List"
|
|
52
|
+
],
|
|
53
|
+
use_indicator=True
|
|
54
|
+
).ask()
|
|
55
|
+
|
|
56
|
+
if action is None or action == "Back to File List":
|
|
57
|
+
break
|
|
58
|
+
|
|
59
|
+
actions = {
|
|
60
|
+
"Inspect File Details": inspect_file,
|
|
61
|
+
"Convert Format": convert_image,
|
|
62
|
+
"Resize": resize_image,
|
|
63
|
+
"Rotate": rotate_image,
|
|
64
|
+
"Flip": flip_image,
|
|
65
|
+
"Crop (Visual)": crop_image,
|
|
66
|
+
}
|
|
67
|
+
if action in actions:
|
|
68
|
+
actions[action](file_path)
|
|
69
|
+
|
|
70
|
+
|
|
35
71
|
def action_menu(file_path):
|
|
36
72
|
"""Display the menu of actions for a selected file."""
|
|
37
73
|
while True:
|
|
@@ -90,7 +126,10 @@ def main_menu():
|
|
|
90
126
|
elif choice == "Process a Single Media File":
|
|
91
127
|
selected_file = select_media_file()
|
|
92
128
|
if selected_file:
|
|
93
|
-
|
|
129
|
+
if Path(selected_file).suffix.lower() in IMAGE_EXTENSIONS:
|
|
130
|
+
image_action_menu(selected_file)
|
|
131
|
+
else:
|
|
132
|
+
action_menu(selected_file)
|
|
94
133
|
elif choice == "Join Multiple Videos":
|
|
95
134
|
join_videos()
|
|
96
135
|
elif choice == "Batch Convert All Media in Directory":
|
|
@@ -37,12 +37,11 @@ def run_command(stream_spec, description="Processing...", show_progress=False):
|
|
|
37
37
|
Runs an ffmpeg command using ffmpeg-python.
|
|
38
38
|
- For simple commands, it runs directly.
|
|
39
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
|
-
|
|
40
|
+
runs them as a subprocess, and parses stderr to show progress.
|
|
41
|
+
Returns True on success, False on failure.
|
|
42
42
|
"""
|
|
43
43
|
console.print(f"[bold cyan]{description}[/bold cyan]")
|
|
44
44
|
|
|
45
|
-
# Get the full command arguments from the ffmpeg-python stream object
|
|
46
45
|
args = stream_spec.get_args()
|
|
47
46
|
full_command = ['ffmpeg'] + args
|
|
48
47
|
logging.info(f"Executing command: {' '.join(full_command)}")
|
|
@@ -50,24 +49,22 @@ def run_command(stream_spec, description="Processing...", show_progress=False):
|
|
|
50
49
|
if not show_progress:
|
|
51
50
|
try:
|
|
52
51
|
# Use ffmpeg.run() for simple, non-progress tasks. It's cleaner.
|
|
53
|
-
|
|
52
|
+
ffmpeg.run(stream_spec, capture_stdout=True, capture_stderr=True, quiet=True)
|
|
54
53
|
logging.info("Command successful (no progress bar).")
|
|
55
|
-
return
|
|
54
|
+
return True
|
|
56
55
|
except ffmpeg.Error as e:
|
|
57
56
|
error_message = e.stderr.decode('utf-8')
|
|
58
57
|
console.print("[bold red]An error occurred:[/bold red]")
|
|
59
58
|
console.print(error_message)
|
|
60
59
|
logging.error(f"ffmpeg error:{error_message}")
|
|
61
|
-
return
|
|
60
|
+
return False
|
|
62
61
|
else:
|
|
63
62
|
# For the progress bar, we must run ffmpeg as a subprocess and parse stderr.
|
|
64
63
|
duration = 0
|
|
65
64
|
try:
|
|
66
|
-
# Find the primary input file from the command arguments to probe it.
|
|
67
65
|
input_file_path = None
|
|
68
66
|
for i, arg in enumerate(full_command):
|
|
69
67
|
if arg == '-i' and i + 1 < len(full_command):
|
|
70
|
-
# This is a robust way to find the first input file.
|
|
71
68
|
input_file_path = full_command[i+1]
|
|
72
69
|
break
|
|
73
70
|
|
|
@@ -90,7 +87,6 @@ def run_command(stream_spec, description="Processing...", show_progress=False):
|
|
|
90
87
|
) as progress:
|
|
91
88
|
task = progress.add_task(description, total=100)
|
|
92
89
|
|
|
93
|
-
# Run the command as a subprocess to capture stderr in real-time
|
|
94
90
|
process = subprocess.Popen(
|
|
95
91
|
full_command,
|
|
96
92
|
stdout=subprocess.PIPE,
|
|
@@ -110,19 +106,18 @@ def run_command(stream_spec, description="Processing...", show_progress=False):
|
|
|
110
106
|
percent_complete = (elapsed_time / duration) * 100
|
|
111
107
|
progress.update(task, completed=min(percent_complete, 100))
|
|
112
108
|
except Exception:
|
|
113
|
-
pass
|
|
109
|
+
pass
|
|
114
110
|
|
|
115
111
|
process.wait()
|
|
116
112
|
progress.update(task, completed=100)
|
|
117
113
|
|
|
118
114
|
if process.returncode != 0:
|
|
119
|
-
# The error was already logged line-by-line, but we can add a final message.
|
|
120
115
|
log_file = logging.getLogger().handlers[0].baseFilename
|
|
121
116
|
console.print(f"[bold red]An error occurred during processing. Check {log_file} for details.[/bold red]")
|
|
122
|
-
return
|
|
117
|
+
return False
|
|
123
118
|
|
|
124
119
|
logging.info("Command successful (with progress bar).")
|
|
125
|
-
return
|
|
120
|
+
return True
|
|
126
121
|
|
|
127
122
|
|
|
128
123
|
def has_audio_stream(file_path):
|
|
@@ -16,7 +16,16 @@ console = Console()
|
|
|
16
16
|
|
|
17
17
|
def get_media_files():
|
|
18
18
|
"""Scan the current directory for media files."""
|
|
19
|
-
media_extensions = [
|
|
19
|
+
media_extensions = [
|
|
20
|
+
# Video
|
|
21
|
+
".mkv", ".mp4", ".avi", ".mov", ".webm", ".flv", ".wmv",
|
|
22
|
+
# Audio
|
|
23
|
+
".mp3", ".flac", ".wav", ".ogg",
|
|
24
|
+
# GIF
|
|
25
|
+
".gif",
|
|
26
|
+
# Image
|
|
27
|
+
".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff"
|
|
28
|
+
]
|
|
20
29
|
files = [f for f in os.listdir('.') if os.path.isfile(f) and Path(f).suffix.lower() in media_extensions]
|
|
21
30
|
return files
|
|
22
31
|
|
|
@@ -31,7 +40,7 @@ def select_media_file():
|
|
|
31
40
|
root.withdraw()
|
|
32
41
|
file_path = filedialog.askopenfilename(
|
|
33
42
|
title="Select a media file",
|
|
34
|
-
filetypes=[("Media Files", "*.mkv *.mp4 *.avi *.mov *.webm *.flv *.wmv *.mp3 *.flac *.wav *.ogg *.gif"), ("All Files", "*.*")]
|
|
43
|
+
filetypes=[("Media Files", "*.mkv *.mp4 *.avi *.mov *.webm *.flv *.wmv *.mp3 *.flac *.wav *.ogg *.gif *.jpg *.jpeg *.png *.webp *.bmp *.tiff"), ("All Files", "*.*")]
|
|
35
44
|
)
|
|
36
45
|
return file_path if file_path else None
|
|
37
46
|
return None
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: peg_this
|
|
3
|
-
Version:
|
|
4
|
-
Summary: A powerful
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: A powerful and intuitive command-line video editor, built on FFmpeg.
|
|
5
5
|
Author-email: Hariharen S S <thisishariharen@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/hariharen9/ffmpeg-this
|
|
7
|
+
Project-URL: Documentation, https://github.com/hariharen9/ffmpeg-this/blob/main/README.md
|
|
8
|
+
Project-URL: Funding, https://www.buymeacoffee.com/hariharen
|
|
9
|
+
Project-URL: Say Thanks!, https://saythanks.io/to/thisishariharen
|
|
10
|
+
Project-URL: Social, https://twitter.com/thisishariharen
|
|
7
11
|
Project-URL: Bug Tracker, https://github.com/hariharen9/ffmpeg-this/issues
|
|
8
12
|
Project-URL: Releases, https://github.com/hariharen9/ffmpeg-this/releases
|
|
13
|
+
Project-URL: Sponsor, https://github.com/sponsors/hariharen9
|
|
9
14
|
Classifier: Programming Language :: Python :: 3
|
|
10
15
|
Classifier: License :: OSI Approved :: MIT License
|
|
11
16
|
Classifier: Operating System :: OS Independent
|
|
@@ -22,12 +27,29 @@ Dynamic: license-file
|
|
|
22
27
|
|
|
23
28
|
# 🎬 ffmPEG-this
|
|
24
29
|
|
|
30
|
+
<p align="center">
|
|
31
|
+
<a href="https://pypi.org/project/peg-this/">
|
|
32
|
+
<img src="https://img.shields.io/pypi/v/peg_this?color=blue&label=version" alt="PyPI Version">
|
|
33
|
+
</a>
|
|
34
|
+
<a href="https://pypi.org/project/peg-this/">
|
|
35
|
+
<img src="https://img.shields.io/pypi/pyversions/peg_this.svg" alt="PyPI Python Versions">
|
|
36
|
+
</a>
|
|
37
|
+
<a href="https://github.com/hariharen9/ffmpeg-this/blob/main/LICENSE">
|
|
38
|
+
<img src="https://img.shields.io/github/license/hariharen9/ffmpeg-this" alt="License">
|
|
39
|
+
</a>
|
|
40
|
+
<a href="https://pepy.tech/project/peg-this">
|
|
41
|
+
<img src="https://static.pepy.tech/badge/peg-this" alt="Downloads">
|
|
42
|
+
</a>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
25
45
|
> Your Video editor within CLI 🚀
|
|
26
46
|
|
|
27
47
|
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
48
|
|
|
29
49
|
|
|
30
|
-
<
|
|
50
|
+
<p align="center">
|
|
51
|
+
<img src="/assets/peg.gif" width="720">
|
|
52
|
+
</p>
|
|
31
53
|
|
|
32
54
|
|
|
33
55
|
## ✨ Features
|
|
@@ -40,11 +62,13 @@ A powerful and user-friendly batch script for converting, manipulating, and insp
|
|
|
40
62
|
- **Extract Audio**: Rip the audio track from any video file into MP3, FLAC, or WAV.
|
|
41
63
|
- **Remove Audio**: Create a silent version of your video by stripping out all audio streams.
|
|
42
64
|
- **Batch Conversion**: Convert all media files in the current directory to a specified format in one go.
|
|
65
|
+
- **CLI Interface**: A user-friendly command-line interface that makes it easy to perform common tasks and navigate the tool's features.
|
|
43
66
|
|
|
44
67
|
|
|
45
68
|
## 🚀 Usage
|
|
46
69
|
### Prerequisite: Install FFmpeg
|
|
47
70
|
|
|
71
|
+
> [NOTE]
|
|
48
72
|
> `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
73
|
|
|
50
74
|
For **macOS** users, the easiest way to install it is with [Homebrew](https://brew.sh/):
|
|
@@ -63,11 +87,10 @@ scoop install ffmpeg
|
|
|
63
87
|
|
|
64
88
|
For other systems, please see the official download page: **[ffmpeg.org/download.html](https://ffmpeg.org/download.html)**
|
|
65
89
|
|
|
66
|
-
There are
|
|
90
|
+
There are three ways to use `peg_this`:
|
|
67
91
|
|
|
68
92
|
### 1. Pip Install (Recommended)
|
|
69
|
-
|
|
70
|
-
This is the easiest way to get started. This will install the tool and all its dependencies, including `ffmpeg`.
|
|
93
|
+
This is the easiest way to get started. This will install the tool and all its dependencies.
|
|
71
94
|
|
|
72
95
|
```bash
|
|
73
96
|
pip install peg_this
|
|
@@ -80,15 +103,13 @@ peg_this
|
|
|
80
103
|
```
|
|
81
104
|
|
|
82
105
|
### 2. Download from Release
|
|
83
|
-
|
|
84
|
-
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.
|
|
106
|
+
If you prefer not to install the package, you can download a pre-built executable from the [Releases](https://github.com/hariharen9/ffmpeg-this/releases/latest) page.
|
|
85
107
|
|
|
86
108
|
1. Download the executable for your operating system (Windows, macOS, or Linux).
|
|
87
|
-
2. Place
|
|
88
|
-
3. Run the executable directly from your terminal
|
|
109
|
+
2. Place it in a directory with your media files.
|
|
110
|
+
3. Run the executable directly from your terminal.
|
|
89
111
|
|
|
90
112
|
### 3. Run from Source
|
|
91
|
-
|
|
92
113
|
If you want to run the script directly from the source code:
|
|
93
114
|
|
|
94
115
|
1. **Clone the repository:**
|
|
@@ -102,9 +123,42 @@ If you want to run the script directly from the source code:
|
|
|
102
123
|
```
|
|
103
124
|
3. **Run the script:**
|
|
104
125
|
```bash
|
|
105
|
-
python src
|
|
126
|
+
python -m src.peg_this.peg_this
|
|
106
127
|
```
|
|
107
128
|
|
|
129
|
+
## 📈 Star History
|
|
130
|
+
|
|
131
|
+
<p align="center">
|
|
132
|
+
<a href="https://star-history.com/#hariharen9/ffmpeg-this&Date">
|
|
133
|
+
<img src="https://api.star-history.com/svg?repos=hariharen9/ffmpeg-this&type=Date" alt="Star History Chart">
|
|
134
|
+
</a>
|
|
135
|
+
</p>
|
|
136
|
+
|
|
137
|
+
## ✨ Sponsor
|
|
138
|
+
|
|
139
|
+
<p align="center">
|
|
140
|
+
<a href="https://github.com/sponsors/hariharen9">
|
|
141
|
+
<img src="https://img.shields.io/github/sponsors/hariharen9?style=for-the-badge&logo=github&color=white" alt="GitHub Sponsors">
|
|
142
|
+
</a>
|
|
143
|
+
<a href="https://www.buymeacoffee.com/hariharen">
|
|
144
|
+
<img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me a Coffee">
|
|
145
|
+
</a>
|
|
146
|
+
</p>
|
|
147
|
+
|
|
148
|
+
## 👥 Contributors
|
|
149
|
+
|
|
150
|
+
<a href="https://github.com/hariharen9/ffmpeg-this/graphs/contributors">
|
|
151
|
+
<img src="https://contrib.rocks/image?repo=hariharen9/ffmpeg-this" />
|
|
152
|
+
</a>
|
|
153
|
+
|
|
154
|
+
## 🤝 Contributing
|
|
155
|
+
|
|
156
|
+
Contributions are welcome! Please see the [Contributing Guidelines](CONTRIBUTING.md) for more information.
|
|
157
|
+
|
|
108
158
|
## 📄 License
|
|
109
159
|
|
|
110
160
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
161
|
+
|
|
162
|
+
<p align="center">
|
|
163
|
+
<h2>Made with ❤️ by <a href="https://hariharen.site">Hariharen</a></h2>
|
|
164
|
+
</p>
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import os
|
|
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
|
-
|
|
11
|
-
console = Console()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def convert_file(file_path):
|
|
15
|
-
"""Convert the file to a different format."""
|
|
16
|
-
is_gif = Path(file_path).suffix.lower() == '.gif'
|
|
17
|
-
has_audio = has_audio_stream(file_path)
|
|
18
|
-
|
|
19
|
-
output_format = questionary.select("Select the output format:", choices=["mp4", "mkv", "mov", "avi", "webm", "mp3", "flac", "wav", "gif"], use_indicator=True).ask()
|
|
20
|
-
if not output_format: return
|
|
21
|
-
|
|
22
|
-
if (is_gif or not has_audio) and output_format in ["mp3", "flac", "wav"]:
|
|
23
|
-
console.print("[bold red]Error: Source has no audio to convert.[/bold red]")
|
|
24
|
-
questionary.press_any_key_to_continue().ask()
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
output_file = f"{Path(file_path).stem}_converted.{output_format}"
|
|
28
|
-
|
|
29
|
-
input_stream = ffmpeg.input(file_path)
|
|
30
|
-
output_stream = None
|
|
31
|
-
kwargs = {'y': None}
|
|
32
|
-
|
|
33
|
-
if output_format in ["mp4", "mkv", "mov", "avi", "webm"]:
|
|
34
|
-
quality = questionary.select("Select quality preset:", choices=["Same as source", "High (CRF 18)", "Medium (CRF 23)", "Low (CRF 28)"], use_indicator=True).ask()
|
|
35
|
-
if not quality: return
|
|
36
|
-
|
|
37
|
-
if quality == "Same as source":
|
|
38
|
-
kwargs['c'] = 'copy'
|
|
39
|
-
else:
|
|
40
|
-
crf = quality.split(" ")[-1][1:-1]
|
|
41
|
-
kwargs['c:v'] = 'libx264'
|
|
42
|
-
kwargs['crf'] = crf
|
|
43
|
-
kwargs['pix_fmt'] = 'yuv420p'
|
|
44
|
-
if has_audio:
|
|
45
|
-
kwargs['c:a'] = 'aac'
|
|
46
|
-
kwargs['b:a'] = '192k'
|
|
47
|
-
else:
|
|
48
|
-
kwargs['an'] = None
|
|
49
|
-
output_stream = input_stream.output(output_file, **kwargs)
|
|
50
|
-
|
|
51
|
-
elif output_format in ["mp3", "flac", "wav"]:
|
|
52
|
-
kwargs['vn'] = None
|
|
53
|
-
if output_format == 'mp3':
|
|
54
|
-
bitrate = questionary.select("Select audio bitrate:", choices=["128k", "192k", "256k", "320k"]).ask()
|
|
55
|
-
if not bitrate: return
|
|
56
|
-
kwargs['c:a'] = 'libmp3lame'
|
|
57
|
-
kwargs['b:a'] = bitrate
|
|
58
|
-
else:
|
|
59
|
-
kwargs['c:a'] = output_format
|
|
60
|
-
output_stream = input_stream.output(output_file, **kwargs)
|
|
61
|
-
|
|
62
|
-
elif output_format == "gif":
|
|
63
|
-
fps = questionary.text("Enter frame rate (e.g., 15):", default="15").ask()
|
|
64
|
-
if not fps: return
|
|
65
|
-
scale = questionary.text("Enter width in pixels (e.g., 480):", default="480").ask()
|
|
66
|
-
if not scale: return
|
|
67
|
-
|
|
68
|
-
palette_file = f"palette_{Path(file_path).stem}.png"
|
|
69
|
-
|
|
70
|
-
# Correctly chain filters for palette generation using explicit w/h arguments
|
|
71
|
-
palette_gen_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos').filter('palettegen')
|
|
72
|
-
run_command(palette_gen_stream.output(palette_file, y=None), "Generating color palette...")
|
|
73
|
-
|
|
74
|
-
if not os.path.exists(palette_file):
|
|
75
|
-
console.print("[bold red]Failed to generate color palette for GIF.[/bold red]")
|
|
76
|
-
questionary.press_any_key_to_continue().ask()
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
palette_input = ffmpeg.input(palette_file)
|
|
80
|
-
video_stream = input_stream.video.filter('fps', fps=fps).filter('scale', w=scale, h=-1, flags='lanczos')
|
|
81
|
-
|
|
82
|
-
final_stream = ffmpeg.filter([video_stream, palette_input], 'paletteuse')
|
|
83
|
-
output_stream = final_stream.output(output_file, y=None)
|
|
84
|
-
|
|
85
|
-
if output_stream and run_command(output_stream, f"Converting to {output_format}...", show_progress=True):
|
|
86
|
-
console.print(f"[bold green]Successfully converted to {output_file}[/bold green]")
|
|
87
|
-
else:
|
|
88
|
-
console.print("[bold red]Conversion failed.[/bold red]")
|
|
89
|
-
|
|
90
|
-
if output_format == "gif" and os.path.exists(f"palette_{Path(file_path).stem}.png"):
|
|
91
|
-
os.remove(f"palette_{Path(file_path).stem}.png")
|
|
92
|
-
|
|
93
|
-
questionary.press_any_key_to_continue().ask()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|