Compare commits
12 Commits
setups-dra
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 00696fc449 | |||
| e966f4625a | |||
| 8ff995a29f | |||
| 2152479d2e | |||
| 53f483b574 | |||
| 4c76cbfe76 | |||
| 4ef0772105 | |||
| 98f9a57ea1 | |||
| 218951f035 | |||
| c062b32dc6 | |||
| 45fb6b35b3 | |||
| 9246fde44a |
@ -8,7 +8,13 @@ This repository is structured into several key directories:
|
|||||||
|
|
||||||
- **scripts/**: Contains individual scripts for various tasks. Currently, it includes:
|
- **scripts/**: Contains individual scripts for various tasks. Currently, it includes:
|
||||||
|
|
||||||
- `video_remove_audio.py`: A script for removing audio from video files.
|
- `scripts/library/`: Libraries used by Python scripts.
|
||||||
|
- `venv_utils.py`: Utility functions for creating, activating, and managing Python virtual environments.
|
||||||
|
- `change_case.py`: A script for renaming files and directories by changing their case.
|
||||||
|
- `video_manage_audio.py`: A script for removing audio from video files.
|
||||||
|
- `video_manage_subtitles.py`: A script for removing subtitles from video files.
|
||||||
|
- `video_autoreduce.py`: A script for automatic resolution reduction of video files.
|
||||||
|
- `video_autoreduce_rename.py`: A script for automated renaming of video files post resolution reduction.
|
||||||
|
|
||||||
- **notes/**: A collection of markdown files containing notes on various topics, including:
|
- **notes/**: A collection of markdown files containing notes on various topics, including:
|
||||||
|
|
||||||
@ -22,6 +28,7 @@ This repository is structured into several key directories:
|
|||||||
- `pdftk.md`: PDF Toolkit usage.
|
- `pdftk.md`: PDF Toolkit usage.
|
||||||
- `pip packaging.md`: Packaging Python projects with pip.
|
- `pip packaging.md`: Packaging Python projects with pip.
|
||||||
- `ssh.md`: Secure Shell (SSH) configuration and tips.
|
- `ssh.md`: Secure Shell (SSH) configuration and tips.
|
||||||
|
- `wordpress.md`: WordPress debugging and tips.
|
||||||
|
|
||||||
- **pages/other/**: Templates for other pages, such as the homepage of my Debian package repository. These are provided as inspiration and should not be used as-is.
|
- **pages/other/**: Templates for other pages, such as the homepage of my Debian package repository. These are provided as inspiration and should not be used as-is.
|
||||||
|
|
||||||
|
|||||||
163
notes/wordpress.md
Normal file
163
notes/wordpress.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# WordPress
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [WordPress](#wordpress)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [Documentation](#documentation)
|
||||||
|
- [Common WordPress Errors](#common-wordpress-errors)
|
||||||
|
- [Debugging Steps](#debugging-steps)
|
||||||
|
- [Debugging Tools](#debugging-tools)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [wp-cli](https://developer.wordpress.org/cli/commands/)
|
||||||
|
- [Documentation Overview](https://www.wordpress.info/doc/overview/)
|
||||||
|
- [Tutorials](https://wordpress.com/learn/)
|
||||||
|
|
||||||
|
## Common WordPress Errors
|
||||||
|
|
||||||
|
1. **HTTP Errors**
|
||||||
|
|
||||||
|
- **404 Not Found**: Page or resource missing.
|
||||||
|
- **403 Forbidden**: Insufficient permissions to access the resource.
|
||||||
|
- **500 Internal Server Error**: Generic error, often caused by server misconfiguration or PHP errors.
|
||||||
|
- **503 Service Unavailable**: Server is overloaded or in maintenance mode.
|
||||||
|
|
||||||
|
2. **Database Errors**
|
||||||
|
|
||||||
|
- **Error Establishing a Database Connection**: Database credentials incorrect or database server is down.
|
||||||
|
- **Table Prefix Issues**: Wrong `$table_prefix` in `wp-config.php`.
|
||||||
|
- **Corrupt Database**: Can be fixed using `wp db repair`.
|
||||||
|
|
||||||
|
3. **PHP Errors**
|
||||||
|
|
||||||
|
- **Parse Error**: Syntax error in PHP files.
|
||||||
|
- **Fatal Error**: Missing function or class.
|
||||||
|
- **Deprecated Function Warnings**: Old functions being used.
|
||||||
|
|
||||||
|
## Debugging Steps
|
||||||
|
|
||||||
|
1. **Enable Debug Mode**
|
||||||
|
|
||||||
|
Edit `wp-config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Enable Debug Mode
|
||||||
|
define('WP_DEBUG', true);
|
||||||
|
// Enable Debug logging to the /wp-content/debug.log file
|
||||||
|
define('WP_DEBUG_LOG', true);
|
||||||
|
// Disable display of WordPress errors and warnings
|
||||||
|
define('WP_DEBUG_DISPLAY', false);
|
||||||
|
// Disable display of PHP errors and warnings
|
||||||
|
@ini_set('display_errors', 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
Logs will be saved in `wp-content/debug.log`.
|
||||||
|
|
||||||
|
2. **Check `.htaccess`**
|
||||||
|
|
||||||
|
Ensure WordPress rules exist:
|
||||||
|
|
||||||
|
```apache
|
||||||
|
# BEGIN WordPress
|
||||||
|
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
RewriteBase /
|
||||||
|
RewriteRule ^index\.php$ - [L]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule . /index.php [L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# END WordPress
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check Apache and VirtualHost config**
|
||||||
|
|
||||||
|
**Redhat and derivatives**
|
||||||
|
|
||||||
|
- `/etc/httpd/conf/httpd.conf`
|
||||||
|
- `/etc/httpd/conf.d/welcome.conf`
|
||||||
|
|
||||||
|
**Debian and derivatives**
|
||||||
|
|
||||||
|
- `/etc/apache2/apache2.conf`
|
||||||
|
- `/etc/apache2/sites-available/000-default.conf`
|
||||||
|
|
||||||
|
```apache
|
||||||
|
<Directory "/var/www">
|
||||||
|
AllowOverride All
|
||||||
|
# Allow open access:
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
<Directory "/var/www/html">
|
||||||
|
Options Indexes FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Reset File Permissions**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find /var/www/html -type d -exec chmod 755 {} \;
|
||||||
|
find /var/www/html -type f -exec chmod 644 {} \;
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Disable Plugins & Themes**
|
||||||
|
|
||||||
|
Hide the plugin directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mv wp-content/plugins wp-content/.plugins
|
||||||
|
```
|
||||||
|
|
||||||
|
Switch to a default theme:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp theme install twentytwentyfive --activate
|
||||||
|
wp theme activate twentytwentyfive
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Check Server Logs**
|
||||||
|
|
||||||
|
For Apache:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -f /var/log/httpd/error_log
|
||||||
|
```
|
||||||
|
|
||||||
|
For Nginx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tail -f /var/log/nginx/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Verify Database Connection**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp db check
|
||||||
|
```
|
||||||
|
|
||||||
|
If corrupt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wp db repair
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Increase Memory Limit**
|
||||||
|
|
||||||
|
Edit `wp-config.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
define('WP_MEMORY_LIMIT', '256M');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Tools
|
||||||
|
|
||||||
|
- **WP-CLI**: Command-line interface for WordPress.
|
||||||
|
- **Query Monitor**: Plugin for analyzing database queries and errors.
|
||||||
|
- **Health Check & Troubleshooting**: Plugin for diagnosing issues.
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
argcomplete
|
||||||
125
scripts/change_case.py
Executable file
125
scripts/change_case.py
Executable file
@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# === Standard library ===
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
from argparse import ArgumentParser, Namespace
|
||||||
|
|
||||||
|
# Allow importing from scripts/library even when run directly
|
||||||
|
project_root = str(Path(__file__).resolve().parent.parent)
|
||||||
|
if project_root not in sys.path:
|
||||||
|
sys.path.append(project_root)
|
||||||
|
|
||||||
|
# === Local import ===
|
||||||
|
from scripts.library import parse_verbose, run_in_venv
|
||||||
|
|
||||||
|
|
||||||
|
def rename_by_case(root_dir: str, case: str, recursive: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Renames all files and directories in a given directory by changing their case.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
root_dir (str): Root path to start renaming.
|
||||||
|
case (str): One of "lower", "upper", "capitalize".
|
||||||
|
recursive (bool): Whether to apply renaming recursively.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
def apply_case(name: str) -> str:
|
||||||
|
if case == "lower":
|
||||||
|
return name.lower()
|
||||||
|
elif case == "upper":
|
||||||
|
return name.upper()
|
||||||
|
elif case == "capitalize":
|
||||||
|
return name.capitalize()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported case transformation: {case}")
|
||||||
|
|
||||||
|
for current_dir, dirs, files in os.walk(root_dir, topdown=False):
|
||||||
|
# Skip descending into subdirectories if not recursive
|
||||||
|
if not recursive and current_dir != root_dir:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Rename files
|
||||||
|
for name in files:
|
||||||
|
old_path = os.path.join(current_dir, name)
|
||||||
|
new_name = apply_case(name)
|
||||||
|
new_path = os.path.join(current_dir, new_name)
|
||||||
|
if old_path != new_path:
|
||||||
|
if not os.path.exists(new_path):
|
||||||
|
os.rename(old_path, new_path)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Cannot rename {old_path} to {new_path}: target already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rename directories
|
||||||
|
for name in dirs:
|
||||||
|
old_path = os.path.join(current_dir, name)
|
||||||
|
new_name = apply_case(name)
|
||||||
|
new_path = os.path.join(current_dir, new_name)
|
||||||
|
if old_path != new_path:
|
||||||
|
if not os.path.exists(new_path):
|
||||||
|
os.rename(old_path, new_path)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Cannot rename {old_path} to {new_path}: target already exists."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> Namespace:
|
||||||
|
"""
|
||||||
|
Parse and validate command-line arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Namespace: Parsed arguments with 'case' and 'recursive' options.
|
||||||
|
"""
|
||||||
|
import argcomplete
|
||||||
|
|
||||||
|
parser = ArgumentParser(
|
||||||
|
description="Rename files and directories by changing case (lower, upper, capitalize)."
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--case",
|
||||||
|
"-c",
|
||||||
|
type=str,
|
||||||
|
choices=["lower", "upper", "capitalize"],
|
||||||
|
default="lower",
|
||||||
|
help="Case transformation to apply: 'lower', 'upper', or 'capitalize'. Default is 'lower'.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--recursive",
|
||||||
|
"-r",
|
||||||
|
action="store_true",
|
||||||
|
help="Apply the transformation recursively in all subdirectories.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-v",
|
||||||
|
"--verbose",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable verbose output (debug information, warnings)",
|
||||||
|
)
|
||||||
|
|
||||||
|
argcomplete.autocomplete(parser)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""
|
||||||
|
Main entry point. Parses arguments and runs case renaming in current working directory.
|
||||||
|
"""
|
||||||
|
args = parse_args()
|
||||||
|
cwd: str = os.getcwd()
|
||||||
|
rename_by_case(cwd, case=args.case, recursive=args.recursive)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
args = parse_verbose()
|
||||||
|
run_in_venv(main, verbose=args.verbose)
|
||||||
5
scripts/library/__init__.py
Normal file
5
scripts/library/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# scripts/library/__init__.py
|
||||||
|
|
||||||
|
from .venv_utils import parse_verbose, run_in_venv
|
||||||
|
|
||||||
|
__all__ = ["parse_verbose", "run_in_venv"]
|
||||||
130
scripts/library/venv_utils.py
Normal file
130
scripts/library/venv_utils.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# === Standard library ===
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import venv
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable
|
||||||
|
from argparse import ArgumentParser, Namespace
|
||||||
|
|
||||||
|
# === Configuration ===
|
||||||
|
# Define the virtual environment directory in the user's home folder
|
||||||
|
VENV_DIR: Path = Path.home() / ".scripts_fabq_venv"
|
||||||
|
|
||||||
|
# Define the path to the requirements.txt file in the project root
|
||||||
|
REQUIREMENTS_FILE: Path = Path(__file__).resolve().parents[2] / "requirements.txt"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_verbose() -> Namespace:
|
||||||
|
"""
|
||||||
|
Parse the --verbose flag before virtual environment setup.
|
||||||
|
|
||||||
|
This is useful when argument parsing requires packages that may
|
||||||
|
only be available after the virtual environment is initialized.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Namespace: Contains a single attribute 'verbose' (bool).
|
||||||
|
"""
|
||||||
|
parser = ArgumentParser(add_help=False)
|
||||||
|
parser.add_argument(
|
||||||
|
"--verbose",
|
||||||
|
"-v",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable verbose output.",
|
||||||
|
)
|
||||||
|
return parser.parse_known_args()[0]
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_venv() -> bool:
|
||||||
|
"""
|
||||||
|
Determine whether the script is currently running inside a virtual environment.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if running inside a virtual environment, False otherwise.
|
||||||
|
"""
|
||||||
|
return hasattr(sys, "real_prefix") or sys.prefix != getattr(
|
||||||
|
sys, "base_prefix", sys.prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_venv(verbose: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Create a virtual environment in the predefined VENV_DIR location.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
verbose (bool): If True, prints progress messages.
|
||||||
|
"""
|
||||||
|
if verbose:
|
||||||
|
print(f"Creating virtual environment in {VENV_DIR}...")
|
||||||
|
venv.create(VENV_DIR, with_pip=True)
|
||||||
|
|
||||||
|
|
||||||
|
def install_requirements(verbose: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Install Python dependencies from requirements.txt into the venv.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
verbose (bool): If True, prints progress and pip output.
|
||||||
|
"""
|
||||||
|
if not REQUIREMENTS_FILE.exists():
|
||||||
|
print(f"Error: requirements.txt not found at {REQUIREMENTS_FILE}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print(f"Installing/updating requirements from {REQUIREMENTS_FILE}...")
|
||||||
|
|
||||||
|
pip_cmd = [str(VENV_DIR / "bin" / "pip")]
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.check_call(
|
||||||
|
pip_cmd + ["install", "-r", str(REQUIREMENTS_FILE)],
|
||||||
|
stdout=None if verbose else subprocess.DEVNULL,
|
||||||
|
stderr=None if verbose else subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"Failed to install requirements: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.check_call(
|
||||||
|
pip_cmd + ["check"],
|
||||||
|
stdout=None if verbose else subprocess.DEVNULL,
|
||||||
|
stderr=None if verbose else subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print("Dependency conflict detected — your environment may be broken.")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def run_in_venv(main_func: Callable[[], None], verbose: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Ensure the script is running inside the expected virtual environment.
|
||||||
|
|
||||||
|
If not already in the venv:
|
||||||
|
- Create it if needed
|
||||||
|
- Install requirements
|
||||||
|
- Relaunch the script inside the venv
|
||||||
|
|
||||||
|
If already in the venv:
|
||||||
|
- Ensure requirements are installed
|
||||||
|
- Execute the given main function
|
||||||
|
|
||||||
|
Args:
|
||||||
|
main_func (Callable[[], None]): The function to execute after setup.
|
||||||
|
verbose (bool): If True, enables progress messages and pip output.
|
||||||
|
"""
|
||||||
|
if not is_in_venv():
|
||||||
|
if not VENV_DIR.exists():
|
||||||
|
create_venv(verbose)
|
||||||
|
install_requirements(verbose)
|
||||||
|
|
||||||
|
# Relaunch the script inside the virtual environment
|
||||||
|
os.execv(
|
||||||
|
str(VENV_DIR / "bin" / "python"),
|
||||||
|
[str(VENV_DIR / "bin" / "python")] + sys.argv,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
install_requirements(verbose)
|
||||||
|
main_func()
|
||||||
493
scripts/video_autoreduce.py
Executable file
493
scripts/video_autoreduce.py
Executable file
@ -0,0 +1,493 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# video_autoreduce.py
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import argcomplete
|
||||||
|
import colorama
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
creset = colorama.Fore.RESET
|
||||||
|
ccyan = colorama.Fore.CYAN
|
||||||
|
cyellow = colorama.Fore.YELLOW
|
||||||
|
cgreen = colorama.Fore.GREEN
|
||||||
|
cred = colorama.Fore.RED
|
||||||
|
|
||||||
|
|
||||||
|
def get_ffmpeg_codecs(codec_type="S"):
|
||||||
|
"""
|
||||||
|
Retrieve a list of supported codecs for a specific type from ffmpeg.
|
||||||
|
|
||||||
|
This function runs the `ffmpeg -codecs` command and parses its output to extract
|
||||||
|
the list of supported codecs based on the provided `codec_type` (e.g., 'S' for subtitles,
|
||||||
|
'V' for video, 'A' for audio). The list of codecs is returned as a set for efficient lookup.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
codec_type (str): The type of codecs to retrieve.
|
||||||
|
- "S" for subtitles (default)
|
||||||
|
- "V" for video
|
||||||
|
- "A" for audio
|
||||||
|
- "D" for data
|
||||||
|
- "T" for attachment
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
set: A set of codec names of the specified type.
|
||||||
|
If an error occurs or no codecs are found, an empty set is returned.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Run ffmpeg -codecs
|
||||||
|
result = subprocess.run(["ffmpeg", "-codecs"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True)
|
||||||
|
output = result.stdout
|
||||||
|
|
||||||
|
# Extract codecs using regex
|
||||||
|
codec_pattern = re.compile(rf"^[ D.E]+{codec_type}[ A-Z.]+ (\S+)",
|
||||||
|
re.MULTILINE)
|
||||||
|
codecs = codec_pattern.findall(output)
|
||||||
|
|
||||||
|
return set(codecs) # Return as a set for easy lookup
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print("Error running ffmpeg:", e)
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def findfreename(filepath, attempt=0):
|
||||||
|
'''
|
||||||
|
Given a filepath, it will try to find a free filename by appending a number to the name.
|
||||||
|
First, it tries using the filepath passed in the argument, then adds a number to the end. If all fail, it appends (#).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath (str): A string containing the full filepath.
|
||||||
|
attempt (int): The number of attempts already made to find a free filename.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The first free filepath found.
|
||||||
|
'''
|
||||||
|
attempt += 1
|
||||||
|
filename = str(filepath)[:str(filepath).rindex(".")]
|
||||||
|
extension = str(filepath)[str(filepath).rindex("."):]
|
||||||
|
copynumpath = filename + f"({attempt})" + extension
|
||||||
|
|
||||||
|
if not os.path.exists(filepath) and attempt <= 2:
|
||||||
|
return filepath
|
||||||
|
elif not os.path.exists(copynumpath):
|
||||||
|
return copynumpath
|
||||||
|
return findfreename(filepath, attempt)
|
||||||
|
|
||||||
|
|
||||||
|
def deletefile(filepath):
|
||||||
|
'''Delete a file, Returns a boolean
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath : A string containing the full filepath
|
||||||
|
Returns:
|
||||||
|
Bool : The success of the operation
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
os.remove(filepath)
|
||||||
|
except OSError:
|
||||||
|
print(f"{cred}Error deleting {filepath}{creset}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"{cgreen}Successfully deleted {filepath}{creset}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_output_path(output_path):
|
||||||
|
'''
|
||||||
|
Ensure that the output directory exists. If it doesn't, attempt to create it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_path (str): The path of the output directory to be ensured.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
os.makedirs(output_path, exist_ok=True)
|
||||||
|
# Attempts to create the output directory if it doesn't exist.
|
||||||
|
# exist_ok=True ensures that an OSError is not raised if the directory already exists.
|
||||||
|
except OSError as e:
|
||||||
|
# If an OSError occurs during directory creation, print an error message and exit the program.
|
||||||
|
print(f"{cred}Failed to create output directory:{creset} {e}",
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def has_supported_subs(video_file, supported_subtitle_codecs, debug=False):
|
||||||
|
"""
|
||||||
|
Check if the given video file contains subtitles in a supported codec.
|
||||||
|
|
||||||
|
This function uses `ffprobe` to extract subtitle stream information from the
|
||||||
|
video file and checks whether any subtitle stream has a codec that matches
|
||||||
|
one of the supported codecs provided in the `supported_subtitle_codecs` list.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
video_file (str): Path to the video file to be checked.
|
||||||
|
supported_subtitle_codecs (set): Set of subtitle codec names that are considered supported.
|
||||||
|
debug (bool): If True, prints debug information for errors. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if at least one subtitle stream is found with a supported codec,
|
||||||
|
False otherwise or in case of an error.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run([
|
||||||
|
'ffprobe', '-v', 'error', '-select_streams', 's', '-show_entries',
|
||||||
|
'stream=index,codec_name', '-of', 'json', video_file
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
if debug:
|
||||||
|
print(f"ffprobe error: {result.stderr.strip()}")
|
||||||
|
return False # Assume no supported subtitles on failure
|
||||||
|
|
||||||
|
try:
|
||||||
|
streams = json.loads(result.stdout).get("streams", [])
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
if debug:
|
||||||
|
print(f"JSON parsing error: {e}")
|
||||||
|
return False # Assume no supported subtitles if JSON is malformed
|
||||||
|
|
||||||
|
return any(
|
||||||
|
stream.get("codec_name") in supported_subtitle_codecs
|
||||||
|
for stream in streams)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if debug:
|
||||||
|
print(f"Unexpected error: {e}")
|
||||||
|
return False # Assume no supported subtitles
|
||||||
|
|
||||||
|
|
||||||
|
def find_videos_to_convert(input_path, max_height=720, debug=False):
|
||||||
|
'''
|
||||||
|
Find video files in the specified directory tree that meet conversion criteria.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_path (str): The path of the directory to search for video files.
|
||||||
|
max_height (int, optional): The maximum height (in pixels) of the video to consider for conversion. Default is 720.
|
||||||
|
debug (bool, optional): If True, print debug messages. Default is False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of paths to video files that meet the conversion criteria.
|
||||||
|
'''
|
||||||
|
# Print a message indicating the start of the scanning process
|
||||||
|
print(
|
||||||
|
f"{cgreen}Scanning {input_path} for video files to convert.{creset}\n")
|
||||||
|
|
||||||
|
# Initialize lists to store all video files and valid video files
|
||||||
|
video_files = []
|
||||||
|
valid_videos_files = []
|
||||||
|
|
||||||
|
# Find all video files in the directory tree
|
||||||
|
for root, dirs, files in os.walk(input_path):
|
||||||
|
for file in files:
|
||||||
|
fullpath = os.path.join(root, file)
|
||||||
|
# Check if the file is a video file with one of the specified extensions
|
||||||
|
if (file.lower().endswith(
|
||||||
|
('.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.divx'))
|
||||||
|
and os.path.isfile(fullpath)):
|
||||||
|
video_files.append(fullpath)
|
||||||
|
|
||||||
|
# List of encodings to try for decoding output from ffprobe
|
||||||
|
ENCODINGS_TO_TRY = ['utf-8', 'latin-1', 'iso-8859-1']
|
||||||
|
|
||||||
|
# Iterate through each video file
|
||||||
|
for video_file in video_files:
|
||||||
|
try:
|
||||||
|
# Run ffprobe command to get video resolution
|
||||||
|
resolution_output = subprocess.check_output(
|
||||||
|
[
|
||||||
|
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
|
||||||
|
'-show_entries', 'stream=width,height', '-of', 'csv=p=0',
|
||||||
|
video_file
|
||||||
|
],
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
|
||||||
|
# Attempt to decode the output using different encodings
|
||||||
|
dimensions_str = None
|
||||||
|
for encoding in ENCODINGS_TO_TRY:
|
||||||
|
try:
|
||||||
|
dimensions_str = resolution_output.decode(
|
||||||
|
encoding).strip().rstrip(',')
|
||||||
|
|
||||||
|
dimensions = [
|
||||||
|
int(d) for d in dimensions_str.split(',')
|
||||||
|
if d.strip().isdigit()
|
||||||
|
]
|
||||||
|
if len(dimensions) >= 2:
|
||||||
|
width, height = dimensions[:2]
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unexpected dimensions format: {dimensions_str}")
|
||||||
|
break # Stop trying encodings once successful decoding and conversion
|
||||||
|
except (UnicodeDecodeError, ValueError):
|
||||||
|
continue # Try the next encoding
|
||||||
|
|
||||||
|
# If decoding was unsuccessful with all encodings, skip this video
|
||||||
|
if dimensions_str is None:
|
||||||
|
if debug:
|
||||||
|
print(
|
||||||
|
f"{cred}Error processing {video_file}: Unable to decode output from ffprobe using any of the specified encodings.{creset}",
|
||||||
|
file=sys.stderr)
|
||||||
|
continue # Skip this video and continue with the next one
|
||||||
|
|
||||||
|
# Ignore vertical videos
|
||||||
|
if height > width:
|
||||||
|
continue # Skip this video and continue with the next one
|
||||||
|
|
||||||
|
# Check if the video height exceeds the maximum allowed height
|
||||||
|
if height > max_height:
|
||||||
|
valid_videos_files.append(video_file)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
if debug:
|
||||||
|
# Print error message if subprocess call fails
|
||||||
|
if e.output is not None:
|
||||||
|
decoded_error = e.output.decode("utf-8")
|
||||||
|
print(
|
||||||
|
f"{cred}Error processing {video_file}: {decoded_error}{creset}\n",
|
||||||
|
file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"{cred}Error processing {video_file}: No output from subprocess{creset}\n",
|
||||||
|
file=sys.stderr)
|
||||||
|
except ValueError as e:
|
||||||
|
if debug:
|
||||||
|
# Print error message if value error occurs during decoding
|
||||||
|
print(
|
||||||
|
f"{cred}Error processing {video_file}: ValueError decoding the file{creset}\n",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
valid_videos_files.sort()
|
||||||
|
|
||||||
|
return valid_videos_files
|
||||||
|
|
||||||
|
|
||||||
|
def convert_videos(input_path, output_path, max_height=720, debug=False):
|
||||||
|
'''
|
||||||
|
Convert videos taller than a specified height to 720p resolution with x265 encoding and MKV container.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_path (str): The path of the directory containing input video files.
|
||||||
|
output_path (str): The path of the directory where converted video files will be saved.
|
||||||
|
max_height (int, optional): The maximum height (in pixels) of the video to consider for conversion. Default is 720.
|
||||||
|
debug (bool, optional): If True, print debug messages. Default is False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if conversion succeeds, False otherwise.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Get supported subtitle codecs
|
||||||
|
supported_subtitle_codecs = get_ffmpeg_codecs("S")
|
||||||
|
|
||||||
|
# Find video files to convert in the input directory
|
||||||
|
video_files = find_videos_to_convert(input_path, max_height, debug)
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
# If no video files found, print a message and return False
|
||||||
|
if len(video_files) == 0:
|
||||||
|
print(f"No video files to convert found.\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ensure the output directory exists
|
||||||
|
ensure_output_path(output_path)
|
||||||
|
|
||||||
|
# Print a message indicating the start of the conversion process
|
||||||
|
print(
|
||||||
|
f"Converting {len(video_files)} videos taller than {max_height} pixels to 720p resolution with x265 encoding and MKV container...\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Variable to keep a track of the current_file in case of failure
|
||||||
|
current_file = None
|
||||||
|
|
||||||
|
# Iterate through each video file for conversion
|
||||||
|
for video_file in video_files:
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# Print conversion progress information
|
||||||
|
print(
|
||||||
|
f"{cgreen}****** Starting conversion {counter} of {len(video_files)}: '{os.path.basename(video_file)}'...{creset}"
|
||||||
|
)
|
||||||
|
print(f"{ccyan}Original file:{creset}")
|
||||||
|
print(video_file)
|
||||||
|
|
||||||
|
# Generate output file path and name
|
||||||
|
output_file = findfreename(
|
||||||
|
os.path.join(
|
||||||
|
output_path,
|
||||||
|
os.path.splitext(os.path.basename(video_file))[0] + '.mkv'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get original video width and height
|
||||||
|
video_info = subprocess.run([
|
||||||
|
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
|
||||||
|
'-show_entries', 'stream=width,height', '-of', 'csv=p=0',
|
||||||
|
video_file
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True)
|
||||||
|
width, height = map(
|
||||||
|
int,
|
||||||
|
video_info.stdout.strip().rstrip(',').split(','))
|
||||||
|
|
||||||
|
# Calculate the scaled width maintaining aspect ratio
|
||||||
|
scaled_width = int(max_height * (width / height))
|
||||||
|
# Ensure the width is even
|
||||||
|
if scaled_width % 2 != 0:
|
||||||
|
scaled_width += 1
|
||||||
|
|
||||||
|
# Keep a track of the current_file in case of failure
|
||||||
|
current_file = output_file
|
||||||
|
|
||||||
|
# Check if the video has supported subtitles
|
||||||
|
include_subs = has_supported_subs(video_file,
|
||||||
|
supported_subtitle_codecs, debug)
|
||||||
|
|
||||||
|
# Construct the FFmpeg command dynamically
|
||||||
|
ffmpeg_command = [
|
||||||
|
'ffmpeg', '-n', '-i', video_file, '-map', '0:v', '-c:v',
|
||||||
|
'libx265', '-vf', f'scale={scaled_width}:{max_height}',
|
||||||
|
'-preset', 'medium', '-crf', '28', '-map', '0:a?', '-c:a',
|
||||||
|
'copy'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Only include subtitles if they are supported
|
||||||
|
if include_subs:
|
||||||
|
ffmpeg_command.extend(['-map', '0:s?', '-c:s', 'copy'])
|
||||||
|
|
||||||
|
ffmpeg_command.append(output_file)
|
||||||
|
|
||||||
|
# Print the FFmpeg command as a single string
|
||||||
|
if (debug):
|
||||||
|
print("FFmpeg command: " + ' '.join(ffmpeg_command))
|
||||||
|
|
||||||
|
# Run the FFmpeg command
|
||||||
|
process = subprocess.run(ffmpeg_command,
|
||||||
|
check=True,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
|
||||||
|
# Check if ffmpeg process returned successfully
|
||||||
|
if process.returncode != 0:
|
||||||
|
raise subprocess.CalledProcessError(process.returncode,
|
||||||
|
process.args,
|
||||||
|
output=process.stderr)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
# If we have a partial video converted, delete it due to the failure
|
||||||
|
if current_file:
|
||||||
|
deletefile(current_file)
|
||||||
|
|
||||||
|
# Handle subprocess errors
|
||||||
|
if debug:
|
||||||
|
if e.output is not None:
|
||||||
|
decoded_error = e.output.decode("utf-8")
|
||||||
|
print(
|
||||||
|
f"{cred}Error processing {video_file}: {decoded_error}{creset}\n",
|
||||||
|
file=sys.stderr)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"{cred}Error processing {video_file}: No output from subprocess{creset}\n",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(f"{cyellow}Conversions cancelled, cleaning up...{creset}")
|
||||||
|
|
||||||
|
# If we have a partial video converted, delete it due to the failure
|
||||||
|
if current_file:
|
||||||
|
deletefile(current_file)
|
||||||
|
current_file = None
|
||||||
|
|
||||||
|
exit()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# If we have a partial video converted, delete it due to the failure
|
||||||
|
if current_file:
|
||||||
|
deletefile(current_file)
|
||||||
|
|
||||||
|
# Handle other exceptions
|
||||||
|
if debug:
|
||||||
|
print(
|
||||||
|
f"{cred}Error processing {video_file}: {str(e)}{creset}\n",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# Print success message if conversion is successful
|
||||||
|
print(
|
||||||
|
f"{ccyan}Successfully converted video to output file:{creset}")
|
||||||
|
print(f"{output_file}\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.chmod(output_file, 0o777)
|
||||||
|
except PermissionError:
|
||||||
|
print(f"{cred}PermissionError on: '{output_file}'{creset}")
|
||||||
|
|
||||||
|
# Remove the now succefully converted filepath
|
||||||
|
current_file = None
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
'''
|
||||||
|
Main function to parse command line arguments and initiate video conversion.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Create argument parser
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=
|
||||||
|
'Convert videos taller than a certain height to 720p resolution with x265 encoding and MKV container.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define command line arguments
|
||||||
|
parser.add_argument(
|
||||||
|
'input_path',
|
||||||
|
nargs='?',
|
||||||
|
default=os.getcwd(),
|
||||||
|
help=
|
||||||
|
'directory path to search for video files (default: current directory)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-o',
|
||||||
|
'--output-path',
|
||||||
|
default=os.path.join(os.getcwd(), 'converted'),
|
||||||
|
help='directory path to save converted videos (default: ./converted)')
|
||||||
|
parser.add_argument(
|
||||||
|
'-mh',
|
||||||
|
'--max-height',
|
||||||
|
type=int,
|
||||||
|
default=720,
|
||||||
|
help='maximum height of videos to be converted (default: 720)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug',
|
||||||
|
action='store_true',
|
||||||
|
help='enable debug mode for printing additional error messages')
|
||||||
|
|
||||||
|
# Enable autocomplete for argparse
|
||||||
|
argcomplete.autocomplete(parser)
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Convert videos
|
||||||
|
convert_videos(args.input_path, args.output_path, args.max_height,
|
||||||
|
args.debug)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Execute main function when the script is run directly
|
||||||
|
main()
|
||||||
133
scripts/video_autoreduce_rename.py
Executable file
133
scripts/video_autoreduce_rename.py
Executable file
@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# video_autoreduce_rename.py
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import argcomplete
|
||||||
|
import colorama
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
creset = colorama.Fore.RESET
|
||||||
|
ccyan = colorama.Fore.CYAN
|
||||||
|
cyellow = colorama.Fore.YELLOW
|
||||||
|
cgreen = colorama.Fore.GREEN
|
||||||
|
cred = colorama.Fore.RED
|
||||||
|
|
||||||
|
|
||||||
|
def autorename(input_path, max_height=720, debug=False):
|
||||||
|
'''
|
||||||
|
Rename video files and folders in the specified directory tree that contain a resolution
|
||||||
|
in their name to the specified max_height resolution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_path (str): The path of the directory to search for video files and folders.
|
||||||
|
max_height (int, optional): The maximum height (in pixels) of the video to consider for conversion. Default is 720.
|
||||||
|
debug (bool, optional): If True, print debug messages. Default is False.
|
||||||
|
'''
|
||||||
|
# Define patterns to search for various resolutions
|
||||||
|
resolution_patterns = {
|
||||||
|
2160: r'(4096x2160|3840x2160|2880p|2160p|2160|1440p|4k)',
|
||||||
|
1080: r'(1920x1080|1080p|1080)',
|
||||||
|
720: r'(1280x720|720p|720)',
|
||||||
|
}
|
||||||
|
|
||||||
|
files_to_rename = []
|
||||||
|
dirs_to_rename = []
|
||||||
|
|
||||||
|
for dirpath, dirnames, filenames in os.walk(input_path, topdown=True):
|
||||||
|
# Collect files to rename
|
||||||
|
for filename in filenames:
|
||||||
|
for resolution, pattern in resolution_patterns.items():
|
||||||
|
# Only process resolutions greater than max_height
|
||||||
|
if resolution > max_height and re.search(
|
||||||
|
pattern, filename, re.IGNORECASE):
|
||||||
|
old_file = os.path.join(dirpath, filename)
|
||||||
|
new_filename = re.sub(pattern,
|
||||||
|
f'{max_height}p',
|
||||||
|
filename,
|
||||||
|
flags=re.IGNORECASE)
|
||||||
|
new_file = os.path.join(dirpath, new_filename)
|
||||||
|
if old_file != new_file:
|
||||||
|
files_to_rename.append((old_file, new_file))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Collect directories to rename
|
||||||
|
for dirname in dirnames:
|
||||||
|
for resolution, pattern in resolution_patterns.items():
|
||||||
|
# Only process resolutions greater than max_height
|
||||||
|
if resolution > max_height and re.search(
|
||||||
|
pattern, dirname, re.IGNORECASE):
|
||||||
|
old_dir = os.path.join(dirpath, dirname)
|
||||||
|
new_dirname = re.sub(pattern,
|
||||||
|
f'{max_height}p',
|
||||||
|
dirname,
|
||||||
|
flags=re.IGNORECASE)
|
||||||
|
new_dir = os.path.join(dirpath, new_dirname)
|
||||||
|
if old_dir != new_dir:
|
||||||
|
dirs_to_rename.append((old_dir, new_dir))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Rename files first
|
||||||
|
for old_file, new_file in files_to_rename:
|
||||||
|
if os.path.exists(new_file):
|
||||||
|
print(f"Error: Target filename {new_file} already exists.")
|
||||||
|
continue
|
||||||
|
os.rename(old_file, new_file)
|
||||||
|
if debug:
|
||||||
|
print(f'Renamed file: {old_file} -> {new_file}')
|
||||||
|
|
||||||
|
# Rename directories after
|
||||||
|
for old_dir, new_dir in sorted(dirs_to_rename, key=lambda x: -len(x[0])):
|
||||||
|
if os.path.exists(new_dir):
|
||||||
|
print(f"Error: Target directory name {new_dir} already exists.")
|
||||||
|
continue
|
||||||
|
os.rename(old_dir, new_dir)
|
||||||
|
if debug:
|
||||||
|
print(f'Renamed directory: {old_dir} -> {new_dir}')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
'''
|
||||||
|
Main function to parse command line arguments and initiate file renamings.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Create argument parser
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=
|
||||||
|
'Rename video files containing resolutions in their filenames to a specified max_height resolution.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Define command line arguments
|
||||||
|
parser.add_argument(
|
||||||
|
'input_path',
|
||||||
|
nargs='?',
|
||||||
|
default=os.getcwd(),
|
||||||
|
help=
|
||||||
|
'directory path to search for video files (default: current directory)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-mh',
|
||||||
|
'--max-height',
|
||||||
|
type=int,
|
||||||
|
default=720,
|
||||||
|
help='maximum height of videos to be converted (default: 720)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug',
|
||||||
|
action='store_true',
|
||||||
|
help='enable debug mode for printing additional messages')
|
||||||
|
|
||||||
|
# Enable autocomplete for argparse
|
||||||
|
argcomplete.autocomplete(parser)
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Rename videos
|
||||||
|
autorename(args.input_path, args.max_height, args.debug)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Execute main function when the script is run directly
|
||||||
|
main()
|
||||||
132
scripts/video_manage_audio.py
Executable file
132
scripts/video_manage_audio.py
Executable file
@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# video_manage_audio.py
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import argcomplete
|
||||||
|
import colorama
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Initialize colorama for colored output in the terminal
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
creset = colorama.Fore.RESET
|
||||||
|
ccyan = colorama.Fore.CYAN
|
||||||
|
cyellow = colorama.Fore.YELLOW
|
||||||
|
cgreen = colorama.Fore.GREEN
|
||||||
|
cred = colorama.Fore.RED
|
||||||
|
|
||||||
|
|
||||||
|
def process_audio(file_path, track, command):
|
||||||
|
"""
|
||||||
|
Modify audio tracks of a video file based on the given command.
|
||||||
|
- 'remove': Remove the specified audio track while keeping everything else.
|
||||||
|
- 'keep': Keep only the specified audio track and remove all others.
|
||||||
|
The function preserves video, subtitles, and metadata.
|
||||||
|
"""
|
||||||
|
print(f"{cgreen}Processing file: {file_path}{creset}")
|
||||||
|
output_file = f"{os.path.splitext(file_path)[0]}_{command}_audio{os.path.splitext(file_path)[1]}"
|
||||||
|
|
||||||
|
# Construct ffmpeg command based on user choice
|
||||||
|
if command == "remove":
|
||||||
|
# Remove only the specified audio track while keeping all other streams
|
||||||
|
ffmpeg_command = [
|
||||||
|
"ffmpeg", "-i", file_path, "-map", "0", "-map", f"-0:a:{track}",
|
||||||
|
"-c", "copy", output_file
|
||||||
|
]
|
||||||
|
elif command == "keep":
|
||||||
|
# Keep only the specified audio track while preserving video, subtitles, and metadata
|
||||||
|
ffmpeg_command = [
|
||||||
|
"ffmpeg", "-i", file_path, "-map", "0:v", "-map", f"0:a:{track}",
|
||||||
|
"-map", "0:s?", "-map", "0:t?", "-c", "copy", output_file
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
print(f"{cred}Invalid command: {command}{creset}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute the ffmpeg command and capture output
|
||||||
|
result = subprocess.run(ffmpeg_command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(
|
||||||
|
f"{ccyan}Audio processing complete. Output saved to {output_file}{creset}\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"{cred}Command failed with return code {result.returncode}{creset}\n"
|
||||||
|
)
|
||||||
|
print(f"{cred}Error output:\n{result.stderr}{creset}\n")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"{cred}Error processing audio: {e}{creset}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def process_directory(dir_path, track, command):
|
||||||
|
"""
|
||||||
|
Recursively processes all video files in the specified directory.
|
||||||
|
Applies the chosen audio modification (remove or keep) to each file.
|
||||||
|
"""
|
||||||
|
for root, _, files in os.walk(dir_path):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith((".mp4", ".mkv", ".avi", ".mov")):
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
process_audio(file_path, track, command)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main function to parse command-line arguments and initiate the audio processing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create argument parser
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Manage audio tracks in video files.")
|
||||||
|
|
||||||
|
# Define command line arguments
|
||||||
|
# Add a positional argument for the command
|
||||||
|
parser.add_argument('command',
|
||||||
|
choices=['remove', 'keep'],
|
||||||
|
help='Command to run')
|
||||||
|
|
||||||
|
# Add other arguments with both short and long options, including defaults
|
||||||
|
parser.add_argument("-t",
|
||||||
|
"--track",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="Audio track index (default is 0).")
|
||||||
|
parser.add_argument("-f",
|
||||||
|
"--file",
|
||||||
|
type=str,
|
||||||
|
help="Path to a specific video file.")
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--dir",
|
||||||
|
type=str,
|
||||||
|
default=os.getcwd(),
|
||||||
|
help="Directory to process (default is current directory).")
|
||||||
|
|
||||||
|
# Enable autocomplete for command-line arguments
|
||||||
|
argcomplete.autocomplete(parser)
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Process a single file if provided, otherwise process a directory
|
||||||
|
if args.file:
|
||||||
|
if os.path.isfile(args.file):
|
||||||
|
process_audio(args.file, args.track, args.command)
|
||||||
|
else:
|
||||||
|
print(f"{cred}File {args.file} does not exist.{creset}")
|
||||||
|
else:
|
||||||
|
if os.path.isdir(args.dir):
|
||||||
|
process_directory(args.dir, args.track, args.command)
|
||||||
|
else:
|
||||||
|
print(f"{cred}Directory {args.dir} does not exist.{creset}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
142
scripts/video_manage_subtitles.py
Executable file
142
scripts/video_manage_subtitles.py
Executable file
@ -0,0 +1,142 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# video_manage_subtitles.py
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import argcomplete
|
||||||
|
import colorama
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
# Initialize colorama for colored output in the terminal
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
creset = colorama.Fore.RESET
|
||||||
|
ccyan = colorama.Fore.CYAN
|
||||||
|
cyellow = colorama.Fore.YELLOW
|
||||||
|
cgreen = colorama.Fore.GREEN
|
||||||
|
cred = colorama.Fore.RED
|
||||||
|
|
||||||
|
|
||||||
|
def process_subtitles(file_path, track, command):
|
||||||
|
"""
|
||||||
|
Modify subtitles tracks of a video file based on the given command.
|
||||||
|
- 'remove': Remove the specified subtitle track while keeping everything else.
|
||||||
|
- 'keep': Keep only the specified subtitle track and remove all others.
|
||||||
|
- 'none': Remove all subtitle tracks.
|
||||||
|
The function preserves video, audio, and metadata.
|
||||||
|
"""
|
||||||
|
print(f"{cgreen}Processing file: {file_path}{creset}")
|
||||||
|
output_file = f"{os.path.splitext(file_path)[0]}_{command}_subtitles{os.path.splitext(file_path)[1]}"
|
||||||
|
|
||||||
|
# Construct ffmpeg command based on user choice
|
||||||
|
if command == "remove":
|
||||||
|
# Remove only the specified subtitle track while keeping all other streams
|
||||||
|
ffmpeg_command = [
|
||||||
|
"ffmpeg", "-i", file_path, "-map", "0", "-map", f"-0:s:{track}",
|
||||||
|
"-c", "copy", output_file
|
||||||
|
]
|
||||||
|
elif command == "keep":
|
||||||
|
# Keep only the specified subtitle track while preserving video, audio, and metadata
|
||||||
|
ffmpeg_command = [
|
||||||
|
"ffmpeg", "-i", file_path, "-map", "0:v", "-map", "0:a", "-map",
|
||||||
|
f"0:s:{track}", "-map", "0:t?", "-c", "copy", output_file
|
||||||
|
]
|
||||||
|
elif command == "none":
|
||||||
|
# Remove all subtitle tracks
|
||||||
|
ffmpeg_command = [
|
||||||
|
"ffmpeg", "-i", file_path, "-map", "0", "-map", "-0:s", "-c",
|
||||||
|
"copy", output_file
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
print(f"{cred}Invalid command: {command}{creset}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Execute the ffmpeg command and capture output
|
||||||
|
result = subprocess.run(ffmpeg_command,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
print(
|
||||||
|
f"{ccyan}Subtitles processing complete. Output saved to {output_file}{creset}\n"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"{cred}Command failed with return code {result.returncode}{creset}\n"
|
||||||
|
)
|
||||||
|
print(f"{cred}Error output:\n{result.stderr}{creset}\n")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"{cred}Error processing subtitles: {e}{creset}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def process_directory(dir_path, track, command):
|
||||||
|
"""
|
||||||
|
Recursively processes all video files in the specified directory.
|
||||||
|
Applies the chosen subtitle modification (remove, keep, none) to each file.
|
||||||
|
"""
|
||||||
|
for root, _, files in os.walk(dir_path):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith((".mp4", ".mkv", ".avi", ".mov")):
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
process_subtitles(file_path, track, command)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main function to parse command-line arguments and initiate the subtitle processing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create argument parser
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Manage subtitle tracks in video files.")
|
||||||
|
|
||||||
|
# Define command line arguments
|
||||||
|
# Add a positional argument for the command
|
||||||
|
parser.add_argument('command',
|
||||||
|
choices=['remove', 'keep', 'none'],
|
||||||
|
help='Command to run (remove, keep, or none)')
|
||||||
|
|
||||||
|
# Add other arguments with both short and long options, including defaults
|
||||||
|
parser.add_argument(
|
||||||
|
"-t",
|
||||||
|
"--track",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help=
|
||||||
|
"Subtitle track index (default is 0). Use 'none' to remove all subtitles."
|
||||||
|
)
|
||||||
|
parser.add_argument("-f",
|
||||||
|
"--file",
|
||||||
|
type=str,
|
||||||
|
help="Path to a specific video file.")
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--dir",
|
||||||
|
type=str,
|
||||||
|
default=os.getcwd(),
|
||||||
|
help="Directory to process (default is current directory).")
|
||||||
|
|
||||||
|
# Enable autocomplete for command-line arguments
|
||||||
|
argcomplete.autocomplete(parser)
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Process a single file if provided, otherwise process a directory
|
||||||
|
if args.file:
|
||||||
|
if os.path.isfile(args.file):
|
||||||
|
process_subtitles(args.file, args.track, args.command)
|
||||||
|
else:
|
||||||
|
print(f"{cred}File {args.file} does not exist.{creset}")
|
||||||
|
else:
|
||||||
|
if os.path.isdir(args.dir):
|
||||||
|
process_directory(args.dir, args.track, args.command)
|
||||||
|
else:
|
||||||
|
print(f"{cred}Directory {args.dir} does not exist.{creset}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import argparse
|
|
||||||
import argcomplete
|
|
||||||
import colorama
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
colorama.init()
|
|
||||||
|
|
||||||
creset = colorama.Fore.RESET
|
|
||||||
ccyan = colorama.Fore.CYAN
|
|
||||||
cyellow = colorama.Fore.YELLOW
|
|
||||||
cgreen = colorama.Fore.GREEN
|
|
||||||
cred = colorama.Fore.RED
|
|
||||||
|
|
||||||
|
|
||||||
# Function to remove audio track from a video using ffmpeg
|
|
||||||
def remove_audio_track(file_path, track):
|
|
||||||
print(f"{cgreen}Processing file: {file_path}{creset}")
|
|
||||||
output_file = f"{os.path.splitext(file_path)[0]}_no_audio{os.path.splitext(file_path)[1]}"
|
|
||||||
# ffmpeg command to remove the specified audio track and keep other streams
|
|
||||||
command = [
|
|
||||||
"ffmpeg", "-i", file_path, "-map", "0", "-map", f"-0:a:{track}", "-c",
|
|
||||||
"copy", output_file
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(command,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(
|
|
||||||
f"{ccyan}Audio track {track} removed. Output saved to {output_file}{creset}\n"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f"{cred}Command failed with return code {result.returncode}{creset}\n"
|
|
||||||
)
|
|
||||||
print(f"{cred}Error output:", result.stderr, f"{creset}\n")
|
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
print(f"{cred}Error removing audio track: {e}{creset}\n")
|
|
||||||
|
|
||||||
|
|
||||||
# Function to recursively process videos in a directory
|
|
||||||
def process_directory(dir_path, track):
|
|
||||||
for root, _, files in os.walk(dir_path):
|
|
||||||
for file in files:
|
|
||||||
if file.endswith((".mp4", ".mkv", ".avi",
|
|
||||||
".mov")): # Add more formats as needed
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
remove_audio_track(file_path, track)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Set up argument parser
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Remove audio track from video files.")
|
|
||||||
parser.add_argument("--track",
|
|
||||||
type=int,
|
|
||||||
default=0,
|
|
||||||
help="Audio track index to remove (default is 0).")
|
|
||||||
parser.add_argument("--file",
|
|
||||||
type=str,
|
|
||||||
help="Path to a specific video file.")
|
|
||||||
parser.add_argument(
|
|
||||||
"--dir",
|
|
||||||
type=str,
|
|
||||||
default=os.getcwd(),
|
|
||||||
help="Directory to process (default is current directory).")
|
|
||||||
|
|
||||||
# Enable autocomplete for argparse
|
|
||||||
argcomplete.autocomplete(parser)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# Process single file if provided
|
|
||||||
if args.file:
|
|
||||||
if os.path.isfile(args.file):
|
|
||||||
remove_audio_track(args.file, args.track)
|
|
||||||
else:
|
|
||||||
print(f"File {args.file} does not exist.")
|
|
||||||
# Otherwise, process all files in the specified directory
|
|
||||||
else:
|
|
||||||
if os.path.isdir(args.dir):
|
|
||||||
process_directory(args.dir, args.track)
|
|
||||||
else:
|
|
||||||
print(f"Directory {args.dir} does not exist.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Loading…
Reference in New Issue
Block a user