Last active
September 23, 2017 19:47
-
-
Save nicka101/513845513f5c04ba2e9a2bebd172e125 to your computer and use it in GitHub Desktop.
A lot of sites provide videos in 2160p and 1080p, and I like high quality, so I go for 2160p, but my monitor is only 1440p, so this script re-encodes videos down to 1440p from 2160p to save space
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Convert 2160p to 1440p | |
import sys | |
import os | |
from os.path import join, getsize, dirname, basename | |
from subprocess import run, PIPE, STDOUT | |
from time import sleep | |
from collections import namedtuple | |
FFPROBE_BIN = os.getenv('FFPROBE_BINARY', 'ffprobe') | |
FFMPEG_BIN = os.getenv('FFMPEG_BINARY', 'ffmpeg') | |
SCALE_TARGET_WIDTH = 2560 | |
SCALE_TARGET_HEIGHT = 1440 | |
SCALE_OPTIONS = ["-vf", "scale=w=" + str(SCALE_TARGET_WIDTH) + ":h=" + str(SCALE_TARGET_HEIGHT) + ":force_original_aspect_ratio=decrease"] | |
SCALE_RESMARKER = ".1440p" | |
#TRANSCODE_OPTIONS = ["-c:v", "libx265", "-preset", "medium", "-crf", "23", "-c:a", "libopus", "-b:a", "128k"] | |
TRANSCODE_OPTIONS = ["-c:v", "hevc_nvenc", "-preset", "slow", "-tier", "high", "-rc", "vbr", "-cq", "21", "-qmin", "0", "-qmax", "25", "-c:a", "libopus", "-b:a", "128k"] | |
TARGET_CODEC = "hevc" | |
VIDEO_FILESUFFIXES = ('.mp4', '.mkv', '.avi', '.wmv', '.mov') | |
#Function defs | |
def is_video(filename): | |
if filename.endswith(VIDEO_FILESUFFIXES): | |
return True | |
return False | |
FfprobeResult = namedtuple('FfprobeResult', ['height', 'width', 'codec']) | |
def ffprobe(filename): | |
res_info = run("\"" + FFPROBE_BIN + "\" -v error -of flat=s=_ -select_streams v:0 -show_entries stream=width,height,codec_name \"" + filename + "\"", shell=True, check=True, stdout=PIPE, stderr=STDOUT) | |
if res_info.returncode != 0: | |
return False | |
result = FfprobeResult(0, 0, "") | |
try: | |
for line in res_info.stdout.decode('utf-8').split('\n'): | |
if 'height=' in line: | |
result = result._replace(height = int(line[line.index('=') + 1:].strip())) | |
if 'width=' in line: | |
result = result._replace(width = int(line[line.index('=') + 1:].strip())) | |
if 'codec_name=' in line: | |
codec = line[line.index('=') + 1:].strip() | |
result = result._replace(codec = codec.replace('"', '')) | |
return result | |
except: | |
return False | |
def check_size_and_codec(filename): | |
if getsize(filename) < 200000000: | |
return 0 | |
result = ffprobe(filename) | |
#print(result) | |
if result == False: | |
return 0 | |
if result.codec == TARGET_CODEC: | |
if result.height > SCALE_TARGET_HEIGHT or result.width > SCALE_TARGET_WIDTH: | |
return 2 | |
else: | |
return 0 | |
else: | |
print(filename + ': ' + result.codec) | |
if result.height > SCALE_TARGET_HEIGHT or result.width > SCALE_TARGET_WIDTH: | |
return 2 | |
else: | |
return 1 | |
def sizeof_fmt(num, suffix='B'): | |
for unit in ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi']: | |
if abs(num) < 1024.0: | |
return "%3.1f%s%s" % (num, unit, suffix) | |
num /= 1024.0 | |
return "%.1f%s%s" % (num, 'Yi', suffix) | |
if len(sys.argv) != 2: | |
print('Invalid argument count. Requires operating directory as param') | |
sys.exit(1) | |
#Start work | |
work_dir = sys.argv[1] | |
print("Beginning walk of filesystem at: ", work_dir) | |
total_files = [] | |
for root, dirs, files in os.walk(work_dir): | |
for file in files: | |
total_files.append(join(root,file)) | |
print("Filetree walk complete, found ", len(total_files), " files") | |
print("Begin parse for suitable files") | |
video_files = [x for x in total_files if is_video(x)] | |
print("Narrowed working set to video files. ", len(video_files), " matching files") | |
print("Dropping working set to files larger than 200MB and not currently encoded as \"" + TARGET_CODEC + "\"") | |
working_set = [] | |
for file in video_files: | |
res = check_size_and_codec(file) | |
if res == 0: | |
continue | |
working_set.append((file, res == 2)) #res == 2 is a boolean representing whether to scale | |
print(len(working_set), " files in working set") | |
print("These files total: ", sizeof_fmt(sum([getsize(file_tup[0]) for file_tup in working_set]))) | |
#Start processing loop | |
for video_tup in working_set: | |
video = video_tup[0] | |
scaling_enabled = video_tup[1] | |
cwd = dirname(video) | |
print("Operating in: ", cwd) | |
print("Working on: ", basename(video)) | |
output_filename = basename(video) | |
if scaling_enabled: | |
print("Scaling is enabled for this video") | |
res_marker = SCALE_RESMARKER | |
scale_opts = SCALE_OPTIONS | |
else: | |
res_marker = "" | |
scale_opts = [] | |
output_filename = output_filename[:output_filename.rfind('.')] + res_marker + ".hevc.mkv" | |
output_fullname = join(cwd, output_filename) | |
print("Output file is: ", output_fullname) | |
ffmpeg_result=run([FFMPEG_BIN, "-i", video] + scale_opts + TRANSCODE_OPTIONS + [output_fullname]) | |
if ffmpeg_result.returncode != 0: | |
print("Transcode failed with non-zero exit code: ", ffmpeg_result.returncode) | |
print("Deleting output file") | |
try: | |
os.remove(output_fullname) | |
except: | |
pass | |
continue | |
print("Old file is ", sizeof_fmt(getsize(video)), ", new file is ", sizeof_fmt(getsize(output_fullname))) | |
sleep(10) #Wait 10 seconds to give time to cancel the deletion if we intentionally quit ffmpeg | |
print("Deleting original video") | |
try: | |
os.remove(video) | |
except: | |
pass |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment