Add All existing

This commit is contained in:
Hannes
2025-11-17 00:41:24 +01:00
parent e309cad1b4
commit af7ccb25b9
33 changed files with 2687 additions and 0 deletions

64
append Executable file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Usage info
if [[ $# -lt 2 ]]; then
echo "Usage: $0 {front|back|ext|extension} <string> [folder]"
exit 1
fi
mode=$1
string=$2
folder=${3:-$(pwd)}
# Ensure the folder exists
if [[ ! -d "$folder" ]]; then
echo "Error: '$folder' is not a valid directory."
exit 1
fi
# Process files
for file in "$folder"/*; do
[[ -f "$file" ]] || continue # skip non-files
base="$(basename "$file")"
# Split filename into name and extension (if any)
name="${base%.*}"
ext="${base##*.}"
case "$mode" in
front)
# Add string to the front of the filename
newname="${string}${base}"
;;
back)
# Add string before the extension (if present)
if [[ "$base" == "$ext" ]]; then
# no extension
newname="${name}${string}"
else
newname="${name}${string}.${ext}"
fi
;;
ext|extension)
# Add string *to* the extension (after a dot)
if [[ "$base" == "$ext" ]]; then
# file has no extension
newname="${base}.${string}"
else
newname="${name}.${ext}${string}"
fi
;;
*)
echo "Invalid mode: '$mode'. Use 'front', 'back', or 'ext/extension'."
exit 1
;;
esac
# Avoid renaming collisions
if [[ "$newname" != "$base" ]]; then
echo "Renaming: $base -> $newname"
mv -n "$file" "$folder/$newname"
fi
done
echo "✅ All applicable files in '$folder' have been renamed."

74
create_folder_counting Executable file
View File

@@ -0,0 +1,74 @@
#!/bin/bash
# Check if the required arguments are provided
if [ "$#" -lt 4 ] || [ "$#" -gt 5 ]; then
echo "Usage: $0 {Folder} name1 {number1-number2 | letter1-letter2} name2 {symbol_between (optional)}"
exit 1
fi
# Set the target directory
if [ "$#" -eq 5 ]; then
TARGET_DIR="$1"
NAME1="$2"
RANGE="$3"
NAME2="$4"
SYMBOL="$5"
elif [ "$#" -eq 4 ]; then
# If 4 arguments are provided, the folder is assumed to be the current directory
TARGET_DIR="."
NAME1="$1"
RANGE="$2"
NAME2="$3"
SYMBOL="$4"
else
# If 3 arguments are provided, the folder is assumed to be the current directory
TARGET_DIR="."
NAME1="$1"
RANGE="$2"
NAME2="$3"
SYMBOL="_" # Default separator is "_"
fi
# Ensure the target directory exists
mkdir -p "$TARGET_DIR"
# Check if RANGE is numeric or alphabetic
if [[ "$RANGE" =~ ^[0-9]+-[0-9]+$ ]]; then
# Numeric range
START_NUM=$(echo "$RANGE" | cut -d'-' -f1)
END_NUM=$(echo "$RANGE" | cut -d'-' -f2)
# Create folders for each number in the range
for ((i=START_NUM; i<=END_NUM; i++)); do
FOLDER_NAME="${NAME1}${SYMBOL}${i}${SYMBOL}${NAME2}"
mkdir -p "$TARGET_DIR/$FOLDER_NAME"
done
elif [[ "$RANGE" =~ ^[a-zA-Z]-[a-zA-Z]$ ]]; then
# Alphabetic range
START_LETTER=$(echo "$RANGE" | cut -d'-' -f1)
END_LETTER=$(echo "$RANGE" | cut -d'-' -f2)
# Convert letters to ASCII values for iteration
START_ASCII=$(printf "%d" "'$START_LETTER")
END_ASCII=$(printf "%d" "'$END_LETTER")
# Ensure alphabetical order
if [ "$START_ASCII" -gt "$END_ASCII" ]; then
echo "Error: The alphabetical range should start with a letter that precedes the end letter."
exit 1
fi
# Create folders for each letter in the range
for ((i=START_ASCII; i<=END_ASCII; i++)); do
LETTER=$(printf "\x$(printf %x $i)")
FOLDER_NAME="${NAME1}${SYMBOL}${LETTER}${SYMBOL}${NAME2}"
mkdir -p "$TARGET_DIR/$FOLDER_NAME"
done
else
echo "Error: RANGE should be in the format number1-number2 or letter1-letter2."
exit 1
fi
echo "Folders created in $TARGET_DIR."

40
create_folder_date Executable file
View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Check for arguments and set variables
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
echo "Usage: $0 {FOLDER} YYYY_MM_DD YYYY_MM_DD"
exit 1
fi
# Set folder path
if [ "$#" -eq 3 ]; then
TARGET_DIR="$1"
START_DATE="$2"
END_DATE="$3"
else
TARGET_DIR="."
START_DATE="$1"
END_DATE="$2"
fi
# Ensure the target directory exists
mkdir -p "$TARGET_DIR"
# Convert input dates to YYYY-MM-DD format for `date` compatibility
START_DATE=$(echo "$START_DATE" | sed 's/_/-/g')
END_DATE=$(echo "$END_DATE" | sed 's/_/-/g')
# Loop through dates and create folders
current_date="$START_DATE"
while [[ "$current_date" < "$END_DATE" ]] || [[ "$current_date" == "$END_DATE" ]]; do
# Format date as YYYY_MM_DD
formatted_date=$(date -d "$current_date" +"%Y_%m_%d")
# Create folder with the formatted date
mkdir -p "$TARGET_DIR/$formatted_date"
# Increment date by one day
current_date=$(date -I -d "$current_date + 1 day")
done
echo "Folders created from ${START_DATE//-/_} to ${END_DATE//-/_} in $TARGET_DIR."

95
deappend Executable file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/python3
import os
import sys
def remove_suffix_back(directory, suffix):
"""Removes the specified suffix from the end of filenames before the extension."""
for filename in os.listdir(directory):
filepath = os.path.join(directory, filename)
# Skip directories
if not os.path.isfile(filepath):
continue
# Separate base name and extension
base, ext = os.path.splitext(filename)
# Remove suffix from the back of the base name
if base.endswith(suffix):
new_name = base[: -len(suffix)] + ext
os.rename(filepath, os.path.join(directory, new_name))
print(f"Renamed: {filename} -> {new_name}")
def remove_prefix_front(directory, prefix):
"""Removes the specified prefix from the start of filenames."""
for filename in os.listdir(directory):
filepath = os.path.join(directory, filename)
# Skip directories
if not os.path.isfile(filepath):
continue
# Remove prefix if it exists
if filename.startswith(prefix):
new_name = filename[len(prefix):]
os.rename(filepath, os.path.join(directory, new_name))
print(f"Renamed: {filename} -> {new_name}")
def remove_from_extension(directory, suffix):
"""Removes the specified string from the file extensions."""
for filename in os.listdir(directory):
filepath = os.path.join(directory, filename)
# Skip directories
if not os.path.isfile(filepath):
continue
# Separate base name and extension
base, ext = os.path.splitext(filename)
# Remove suffix from the extension
if ext.endswith(suffix):
new_ext = ext[: -len(suffix)]
new_name = base + new_ext
os.rename(filepath, os.path.join(directory, new_name))
print(f"Renamed: {filename} -> {new_name}")
def main():
# Validate arguments
if len(sys.argv) < 3:
print("Usage: deappend.py <mode> <string> [directory]")
sys.exit(1)
# Get arguments
mode = sys.argv[1] # Mode: back, front, ext/extension
string = sys.argv[2] # String to remove
directory = sys.argv[3] if len(sys.argv) > 3 else os.getcwd() # Default: current directory
# Check that the string is not empty
if string == "":
print("Error: The string to remove cannot be empty.")
sys.exit(1)
# Validate directory
if not os.path.isdir(directory):
print(f"Error: Directory '{directory}' does not exist.")
sys.exit(1)
# Process based on mode
if mode == "back":
remove_suffix_back(directory, string)
elif mode == "front":
remove_prefix_front(directory, string)
elif mode == "ext" or mode == "extension":
remove_from_extension(directory, string)
else:
print(f"Error: Unsupported mode '{mode}'. Supported modes: front, back, ext/extension.")
sys.exit(1)
if __name__ == "__main__":
main()

64
deinterlacing_cpu.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/bin/bash
INPUT="$HOME/mount/Ripping/Lutz/noch zu rendern/Brücke nach Terabithia/A1_t00.mkv"
OUTPUT="${INPUT%.*}_deinterlaced.mp4"
LOGFILE=$(mktemp)
if [ ! -f "$INPUT" ]; then
echo "Input file not found."
exit 1
fi
# Step 1: Try runtime detection with idet
ffmpeg -hide_banner -vstats -nostats \
-i "$INPUT" \
-filter:v "format=yuv420p,idet" \
-frames:v 500 \
-an -f rawvideo -y /dev/null \
2> "$LOGFILE"
TFF=$(grep -o 'TFF:[0-9]*' "$LOGFILE" | head -1 | cut -d: -f2)
BFF=$(grep -o 'BFF:[0-9]*' "$LOGFILE" | head -1 | cut -d: -f2)
PROG=$(grep -o 'Progressive:[0-9]*' "$LOGFILE" | head -1 | cut -d: -f2)
rm "$LOGFILE"
TFF=${TFF:-0}
BFF=${BFF:-0}
PROG=${PROG:-0}
echo "Detected via idet - Progressive: $PROG, TFF: $TFF, BFF: $BFF"
# Step 2: Fallback to metadata if no detection occurred
if [ "$((TFF + BFF + PROG))" -eq 0 ]; then
echo "idet returned 0 frames — falling back to metadata..."
FIELD_ORDER=$(ffprobe -v error -select_streams v:0 \
-show_entries stream=field_order \
-of default=noprint_wrappers=1:nokey=1 "$INPUT")
case "$FIELD_ORDER" in
tt|bb)
echo "Field order is interlaced ($FIELD_ORDER)."
INTERLACED=true
;;
progressive|unknown|"")
echo "Metadata says progressive or unknown."
INTERLACED=false
;;
esac
else
INTERLACED=$((TFF + BFF > PROG))
fi
# Step 3: Skip or apply deinterlacing
if [ "$INTERLACED" = false ]; then
echo "Video is progressive. No deinterlacing needed."
exit 0
fi
echo "Video is interlaced. Proceeding with deinterlacing..."
# Step 4: Apply yadif
ffmpeg -i "$INPUT" \
-vf "yadif=mode=1:parity=auto" \
-c:v libx264 -preset slow -crf 18 -c:a copy \
"$OUTPUT"

32
deinterlacing_gpu.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
INPUT="$HOME/mount/Ripping/Lutz/noch zu rendern/Brücke nach Terabithia/A1_t00.mkv"
OTPUT="${INPUT%.*}_deinterlaced_vaapi.mp4"
TEMP_STATS=$(mktemp)
ffmpeg -filter:v idet -frames:v 5000 -an -f rawvideo -y /dev/null -i "$INPUT" 2> "$TEMP_STATS"
SUMMARY_LINE=$(grep "Multi frame detection" "$TEMP_STATS")
# Extract counts allowing for leading spaces
TFF_COUNT=$(echo "$SUMMARY_LINE" | grep -oP 'TFF:\s*\K[0-9]+' || echo 0)
BFF_COUNT=$(echo "$SUMMARY_LINE" | grep -oP 'BFF:\s*\K[0-9]+' || echo 0)
PROG_COUNT=$(echo "$SUMMARY_LINE" | grep -oP 'Progressive:\s*\K[0-9]+' || echo 0)
rm "$TEMP_STATS"
echo "Detected: TFF=$TFF_COUNT, BFF=$BFF_COUNT, Progressive=$PROG_COUNT"
TOTAL_INTERLACED=$((TFF_COUNT + BFF_COUNT))
if [ "$TOTAL_INTERLACED" -lt 100 ]; then
echo "Video is mostly progressive. No deinterlacing needed."
exit 0
fi
echo "Deinterlacing required."
ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format vaapi \
-i "$INPUT" \
-vf 'format=nv12,hwupload,deinterlace_vaapi' \U
-c:v h264_vaapi -b:v 5M -c:a copy "$OUTPUT"

78
embed_audio Executable file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/python3
import sys
import subprocess
import os
import glob
def embed_audio(video_file, audio_file, output_file):
"""
Embeds an audio track into a video file as the first audio track.
Parameters:
video_file (str): Path to the video file.
audio_file (str): Path to the audio file.
output_file (str): Path to save the output file.
"""
try:
result = subprocess.run(
[
"ffmpeg", "-i", video_file, "-i", audio_file,
"-c:v", "copy", "-c:a", "aac",
"-map", "0:v:0", "-map", "1:a:0",
"-map", "0:a?", # Add any existing audio tracks as secondary tracks
"-shortest", output_file, "-y"
],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
if result.returncode != 0:
print(f"Error embedding audio for {video_file}: {result.stderr}")
else:
print(f"Audio successfully embedded: {output_file}")
except Exception as e:
print(f"An error occurred for {video_file}: {e}")
def main():
# No arguments: auto-pairing mode
if len(sys.argv) == 1:
video_extensions = ["*.mp4", "*.mkv", "*.avi", "*.mov"]
files_processed = 0
for ext in video_extensions:
for video_file in glob.glob(ext):
base_name = os.path.splitext(video_file)[0]
audio_file = f"{base_name}.mp3"
if os.path.isfile(audio_file):
output_file = f"{base_name}_with_audio.mp4"
print(f"Processing: {video_file} with {audio_file} -> {output_file}")
embed_audio(video_file, audio_file, output_file)
files_processed += 1
else:
print(f"No matching audio file found for: {video_file}")
if files_processed == 0:
print("No video-audio pairs found in the current directory.")
sys.exit(1)
# Explicit arguments mode
elif len(sys.argv) == 4:
video_file = sys.argv[1]
audio_file = sys.argv[2]
output_file = sys.argv[3]
if not os.path.isfile(video_file):
print(f"Error: Video file '{video_file}' not found.")
sys.exit(1)
if not os.path.isfile(audio_file):
print(f"Error: Audio file '{audio_file}' not found.")
sys.exit(1)
embed_audio(video_file, audio_file, output_file)
else:
print("Usage:")
print(" Auto-pairing mode (no arguments): script will pair videos and matching audio in current directory")
print(" Explicit mode: <video_file> <audio_file> <output_file>")
sys.exit(1)
if __name__ == "__main__":
main()

49
embed_subtitles Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
# Paths to Python scripts
SUBTITLE_SCRIPT="$HOME/.bin/auto_embed_subtitles.py"
# Check both Python scripts exist
if [ ! -f "$SUBTITLE_SCRIPT" ]; then
echo "Error: Subtitle script '$SUBTITLE_SCRIPT' not found."
exit 1
fi
# === Mode: Manual Audio Embed ===
if [ "$#" -eq 3 ]; then
echo "Embedding subtitle: $1 + $2 -> $3"
python "$SUBTITLE_SCRIPT" "$1" "$2" "$3"
exit 0
# === Mode: Auto Detection ===
elif [ "$#" -eq 0 ]; then
echo "No arguments provided. Attempting to embed audio and subtitles in the current directory."
files_processed=0
# Process audio embeddings
for video_file in *.mp4 *.mkv *.avi *.mov; do
[ -e "$video_file" ] || continue
base_name="${video_file%.*}"
audio_file="${base_name}.mp3"
if [ -f "$audio_file" ]; then
output_file="${base_name}_with_audio.mp4"
echo "Embedding subtitle: $video_file + $audio_file -> $output_file"
python "$SUBTITLE_SCRIPT" "$video_file" "$audio_file" "$output_file"
files_processed=1
fi
done
# Process subtitle embeddings
echo "Scanning for subtitles to embed..."
python "$SUBTITLE_SCRIPT" "."
exit 0
else
echo "Usage:"
echo " $0 <video_file> <subtitle_file> <output_file> # Manual subtitel embed"
echo " $0 # Auto mode (pair audio + embed subtitles)"
exit 1
fi

66
embed_subtitles.py Normal file
View File

@@ -0,0 +1,66 @@
import os
import sys
import subprocess
import re
def find_matching_subtitles(video_path, search_dir):
base_name = os.path.splitext(os.path.basename(video_path))[0]
subtitle_files = []
for file in os.listdir(search_dir):
if re.match(rf"^{re.escape(base_name)}__.*\.srt$", file):
subtitle_files.append(os.path.join(search_dir, file))
return sorted(subtitle_files)
def embed_subtitles(video_path, subtitle_paths, output_path):
try:
cmd = ["ffmpeg", "-i", video_path]
for subtitle in subtitle_paths:
cmd += ["-i", subtitle]
cmd += ["-c:v", "copy", "-c:a", "copy"]
if output_path.lower().endswith(".mp4"):
cmd += ["-c:s", "mov_text"]
else:
cmd += ["-c:s", "copy"]
cmd += ["-map", "0"]
for i in range(1, len(subtitle_paths) + 1):
cmd += ["-map", str(i)]
cmd += ["-y", output_path]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode != 0:
print(f"Error embedding subtitles into {video_path}:\n{result.stderr}")
else:
print(f"Embedded subtitles into: {output_path}")
except Exception as e:
print(f"Failed to embed subtitles: {e}")
def process_path(input_path):
if os.path.isfile(input_path):
directory = os.path.dirname(input_path)
subtitles = find_matching_subtitles(input_path, directory)
if subtitles:
output_file = os.path.splitext(input_path)[0] + "_subbed" + os.path.splitext(input_path)[1]
embed_subtitles(input_path, subtitles, output_file)
else:
print(f"No matching subtitles found for: {input_path}")
elif os.path.isdir(input_path):
for file_name in os.listdir(input_path):
full_path = os.path.join(input_path, file_name)
if os.path.isfile(full_path) and file_name.lower().endswith(('.mp4', '.mkv', '.avi', '.mov')):
process_path(full_path)
else:
print(f"Invalid input path: {input_path}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python auto_embed_subtitles.py <video_file_or_directory>")
sys.exit(1)
input_path = sys.argv[1]
process_path(input_path)

80
episode_numbers Executable file
View File

@@ -0,0 +1,80 @@
#!/bin/bash
LOG_DIR="/home/honney/.log"
LOG_FILE="${LOG_DIR}/rename.log"
mkdir -p "$LOG_DIR"
reverse_mode=false
if [[ "$1" == "reverse" ]]; then
reverse_mode=true
elif [[ -z "$1" ]]; then
echo "Usage: $0 {start_number} | reverse"
exit 1
fi
if $reverse_mode; then
if [[ ! -f "$LOG_FILE" ]]; then
echo "No log file found. Nothing to reverse."
exit 1
fi
current_dir=$(realpath .)
match_start=$(grep -n "=== $current_dir ===" "$LOG_FILE" | cut -d: -f1)
if [[ -z "$match_start" ]]; then
echo "No entries for this directory in the log."
exit 1
fi
sed -n "$((match_start + 1)),/^===/p" "$LOG_FILE" | grep -v "^===" | while IFS="|" read -r original renamed; do
if [[ -e "$renamed" ]]; then
mv -v "$renamed" "$original"
else
echo "Warning: '$renamed' not found. Skipping."
fi
done
echo "Reversal complete."
exit 0
fi
# Start of normal renaming
start_number=$1
counter=0
extensions=("mkv" "mp4" "avi" "mov" "jpg" "png")
files=()
for ext in "${extensions[@]}"; do
while IFS= read -r -d '' file; do
files+=("$file")
done < <(find . -maxdepth 1 -type f -iname "*.${ext}" -print0)
done
IFS=$'\n' sorted_files=($(printf '%s\n' "${files[@]}" | sort -rz | tr '\0' '\n'))
# Log the current session
current_dir=$(realpath .)
echo "=== $current_dir ===" >> "$LOG_FILE"
for file in "${sorted_files[@]}"; do
new_number=$(printf "%02d" $((start_number + counter)))
extension="${file##*.}"
base_name="E${new_number}"
new_name="${base_name}.${extension}"
suffix=1
# Avoid overwriting
while [ -e "$new_name" ]; do
new_name="${base_name}_$suffix.${extension}"
((suffix++))
done
mv "$file" "$new_name"
echo "${file}|${new_name}" >> "$LOG_FILE"
((counter++))
done
echo "Renaming complete! Changes logged to $LOG_FILE"

37
extract_audio Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Get the path to the Python script (adjust if needed)
PYTHON_SCRIPT="$HOME/.bin/extract_audio.py"
# Check if the Python script exists
if [ ! -f "$PYTHON_SCRIPT" ]; then
echo "Error: Python script '$PYTHON_SCRIPT' not found."
exit 1
fi
# Check if arguments are provided
if [ "$#" -eq 0 ]; then
echo "No arguments provided. Attempting to process all video files in the current directory."
# Initialize a flag to track if any files are processed
files_processed=0
# Find all video files in the current directory
for video_file in *.mp4 *.mkv *.avi *.mov; do
# Check if the file exists (in case no matches were found)
if [ -e "$video_file" ]; then
echo "Processing: $video_file"
python "$PYTHON_SCRIPT" "$video_file"
files_processed=1
fi
done
# If no files were processed, print a message
if [ "$files_processed" -eq 0 ]; then
echo "No video files found in the current directory."
exit 1
fi
else
# Run the Python script with the provided arguments
python "$PYTHON_SCRIPT" "$@"
fi

95
extract_audio.py Normal file
View File

@@ -0,0 +1,95 @@
import os
import ffmpeg
import subprocess
import re
import json
def sanitize_filename(name):
"""
Removes invalid characters from the filename.
"""
return re.sub(r'[<>:"/\\|?*@]', '_', name)
def extract_audio_tracks(video_path):
"""
Extracts all audio tracks from a video file and saves them with their respective titles.
Parameters:
video_path (str): Path to the video file.
"""
try:
# Get the base name and directory of the video file
base_name = os.path.splitext(os.path.basename(video_path))[0]
directory = os.path.dirname(video_path)
# Use ffmpeg to get information about the video file
probe_cmd = ["ffprobe", "-v", "error", "-show_streams", "-of", "json", video_path]
probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
streams = json.loads(probe_result.stdout).get('streams', [])
# Process each audio stream
audio_streams = [s for s in streams if s.get('codec_type') == 'audio']
if not audio_streams:
print(f"No audio tracks found in {video_path}.")
return
for i, stream in enumerate(audio_streams):
# Extract title or fallback to index
title = stream.get('tags', {}).get('title', f"track_{i + 1}")
language = stream.get('tags', {}).get('language', 'unknown')
sanitized_title = sanitize_filename(title)
sanitized_language = sanitize_filename(language)
output_name = f"{base_name}__{sanitized_title}__{sanitized_language}.mp3"
output_path = os.path.join(directory, output_name)
# Extract the specific audio stream
print(f"Extracting audio: {sanitized_title} ({sanitized_language}) to {output_path}")
result = subprocess.run([
"ffmpeg", "-i", video_path,
"-map", f"0:a:{i}", # Select the specific audio stream
"-c:a", "mp3", # Set the codec to mp3
output_path,
"-y" # Overwrite if exists
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode != 0:
print(f"ffmpeg error for stream {i}: {result.stderr}")
print(f"Extraction completed for {video_path}.")
except Exception as e:
print(f"Error processing {video_path}: {e}")
def process_path(input_path):
"""
Processes a file or directory, extracting all audio tracks from video files.
Parameters:
input_path (str): Path to the video file or directory.
"""
if os.path.isfile(input_path):
# Process a single file
extract_audio_tracks(input_path)
elif os.path.isdir(input_path):
# Process all video files in the directory
for file_name in os.listdir(input_path):
file_path = os.path.join(input_path, file_name)
if os.path.isfile(file_path) and file_name.lower().endswith(('.mp4', '.mkv', '.avi', '.mov')):
extract_audio_tracks(file_path)
else:
print(f"Invalid path: {input_path}")
if __name__ == "__main__":
import argparse
# Set up argument parser
parser = argparse.ArgumentParser(description="Extract all audio tracks from video files.")
parser.add_argument("input_path", help="Path to the video file or directory.")
# Parse arguments
args = parser.parse_args()
# Process the input path
process_path(args.input_path)

37
extract_subtitles Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Get the path to the Python script (adjust if needed)
PYTHON_SCRIPT="$HOME/.bin/extract_subtitles.py"
# Check if the Python script exists
if [ ! -f "$PYTHON_SCRIPT" ]; then
echo "Error: Python script '$PYTHON_SCRIPT' not found."
exit 1
fi
# Check if arguments are provided
if [ "$#" -eq 0 ]; then
echo "No arguments provided. Attempting to process all video files in the current directory."
# Initialize a flag to track if any files are processed
files_processed=0
# Find all video files in the current directory
for video_file in *.mp4 *.mkv *.avi *.mov; do
# Check if the file exists (in case no matches were found)
if [ -e "$video_file" ]; then
echo "Processing: $video_file"
python "$PYTHON_SCRIPT" "$video_file"
files_processed=1
fi
done
# If no files were processed, print a message
if [ "$files_processed" -eq 0 ]; then
echo "No video files found in the current directory."
exit 1
fi
else
# Run the Python script with the provided arguments
python "$PYTHON_SCRIPT" "$@"
fi

88
extract_subtitles.py Normal file
View File

@@ -0,0 +1,88 @@
import os
import subprocess
import re
import json
def sanitize_filename(name):
"""
Removes invalid characters from the filename.
"""
return re.sub(r'[<>:"/\\|?*@]', '_', name)
def extract_subtitle_tracks(video_path):
"""
Extracts all subtitle tracks from a video file and saves them with their respective titles.
Parameters:
video_path (str): Path to the video file.
"""
try:
base_name = os.path.splitext(os.path.basename(video_path))[0]
directory = os.path.dirname(video_path)
# Get stream info using ffprobe
probe_cmd = ["ffprobe", "-v", "error", "-show_streams", "-of", "json", video_path]
probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
streams = json.loads(probe_result.stdout).get('streams', [])
# Filter subtitle streams
subtitle_streams = [s for s in streams if s.get('codec_type') == 'subtitle']
if not subtitle_streams:
print(f"No subtitle tracks found in {video_path}.")
return
for i, stream in enumerate(subtitle_streams):
title = stream.get('tags', {}).get('title', f"subtitle_{i + 1}")
language = stream.get('tags', {}).get('language', 'unknown')
sanitized_title = sanitize_filename(title)
sanitized_language = sanitize_filename(language)
output_name = f"{base_name}__{sanitized_title}__{sanitized_language}.srt"
output_path = os.path.join(directory, output_name)
# Extract subtitle stream
print(f"Extracting subtitle: {sanitized_title} ({sanitized_language}) to {output_path}")
result = subprocess.run([
"ffmpeg", "-i", video_path,
"-map", f"0:s:{i}",
"-c:s", "srt",
output_path,
"-y"
], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode != 0:
print(f"ffmpeg error for subtitle stream {i}: {result.stderr}")
print(f"Subtitle extraction completed for {video_path}.")
except Exception as e:
print(f"Error processing {video_path}: {e}")
def process_path(input_path):
"""
Processes a file or directory, extracting all subtitle tracks from video files.
Parameters:
input_path (str): Path to the video file or directory.
"""
if os.path.isfile(input_path):
extract_subtitle_tracks(input_path)
elif os.path.isdir(input_path):
for file_name in os.listdir(input_path):
file_path = os.path.join(input_path, file_name)
if os.path.isfile(file_path) and file_name.lower().endswith(('.mp4', '.mkv', '.avi', '.mov')):
extract_subtitle_tracks(file_path)
else:
print(f"Invalid path: {input_path}")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Extract all subtitle tracks from video files.")
parser.add_argument("input_path", help="Path to the video file or directory.")
args = parser.parse_args()
process_path(args.input_path)

19
ffbitrate Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Check if the user provided a file as an argument
if [ -z "$1" ]; then
echo "Usage: $0 <video_file>"
exit 1
fi
# Get the video file path from the argument
VIDEO_FILE="$1"
# Use ffmpeg to extract the bitrate
BITRATE=$(ffmpeg -i "$VIDEO_FILE" 2>&1 | grep -oP 'bitrate:\s+\K[\d.]+')
if [ -n "$BITRATE" ]; then
echo "Bitrate: ${BITRATE} kb/s"
else
echo "Unable to retrieve bitrate."
fi

88
ffsplit Executable file
View File

@@ -0,0 +1,88 @@
#!/bin/bash
# Function to print usage instructions
usage() {
echo "Usage: $0 input_file HH:MM:SS_1 HH:MM:SS_2 ... HH:MM:SS_N"
exit 1
}
# Check if there are at least two arguments (input file and one time code)
if [ "$#" -lt 2 ]; then
usage
fi
# Get the input file and validate its existence
input_file="$1"
if [ ! -f "$input_file" ]; then
echo "Error: File '$input_file' does not exist."
exit 1
fi
# Extract file extension and base name
extension="${input_file##*.}"
base_name="${input_file%.*}"
# Get the duration of the input file using ffprobe
duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$input_file")
if [ -z "$duration" ]; then
echo "Error: Unable to determine the duration of the file."
exit 1
fi
duration=${duration%.*} # Convert to integer seconds
# Get the list of time codes
time_codes=("${@:2}")
# Add the end of the video to the list of time codes
time_codes+=("$(printf '%02d:%02d:%02d\n' $((duration/3600)) $((duration%3600/60)) $((duration%60)))")
# Function to convert HH:MM:SS to seconds
time_to_seconds() {
local time="$1"
IFS=: read -r hh mm ss <<< "$time"
echo $((10#$hh * 3600 + 10#$mm * 60 + 10#$ss))
}
# Validate each time code
for time in "${time_codes[@]}"; do
total_seconds=$(time_to_seconds "$time")
if [ "$total_seconds" -gt "$duration" ]; then
echo "Error: Time code $time exceeds video duration."
exit 1
fi
done
# Process each segment
previous_time="00:00:00"
for i in "${!time_codes[@]}"; do
current_time="${time_codes[i]}"
if [ "$i" -eq $((${#time_codes[@]} - 1)) ]; then
# If this is the last time code, process until the end of the video
current_time=$(printf '%02d:%02d:%02d\n' $((duration/3600)) $((duration%3600/60)) $((duration%60)))
fi
current_seconds=$(time_to_seconds "$current_time")
previous_seconds=$(time_to_seconds "$previous_time")
segment_duration=$((current_seconds - previous_seconds))
if [ "$segment_duration" -le 0 ]; then
echo "Error: Invalid time range $previous_time to $current_time."
exit 1
fi
# Generate output file name
output_file="${base_name}_${previous_time}-${current_time}.${extension}"
echo "Creating segment: $output_file"
if [ "$previous_time" == "00:00:00" ]; then
# For the first segment, ensure that it starts from the very first frame (avoiding keyframe issues)
ffmpeg -y -i "$input_file" -ss "$previous_time" -to "$current_time" -c:v copy -c:a copy -c:s copy -map 0 -copyts -vsync 1 "$output_file"
else
# For subsequent segments, simply seek and copy streams
ffmpeg -y -i "$input_file" -ss "$previous_time" -to "$current_time" -c:v copy -c:a copy -c:s copy -map 0 -copyts "$output_file"
fi
previous_time="$current_time"
done
echo "Splitting completed successfully."

36
fftesting Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Directory to scan for video files (default: current directory)
DIR=${1:-.}
# Supported video file extensions
EXTENSIONS=("mp4" "mkv" "avi" "mov" "wmv" "flv" "webm" "MP4" "MKV" "AVI" "MOV" "WMV" "FLV" "WEBM")
# Function to test a video file with ffmpeg
test_video() {
local file="$1"
echo "Testing video: $file"
# Run ffmpeg in error-detection mode
ffmpeg -v error -i "$file" -f null - 2> >(grep -E '.*' || true)
}
# Find and test video files
if [[ ! -d "$DIR" ]]; then
echo "Error: Directory '$DIR' does not exist."
exit 1
fi
echo "Scanning directory: $DIR"
for ext in "${EXTENSIONS[@]}"; do
# Find files with the current extension
find "$DIR" -type f -iname "*.$ext" | while IFS= read -r file; do
# Quote the filename to handle spaces correctly
ERRORS=$(test_video "$file")
if [[ -n "$ERRORS" ]]; then
echo -e "\nErrors found in: $file"
echo "$ERRORS"
fi
done
done
echo -e "\nScan complete."

49
filter_extra Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/bash
# Check if the user provided a time argument
if [ -z "$1" ]; then
echo "Usage: sort_extra {time(HH:MM:SS)}"
exit 1
fi
# Convert the provided time into total seconds
IFS=: read -r hours minutes seconds <<< "$1"
target_time=$((hours * 3600 + minutes * 60 + seconds))
# Create the extra folder if it doesnt exist
mkdir -p extra
# Iterate through all subdirectories except 'extra'
for dir in */; do
if [ "$dir" != "extra/" ]; then
find "$dir" -type f -name "*.mkv" | while read -r file; do
# Get the duration of the mkv file in seconds
duration=$(ffprobe -v error -select_streams v:0 -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$file")
duration=${duration%.*} # Convert float to integer
# Convert duration to HH:MM:SS format
hh=$((duration / 3600))
mm=$(((duration % 3600) / 60))
ss=$((duration % 60))
formatted_duration=$(printf "%02d:%02d:%02d" $hh $mm $ss)
# Check if duration is less than the target time
if [ "$duration" -lt "$target_time" ]; then
base_name=$(basename -- "$file")
target_file="extra/$base_name"
# Ensure the file does not overwrite an existing file
if [ -e "$target_file" ]; then
count=1
while [ -e "extra/${count}_$base_name" ]; do
((count++))
done
target_file="extra/${count}_$base_name"
fi
mv "$file" "$target_file"
echo "Moved: $file -> $target_file (Length: $formatted_duration)"
fi
done
fi
done

120
generate-thumb Executable file
View File

@@ -0,0 +1,120 @@
#!/bin/bash
SCENEDETECT="$HOME/.venvs/scenedetect-env/bin/scenedetect"
PROCESSED_LOG="$HOME/.cache/processed_videos.log"
set -e
# Ensure log file exists
mkdir -p "$(dirname "$PROCESSED_LOG")"
touch "$PROCESSED_LOG"
# Find all video files (case-insensitive match)
find . -type f \( -iname "*.mp4" -o -iname "*.mkv" -o -iname "*.avi" \) -print0 |
while IFS= read -r -d '' video; do
echo "🔍 Processing: $video"
# Skip if already processed
if grep -Fxq "$video" "$PROCESSED_LOG"; then
echo "⚠️ Already processed, skipping: $video"
continue
fi
# Get duration in seconds
duration=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$video")
duration=${duration%.*}
start=180
end=$((duration - 180))
[[ "$end" -le "$start" ]] && echo "❌ Skipping (too short): $video" && continue
length=$((end - start))
base="${video%.*}"
trimmed="${base}_trimmed_tmp.mp4"
workdir="${base}_thumb_tmp"
mkdir -p "$workdir"
# Suppress ffmpeg output
ffmpeg -hide_banner -loglevel error -y -ss "$start" -i "$video" -t "$length" -c copy "$trimmed"
# Scene detection
"$SCENEDETECT" -i "$trimmed" -o "$workdir" detect-content save-images
# Rank images by sharpness
best_image=""
best_score=0
for img in "$workdir"/*.jpg; do
score=$(identify -format "%[standard-deviation]" "$img" 2>/dev/null | cut -d',' -f1)
score_float=$(printf "%.6f\n" "$score")
if (( $(echo "$score_float > $best_score" | bc -l) )); then
best_score=$score_float
best_image="$img"
fi
done
thumb="${base}-thumb.jpg"
cp "$best_image" "$thumb"
echo "✅ Thumbnail saved: $thumb"
echo "$video" >> "$PROCESSED_LOG"
rm -rf "$workdir" "$trimmed"
done
########## Using full video duration ##########
# #!/bin/bash
# SCENEDETECT="$HOME/.venvs/scenedetect-env/bin/scenedetect"
# PROCESSED_LOG="$HOME/.cache/processed_videos.log"
# set -e
# video="$1"
# [[ ! -f "$video" ]] && echo "Video not found: $video" && exit 1
# # Ensure log file exists
# mkdir -p "$(dirname "$PROCESSED_LOG")"
# touch "$PROCESSED_LOG"
# # Check if video is already processed
# if grep -Fxq "$video" "$PROCESSED_LOG"; then
# echo "⚠️ Video already processed: $video"
# exit 0
# fi
# # Prepare paths
# base="${video%.*}"
# workdir="${base}_thumb_tmp"
# mkdir -p "$workdir"
# # Step 1: Scene detection
# "$SCENEDETECT" -i "$video" -o "$workdir" detect-content save-images
# # Step 2: Rank images by sharpness (standard deviation)
# best_image=""
# best_score=0
# for img in "$workdir"/*.jpg; do
# # Get standard deviation of luminance
# score=$(identify -format "%[standard-deviation]" "$img" 2>/dev/null | cut -d',' -f1)
# # Compare scores
# score_float=$(printf "%.6f\n" "$score")
# if (( $(echo "$score_float > $best_score" | bc -l) )); then
# best_score=$score_float
# best_image="$img"
# fi
# done
# # Step 3: Save best image with Jellyfin naming scheme
# thumb="${base}-thumb.jpg"
# cp "$best_image" "$thumb"
# echo "✅ Thumbnail saved: $thumb"
# # Step 4: Mark as processed
# echo "$video" >> "$PROCESSED_LOG"
# # Step 5: Cleanup
# rm -rf "$workdir"

12
grayjay Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
APP_DIR="$HOME/.local/share/grayjay"
# Check if app is already installed in user directory
if [ ! -d "$APP_DIR" ]; then
echo "First run - installing Grayjay to $APP_DIR"
mkdir -p "$APP_DIR"
cp -r /usr/share/grayjay/* "$APP_DIR/"
chmod u+w -R "$APP_DIR"
fi
exec sh -c "cd '$APP_DIR' && exec ./Grayjay \"\$@\"" -- "$@"

51
list_folder_empty Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
# Function to list files in a directory
list_contents() {
local dir="$1"
local result=()
# Check if the directory contains files or subdirectories
local has_file=false
local has_folder=false
# Loop through the directory contents
for item in "$dir"/*; do
if [ -f "$item" ]; then
# If it's a file, get its extension and add it to the result
ext="${item##*.}"
result+=("$ext")
has_file=true
elif [ -d "$item" ]; then
has_folder=true
fi
done
# If files are found, return their extensions
if [ "$has_file" = true ]; then
echo "$(printf "%s, " "${result[@]}" | sed 's/, $//')"
elif [ "$has_folder" = true ]; then
echo "folder"
fi
}
# Function to recursively list non-empty directories
list_non_empty_directories() {
local base_dir="$1"
# Loop through the directories in the base directory
for dir in "$base_dir"/*; do
if [ -d "$dir" ]; then
# List contents of the directory
contents=$(list_contents "$dir")
if [ -n "$contents" ]; then
echo "$(basename "$dir") ($contents)"
fi
# Recursively check subdirectories
list_non_empty_directories "$dir"
fi
done
}
# Start listing from the current directory
list_non_empty_directories "."

16
nohup.out Executable file
View File

@@ -0,0 +1,16 @@
node:events:502
throw er; // Unhandled 'error' event
^
Error: EBADF: bad file descriptor, read
Emitted 'error' event on ReadStream instance at:
at emitErrorNT (node:internal/streams/destroy:169:8)
at errorOrDestroy (node:internal/streams/destroy:238:7)
at node:internal/fs/streams:272:9
at FSReqCallback.wrapper [as oncomplete] (node:fs:683:5) {
errno: -9,
code: 'EBADF',
syscall: 'read'
}
Node.js v20.19.0

27
rename_episodes Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Use the first argument as the directory or default to the current directory
directory="${1:-$(pwd)}"
# Change to the specified directory
cd "$directory" || { echo "Directory not found: $directory"; exit 1; }
# Regular expression to match " 00 ", " 01 ", ..., " 99 "
for file in *; do
if [[ "$file" =~ ([[:space:]]([0-9]{2})[[:space:]]) ]]; then
# Extract the number (e.g., "00", "01")
number="${BASH_REMATCH[2]}"
# Get the file extension
extension="${file##*.}"
[[ "$file" == "$extension" ]] && extension="" || extension=".$extension"
# Construct the new filename
new_filename="E$number$extension"
# Rename the file
mv -v "$file" "$new_filename"
else
echo "Skipped: $file (no match)"
fi
done

27
rename_filtered Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Check if scheme is provided
if [ -z "$1" ]; then
echo "Usage: rename_filtered <scheme> [folder]"
echo
echo "Example:"
echo " Files in folder:"
echo " myfile_E123.txt"
echo " myfile_E456.txt"
echo " myfile_E789.txt"
echo " myfile_E2468.txt"
echo
echo " Run:"
echo " rename_filtered \"????????E!!!\" ./"
echo
echo " Result:"
echo " myfile_E123.txt → 123.txt"
echo " myfile_E456.txt → 456.txt"
echo " myfile_E789.txt → 789.txt"
echo " myfile_E2468.txt → myfile_2468.txt"
exit 1
fi
# Call the Python script with scheme and optional folder argument
python3 /home/honney/.bin/rename_filtered.py "$@"

64
rename_filtered.py Normal file
View File

@@ -0,0 +1,64 @@
import os
import sys
import glob
def process_files(files, scheme):
scheme_len = len(scheme)
for filename in files:
print(f"Processing file: {filename}")
print(f"Against scheme: {scheme}")
# Get the actual filename without the directory path
base = os.path.basename(filename)
name, ext = os.path.splitext(base)
if scheme_len == len(name):
new_name = ""
for i in range(scheme_len):
current_letter = scheme[i]
if current_letter == "?":
pass
elif current_letter == "!":
new_name += name[i]
elif current_letter == name[i]:
new_name += current_letter
else:
print(f"Mismatch at position {i}, skipping {filename}")
new_name = None
break
if new_name:
rename_file(filename, new_name, ext)
else:
print("No new name Generated. It is not supposed to do that")
return
def rename_file(file, new_name, ext):
dirname = os.path.dirname(file) # correct: preserve folder
old_base = os.path.basename(file)
if old_base != new_name:
new_filepath = os.path.join(dirname, new_name + ext)
os.rename(file, new_filepath)
print(f"Renamed '{old_base}''{new_name + ext}'")
else:
print(f"No renaming needed for '{old_base}'.")
def main():
if len(sys.argv) < 2:
print("Usage: scheme.py <scheme> [folder]")
sys.exit(1)
scheme = sys.argv[1]
folder = sys.argv[2] if len(sys.argv) > 2 else "."
# Get all files in the folder (assuming files are in the current directory by default)
files = glob.glob(os.path.join(folder, "*"))
# Run the renaming logic
process_files(files, scheme)
if __name__ == "__main__":
main()

47
rename_to_first4.py Executable file
View File

@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
rename_to_first4.py
Usage:
python3 rename_to_first4.py <directory>
Renames each file in <directory> so its name becomes the first
four characters of the original filename (before the extension).
Keeps the file extension and avoids overwriting existing files.
"""
import os
import sys
from pathlib import Path
def main():
if len(sys.argv) != 2:
print("Usage: rename_to_first4.py <directory>")
sys.exit(1)
directory = Path(sys.argv[1])
if not directory.is_dir():
print(f"Error: '{directory}' is not a directory.")
sys.exit(1)
for f in directory.iterdir():
if not f.is_file():
continue
stem = f.stem[:4] # first 4 letters of filename (no extension)
new_name = f"{stem}{f.suffix}"
new_path = directory / new_name
# avoid overwriting existing files
counter = 1
while new_path.exists():
new_name = f"{stem}_{counter}{f.suffix}"
new_path = directory / new_name
counter += 1
print(f"Renaming: {f.name} -> {new_name}")
f.rename(new_path)
print("✅ Done renaming all files.")
if __name__ == "__main__":
main()

525
simple_ffprobe_script Executable file
View File

@@ -0,0 +1,525 @@
#!/bin/bash
# Choose which Extensions are acceptable
extensions=("mp4" "mkv" "avi" "mov" "wmv" "flv" "webm" "lrv" "MP4" "MKV" "AVI" "MOV" "WMV" "FLV" "WEBM" "LRV" "GIF" "gif")
# Language code mapping
declare -A LANG_CODES=(
["eng"]="English" ["en"]="English"
["spa"]="Spanish" ["es"]="Spanish"
["fra"]="French" ["fr"]="French"
["deu"]="German" ["ger"]="German" ["de"]="German"
["jpn"]="Japanese" ["ja"]="Japanese"
["ita"]="Italian" ["it"]="Italian"
["por"]="Portuguese" ["pt"]="Portuguese"
["rus"]="Russian" ["ru"]="Russian"
["chi"]="Chinese" ["zh"]="Chinese"
["kor"]="Korean" ["ko"]="Korean"
["dut"]="Dutch" ["nl"]="Dutch"
["swe"]="Swedish" ["sv"]="Swedish"
["fin"]="Finnish" ["fi"]="Finnish"
["pol"]="Polish" ["pl"]="Polish"
["ara"]="Arabic" ["ar"]="Arabic"
["hin"]="Hindi" ["hi"]="Hindi"
["tur"]="Turkish" ["tr"]="Turkish"
["und"]="Undefined" [" "]="Undefined"
["ab"]="Abkhazian" ["abk"]="Abkhazian"
["aa"]="Afar" ["aar"]="Afar"
["af"]="Afrikaans" ["afr"]="Afrikaans"
["ak"]="Akan" ["aka"]="Akan"
["twi"]="Twi"
["fat"]="Fanti"
["sq"]="Albanian" ["sqi"]="Albanian" ["alb"]="Albanian"
["am"]="Amharic"
["amh"]="Amharic"
["arb"]="Arabic"
["an"]="Aragonese" ["arg"]="Aragonese"
["hy"]="Armenian" ["hye"]="Armenian" ["arm"]="Armenian"
["as"]="Assamese" ["asm"]="Assamese"
["av"]="Avaric" ["ava"]="Avaric"
["ae"]="Avestan" ["ave"]="Avestan"
["ay"]="Aymara" ["aym"]="Aymara"
["az"]="Azerbaijani" ["aze"]="Azerbaijani"
["bm"]="Bambara" ["bam"]="Bambara"
["ba"]="Bashkir" ["bak"]="Bashkir"
["eu"]="Basque" ["eus"]="Basque" ["baq"]="Basque"
["be"]="Belarusian" ["bel"]="Belarusian"
["bn"]="Bengali" ["ben"]="Bengali"
["bi"]="Bislama" ["bis"]="Bislama"
["bs"]="Bosnian" ["bos"]="Bosnian"
["br"]="Breton" ["bre"]="Breton"
["bg"]="Bulgarian" ["bul"]="Bulgarian"
["my"]="Burmese" ["mya"]="Burmese"
["ca"]="Catalan" ["cat"]="Catalan"
["ch"]="Chamorro" ["cha"]="Chamorro"
["ce"]="Chechen" ["che"]="Chechen"
["ny"]="Chichewa" ["nya"]="Chichewa" ["zho"]="Chinese"
["cu"]="Church Slavonic" ["chu"]="Church Slavonic"
["cv"]="Chuvash" ["chv"]="Chuvash"
["kw"]="Cornish" ["cor"]="Cornish"
["co"]="Corsican" ["cos"]="Corsican"
["cr"]="Cree" ["cre"]="Cree"
["hr"]="Croatian" ["hrv"]="Croatian"
["cs"]="Czech" ["ces"]="Czech" ["cze"]="Czech"
["da"]="Danish" ["dan"]="Danish"
["dv"]="Divehi" ["div"]="Divehi"
["dz"]="Dzongkha" ["dzo"]="Dzongkha"
["eo"]="Esperanto" ["epo"]="Esperanto"
["et"]="Estonian" ["est"]="Estonian"
["ee"]="Ewe" ["ewe"]="Ewe"
["fo"]="Faroese" ["fao"]="Faroese"
["fj"]="Fijian" ["fij"]="Fijian"
["fre"]="French"
["fy"]="Western Frisian" ["fry"]="Western Frisian"
["ff"]="Fulah" ["ful"]="Fulah"
["gd"]="Gaelic, Scottish Gaelic"
["gla"]="Gaelic"
["gl"]="Galician" ["glg"]="Galician"
["lg"]="Ganda" ["lug"]="Ganda"
["ka"]="Georgian" ["kat"]="Georgian" ["geo"]="Georgian"
["el"]="Greek" ["ell"]="Greek" ["gre"]="Greek"
["kl"]="Kalaallisut" ["kal"]="Kalaallisut"
["gn"]="Guarani" ["grn"]="Guarani"
["gu"]="Gujarati" ["guj"]="Gujarati"
["ht"]="Haitian Creole" ["hat"]="Haitian Creole"
["ha"]="Hausa" ["hau"]="Hausa"
["he"]="Hebrew" ["heb"]="Hebrew"
["hz"]="Herero" ["her"]="Herero"
["ho"]="Hiri Motu" ["hmo"]="Hiri Motu"
["hu"]="Hungarian" ["hun"]="Hungarian"
["is"]="Icelandic" ["isl"]="Icelandic" ["ice"]="Icelandic"
["io"]="Ido" ["ido"]="Ido"
["ig"]="Igbo" ["ibo"]="Igbo"
["id"]="Indonesian" ["ind"]="Indonesian"
["ia"]="Interlingua" ["ina"]="Interlingua"
["ie"]="Interlingue" ["ile"]="Interlingue"
["iu"]="Inuktitut" ["iku"]="Inuktitut"
["ik"]="Inupiaq" ["ipk"]="Inupiaq"
["ga"]="Irish" ["gle"]="Irish"
["jv"]="Javanese" ["jav"]="Javanese"
["kn"]="Kannada" ["kan"]="Kannada"
["kr"]="Kanuri" ["kau"]="Kanuri"
["ks"]="Kashmiri" ["kas"]="Kashmiri"
["kk"]="Kazakh" ["kaz"]="Kazakh"
["km"]="Central Khmer" ["khm"]="Central Khmer"
["ki"]="Kikuyu" ["kik"]="Kikuyu"
["rw"]="Kinyarwanda" ["kin"]="Kinyarwanda"
["ky"]="Kyrgyz" ["kir"]="Kyrgyz"
["kv"]="Komi" ["kom"]="Komi"
["kg"]="Kongo" ["kon"]="Kongo"
["kj"]="Kuanyama" ["kua"]="Kuanyama"
["ku"]="Kurdish" ["kur"]="Kurdish"
["lo"]="Lao" ["lao"]="Lao"
["la"]="Latin" ["lat"]="Latin"
["lv"]="Latvian" ["lav"]="Latvian"
["li"]="Limburgan" ["lim"]="Limburgan"
["ln"]="Lingala" ["lin"]="Lingala"
["lt"]="Lithuanian" ["lit"]="Lithuanian"
["lu"]="Luba-Katanga" ["lub"]="Luba-Katanga"
["lb"]="Luxembourgish" ["ltz"]="Luxembourgish"
["mk"]="Macedonian" ["mkd"]="Macedonian" ["mac"]="Macedonian"
["mg"]="Malagasy" ["mlg"]="Malagasy"
["ms"]="Malay" ["msa"]="Malay"
["ml"]="Malayalam" ["mal"]="Malayalam"
["mt"]="Maltese" ["mlt"]="Maltese"
["gv"]="Manx" ["glv"]="Manx"
["mi"]="Maori" ["mri"]="Maori" ["mao"]="Maori"
["mr"]="Marathi" ["mar"]="Marathi"
["mh"]="Marshallese" ["mah"]="Marshallese"
["mn"]="Mongolian" ["mon"]="Mongolian"
["na"]="Nauru" ["nau"]="Nauru"
["nv"]="Navajo" ["nav"]="Navajo"
["nd"]="North Ndebele" ["nde"]="North Ndebele"
["nr"]="South Ndebele" ["nbl"]="South Ndebele"
["ng"]="Ndonga" ["ndo"]="Ndonga"
["ne"]="Nepali" ["nep"]="Nepali"
["no"]="Norwegian" ["nor"]="Norwegian"
["nb"]="Norwegian Bokmål" ["nob"]="Norwegian Bokmål"
["nn"]="Norwegian Nynorsk" ["nno"]="Norwegian Nynorsk"
["oc"]="Occitan" ["oci"]="Occitan"
["oj"]="Ojibwa" ["oji"]="Ojibwa"
["or"]="Oriya" ["ori"]="Oriya"
["om"]="Oromo" ["orm"]="Oromo"
["os"]="Ossetian" ["oss"]="Ossetian"
["pi"]="Pali" ["pli"]="Pali"
["ps"]="Pashto" ["pus"]="Pashto"
["fa"]="Persian" ["fas"]="Persian" ["per"]="Persian"
["pa"]="Punjabi" ["pan"]="Punjabi"
["qu"]="Quechua" ["que"]="Quechua"
["ro"]="Romanian" ["ron"]="Romanian" ["rum"]="Romanian"
["rm"]="Romansh" ["roh"]="Romansh"
["rn"]="Rundi" ["run"]="Rundi"
["se"]="Northern Sami" ["sme"]="Northern Sami"
["sm"]="Samoan" ["smo"]="Samoan"
["sg"]="Sango" ["sag"]="Sango"
["sa"]="Sanskrit" ["san"]="Sanskrit"
["sc"]="Sardinian" ["srd"]="Sardinian"
["sr"]="Serbian" ["srp"]="Serbian"
["sn"]="Shona" ["sna"]="Shona"
["sd"]="Sindhi" ["snd"]="Sindhi" ["si"]="Sinhala" ["sin"]="Sinhala"
["sk"]="Slovak" ["slk"]="Slovak" ["slo"]="Slovak"
["sl"]="Slovenian" ["slv"]="Slovenian"
["so"]="Somali" ["som"]="Somali"
["st"]="Southern Sotho" ["sot"]="Southern Sotho"
["su"]="Sundanese" ["sun"]="Sundanese"
["sw"]="Swahil" ["swa"]="Swahili"
["ss"]="Swati" ["ssw"]="Swati"
["tl"]="Tagalog" ["tgl"]="Tagalog"
["ty"]="Tahitian" ["tah"]="Tahitian"
["tg"]="Tajik" ["tgk"]="Tajik"
["ta"]="Tamil" ["tam"]="Tamil"
["tt"]="Tatar" ["tat"]="Tatar"
["te"]="Telugu" ["tel"]="Telugu"
["th"]="Thai" ["tha"]="Thai"
["bo"]="Tibetan" ["bod"]="Tibetan" ["tib"]="Tibetan"
["ti"]="Tigrinya" ["tir"]="Tigrinya"
["to"]="Tongan" ["ton"]="Tongan" ["ts"]="Tsonga" ["tso"]="Tsonga"
["tn"]="Tswana" ["tsn"]="Tswana"
["tk"]="Turkmen" ["tuk"]="Turkmen"
["ug"]="Uighur" ["uig"]="Uighur"
["uk"]="Ukrainian" ["ukr"]="Ukrainian"
["ur"]="Urdu" ["urd"]="Urdu"
["uz"]="Uzbek" ["uzb"]="Uzbek"
["ve"]="Venda" ["ven"]="Venda"
["vi"]="Vietnamese" ["vie"]="Vietnamese"
["vo"]="Volapük" ["vol"]="Volapük"
["wa"]="Walloon" ["wln"]="Walloon"
["cy"]="Welsh" ["cym"]="Welsh" ["wel"]="Welsh"
["wo"]="Wolof" ["wol"]="Wolof"
["xh"]="Xhosa" ["xho"]="Xhosa"
["ii"]="Sichuan Yi" ["iii"]="Sichuan Yi"
["yi"]="Yiddish" ["yid"]="Yiddish"
["yo"]="Yoruba" ["yor"]="Yoruba"
["za"]="Zhuang" ["zha"]="Zhuang"
["zu"]="Zulu" ["zul"]="Zulu"
)
declare -A CONTAINER_NAMES=(
["mov"]="QuickTime"
["mp4"]="MP4"
["mkv"]="Matroska"
["webm"]="WebM"
["avi"]="AVI"
["flv"]="FLV"
["wmv"]="WMV"
)
# Function to translate language codes
translate_language() {
local code="$1"
# Sanitize input: remove spaces and convert to lowercase
code=$(echo "$code" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
# Check for valid subscript and handle fallback
if [[ -z "$code" || ! ${LANG_CODES[$code]+_} ]]; then
echo "Undefined"
else
echo "${LANG_CODES[$code]}"
fi
}
# Function to format file size in human-readable form
format_file_size() {
local size_bytes="$1"
if [ "$size_bytes" -lt 1024 ]; then
echo "${size_bytes}B"
elif [ "$size_bytes" -lt 1048576 ]; then
echo "$((size_bytes / 1024))KB"
elif [ "$size_bytes" -lt 1073741824 ]; then
echo "$((size_bytes / 1048576))MB"
else
echo "$((size_bytes / 1073741824))GB"
fi
}
# Function to format duration into HH:MM:SS
format_duration() {
local duration="$1"
printf "%02d:%02d:%02d" $((duration/3600)) $(((duration%3600)/60)) $((duration%60))
}
# Function to extract video, audio, and subtitle details for a single file
process_file() {
local file="$1"
local file_size
file_size=$(stat --printf="%s" "$file")
formatted_size=$(format_file_size "$file_size")
# Extract video duration in seconds
duration_seconds=$(ffprobe -v error -select_streams v:0 -show_entries format=duration -of csv=p=0 "$file" | cut -d'.' -f1)
duration_formatted=$(format_duration "$duration_seconds")
# Extract video codec, width, height, and pixel format (like yuv420p(tv, bt709))
video_info=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name,width,height,pix_fmt,color_space \
-of default=noprint_wrappers=1:nokey=1 "$file")
codec=$(echo "$video_info" | sed -n '1p')
width=$(echo "$video_info" | sed -n '2p')
height=$(echo "$video_info" | sed -n '3p')
framerate=$(ffprobe -v 0 -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 "$file" | awk -F/ '{printf "%.3f", $1/$2}')
dar=$(ffprobe -v error -select_streams v:0 -show_entries stream=display_aspect_ratio -of default=noprint_wrappers=1:nokey=1 "$file")
pix_fmt=$(echo "$video_info" | sed -n '4p')
color_space=$(echo "$video_info" | sed -n '5p')
# Detect interlacing using ffmpeg idet filter (analyzing a short portion to keep it fast)
interlace_result=$(ffmpeg -filter:v idet -frames:v 100 -an -f rawvideo -y /dev/null -i "$file" 2>&1)
tff_count=$(echo "$interlace_result" | grep 'TFF:' | tail -n1 | grep -o 'TFF:[ ]*[0-9]*' | grep -o '[0-9]*')
bff_count=$(echo "$interlace_result" | grep 'BFF:' | tail -n1 | grep -o 'BFF:[ ]*[0-9]*' | grep -o '[0-9]*')
progressive_count=$(echo "$interlace_result" | grep 'Progressive:' | tail -n1 | grep -o 'Progressive:[ ]*[0-9]*' | grep -o '[0-9]*')
if [[ "$progressive_count" -gt "$tff_count" && "$progressive_count" -gt "$bff_count" ]]; then
interlace_status="Progressive"
elif [[ "$tff_count" -gt "$bff_count" ]]; then
interlace_status="Interlaced (TFF)"
elif [[ "$bff_count" -gt "$tff_count" ]]; then
interlace_status="Interlaced (BFF)"
else
interlace_status="Unknown"
fi
# Extract audio information (language, codec, and channel layout)
audio_info=$(ffprobe -v quiet -select_streams a -show_entries stream=codec_name,channel_layout:stream_tags=language \
-of default=noprint_wrappers=1:nokey=1 "$file" | paste -sd "," -)
# Extract subtitle information (language)
subtitle_info=$(ffprobe -v quiet -select_streams s -show_entries stream_tags=language \
-of default=noprint_wrappers=1:nokey=1 "$file" | paste -sd "," -)
# Format video output
video_output="Video: ${codec} (${width}x${height}@${framerate}) [${dar}], ${pix_fmt} (${color_space}), ${interlace_status}"
# Format audio output
audio_output="Audio: "
languages=""
details=""
IFS=',' read -r -a audio_streams <<< "$audio_info"
for (( i=0; i<${#audio_streams[@]}; i+=3 )); do
audio_codec="${audio_streams[i]}"
channel_layout="${audio_streams[i+1]}"
lang_code="${audio_streams[i+2]}"
lang=$(translate_language "${lang_code:-" "}")
# Collect languages
languages+="$lang"
# Collect detailed information
details+="$lang $audio_codec $channel_layout"
# Add comma separator if not the last item
if (( i+3 < ${#audio_streams[@]} )); then
languages+=", "
details+=", "
fi
done
# Format output with languages first, followed by detailed info in brackets
audio_output+="$languages [$details]"
# Format subtitle output
subtitle_output="Subtitles: "
if [ -n "$subtitle_info" ]; then
IFS=',' read -r -a subtitle_streams <<< "$subtitle_info"
for lang in "${subtitle_streams[@]}"; do
subtitle_output+=$(translate_language "$lang")
subtitle_output+=", "
done
subtitle_output=${subtitle_output%, }
else
subtitle_output="Subtitles: None"
fi
# Output the result for the file
echo "$file ($formatted_size, Duration: $duration_formatted):"
echo -e "\t$video_output"
echo -e "\t$audio_output"
echo -e "\t$subtitle_output"
}
# Main script logic
if [ -z "$1" ]; then
for ext in "${extensions[@]}"; do
for file in *."$ext"; do
[ -e "$file" ] || continue
process_file "$file"
done
done
elif [ -d "$1" ]; then
for file in "$1"/*.{mp4,mkv,avi,mov}; do
[ -e "$file" ] || continue
process_file "$file"
done
elif [ -f "$1" ]; then
process_file "$1"
else
echo "Error: $1 is not a valid file or directory."
exit 1
fi
# #!/bin/bash
# # Choose which Extensions are acceptable
# extensions=("mp4" "mkv" "avi" "mov" "m4v" "flv" "lrv")
# # Language code translation associative array
# declare -A LANG_CODES=(
# ["eng"]="English"
# ["en"]="English"
# ["spa"]="Spanish"
# ["es"]="Spanish"
# ["fra"]="French"
# ["fr"]="French"
# ["deu"]="German"
# ["ger"]="German"
# ["de"]="German"
# ["jpn"]="Japanese"
# ["ja"]="Japanese"
# ["ita"]="Italian"
# ["it"]="Italian"
# ["por"]="Portuguese"
# ["pt"]="Portuguese"
# ["rus"]="Russian"
# ["ru"]="Russian"
# ["chi"]="Chinese"
# ["zh"]="Chinese"
# ["kor"]="Korean"
# ["ko"]="Korean"
# ["dut"]="Dutch"
# ["nl"]="Dutch"
# ["swe"]="Swedish"
# ["sv"]="Swedish"
# ["fin"]="Finnish"
# ["fi"]="Finnish"
# ["pol"]="Polish"
# ["pl"]="Polish"
# ["ara"]="Arabic"
# ["ar"]="Arabic"
# ["hin"]="Hindi"
# ["hi"]="Hindi"
# ["tur"]="Turkish"
# ["tr"]="Turkish"
# ["und"]="Undefined"
# [" "]="Undefined"
# )
# # Function to translate language codes
# translate_language() {
# local code="$1"
# # Sanitize input: remove spaces and convert to lowercase
# code=$(echo "$code" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
# # Debugging output
# # echo "DEBUG: Translating code: '$code'" >&2
# # Check for valid subscript and handle fallback
# if [[ -z "$code" || ! ${LANG_CODES[$code]+_} ]]; then
# echo "Undefined"
# else
# echo "${LANG_CODES[$code]}"
# fi
# }
# # Function to format file size in human-readable form
# format_file_size() {
# local size_bytes="$1"
# if [ "$size_bytes" -lt 1024 ]; then
# echo "${size_bytes}B"
# elif [ "$size_bytes" -lt 1048576 ]; then
# echo "$((size_bytes / 1024))KB"
# elif [ "$size_bytes" -lt 1073741824 ]; then
# echo "$((size_bytes / 1048576))MB"
# else
# echo "$((size_bytes / 1073741824))GB"
# fi
# }
# # Function to extract video, audio, and subtitle details for a single file
# process_file() {
# local file="$1"
# local file_size
# file_size=$(stat --printf="%s" "$file")
# formatted_size=$(format_file_size "$file_size")
# # Extract video codec, width, height, and pixel format (like yuv420p(tv, bt709))
# video_info=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name,width,height,pix_fmt,color_space \
# -of default=noprint_wrappers=1:nokey=1 "$file")
# codec=$(echo "$video_info" | sed -n '1p')
# width=$(echo "$video_info" | sed -n '2p')
# height=$(echo "$video_info" | sed -n '3p')
# pix_fmt=$(echo "$video_info" | sed -n '4p')
# color_space=$(echo "$video_info" | sed -n '5p')
# # Extract audio information (language, codec, and channel layout)
# audio_info=$(ffprobe -v quiet -select_streams a -show_entries stream=codec_name,channel_layout:stream_tags=language \
# -of default=noprint_wrappers=1:nokey=1 "$file" | paste -sd "," -)
# # Extract subtitle information (language)
# subtitle_info=$(ffprobe -v quiet -select_streams s -show_entries stream_tags=language \
# -of default=noprint_wrappers=1:nokey=1 "$file" | paste -sd "," -)
# # Format video output
# video_output="Video: ${codec} (${width}x${height}), ${pix_fmt} (${color_space})"
# # Format audio output
# audio_output="Audio: "
# IFS=',' read -r -a audio_streams <<< "$audio_info"
# for (( i=0; i<${#audio_streams[@]}; i+=3 )); do
# audio_codec="${audio_streams[i]}"
# channel_layout="${audio_streams[i+1]}"
# lang_code="${audio_streams[i+2]}"
# lang=$(translate_language "${lang_code:-" "}")
# audio_output+="${lang} ${audio_codec} ${channel_layout}"
# if (( i+3 < ${#audio_streams[@]} )); then
# audio_output+=", "
# fi
# done
# # Format subtitle output
# subtitle_output="Subtitles: "
# if [ -n "$subtitle_info" ]; then
# IFS=',' read -r -a subtitle_streams <<< "$subtitle_info"
# for lang in "${subtitle_streams[@]}"; do
# subtitle_output+=$(translate_language "$lang")
# subtitle_output+=", "
# done
# # Remove the last comma
# subtitle_output=${subtitle_output%, }
# else
# subtitle_output="Subtitles: None"
# fi
# # Output the result for the file
# echo "$file ($formatted_size):"
# echo -e "\t$video_output"
# echo -e "\t$audio_output"
# echo -e "\t$subtitle_output"
# }
# # Main script logic
# if [ -z "$1" ]; then
# for ext in "${extensions[@]}"; do
# # No argument provided, process all video files in the current directory
# for file in *."$ext"; do
# [ -e "$file" ] || continue # Skip if no matching files
# process_file "$file"
# done
# done
# elif [ -d "$1" ]; then
# # Argument is a directory, process all video files in that directory
# for file in "$1"/*.{mp4,mkv,avi,mov}; do
# [ -e "$file" ] || continue # Skip if no matching files
# process_file "$file"
# done
# elif [ -f "$1" ]; then
# # Argument is a single file, process only that file
# process_file "$1"
# else
# echo "Error: $1 is not a valid file or directory."
# exit 1
# fi

137
split_mkv_by_chapter Executable file
View File

@@ -0,0 +1,137 @@
#!/bin/bash
# Function to convert timestamp to seconds, including milliseconds
timestamp_to_seconds() {
IFS=':' read -r hours minutes seconds <<< "$1"
IFS='.' read -r seconds milliseconds <<< "$seconds"
total_seconds=$(bc <<< "$hours * 3600 + $minutes * 60 + $seconds")
total_milliseconds=$(bc <<< "$milliseconds / 1000")
echo "$total_seconds + $total_milliseconds" | bc
}
# Function to convert seconds back to HH:MM:SS.mmm format for mkvmerge
seconds_to_hms() {
total_seconds="$1"
# Separate whole seconds and milliseconds
seconds=$(echo "$total_seconds" | cut -d'.' -f1)
milliseconds=$(echo "$total_seconds" | cut -d'.' -f2 | sed 's/^[0-9]\{1,3\}$/&000/') # Add trailing zeros if necessary
# Ensure milliseconds have 3 digits
milliseconds=$(echo "$milliseconds" | cut -c1-3)
# Convert seconds to HH:MM:SS format
hours=$(bc <<< "$seconds / 3600")
minutes=$(bc <<< "($seconds % 3600) / 60")
seconds=$(bc <<< "$seconds % 60")
# Print in HH:MM:SS.mmm format
printf "%02d:%02d:%02d.%03d" "$hours" "$minutes" "$seconds" "$milliseconds"
}
# Function to split the video using mkvmerge
split_video() {
input_file="$1"
start_time="$2"
end_time="$3"
output_file="$4"
start_hms=$(seconds_to_hms "$start_time")
end_hms=$(seconds_to_hms "$end_time")
# Print the mkvmerge command before running it
echo "Running command: mkvmerge -o \"$output_file\" --split parts:\"$start_hms\"-\"$end_hms\" \"$input_file\""
# Execute the command
mkvmerge -o "$output_file" --split parts:"$start_hms"-"$end_hms" "$input_file"
}
# Function to get video duration
get_duration() {
input_file="$1"
duration=$(ffprobe -v quiet -print_format json -show_format "$input_file" | jq -r '.format.duration')
echo "$duration"
}
# Function to extract chapter start and end times from the input file
get_chapters() {
input_file="$1"
chapters=()
# Extract the chapters using mkvinfo, then parse the start and end times
while IFS= read -r line; do
if [[ "$line" =~ "start" ]]; then
start_time=$(echo "$line" | awk '{print $2}')
chapters+=("$start_time")
fi
done < <(mkvinfo "$input_file" | grep "Chapter")
echo "${chapters[@]}"
}
# Main function
main() {
only_between=false
if [ "$1" == "--only_between" ]; then
only_between=true
shift
fi
if [ "$#" -lt 3 ]; then
echo "Usage: $0 [--only_between] <input_file> <timestamp1> <timestamp2> ..."
exit 1
fi
input_file="$1"
shift
timestamps=($@)
output_folder="split_output"
mkdir -p "$output_folder"
echo "Splitting video from provided timestamps..."
# Handle chapter timecodes if present
if [[ "${timestamps[0]}" == "chapters" ]]; then
# Extract chapter start times
chapter_times=($(get_chapters "$input_file"))
echo "Extracting chapters from the input file..."
for ((i = 0; i < ${#chapter_times[@]} - 1; i++)); do
start_time="${chapter_times[$i]}"
end_time="${chapter_times[$i+1]}"
output_filename="$output_folder/chapter_$((i+1)).mkv"
echo "Splitting from Chapter $((i+1)) time $start_time to Chapter $((i+2)) time $end_time..."
split_video "$input_file" "$start_time" "$end_time" "$output_filename"
done
elif $only_between; then
echo "Extracting only specified segments..."
for ((i = 0; i < ${#timestamps[@]} - 1; i++)); do
start_time=$(timestamp_to_seconds "${timestamps[$i]}")
end_time=$(timestamp_to_seconds "${timestamps[$i+1]}")
output_filename="$output_folder/part_$((i+1)).mkv"
echo "Splitting from ${timestamps[$i]} to ${timestamps[$i+1]}..."
split_video "$input_file" "$start_time" "$end_time" "$output_filename"
done
else
echo "Extracting all segments..."
start_time=0
output_index=1
for timestamp in "${timestamps[@]}"; do
end_time=$(timestamp_to_seconds "$timestamp")
output_filename="$output_folder/part_${output_index}.mkv"
echo "Splitting from $start_time to $timestamp..."
split_video "$input_file" "$start_time" "$end_time" "$output_filename"
start_time=$end_time
((output_index++))
done
duration=$(get_duration "$input_file")
output_filename="$output_folder/part_${output_index}.mkv"
echo "Splitting from $start_time to $duration..."
split_video "$input_file" "$start_time" "$duration" "$output_filename"
fi
echo "Splitting complete. Files saved in $output_folder."
}
main "$@"

122
split_mkv_by_chapter.py Normal file
View File

@@ -0,0 +1,122 @@
import subprocess
import sys
import os
import json
# Function to convert timestamp to seconds
def timestamp_to_seconds(timestamp):
hours, minutes, seconds = map(float, timestamp.split(":"))
return hours * 3600 + minutes * 60 + seconds
# Function to convert seconds back to HH:MM:SS format for mkvmerge
def seconds_to_hms(seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = seconds % 60
return f"{hours:02}:{minutes:02}:{seconds:.3f}"
# Function to get chapters from the MKV file using ffprobe
def get_chapters(input_file):
cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_chapters", input_file]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print("Error: Unable to retrieve chapter information.")
sys.exit(1)
chapters = json.loads(result.stdout)["chapters"]
chapter_times = [float(chapter["start_time"]) for chapter in chapters] # Convert to float
return chapter_times
# Function to find the nearest chapter time to a given timestamp
def find_nearest_chapter(timestamp, chapter_times):
timestamp_seconds = timestamp_to_seconds(timestamp)
nearest_chapter = min(chapter_times, key=lambda chapter: abs(chapter - timestamp_seconds))
return nearest_chapter
# Function to split the video using mkvmerge
# Function to split the video using mkvmerge
def split_video(input_file, start_time, end_time, output_file):
# Convert start_time and end_time to HH:MM:SS format for mkvmerge
start_time_hms = seconds_to_hms(start_time)
end_time_hms = seconds_to_hms(end_time)
cmd = [
"mkvmerge",
"-o", output_file,
"--split", f"parts:{start_time_hms}-{end_time_hms}",
input_file
]
print(f"Running command: {' '.join(cmd)}") # Debugging: print the command
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error splitting video: {result.stderr}")
sys.exit(1)
# Main function
def main(input_file, timestamps):
# Create output folder
output_folder = "split_output"
os.makedirs(output_folder, exist_ok=True)
# Get chapter times from the video
print("Retrieving chapter times...")
chapter_times = get_chapters(input_file)
print(f"Found chapter times: {chapter_times}")
# Initialize start time
start_time = 0.0
output_index = 1
# Process each timestamp
for timestamp in timestamps:
print(f"Processing timestamp: {timestamp}")
# Find the nearest chapter
nearest_chapter = find_nearest_chapter(timestamp, chapter_times)
# Generate the output filename
output_filename = os.path.join(output_folder, f"part_{output_index}.mkv")
# Split the video
print(f"Splitting from {start_time} to {nearest_chapter}...")
split_video(input_file, start_time, nearest_chapter, output_filename)
# Update start time for the next part
start_time = nearest_chapter
output_index += 1
# Handle the last segment (from last chapter to the end of the video)
print(f"Handling final segment from {start_time} to the end of the video...")
# Get the duration of the video
cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", input_file]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print("Error: Unable to retrieve video duration.")
sys.exit(1)
duration = json.loads(result.stdout)["format"]["duration"]
print(f"Video duration: {duration}")
# Final segment
output_filename = os.path.join(output_folder, f"part_{output_index}.mkv")
print(f"Splitting from {start_time} to {duration}...")
split_video(input_file, start_time, float(duration), output_filename)
print("Splitting complete. Files saved in split_output.")
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python split_mkv_by_chapter.py <input_file> <timestamp1> <timestamp2> ...")
sys.exit(1)
input_file = sys.argv[1]
timestamps = sys.argv[2:]
main(input_file, timestamps)

282
sub_dub Executable file
View File

@@ -0,0 +1,282 @@
#!/bin/bash
# Language code mapping
declare -A LANG_CODES=(
["eng"]="English" ["en"]="English"
["spa"]="Spanish" ["es"]="Spanish"
["fra"]="French" ["fr"]="French"
["deu"]="German" ["ger"]="German" ["de"]="German"
["jpn"]="Japanese" ["ja"]="Japanese"
["ita"]="Italian" ["it"]="Italian"
["por"]="Portuguese" ["pt"]="Portuguese"
["rus"]="Russian" ["ru"]="Russian"
["chi"]="Chinese" ["zh"]="Chinese"
["kor"]="Korean" ["ko"]="Korean"
["dut"]="Dutch" ["nl"]="Dutch"
["swe"]="Swedish" ["sv"]="Swedish"
["fin"]="Finnish" ["fi"]="Finnish"
["pol"]="Polish" ["pl"]="Polish"
["ara"]="Arabic" ["ar"]="Arabic"
["hin"]="Hindi" ["hi"]="Hindi"
["tur"]="Turkish" ["tr"]="Turkish"
["und"]="Undefined" [" "]="Undefined"
["ab"]="Abkhazian" ["abk"]="Abkhazian"
["aa"]="Afar" ["aar"]="Afar"
["af"]="Afrikaans" ["afr"]="Afrikaans"
["ak"]="Akan" ["aka"]="Akan"
["twi"]="Twi"
["fat"]="Fanti"
["sq"]="Albanian" ["sqi"]="Albanian" ["alb"]="Albanian"
["am"]="Amharic"
["amh"]="Amharic"
["arb"]="Arabic"
["an"]="Aragonese" ["arg"]="Aragonese"
["hy"]="Armenian" ["hye"]="Armenian" ["arm"]="Armenian"
["as"]="Assamese" ["asm"]="Assamese"
["av"]="Avaric" ["ava"]="Avaric"
["ae"]="Avestan" ["ave"]="Avestan"
["ay"]="Aymara" ["aym"]="Aymara"
["az"]="Azerbaijani" ["aze"]="Azerbaijani"
["bm"]="Bambara" ["bam"]="Bambara"
["ba"]="Bashkir" ["bak"]="Bashkir"
["eu"]="Basque" ["eus"]="Basque" ["baq"]="Basque"
["be"]="Belarusian" ["bel"]="Belarusian"
["bn"]="Bengali" ["ben"]="Bengali"
["bi"]="Bislama" ["bis"]="Bislama"
["bs"]="Bosnian" ["bos"]="Bosnian"
["br"]="Breton" ["bre"]="Breton"
["bg"]="Bulgarian" ["bul"]="Bulgarian"
["my"]="Burmese" ["mya"]="Burmese"
["ca"]="Catalan" ["cat"]="Catalan"
["ch"]="Chamorro" ["cha"]="Chamorro"
["ce"]="Chechen" ["che"]="Chechen"
["ny"]="Chichewa" ["nya"]="Chichewa" ["zho"]="Chinese"
["cu"]="Church Slavonic" ["chu"]="Church Slavonic"
["cv"]="Chuvash" ["chv"]="Chuvash"
["kw"]="Cornish" ["cor"]="Cornish"
["co"]="Corsican" ["cos"]="Corsican"
["cr"]="Cree" ["cre"]="Cree"
["hr"]="Croatian" ["hrv"]="Croatian"
["cs"]="Czech" ["ces"]="Czech" ["cze"]="Czech"
["da"]="Danish" ["dan"]="Danish"
["dv"]="Divehi" ["div"]="Divehi"
["dz"]="Dzongkha" ["dzo"]="Dzongkha"
["eo"]="Esperanto" ["epo"]="Esperanto"
["et"]="Estonian" ["est"]="Estonian"
["ee"]="Ewe" ["ewe"]="Ewe"
["fo"]="Faroese" ["fao"]="Faroese"
["fj"]="Fijian" ["fij"]="Fijian"
["fre"]="French"
["fy"]="Western Frisian" ["fry"]="Western Frisian"
["ff"]="Fulah" ["ful"]="Fulah"
["gd"]="Gaelic, Scottish Gaelic"
["gla"]="Gaelic"
["gl"]="Galician" ["glg"]="Galician"
["lg"]="Ganda" ["lug"]="Ganda"
["ka"]="Georgian" ["kat"]="Georgian" ["geo"]="Georgian"
["el"]="Greek" ["ell"]="Greek" ["gre"]="Greek"
["kl"]="Kalaallisut" ["kal"]="Kalaallisut"
["gn"]="Guarani" ["grn"]="Guarani"
["gu"]="Gujarati" ["guj"]="Gujarati"
["ht"]="Haitian Creole" ["hat"]="Haitian Creole"
["ha"]="Hausa" ["hau"]="Hausa"
["he"]="Hebrew" ["heb"]="Hebrew"
["hz"]="Herero" ["her"]="Herero"
["ho"]="Hiri Motu" ["hmo"]="Hiri Motu"
["hu"]="Hungarian" ["hun"]="Hungarian"
["is"]="Icelandic" ["isl"]="Icelandic" ["ice"]="Icelandic"
["io"]="Ido" ["ido"]="Ido"
["ig"]="Igbo" ["ibo"]="Igbo"
["id"]="Indonesian" ["ind"]="Indonesian"
["ia"]="Interlingua" ["ina"]="Interlingua"
["ie"]="Interlingue" ["ile"]="Interlingue"
["iu"]="Inuktitut" ["iku"]="Inuktitut"
["ik"]="Inupiaq" ["ipk"]="Inupiaq"
["ga"]="Irish" ["gle"]="Irish"
["jv"]="Javanese" ["jav"]="Javanese"
["kn"]="Kannada" ["kan"]="Kannada"
["kr"]="Kanuri" ["kau"]="Kanuri"
["ks"]="Kashmiri" ["kas"]="Kashmiri"
["kk"]="Kazakh" ["kaz"]="Kazakh"
["km"]="Central Khmer" ["khm"]="Central Khmer"
["ki"]="Kikuyu" ["kik"]="Kikuyu"
["rw"]="Kinyarwanda" ["kin"]="Kinyarwanda"
["ky"]="Kyrgyz" ["kir"]="Kyrgyz"
["kv"]="Komi" ["kom"]="Komi"
["kg"]="Kongo" ["kon"]="Kongo"
["kj"]="Kuanyama" ["kua"]="Kuanyama"
["ku"]="Kurdish" ["kur"]="Kurdish"
["lo"]="Lao" ["lao"]="Lao"
["la"]="Latin" ["lat"]="Latin"
["lv"]="Latvian" ["lav"]="Latvian"
["li"]="Limburgan" ["lim"]="Limburgan"
["ln"]="Lingala" ["lin"]="Lingala"
["lt"]="Lithuanian" ["lit"]="Lithuanian"
["lu"]="Luba-Katanga" ["lub"]="Luba-Katanga"
["lb"]="Luxembourgish" ["ltz"]="Luxembourgish"
["mk"]="Macedonian" ["mkd"]="Macedonian" ["mac"]="Macedonian"
["mg"]="Malagasy" ["mlg"]="Malagasy"
["ms"]="Malay" ["msa"]="Malay"
["ml"]="Malayalam" ["mal"]="Malayalam"
["mt"]="Maltese" ["mlt"]="Maltese"
["gv"]="Manx" ["glv"]="Manx"
["mi"]="Maori" ["mri"]="Maori" ["mao"]="Maori"
["mr"]="Marathi" ["mar"]="Marathi"
["mh"]="Marshallese" ["mah"]="Marshallese"
["mn"]="Mongolian" ["mon"]="Mongolian"
["na"]="Nauru" ["nau"]="Nauru"
["nv"]="Navajo" ["nav"]="Navajo"
["nd"]="North Ndebele" ["nde"]="North Ndebele"
["nr"]="South Ndebele" ["nbl"]="South Ndebele"
["ng"]="Ndonga" ["ndo"]="Ndonga"
["ne"]="Nepali" ["nep"]="Nepali"
["no"]="Norwegian" ["nor"]="Norwegian"
["nb"]="Norwegian Bokmål" ["nob"]="Norwegian Bokmål"
["nn"]="Norwegian Nynorsk" ["nno"]="Norwegian Nynorsk"
["oc"]="Occitan" ["oci"]="Occitan"
["oj"]="Ojibwa" ["oji"]="Ojibwa"
["or"]="Oriya" ["ori"]="Oriya"
["om"]="Oromo" ["orm"]="Oromo"
["os"]="Ossetian" ["oss"]="Ossetian"
["pi"]="Pali" ["pli"]="Pali"
["ps"]="Pashto" ["pus"]="Pashto"
["fa"]="Persian" ["fas"]="Persian" ["per"]="Persian"
["pa"]="Punjabi" ["pan"]="Punjabi"
["qu"]="Quechua" ["que"]="Quechua"
["ro"]="Romanian" ["ron"]="Romanian" ["rum"]="Romanian"
["rm"]="Romansh" ["roh"]="Romansh"
["rn"]="Rundi" ["run"]="Rundi"
["se"]="Northern Sami" ["sme"]="Northern Sami"
["sm"]="Samoan" ["smo"]="Samoan"
["sg"]="Sango" ["sag"]="Sango"
["sa"]="Sanskrit" ["san"]="Sanskrit"
["sc"]="Sardinian" ["srd"]="Sardinian"
["sr"]="Serbian" ["srp"]="Serbian"
["sn"]="Shona" ["sna"]="Shona"
["sd"]="Sindhi" ["snd"]="Sindhi" ["si"]="Sinhala" ["sin"]="Sinhala"
["sk"]="Slovak" ["slk"]="Slovak" ["slo"]="Slovak"
["sl"]="Slovenian" ["slv"]="Slovenian"
["so"]="Somali" ["som"]="Somali"
["st"]="Southern Sotho" ["sot"]="Southern Sotho"
["su"]="Sundanese" ["sun"]="Sundanese"
["sw"]="Swahil" ["swa"]="Swahili"
["ss"]="Swati" ["ssw"]="Swati"
["tl"]="Tagalog" ["tgl"]="Tagalog"
["ty"]="Tahitian" ["tah"]="Tahitian"
["tg"]="Tajik" ["tgk"]="Tajik"
["ta"]="Tamil" ["tam"]="Tamil"
["tt"]="Tatar" ["tat"]="Tatar"
["te"]="Telugu" ["tel"]="Telugu"
["th"]="Thai" ["tha"]="Thai"
["bo"]="Tibetan" ["bod"]="Tibetan" ["tib"]="Tibetan"
["ti"]="Tigrinya" ["tir"]="Tigrinya"
["to"]="Tongan" ["ton"]="Tongan" ["ts"]="Tsonga" ["tso"]="Tsonga"
["tn"]="Tswana" ["tsn"]="Tswana"
["tk"]="Turkmen" ["tuk"]="Turkmen"
["ug"]="Uighur" ["uig"]="Uighur"
["uk"]="Ukrainian" ["ukr"]="Ukrainian"
["ur"]="Urdu" ["urd"]="Urdu"
["uz"]="Uzbek" ["uzb"]="Uzbek"
["ve"]="Venda" ["ven"]="Venda"
["vi"]="Vietnamese" ["vie"]="Vietnamese"
["vo"]="Volapük" ["vol"]="Volapük"
["wa"]="Walloon" ["wln"]="Walloon"
["cy"]="Welsh" ["cym"]="Welsh" ["wel"]="Welsh"
["wo"]="Wolof" ["wol"]="Wolof"
["xh"]="Xhosa" ["xho"]="Xhosa"
["ii"]="Sichuan Yi" ["iii"]="Sichuan Yi"
["yi"]="Yiddish" ["yid"]="Yiddish"
["yo"]="Yoruba" ["yor"]="Yoruba"
["za"]="Zhuang" ["zha"]="Zhuang"
["zu"]="Zulu" ["zul"]="Zulu"
)
# Function to translate language codes
translate_language() {
local code="$1"
# Sanitize input: remove spaces and convert to lowercase
code=$(echo "$code" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
# Check for valid subscript and handle fallback
if [[ -z "$code" || ! ${LANG_CODES[$code]+_} ]]; then
echo "Undefined"
else
echo "${LANG_CODES[$code]}"
fi
}
extract_audio_languages() {
audio_info=$(ffprobe -v quiet -select_streams a -show_entries stream=codec_name,channel_layout:stream_tags=language \
-of default=noprint_wrappers=1:nokey=1 "$file" | paste -sd "," -)
languages=""
IFS=',' read -r -a audio_streams <<< "$audio_info"
for (( i=0; i<${#audio_streams[@]}; i+=3 )); do
lang_code="${audio_streams[i+2]}"
lang=$(translate_language "${lang_code:-" "}")
# Collect languages
languages+="$lang"
# Add comma separator if not the last item
if (( i+3 < ${#audio_streams[@]} )); then
languages+=", "
fi
done
# Format output with languages first, followed by detailed info in brackets
echo "$languages"
}
extract_subtitle_languages() {
subtitle_info=$(ffprobe -v quiet -select_streams s -show_entries stream_tags=language \
-of default=noprint_wrappers=1:nokey=1 "$file" | paste -sd "," -)
languages=""
# echo "Subtitle Info: $subtitle_info" # Debugging line
IFS=',' read -r -a subtitle_streams <<< "$subtitle_info"
for ((i=0; i<${#subtitle_streams[@]}; i++)); do
lang_code="${subtitle_streams[i]}"
lang_code=$(echo "$lang_code" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]') # Normalize case
lang=$(translate_language "$lang_code")
languages+="$lang"
# Add comma separator if not the last item
if (( i < ${#subtitle_streams[@]} - 1 )); then
languages+=", "
fi
done
# Format output with languages
echo "$languages"
}
# Function to detect if codec is h265
check_h265_codec() {
local file="$1"
ffmpeg -i "$file" 2>&1 | grep -qi 'Video: hevc' && echo "h265" || echo "other"
}
# Process all .mkv files in the directory
for file in *.mkv; do
if [[ -f "$file" ]]; then
# echo "Processing: $file"
# Extract filename without extension
filename=$(basename "$file" .mkv)
# Extract and translate audio and subtitle languages
audio_languages=$(extract_audio_languages "$file" "Audio")
subtitle_languages=$(extract_subtitle_languages "$file" "Subtitle")
# Debugging: Ensure languages are detected
# echo "Audio Languages: $audio_languages"
# echo "Subtitle Languages: $subtitle_languages"
# Detect codec
codec=$(check_h265_codec "$file")
# Call Python script to update Excel
python3 /home/honney/.bin/sub_dub.py "$filename" "$audio_languages" "$subtitle_languages" "$codec"
fi
done
# echo "Processing completed."

88
sub_dub.py Normal file
View File

@@ -0,0 +1,88 @@
import openpyxl
import sys
# Ensure correct number of arguments
if len(sys.argv) != 5:
print("Usage: update_excel.py <filename> <audio_languages> <subtitle_languages> <codec>")
sys.exit(1)
# Read command-line arguments
filename = sys.argv[1].strip()
audio_languages = sys.argv[2].strip() if sys.argv[2].strip() else "NONE"
subtitle_languages = sys.argv[3].strip() if sys.argv[3].strip() else "NONE"
codec = sys.argv[4].strip().lower()
# Excel file path
excel_file = "/home/honney/mount/Storage/Nextcloud/DVDs_Blueray.xlsx"
# Load the workbook and select the first sheet
workbook = openpyxl.load_workbook(excel_file)
sheet = workbook.active
temp = 0
# Find the row containing the filename
for row in sheet.iter_rows(min_row=2, max_row=sheet.max_row, min_col=1, max_col=1):
file_cell = row[0]
if file_cell.value and isinstance(file_cell.value, str) and file_cell.value.strip() == filename:
temp += 1
row_index = file_cell.row # Get row number
# Ensure filename stays unchanged
sheet.cell(row=row_index, column=1).value = filename # Column A (Filename)
# Update Audio and Subtitle languages (if empty, set to "NONE")
if sheet.cell(row=row_index, column=7).value == audio_languages:
temp += 1
else:
sheet.cell(row=row_index, column=7).value = audio_languages # Column G (Audio)
if sheet.cell(row=row_index, column=8).value == subtitle_languages:
temp += 1
else:
sheet.cell(row=row_index, column=8).value = subtitle_languages # Column H (Subtitles)
# If codec is h265, set columns D, E, F to 1
if codec == "h265":
if sheet.cell(row=row_index, column=4).value == 1:
temp += 1
else:
sheet.cell(row=row_index, column=4).value = 1
if sheet.cell(row=row_index, column=5).value == 1:
temp += 1
else:
sheet.cell(row=row_index, column=5).value = 1
if sheet.cell(row=row_index, column=6).value == 1:
temp += 1
else:
sheet.cell(row=row_index, column=6).value = 1
if sheet.cell(row=row_index, column=3).value:
dvd = sheet.cell(row=row_index, column=3).value
else:
sheet.cell(row=row_index, column=3).value = "DVD"
dvd = "DVD"
if sheet.cell(row=row_index, column=2).value == "NOT CHECKED":
sheet.cell(row=row_index, column=2).value = ""
# Debugging: Confirm columns updated
else: print(f"❌ Codec Wrong {filename}")
# Debugging: Confirm updates
# print(f"✅ Updated row {row_index} -> Audio: {audio_languages}, Subtitles: {subtitle_languages}, Codec: {codec}")
break # Stop loop once match is found
if temp == 0:
print(f"❌ No matching row found for {filename}")
elif temp < 0 and temp != 6:
print(f"🟨 Updated Info for {filename}")
elif temp == 6:
print(f"🔵 Allready has info for {filename}")
else:
print(f"{row_index}: {filename} | | {dvd} | 1 | 1 | 1 | {audio_languages} | {subtitle_languages}")
# Save the updated workbook
workbook.save(excel_file)
# print(f"✅ Successfully updated Excel for {filename}")

55
sync_delete Executable file
View File

@@ -0,0 +1,55 @@
#!/bin/bash
# ---
# A script to delete files in a target directory if they also exist in a reference directory.
# ---
# Exit immediately if a command exits with a non-zero status.
set -e
# Check if the correct number of arguments (two folders) was provided.
if [ "$#" -ne 2 ]; then
echo "Error: You must provide two directory paths."
echo "Usage: $0 <target_directory> <reference_directory>"
echo " - target_directory: The folder FROM WHICH to delete files."
echo " - reference_directory: The folder used as a reference FOR deletion."
exit 1
fi
TARGET_DIR="$1"
REFERENCE_DIR="$2"
# Check that both arguments are actual directories.
if [ ! -d "$TARGET_DIR" ]; then
echo "Error: Target directory '$TARGET_DIR' does not exist."
exit 1
fi
if [ ! -d "$REFERENCE_DIR" ]; then
echo "Error: Reference directory '$REFERENCE_DIR' does not exist."
exit 1
fi
echo "Scanning for files in '$TARGET_DIR' to delete based on the contents of '$REFERENCE_DIR'..."
echo "------------------------------------------------------------------"
# Loop through every file in the reference directory.
# This method correctly handles filenames that may contain spaces or special characters.
find "$REFERENCE_DIR" -type f -print0 | while IFS= read -r -d '' reference_file; do
# Get just the filename from the path.
filename=$(basename "$reference_file")
# Create the full path for the corresponding file in the target directory.
target_file_path="$TARGET_DIR/$filename"
# Check if that file actually exists in the target directory.
if [ -f "$target_file_path" ]; then
echo "Found match. Deleting: $target_file_path"
# The actual delete command.
rm "$target_file_path"
fi
done
echo "------------------------------------------------------------------"
echo "Script finished."

23
time_add.py Normal file
View File

@@ -0,0 +1,23 @@
import re
from datetime import timedelta
def parse_timecode(timecode):
parts = list(map(int, timecode.split(':')))
if len(parts) == 2: # mm:ss format
return timedelta(minutes=parts[0], seconds=parts[1])
elif len(parts) == 3: # hh:mm:ss format
return timedelta(hours=parts[0], minutes=parts[1], seconds=parts[2])
else:
raise ValueError(f"Invalid timecode format: {timecode}")
def add_timecodes(*timecodes):
total_time = sum((parse_timecode(tc) for tc in timecodes), timedelta())
hours, remainder = divmod(total_time.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
hours += total_time.days * 24 # Account for days if needed
return f"{hours:02}:{minutes:02}:{seconds:02}"
if __name__ == "__main__":
timecodes = input("Enter timecodes separated by spaces: ").split()
total = add_timecodes(*timecodes)
print("Total time:", total)