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.
Files changed (25) hide show
  1. {peg_this-3.0.6/src/peg_this.egg-info → peg_this-4.0.0}/PKG-INFO +66 -12
  2. {peg_this-3.0.6 → peg_this-4.0.0}/README.md +60 -11
  3. {peg_this-3.0.6 → peg_this-4.0.0}/pyproject.toml +7 -2
  4. peg_this-4.0.0/src/peg_this/features/convert.py +225 -0
  5. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/crop.py +72 -0
  6. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/peg_this.py +42 -3
  7. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/utils/ffmpeg_utils.py +8 -13
  8. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/utils/ui_utils.py +11 -2
  9. {peg_this-3.0.6 → peg_this-4.0.0/src/peg_this.egg-info}/PKG-INFO +66 -12
  10. peg_this-3.0.6/src/peg_this/features/convert.py +0 -93
  11. {peg_this-3.0.6 → peg_this-4.0.0}/LICENSE +0 -0
  12. {peg_this-3.0.6 → peg_this-4.0.0}/setup.cfg +0 -0
  13. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/__init__.py +0 -0
  14. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/__init__.py +0 -0
  15. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/audio.py +0 -0
  16. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/batch.py +0 -0
  17. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/inspect.py +0 -0
  18. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/join.py +0 -0
  19. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/features/trim.py +0 -0
  20. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this/utils/__init__.py +0 -0
  21. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/SOURCES.txt +0 -0
  22. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/dependency_links.txt +0 -0
  23. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/entry_points.txt +0 -0
  24. {peg_this-3.0.6 → peg_this-4.0.0}/src/peg_this.egg-info/requires.txt +0 -0
  25. {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: 3.0.6
4
- Summary: A powerful, intuitive command-line video editor suite, built on FFmpeg.
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
- <img src="/assets/peg.gif" width="720">
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 ***three*** ways to use `peg_this`:
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 the downloaded file in a directory with your media files.
88
- 3. Run the executable directly from your terminal or command prompt.
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/peg_this/peg_this.py
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
- <img src="/assets/peg.gif" width="720">
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 ***three*** ways to use `peg_this`:
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 the downloaded file in a directory with your media files.
66
- 3. Run the executable directly from your terminal or command prompt.
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/peg_this/peg_this.py
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 = "3.0.6"
7
+ version = "4.0.0"
8
8
  authors = [
9
9
  { name="Hariharen S S", email="thisishariharen@gmail.com" },
10
10
  ]
11
- description = "A powerful, intuitive command-line video editor suite, built on FFmpeg."
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
- action_menu(selected_file)
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
- mimicking the logic from the original script for accuracy.
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
- out, err = ffmpeg.run(stream_spec, capture_stdout=True, capture_stderr=True, quiet=True)
52
+ ffmpeg.run(stream_spec, capture_stdout=True, capture_stderr=True, quiet=True)
54
53
  logging.info("Command successful (no progress bar).")
55
- return out.decode('utf-8')
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 None
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 # Ignore any parsing errors
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 None
117
+ return False
123
118
 
124
119
  logging.info("Command successful (with progress bar).")
125
- return "Success"
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 = [".mkv", ".mp4", ".avi", ".mov", ".webm", ".flv", ".wmv", ".mp3", ".flac", ".wav", ".ogg", ".gif"]
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: 3.0.6
4
- Summary: A powerful, intuitive command-line video editor suite, built on FFmpeg.
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
- <img src="/assets/peg.gif" width="720">
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 ***three*** ways to use `peg_this`:
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 the downloaded file in a directory with your media files.
88
- 3. Run the executable directly from your terminal or command prompt.
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/peg_this/peg_this.py
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