Skip to content

Commit

Permalink
utils refactor, extensions, logging, tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
patillacode committed Nov 19, 2023
1 parent 710a930 commit 69a9ee5
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 169 deletions.
6 changes: 1 addition & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
# Makefile for space_saver.py

# Variables
VENV_NAME?=venv
PYTHON=${VENV_NAME}/bin/python3

# Targets

.PHONY: help setup run clean install test lint format docs

help:
@echo "make clean"
Expand All @@ -16,7 +12,7 @@ help:

clean:
rm -rf $(VENV_NAME)
find -iname "*.pyc" -delete
find . -name "*.pyc" -delete

install:
python3 -m venv $(VENV_NAME)
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Space Saver

This project includes a script (`space_saver.py`) that converts video files to the H.265 codec, which provides more efficient encoding and results in smaller file sizes.
`space_saver.py` converts video files to .mp4 format with H.265 codec.
It walks through a directory and its subdirectories looking for files with a given
extension (or all files if no extension is given), and converts them to .mp4 with
H.265 codec.

The original file is deleted if the conversion is successful,
and the new file is given permissions of 755.
The script also calculates the space saved by the conversion and prints it at the end.

## Setup

Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
ffmpeg-python==0.2.0
termcolor==1.1.0
3 changes: 0 additions & 3 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ known-third-party = []
section-order = [
"future",
"standard-library",
"django",
"third-party",
"first-party",
"local-folder",
Expand All @@ -43,5 +42,3 @@ combine-as-imports = true
split-on-trailing-comma = false
lines-between-types = 1

# [isort.sections]
# "django" = ["django"]
201 changes: 42 additions & 159 deletions space_saver.py
Original file line number Diff line number Diff line change
@@ -1,177 +1,50 @@
"""
This script converts video files to .mp4 format with H.265 codec.
It walks through a directory and its subdirectories looking for files with a given
extension (or all files if no extension is given), and converts them to .mp4 with
H.265 codec.
The original file is deleted if the conversion is successful,
and the new file is given permissions of 755.
The script also calculates the space saved by the conversion and prints it at the end.
"""

import argparse
import math
import os
import sys
import time

import ffmpeg

from termcolor import colored


def convert_size(size_bytes):
"""
Converts the given size in bytes to a human-readable format.
from utils.logger import configure_logger, logger
from utils.video import convert_to_H265_codec

Args:
size_bytes (int): The size in bytes.

Returns:
str: The size in a human-readable format.
def examples():
return """
Examples:
python space_saver.py -p /home/user/videos -e mp4 mkv
python space_saver.py -p /home/user/videos -d
python space_saver.py -p /home/user/videos -q
python space_saver.py -p /home/user/videos -q -crf 28
"""
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return f"{s} {size_name[i]}"


def convert_single_file(input_file_path, mp4_file_path, quiet, crf, extension):
"""
Converts a single video file to .mp4 format with H.265 codec.
Args:
input_file_path (str): The path to the input file.
mp4_file_path (str): The path to the output .mp4 file.
quiet (bool): Whether to keep the ffmpeg output to a minimum while converting.
crf (int): The Constant Rate Factor (CRF) value for the ffmpeg command.
extension (str): The extension of the input file.
Returns:
int: The space saved by the conversion, in bytes.
"""
original_size = os.path.getsize(input_file_path)
probe = ffmpeg.probe(input_file_path)
video_stream = next(
(stream for stream in probe["streams"] if stream["codec_type"] == "video"), None
)
if video_stream is None:
print(f"No video stream found in {colored(input_file_path, 'red')}")
return
if video_stream["codec_name"] == "hevc":
print(f"{input_file_path} is already using H.265 codec, skipping conversion")
return
try:
print(
f"Converting {colored(input_file_path, 'yellow')} to mp4 with H.265 codec"
)
start_time = time.time()
ffmpeg.input(input_file_path).output(
mp4_file_path,
vcodec="libx265",
crf=str(crf),
acodec="aac",
strict="experimental",
).overwrite_output().run(quiet=quiet)
end_time = time.time()
except ffmpeg.Error as err:
print(
f"Error occurred while converting {colored(input_file_path, 'yellow')} to "
f"mp4: {colored(err, 'red')}"
)
sys.exit()
else:
# If the conversion was successful, delete the original file and change
# permissions of the new file
if os.path.isfile(input_file_path):
os.remove(input_file_path)
new_size = os.path.getsize(mp4_file_path)
os.chmod(mp4_file_path, 0o755)
space_saved = original_size - new_size
print_file_sizes(original_size, new_size, extension)
hours, rem = divmod(end_time - start_time, 3600)
minutes, seconds = divmod(rem, 60)
formatted_time = "{:0>2}:{:0>2}:{:05.2f}".format(
int(hours), int(minutes), seconds
)
print(f"Time elapsed: {colored(formatted_time, 'white')}")
return space_saved


def print_file_sizes(original_size, new_size, extension):
"""
Prints the original and new file sizes, and the space saved by the conversion.
Args:
original_size (int): The size of the original file, in bytes.
new_size (int): The size of the new file, in bytes.
extension (str): The extension of the original file.
"""
size_reduction = ((original_size - new_size) / original_size) * 100
print(
f"Original {extension} file size: "
f"{colored(convert_size(original_size), 'magenta')}\n"
f"New mp4 file size: {colored(convert_size(new_size), 'cyan')}\n"
f"Size reduction: {colored(int(size_reduction), 'cyan')}%"
)


def convert_to_H265_codec(path, extension, dry_run, crf, quiet=False):
"""
Converts all video files in the given directory and its subdirectories to .mp4 format
with H.265 codec.
Args:
path (str): The path to the directory containing the video files to convert.
extension (str): The extension of the video files to convert, or None to convert
all files.
dry_run (bool): Whether to perform a dry run without actually converting files.
crf (int): The Constant Rate Factor (CRF) value for the ffmpeg command.
quiet (bool): Whether to keep the ffmpeg output to a minimum while converting.
"""
total_space_saved = 0
for root, _, files in os.walk(path):
for file in files:
print("Looking at file: ", file)
if (
extension is None
or file.endswith(f".{extension}")
or file.endswith(f".{extension.upper()}")
):
file_path = os.path.join(root, file)
base_file_path = os.path.splitext(file_path)[0]
mp4_file_path = f"{base_file_path}_H265.mp4"
if dry_run:
print(f"Would convert {file_path} to {mp4_file_path}")
else:
space_saved = convert_single_file(
file_path, mp4_file_path, quiet, crf, extension
)
total_space_saved += space_saved if space_saved else 0
print(colored(f"Total space saved: {convert_size(total_space_saved)}", "yellow"))


def parse_arguments():
parser = argparse.ArgumentParser(
description="Convert files to .mp4 with H.265 codec"
prog="space_saver.py",
description="Convert files to .mp4 with H.265 codec",
epilog=examples(),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-p",
"--path",
required=True,
help=(
r"Path to the directory containing the .\{format\} files to convert into "
"Path to the directory containing the files to convert into "
".mp4 files with H.265 codec"
),
)
parser.add_argument(
"-f",
"--format",
"-e",
"--extensions",
required=False,
help="File formats to convert, if not given all files will be checked",
default=["mp4", "mkv", "avi", "mpg", "mpeg", "mov", "wmv", "flv", "webm"],
nargs="*",
type=str,
help=r"Extensions to filter, default: %(default)s",
)
parser.add_argument(
"-c",
"--crf",
type=int,
default=23,
help="Constant Rate Factor for the H.265 codec, scale is 0-51 (default: 23)",
)
parser.add_argument(
"-d",
Expand All @@ -186,14 +59,24 @@ def parse_arguments():
help="Keep the ffmpeg output to a minimum while converting",
)
parser.add_argument(
"-c",
"--crf",
type=int,
default=23,
"-l",
"--log",
default="INFO",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
help="Set the logging level",
)
return parser.parse_args()


if __name__ == "__main__":
args = parse_arguments()
convert_to_H265_codec(args.path, args.format, args.dry_run, args.crf, args.quiet)
configure_logger(args.log)
logger.info(f"Starting conversion process. {'(DRY RUN)' if args.dry_run else ''}")
try:
convert_to_H265_codec(
args.path, args.extensions, args.dry_run, args.crf, args.quiet
)
except Exception as err:
logger.error(f"An error occurred during conversion: {err}")
else:
logger.info("Conversion process finished.")
Empty file added utils/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions utils/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import logging


def configure_logger(log_level):
numeric_level = getattr(logging, log_level.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError(f"Invalid log level: {log_level}")
logging.basicConfig(
level=numeric_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
handlers=[logging.StreamHandler()],
)


logger = logging.getLogger("space_saver")
53 changes: 53 additions & 0 deletions utils/misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import math
import os

from pathlib import Path

from utils.logger import logger


def convert_size(size_bytes):
if size_bytes == 0:
return "0B"
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return f"{s} {size_name[i]}"


def print_file_sizes(original_size, new_size):
size_reduction = ((original_size - new_size) / original_size) * 100
logger.info(
f"Original file size: {convert_size(original_size)}\n"
f"New mp4 file size: {convert_size(new_size)}\n"
f"Size reduction: {int(size_reduction)}%"
)


def check_file_type(file, extensions):
file_extension = Path(file).suffix.lower()[1:]
if file_extension not in extensions:
logger.warning(
f'Skipping file "{Path(file).name}", it doesn\'t have a valid extension'
)
return False
return True


def after_conversion_clean_up(
input_file_path, original_size, output_file_path, start_time, end_time
):
if os.path.isfile(input_file_path):
os.remove(input_file_path)
new_size = os.path.getsize(output_file_path)
os.chmod(output_file_path, 0o755)
space_saved = original_size - new_size
print_file_sizes(original_size, new_size)
hours, rem = divmod(end_time - start_time, 3600)
minutes, seconds = divmod(rem, 60)
formatted_time = "{:0>2}:{:0>2}:{:05.2f}".format(
int(hours), int(minutes), seconds
)
logger.info(f"Time elapsed: {formatted_time}")
return space_saved
Loading

0 comments on commit 69a9ee5

Please sign in to comment.