Compare commits
4 Commits
642f6d90db
...
29446a718d
| Author | SHA1 | Date | |
|---|---|---|---|
| 29446a718d | |||
| df8bdd903d | |||
| c31007498e | |||
| 87847859c5 |
@ -1,5 +1,20 @@
|
|||||||
# scripts/library/__init__.py
|
# scripts/library/__init__.py
|
||||||
|
|
||||||
|
from .tools import (
|
||||||
|
apply_resolution_rename,
|
||||||
|
deletefile,
|
||||||
|
findfreename,
|
||||||
|
get_intermediate_dirs,
|
||||||
|
get_spacer,
|
||||||
|
)
|
||||||
from .venv_utils import parse_verbose, run_in_venv
|
from .venv_utils import parse_verbose, run_in_venv
|
||||||
|
|
||||||
__all__ = ["parse_verbose", "run_in_venv"]
|
__all__ = [
|
||||||
|
"apply_resolution_rename",
|
||||||
|
"deletefile",
|
||||||
|
"findfreename",
|
||||||
|
"get_intermediate_dirs",
|
||||||
|
"get_spacer",
|
||||||
|
"parse_verbose",
|
||||||
|
"run_in_venv",
|
||||||
|
]
|
||||||
|
|||||||
134
scripts/library/tools.py
Normal file
134
scripts/library/tools.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import colorama
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
colorama.init()
|
||||||
|
|
||||||
|
creset = colorama.Fore.RESET
|
||||||
|
cgreen = colorama.Fore.GREEN
|
||||||
|
cred = colorama.Fore.RED
|
||||||
|
|
||||||
|
# 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)",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wrap each pattern to only match when isolated by non-alphanumeric chars or string boundaries
|
||||||
|
resolution_patterns = {
|
||||||
|
res: rf"(?<![a-zA-Z0-9]){pat}(?![a-zA-Z0-9])"
|
||||||
|
for res, pat in resolution_patterns.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_resolution_rename(name, max_height):
|
||||||
|
"""
|
||||||
|
Apply resolution substitution to a filename or directory name if an isolated
|
||||||
|
resolution pattern greater than max_height is found.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The filename or directory name to process (without path).
|
||||||
|
max_height (int): The target resolution height to substitute to.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The renamed string if a match was found, or the original string unchanged.
|
||||||
|
"""
|
||||||
|
for resolution, pattern in resolution_patterns.items():
|
||||||
|
if resolution > max_height and re.search(pattern, name, re.IGNORECASE):
|
||||||
|
return re.sub(pattern, f"{max_height}p", name, flags=re.IGNORECASE)
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
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 get_intermediate_dirs(input_path, file_path):
|
||||||
|
"""
|
||||||
|
Return the list of directory paths between input_path (exclusive) and
|
||||||
|
the directory containing file_path (inclusive), ordered deepest first.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_path (str): The root path (exclusive upper bound).
|
||||||
|
file_path (str): The full path to a file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of directory paths, deepest first.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If file_path not under input_path.
|
||||||
|
"""
|
||||||
|
input_path = os.path.realpath(input_path)
|
||||||
|
current = os.path.realpath(os.path.dirname(file_path))
|
||||||
|
|
||||||
|
if current != input_path and not current.startswith(input_path + os.sep):
|
||||||
|
raise ValueError(
|
||||||
|
f"file_path '{file_path}' is not under input_path '{input_path}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
dirs = []
|
||||||
|
|
||||||
|
while current != input_path and current != os.path.dirname(current):
|
||||||
|
dirs.append(current)
|
||||||
|
current = os.path.dirname(current)
|
||||||
|
|
||||||
|
return dirs
|
||||||
|
|
||||||
|
|
||||||
|
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 get_spacer(name: str) -> str:
|
||||||
|
"""
|
||||||
|
Determine the spacer character ('-' or '_') to use for naming a virtual environment
|
||||||
|
based on the given project name. Default to underscore.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The project name to analyze.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The detected spacer ('-' or '_'), or '_' if none found.
|
||||||
|
"""
|
||||||
|
if "-" in name:
|
||||||
|
return "-"
|
||||||
|
elif "_" in name:
|
||||||
|
return "_"
|
||||||
|
return "_"
|
||||||
@ -9,6 +9,20 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 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 (
|
||||||
|
deletefile,
|
||||||
|
findfreename,
|
||||||
|
apply_resolution_rename,
|
||||||
|
get_intermediate_dirs,
|
||||||
|
)
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
@ -18,6 +32,8 @@ cyellow = colorama.Fore.YELLOW
|
|||||||
cgreen = colorama.Fore.GREEN
|
cgreen = colorama.Fore.GREEN
|
||||||
cred = colorama.Fore.RED
|
cred = colorama.Fore.RED
|
||||||
|
|
||||||
|
VIDEO_EXTENSIONS = (".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".divx")
|
||||||
|
|
||||||
|
|
||||||
def get_ffmpeg_codecs(codec_type="S"):
|
def get_ffmpeg_codecs(codec_type="S"):
|
||||||
"""
|
"""
|
||||||
@ -41,15 +57,13 @@ def get_ffmpeg_codecs(codec_type="S"):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Run ffmpeg -codecs
|
# Run ffmpeg -codecs
|
||||||
result = subprocess.run(["ffmpeg", "-codecs"],
|
result = subprocess.run(
|
||||||
capture_output=True,
|
["ffmpeg", "-codecs"], capture_output=True, text=True, check=True
|
||||||
text=True,
|
)
|
||||||
check=True)
|
|
||||||
output = result.stdout
|
output = result.stdout
|
||||||
|
|
||||||
# Extract codecs using regex
|
# Extract codecs using regex
|
||||||
codec_pattern = re.compile(rf"^[ D.E]+{codec_type}[ A-Z.]+ (\S+)",
|
codec_pattern = re.compile(rf"^[ D.E]+{codec_type}[ A-Z.]+ (\S+)", re.MULTILINE)
|
||||||
re.MULTILINE)
|
|
||||||
codecs = codec_pattern.findall(output)
|
codecs = codec_pattern.findall(output)
|
||||||
|
|
||||||
return set(codecs) # Return as a set for easy lookup
|
return set(codecs) # Return as a set for easy lookup
|
||||||
@ -59,50 +73,29 @@ def get_ffmpeg_codecs(codec_type="S"):
|
|||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
|
||||||
def findfreename(filepath, attempt=0):
|
def user_confirm(message, color="yellow"):
|
||||||
'''
|
"""
|
||||||
Given a filepath, it will try to find a free filename by appending a number to the name.
|
Prompt the user for a yes/no confirmation.
|
||||||
First, it tries using the filepath passed in the argument, then adds a number to the end. If all fail, it appends (#).
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filepath (str): A string containing the full filepath.
|
message (str): The confirmation message to display.
|
||||||
attempt (int): The number of attempts already made to find a free filename.
|
color (str): Color for the prompt ('yellow', 'red', 'green'). Default is 'yellow'.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The first free filepath found.
|
bool: True if the user confirms with 'Y', False otherwise.
|
||||||
'''
|
"""
|
||||||
attempt += 1
|
color_map = {
|
||||||
filename = str(filepath)[:str(filepath).rindex(".")]
|
"yellow": cyellow,
|
||||||
extension = str(filepath)[str(filepath).rindex("."):]
|
"red": cred,
|
||||||
copynumpath = filename + f"({attempt})" + extension
|
"green": cgreen,
|
||||||
|
}
|
||||||
if not os.path.exists(filepath) and attempt <= 2:
|
c = color_map.get(color, cyellow)
|
||||||
return filepath
|
response = input(f"{c}{message}{creset} ").strip().upper()
|
||||||
elif not os.path.exists(copynumpath):
|
return response == "Y"
|
||||||
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):
|
def ensure_output_path(output_path):
|
||||||
'''
|
"""
|
||||||
Ensure that the output directory exists. If it doesn't, attempt to create it.
|
Ensure that the output directory exists. If it doesn't, attempt to create it.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -110,15 +103,14 @@ def ensure_output_path(output_path):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
'''
|
"""
|
||||||
try:
|
try:
|
||||||
os.makedirs(output_path, exist_ok=True)
|
os.makedirs(output_path, exist_ok=True)
|
||||||
# Attempts to create the output directory if it doesn't exist.
|
# 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.
|
# exist_ok=True ensures that an OSError is not raised if the directory already exists.
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
# If an OSError occurs during directory creation, print an error message and exit the program.
|
# 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}",
|
print(f"{cred}Failed to create output directory:{creset} {e}", file=sys.stderr)
|
||||||
file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@ -140,13 +132,23 @@ def has_supported_subs(video_file, supported_subtitle_codecs, debug=False):
|
|||||||
False otherwise or in case of an error.
|
False otherwise or in case of an error.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = subprocess.run([
|
result = subprocess.run(
|
||||||
'ffprobe', '-v', 'error', '-select_streams', 's', '-show_entries',
|
[
|
||||||
'stream=index,codec_name', '-of', 'json', video_file
|
"ffprobe",
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"s",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=index,codec_name",
|
||||||
|
"-of",
|
||||||
|
"json",
|
||||||
|
video_file,
|
||||||
],
|
],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True)
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
if debug:
|
if debug:
|
||||||
@ -161,8 +163,8 @@ def has_supported_subs(video_file, supported_subtitle_codecs, debug=False):
|
|||||||
return False # Assume no supported subtitles if JSON is malformed
|
return False # Assume no supported subtitles if JSON is malformed
|
||||||
|
|
||||||
return any(
|
return any(
|
||||||
stream.get("codec_name") in supported_subtitle_codecs
|
stream.get("codec_name") in supported_subtitle_codecs for stream in streams
|
||||||
for stream in streams)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if debug:
|
if debug:
|
||||||
@ -171,7 +173,7 @@ def has_supported_subs(video_file, supported_subtitle_codecs, debug=False):
|
|||||||
|
|
||||||
|
|
||||||
def find_videos_to_convert(input_path, max_height=720, debug=False):
|
def find_videos_to_convert(input_path, max_height=720, debug=False):
|
||||||
'''
|
"""
|
||||||
Find video files in the specified directory tree that meet conversion criteria.
|
Find video files in the specified directory tree that meet conversion criteria.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -181,10 +183,9 @@ def find_videos_to_convert(input_path, max_height=720, debug=False):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: A list of paths to video files that meet the conversion criteria.
|
list: A list of paths to video files that meet the conversion criteria.
|
||||||
'''
|
"""
|
||||||
# Print a message indicating the start of the scanning process
|
# Print a message indicating the start of the scanning process
|
||||||
print(
|
print(f"{cgreen}Scanning {input_path} for video files to convert.{creset}\n")
|
||||||
f"{cgreen}Scanning {input_path} for video files to convert.{creset}\n")
|
|
||||||
|
|
||||||
# Initialize lists to store all video files and valid video files
|
# Initialize lists to store all video files and valid video files
|
||||||
video_files = []
|
video_files = []
|
||||||
@ -195,13 +196,11 @@ def find_videos_to_convert(input_path, max_height=720, debug=False):
|
|||||||
for file in files:
|
for file in files:
|
||||||
fullpath = os.path.join(root, file)
|
fullpath = os.path.join(root, file)
|
||||||
# Check if the file is a video file with one of the specified extensions
|
# Check if the file is a video file with one of the specified extensions
|
||||||
if (file.lower().endswith(
|
if file.lower().endswith(VIDEO_EXTENSIONS) and os.path.isfile(fullpath):
|
||||||
('.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.divx'))
|
|
||||||
and os.path.isfile(fullpath)):
|
|
||||||
video_files.append(fullpath)
|
video_files.append(fullpath)
|
||||||
|
|
||||||
# List of encodings to try for decoding output from ffprobe
|
# List of encodings to try for decoding output from ffprobe
|
||||||
ENCODINGS_TO_TRY = ['utf-8', 'latin-1', 'iso-8859-1']
|
ENCODINGS_TO_TRY = ["utf-8", "latin-1", "iso-8859-1"]
|
||||||
|
|
||||||
# Iterate through each video file
|
# Iterate through each video file
|
||||||
for video_file in video_files:
|
for video_file in video_files:
|
||||||
@ -209,38 +208,58 @@ def find_videos_to_convert(input_path, max_height=720, debug=False):
|
|||||||
# Run ffprobe command to get video resolution
|
# Run ffprobe command to get video resolution
|
||||||
resolution_output = subprocess.check_output(
|
resolution_output = subprocess.check_output(
|
||||||
[
|
[
|
||||||
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
|
"ffprobe",
|
||||||
'-show_entries', 'stream=width,height', '-of', 'csv=p=0',
|
"-v",
|
||||||
video_file
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=width,height",
|
||||||
|
"-of",
|
||||||
|
"csv=p=0",
|
||||||
|
video_file,
|
||||||
],
|
],
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
|
||||||
# Attempt to decode the output using different encodings
|
# Attempt to decode the output using different encodings
|
||||||
dimensions_str = None
|
dimensions_str = None
|
||||||
|
width = height = None
|
||||||
for encoding in ENCODINGS_TO_TRY:
|
for encoding in ENCODINGS_TO_TRY:
|
||||||
try:
|
try:
|
||||||
dimensions_str = resolution_output.decode(
|
dimensions_str = (
|
||||||
encoding).strip().rstrip(',')
|
resolution_output.decode(encoding).strip().rstrip(",")
|
||||||
|
)
|
||||||
|
|
||||||
dimensions = [
|
dimensions = [
|
||||||
int(d) for d in dimensions_str.split(',')
|
int(d) for d in dimensions_str.split(",") if d.strip().isdigit()
|
||||||
if d.strip().isdigit()
|
|
||||||
]
|
]
|
||||||
if len(dimensions) >= 2:
|
if len(dimensions) >= 2:
|
||||||
width, height = dimensions[:2]
|
width, height = dimensions[:2]
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unexpected dimensions format: {dimensions_str}")
|
f"Unexpected dimensions format: {dimensions_str}"
|
||||||
|
)
|
||||||
break # Stop trying encodings once successful decoding and conversion
|
break # Stop trying encodings once successful decoding and conversion
|
||||||
except (UnicodeDecodeError, ValueError):
|
except (UnicodeDecodeError, ValueError):
|
||||||
continue # Try the next encoding
|
continue # Try the next encoding
|
||||||
|
|
||||||
|
# If decoding was unsuccessful with all encodings, skip this video
|
||||||
|
if width is None or height is None:
|
||||||
|
if debug:
|
||||||
|
print(
|
||||||
|
f"{cred}Error processing {video_file}: Unable to decode dimensions from ffprobe output.{creset}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
# If decoding was unsuccessful with all encodings, skip this video
|
# If decoding was unsuccessful with all encodings, skip this video
|
||||||
if dimensions_str is None:
|
if dimensions_str is None:
|
||||||
if debug:
|
if debug:
|
||||||
print(
|
print(
|
||||||
f"{cred}Error processing {video_file}: Unable to decode output from ffprobe using any of the specified encodings.{creset}",
|
f"{cred}Error processing {video_file}: Unable to decode output from ffprobe using any of the specified encodings.{creset}",
|
||||||
file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
continue # Skip this video and continue with the next one
|
continue # Skip this video and continue with the next one
|
||||||
|
|
||||||
# Ignore vertical videos
|
# Ignore vertical videos
|
||||||
@ -258,36 +277,49 @@ def find_videos_to_convert(input_path, max_height=720, debug=False):
|
|||||||
decoded_error = e.output.decode("utf-8")
|
decoded_error = e.output.decode("utf-8")
|
||||||
print(
|
print(
|
||||||
f"{cred}Error processing {video_file}: {decoded_error}{creset}\n",
|
f"{cred}Error processing {video_file}: {decoded_error}{creset}\n",
|
||||||
file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
f"{cred}Error processing {video_file}: No output from subprocess{creset}\n",
|
f"{cred}Error processing {video_file}: No output from subprocess{creset}\n",
|
||||||
file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if debug:
|
if debug:
|
||||||
# Print error message if value error occurs during decoding
|
# Print error message if value error occurs during decoding
|
||||||
print(
|
print(
|
||||||
f"{cred}Error processing {video_file}: ValueError decoding the file{creset}\n",
|
f"{cred}Error processing {video_file}: ValueError decoding the file{creset}\n",
|
||||||
file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
valid_videos_files.sort()
|
valid_videos_files.sort()
|
||||||
|
|
||||||
return valid_videos_files
|
return valid_videos_files
|
||||||
|
|
||||||
|
|
||||||
def convert_videos(input_path, output_path, max_height=720, debug=False):
|
def convert_videos(
|
||||||
'''
|
input_path,
|
||||||
|
output_path=None,
|
||||||
|
max_height=720,
|
||||||
|
delete=False,
|
||||||
|
rename=False,
|
||||||
|
debug=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
Convert videos taller than a specified height to 720p resolution with x265 encoding and MKV container.
|
Convert videos taller than a specified height to 720p resolution with x265 encoding and MKV container.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_path (str): The path of the directory containing input video files.
|
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.
|
output_path (str, optional): The path of the directory where converted video files will be saved.
|
||||||
|
If None, output files are written alongside the originals (in-place).
|
||||||
max_height (int, optional): The maximum height (in pixels) of the video to consider for conversion. Default is 720.
|
max_height (int, optional): The maximum height (in pixels) of the video to consider for conversion. Default is 720.
|
||||||
|
delete (bool, optional): If True, delete the original file after successful conversion. Default is False.
|
||||||
|
rename (bool, optional): If True, rename files and intermediate folders to reflect the target resolution. Default is False.
|
||||||
debug (bool, optional): If True, print debug messages. Default is False.
|
debug (bool, optional): If True, print debug messages. Default is False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if conversion succeeds, False otherwise.
|
bool: True if conversion succeeds, False otherwise.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# Get supported subtitle codecs
|
# Get supported subtitle codecs
|
||||||
supported_subtitle_codecs = get_ffmpeg_codecs("S")
|
supported_subtitle_codecs = get_ffmpeg_codecs("S")
|
||||||
@ -301,17 +333,21 @@ def convert_videos(input_path, output_path, max_height=720, debug=False):
|
|||||||
print(f"No video files to convert found.\n")
|
print(f"No video files to convert found.\n")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Ensure the output directory exists
|
# Ensure the fixed output directory exists (only when output_path is set)
|
||||||
|
if output_path:
|
||||||
ensure_output_path(output_path)
|
ensure_output_path(output_path)
|
||||||
|
|
||||||
# Print a message indicating the start of the conversion process
|
# Print a message indicating the start of the conversion process
|
||||||
print(
|
print(
|
||||||
f"Converting {len(video_files)} videos taller than {max_height} pixels to 720p resolution with x265 encoding and MKV container...\n"
|
f"Converting {len(video_files)} videos taller than {max_height} pixels to {max_height}p resolution with x265 encoding and MKV container...\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Variable to keep a track of the current_file in case of failure
|
# Variable to keep a track of the current_file in case of failure
|
||||||
current_file = None
|
current_file = None
|
||||||
|
|
||||||
|
# Track successfully converted original paths (used for folder rename pass)
|
||||||
|
converted_files = set()
|
||||||
|
|
||||||
# Iterate through each video file for conversion
|
# Iterate through each video file for conversion
|
||||||
for video_file in video_files:
|
for video_file in video_files:
|
||||||
counter += 1
|
counter += 1
|
||||||
@ -323,25 +359,45 @@ def convert_videos(input_path, output_path, max_height=720, debug=False):
|
|||||||
print(f"{ccyan}Original file:{creset}")
|
print(f"{ccyan}Original file:{creset}")
|
||||||
print(video_file)
|
print(video_file)
|
||||||
|
|
||||||
# Generate output file path and name
|
# Determine output directory: fixed path, or same dir as the source file
|
||||||
|
effective_output_path = (
|
||||||
|
output_path if output_path else os.path.dirname(video_file)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compute base output filename, applying resolution rename if requested
|
||||||
|
base_name = os.path.splitext(os.path.basename(video_file))[0]
|
||||||
|
if rename:
|
||||||
|
renamed_base = apply_resolution_rename(base_name, max_height)
|
||||||
|
if renamed_base == base_name and f"{max_height}p" not in renamed_base:
|
||||||
|
# No resolution found in filename: append .{max_height}p before extension
|
||||||
|
renamed_base = f"{base_name}.{max_height}p"
|
||||||
|
base_name = renamed_base
|
||||||
|
|
||||||
|
# Generate output file path, using findfreename to avoid conflicts
|
||||||
output_file = findfreename(
|
output_file = findfreename(
|
||||||
os.path.join(
|
os.path.join(effective_output_path, base_name + ".mkv")
|
||||||
output_path,
|
)
|
||||||
os.path.splitext(os.path.basename(video_file))[0] + '.mkv'))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get original video width and height
|
# Get original video width and height
|
||||||
video_info = subprocess.run([
|
video_info = subprocess.run(
|
||||||
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
|
[
|
||||||
'-show_entries', 'stream=width,height', '-of', 'csv=p=0',
|
"ffprobe",
|
||||||
video_file
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
"v:0",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=width,height",
|
||||||
|
"-of",
|
||||||
|
"csv=p=0",
|
||||||
|
video_file,
|
||||||
],
|
],
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
text=True)
|
text=True,
|
||||||
width, height = map(
|
)
|
||||||
int,
|
width, height = map(int, video_info.stdout.strip().rstrip(",").split(","))
|
||||||
video_info.stdout.strip().rstrip(',').split(','))
|
|
||||||
|
|
||||||
# Calculate the scaled width maintaining aspect ratio
|
# Calculate the scaled width maintaining aspect ratio
|
||||||
scaled_width = int(max_height * (width / height))
|
scaled_width = int(max_height * (width / height))
|
||||||
@ -353,38 +409,55 @@ def convert_videos(input_path, output_path, max_height=720, debug=False):
|
|||||||
current_file = output_file
|
current_file = output_file
|
||||||
|
|
||||||
# Check if the video has supported subtitles
|
# Check if the video has supported subtitles
|
||||||
include_subs = has_supported_subs(video_file,
|
include_subs = has_supported_subs(
|
||||||
supported_subtitle_codecs, debug)
|
video_file, supported_subtitle_codecs, debug
|
||||||
|
)
|
||||||
|
|
||||||
# Construct the FFmpeg command dynamically
|
# Construct the FFmpeg command dynamically
|
||||||
ffmpeg_command = [
|
ffmpeg_command = [
|
||||||
'ffmpeg', '-n', '-i', video_file, '-map', '0:v', '-c:v',
|
"ffmpeg",
|
||||||
'libx265', '-vf', f'scale={scaled_width}:{max_height}',
|
"-n",
|
||||||
'-preset', 'medium', '-crf', '28', '-map', '0:a?', '-c:a',
|
"-i",
|
||||||
'copy'
|
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
|
# Only include subtitles if they are supported
|
||||||
if include_subs:
|
if include_subs:
|
||||||
ffmpeg_command.extend(['-map', '0:s?', '-c:s', 'copy'])
|
ffmpeg_command.extend(["-map", "0:s?", "-c:s", "copy"])
|
||||||
|
|
||||||
ffmpeg_command.append(output_file)
|
ffmpeg_command.append(output_file)
|
||||||
|
|
||||||
# Print the FFmpeg command as a single string
|
# Print the FFmpeg command as a single string
|
||||||
if (debug):
|
if debug:
|
||||||
print("FFmpeg command: " + ' '.join(ffmpeg_command))
|
print("FFmpeg command: " + " ".join(ffmpeg_command))
|
||||||
|
|
||||||
# Run the FFmpeg command
|
# Run the FFmpeg command
|
||||||
process = subprocess.run(ffmpeg_command,
|
process = subprocess.run(
|
||||||
|
ffmpeg_command,
|
||||||
check=True,
|
check=True,
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
|
||||||
# Check if ffmpeg process returned successfully
|
# Check if ffmpeg process returned successfully
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
raise subprocess.CalledProcessError(process.returncode,
|
raise subprocess.CalledProcessError(
|
||||||
process.args,
|
process.returncode, process.args, output=process.stderr
|
||||||
output=process.stderr)
|
)
|
||||||
|
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
# If we have a partial video converted, delete it due to the failure
|
# If we have a partial video converted, delete it due to the failure
|
||||||
@ -397,11 +470,13 @@ def convert_videos(input_path, output_path, max_height=720, debug=False):
|
|||||||
decoded_error = e.output.decode("utf-8")
|
decoded_error = e.output.decode("utf-8")
|
||||||
print(
|
print(
|
||||||
f"{cred}Error processing {video_file}: {decoded_error}{creset}\n",
|
f"{cred}Error processing {video_file}: {decoded_error}{creset}\n",
|
||||||
file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
f"{cred}Error processing {video_file}: No output from subprocess{creset}\n",
|
f"{cred}Error processing {video_file}: No output from subprocess{creset}\n",
|
||||||
file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print(f"{cyellow}Conversions cancelled, cleaning up...{creset}")
|
print(f"{cyellow}Conversions cancelled, cleaning up...{creset}")
|
||||||
@ -422,13 +497,12 @@ def convert_videos(input_path, output_path, max_height=720, debug=False):
|
|||||||
if debug:
|
if debug:
|
||||||
print(
|
print(
|
||||||
f"{cred}Error processing {video_file}: {str(e)}{creset}\n",
|
f"{cred}Error processing {video_file}: {str(e)}{creset}\n",
|
||||||
file=sys.stderr)
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# Print success message if conversion is successful
|
# Print success message if conversion is successful
|
||||||
print(
|
print(f"{ccyan}Successfully converted video to output file:{creset}")
|
||||||
f"{ccyan}Successfully converted video to output file:{creset}")
|
|
||||||
print(f"{output_file}\n")
|
print(f"{output_file}\n")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -436,56 +510,151 @@ def convert_videos(input_path, output_path, max_height=720, debug=False):
|
|||||||
except PermissionError:
|
except PermissionError:
|
||||||
print(f"{cred}PermissionError on: '{output_file}'{creset}")
|
print(f"{cred}PermissionError on: '{output_file}'{creset}")
|
||||||
|
|
||||||
# Remove the now succefully converted filepath
|
# Delete the original file if requested
|
||||||
|
if delete:
|
||||||
|
deletefile(video_file)
|
||||||
|
|
||||||
|
# Track this original and its converted output as successfully converted
|
||||||
|
converted_files.add(os.path.realpath(video_file))
|
||||||
|
converted_files.add(os.path.realpath(output_file))
|
||||||
|
|
||||||
|
# Remove the now successfully converted filepath
|
||||||
current_file = None
|
current_file = None
|
||||||
|
|
||||||
|
# Folder rename second pass
|
||||||
|
if rename and converted_files:
|
||||||
|
# Collect all intermediate dirs touched by successful conversions, deepest first
|
||||||
|
dirs_to_consider = {}
|
||||||
|
for original_file in converted_files:
|
||||||
|
for d in get_intermediate_dirs(input_path, original_file):
|
||||||
|
dirs_to_consider[d] = True
|
||||||
|
|
||||||
|
# For each candidate dir, check that ALL video files under it were converted
|
||||||
|
dirs_to_rename = []
|
||||||
|
for d in sorted(dirs_to_consider, key=len, reverse=True):
|
||||||
|
all_converted = True
|
||||||
|
for root, _, files in os.walk(d):
|
||||||
|
for f in files:
|
||||||
|
if f.lower().endswith(VIDEO_EXTENSIONS):
|
||||||
|
fpath = os.path.realpath(os.path.join(root, f))
|
||||||
|
if fpath not in converted_files:
|
||||||
|
all_converted = False
|
||||||
|
break
|
||||||
|
if not all_converted:
|
||||||
|
break
|
||||||
|
|
||||||
|
if all_converted:
|
||||||
|
new_dirname = apply_resolution_rename(os.path.basename(d), max_height)
|
||||||
|
if new_dirname != os.path.basename(d):
|
||||||
|
new_dir = os.path.join(os.path.dirname(d), new_dirname)
|
||||||
|
dirs_to_rename.append((d, new_dir))
|
||||||
|
|
||||||
|
# Rename dirs deepest first to avoid breaking paths
|
||||||
|
for old_dir, new_dir in dirs_to_rename:
|
||||||
|
if os.path.exists(new_dir):
|
||||||
|
print(
|
||||||
|
f"{cred}Error: Target directory {new_dir} already exists, skipping rename.{creset}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
os.rename(old_dir, new_dir)
|
||||||
|
if debug:
|
||||||
|
print(f"Renamed directory: {old_dir} -> {new_dir}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def parse_args():
|
||||||
'''
|
"""
|
||||||
Main function to parse command line arguments and initiate video conversion.
|
Parse command line arguments.
|
||||||
'''
|
|
||||||
|
Returns:
|
||||||
|
argparse.Namespace: Parsed arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
# Create argument parser
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=
|
description="Convert videos taller than a certain height to 720p resolution with x265 encoding and MKV container."
|
||||||
'Convert videos taller than a certain height to 720p resolution with x265 encoding and MKV container.'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Define command line arguments
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'input_path',
|
"input_path",
|
||||||
nargs='?',
|
nargs="?",
|
||||||
default=os.getcwd(),
|
default=os.getcwd(),
|
||||||
help=
|
help="directory path to search for video files (default: current directory)",
|
||||||
'directory path to search for video files (default: current directory)'
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-o',
|
"-o",
|
||||||
'--output-path',
|
"--output-path",
|
||||||
default=os.path.join(os.getcwd(), 'converted'),
|
default=None,
|
||||||
help='directory path to save converted videos (default: ./converted)')
|
help="directory path to save converted videos (default: ./converted, or alongside originals with --delete)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-mh',
|
"-mh",
|
||||||
'--max-height',
|
"--max-height",
|
||||||
type=int,
|
type=int,
|
||||||
default=720,
|
default=720,
|
||||||
help='maximum height of videos to be converted (default: 720)')
|
help="maximum height of videos to be converted (default: 720)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--debug',
|
"-del",
|
||||||
action='store_true',
|
"--delete",
|
||||||
help='enable debug mode for printing additional error messages')
|
action="store_true",
|
||||||
|
help="delete original files after successful conversion",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-y",
|
||||||
|
"--yes",
|
||||||
|
action="store_true",
|
||||||
|
help="skip confirmation prompts",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-r",
|
||||||
|
"--rename",
|
||||||
|
action="store_true",
|
||||||
|
help="rename converted files and intermediate folders to reflect the target resolution",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--debug",
|
||||||
|
action="store_true",
|
||||||
|
help="enable debug mode for printing additional error messages",
|
||||||
|
)
|
||||||
|
|
||||||
# Enable autocomplete for argparse
|
# Enable autocomplete for argparse
|
||||||
argcomplete.autocomplete(parser)
|
argcomplete.autocomplete(parser)
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.output_path is None and not args.delete:
|
||||||
|
args.output_path = os.path.join(os.getcwd(), "converted")
|
||||||
|
|
||||||
|
# Warn and confirm if --delete is active
|
||||||
|
if args.delete:
|
||||||
|
print(f"{cyellow}WARNING: Delete option selected!{creset}")
|
||||||
|
if not args.yes and not user_confirm(
|
||||||
|
"Are you sure you wish to delete original files after successful conversion? [Y/N] ?",
|
||||||
|
color="yellow",
|
||||||
|
):
|
||||||
|
print(f"{cgreen}Exiting!{creset}")
|
||||||
|
exit()
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Main function to parse command line arguments and initiate video conversion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
# Convert videos
|
# Convert videos
|
||||||
convert_videos(args.input_path, args.output_path, args.max_height,
|
convert_videos(
|
||||||
args.debug)
|
args.input_path,
|
||||||
|
args.output_path,
|
||||||
|
args.max_height,
|
||||||
|
args.delete,
|
||||||
|
args.rename,
|
||||||
|
args.debug,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -5,7 +5,16 @@ import argparse
|
|||||||
import argcomplete
|
import argcomplete
|
||||||
import colorama
|
import colorama
|
||||||
import os
|
import os
|
||||||
import re
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 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 apply_resolution_rename
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
|
|
||||||
@ -17,7 +26,7 @@ cred = colorama.Fore.RED
|
|||||||
|
|
||||||
|
|
||||||
def autorename(input_path, max_height=720, debug=False):
|
def autorename(input_path, max_height=720, debug=False):
|
||||||
'''
|
"""
|
||||||
Rename video files and folders in the specified directory tree that contain a resolution
|
Rename video files and folders in the specified directory tree that contain a resolution
|
||||||
in their name to the specified max_height resolution.
|
in their name to the specified max_height resolution.
|
||||||
|
|
||||||
@ -25,13 +34,7 @@ def autorename(input_path, max_height=720, debug=False):
|
|||||||
input_path (str): The path of the directory to search for video files and folders.
|
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.
|
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.
|
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 = []
|
files_to_rename = []
|
||||||
dirs_to_rename = []
|
dirs_to_rename = []
|
||||||
@ -39,35 +42,19 @@ def autorename(input_path, max_height=720, debug=False):
|
|||||||
for dirpath, dirnames, filenames in os.walk(input_path, topdown=True):
|
for dirpath, dirnames, filenames in os.walk(input_path, topdown=True):
|
||||||
# Collect files to rename
|
# Collect files to rename
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
for resolution, pattern in resolution_patterns.items():
|
new_filename = apply_resolution_rename(filename, max_height)
|
||||||
# Only process resolutions greater than max_height
|
if new_filename != filename:
|
||||||
if resolution > max_height and re.search(
|
|
||||||
pattern, filename, re.IGNORECASE):
|
|
||||||
old_file = os.path.join(dirpath, filename)
|
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)
|
new_file = os.path.join(dirpath, new_filename)
|
||||||
if old_file != new_file:
|
|
||||||
files_to_rename.append((old_file, new_file))
|
files_to_rename.append((old_file, new_file))
|
||||||
break
|
|
||||||
|
|
||||||
# Collect directories to rename
|
# Collect directories to rename
|
||||||
for dirname in dirnames:
|
for dirname in dirnames:
|
||||||
for resolution, pattern in resolution_patterns.items():
|
new_dirname = apply_resolution_rename(dirname, max_height)
|
||||||
# Only process resolutions greater than max_height
|
if new_dirname != dirname:
|
||||||
if resolution > max_height and re.search(
|
|
||||||
pattern, dirname, re.IGNORECASE):
|
|
||||||
old_dir = os.path.join(dirpath, dirname)
|
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)
|
new_dir = os.path.join(dirpath, new_dirname)
|
||||||
if old_dir != new_dir:
|
|
||||||
dirs_to_rename.append((old_dir, new_dir))
|
dirs_to_rename.append((old_dir, new_dir))
|
||||||
break
|
|
||||||
|
|
||||||
# Rename files first
|
# Rename files first
|
||||||
for old_file, new_file in files_to_rename:
|
for old_file, new_file in files_to_rename:
|
||||||
@ -76,7 +63,7 @@ def autorename(input_path, max_height=720, debug=False):
|
|||||||
continue
|
continue
|
||||||
os.rename(old_file, new_file)
|
os.rename(old_file, new_file)
|
||||||
if debug:
|
if debug:
|
||||||
print(f'Renamed file: {old_file} -> {new_file}')
|
print(f"Renamed file: {old_file} -> {new_file}")
|
||||||
|
|
||||||
# Rename directories after
|
# Rename directories after
|
||||||
for old_dir, new_dir in sorted(dirs_to_rename, key=lambda x: -len(x[0])):
|
for old_dir, new_dir in sorted(dirs_to_rename, key=lambda x: -len(x[0])):
|
||||||
@ -85,38 +72,38 @@ def autorename(input_path, max_height=720, debug=False):
|
|||||||
continue
|
continue
|
||||||
os.rename(old_dir, new_dir)
|
os.rename(old_dir, new_dir)
|
||||||
if debug:
|
if debug:
|
||||||
print(f'Renamed directory: {old_dir} -> {new_dir}')
|
print(f"Renamed directory: {old_dir} -> {new_dir}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
'''
|
"""
|
||||||
Main function to parse command line arguments and initiate file renamings.
|
Main function to parse command line arguments and initiate file renamings.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
# Create argument parser
|
# Create argument parser
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description=
|
description="Rename video files containing resolutions in their filenames to a specified max_height resolution."
|
||||||
'Rename video files containing resolutions in their filenames to a specified max_height resolution.'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Define command line arguments
|
# Define command line arguments
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'input_path',
|
"input_path",
|
||||||
nargs='?',
|
nargs="?",
|
||||||
default=os.getcwd(),
|
default=os.getcwd(),
|
||||||
help=
|
help="directory path to search for video files (default: current directory)",
|
||||||
'directory path to search for video files (default: current directory)'
|
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-mh',
|
"-mh",
|
||||||
'--max-height',
|
"--max-height",
|
||||||
type=int,
|
type=int,
|
||||||
default=720,
|
default=720,
|
||||||
help='maximum height of videos to be converted (default: 720)')
|
help="maximum height of videos to be converted (default: 720)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--debug',
|
"--debug",
|
||||||
action='store_true',
|
action="store_true",
|
||||||
help='enable debug mode for printing additional messages')
|
help="enable debug mode for printing additional messages",
|
||||||
|
)
|
||||||
|
|
||||||
# Enable autocomplete for argparse
|
# Enable autocomplete for argparse
|
||||||
argcomplete.autocomplete(parser)
|
argcomplete.autocomplete(parser)
|
||||||
|
|||||||
@ -32,20 +32,48 @@ def process_subtitles(file_path, track, command):
|
|||||||
if command == "remove":
|
if command == "remove":
|
||||||
# Remove only the specified subtitle track while keeping all other streams
|
# Remove only the specified subtitle track while keeping all other streams
|
||||||
ffmpeg_command = [
|
ffmpeg_command = [
|
||||||
"ffmpeg", "-i", file_path, "-map", "0", "-map", f"-0:s:{track}",
|
"ffmpeg",
|
||||||
"-c", "copy", output_file
|
"-i",
|
||||||
|
file_path,
|
||||||
|
"-map",
|
||||||
|
"0",
|
||||||
|
"-map",
|
||||||
|
f"-0:s:{track}",
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
output_file,
|
||||||
]
|
]
|
||||||
elif command == "keep":
|
elif command == "keep":
|
||||||
# Keep only the specified subtitle track while preserving video, audio, and metadata
|
# Keep only the specified subtitle track while preserving video, audio, and metadata
|
||||||
ffmpeg_command = [
|
ffmpeg_command = [
|
||||||
"ffmpeg", "-i", file_path, "-map", "0:v", "-map", "0:a", "-map",
|
"ffmpeg",
|
||||||
f"0:s:{track}", "-map", "0:t?", "-c", "copy", output_file
|
"-i",
|
||||||
|
file_path,
|
||||||
|
"-map",
|
||||||
|
"0:v",
|
||||||
|
"-map",
|
||||||
|
"0:a",
|
||||||
|
"-map",
|
||||||
|
f"0:s:{track}",
|
||||||
|
"-map",
|
||||||
|
"0:t?",
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
output_file,
|
||||||
]
|
]
|
||||||
elif command == "none":
|
elif command == "none":
|
||||||
# Remove all subtitle tracks
|
# Remove all subtitle tracks
|
||||||
ffmpeg_command = [
|
ffmpeg_command = [
|
||||||
"ffmpeg", "-i", file_path, "-map", "0", "-map", "-0:s", "-c",
|
"ffmpeg",
|
||||||
"copy", output_file
|
"-i",
|
||||||
|
file_path,
|
||||||
|
"-map",
|
||||||
|
"0",
|
||||||
|
"-map",
|
||||||
|
"-0:s",
|
||||||
|
"-c",
|
||||||
|
"copy",
|
||||||
|
output_file,
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
print(f"{cred}Invalid command: {command}{creset}")
|
print(f"{cred}Invalid command: {command}{creset}")
|
||||||
@ -53,10 +81,9 @@ def process_subtitles(file_path, track, command):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Execute the ffmpeg command and capture output
|
# Execute the ffmpeg command and capture output
|
||||||
result = subprocess.run(ffmpeg_command,
|
result = subprocess.run(
|
||||||
stdout=subprocess.PIPE,
|
ffmpeg_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||||
stderr=subprocess.PIPE,
|
)
|
||||||
text=True)
|
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print(
|
print(
|
||||||
@ -91,13 +118,16 @@ def main():
|
|||||||
|
|
||||||
# Create argument parser
|
# Create argument parser
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Manage subtitle tracks in video files.")
|
description="Manage subtitle tracks in video files."
|
||||||
|
)
|
||||||
|
|
||||||
# Define command line arguments
|
# Define command line arguments
|
||||||
# Add a positional argument for the command
|
# Add a positional argument for the command
|
||||||
parser.add_argument('command',
|
parser.add_argument(
|
||||||
choices=['remove', 'keep', 'none'],
|
"command",
|
||||||
help='Command to run (remove, keep, or none)')
|
choices=["remove", "keep", "none"],
|
||||||
|
help="Command to run (remove, keep, or none)",
|
||||||
|
)
|
||||||
|
|
||||||
# Add other arguments with both short and long options, including defaults
|
# Add other arguments with both short and long options, including defaults
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@ -105,19 +135,16 @@ def main():
|
|||||||
"--track",
|
"--track",
|
||||||
type=int,
|
type=int,
|
||||||
default=0,
|
default=0,
|
||||||
help=
|
help="Subtitle track index (default is 0). Use 'none' to remove all subtitles.",
|
||||||
"Subtitle track index (default is 0). Use 'none' to remove all subtitles."
|
|
||||||
)
|
)
|
||||||
parser.add_argument("-f",
|
parser.add_argument("-f", "--file", type=str, help="Path to a specific video file.")
|
||||||
"--file",
|
|
||||||
type=str,
|
|
||||||
help="Path to a specific video file.")
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-d",
|
"-d",
|
||||||
"--dir",
|
"--dir",
|
||||||
type=str,
|
type=str,
|
||||||
default=os.getcwd(),
|
default=os.getcwd(),
|
||||||
help="Directory to process (default is current directory).")
|
help="Directory to process (default is current directory).",
|
||||||
|
)
|
||||||
|
|
||||||
# Enable autocomplete for command-line arguments
|
# Enable autocomplete for command-line arguments
|
||||||
argcomplete.autocomplete(parser)
|
argcomplete.autocomplete(parser)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user