Skip to content

Instantly share code, notes, and snippets.

@nicka101
Last active September 23, 2017 19:47
Show Gist options
  • Save nicka101/513845513f5c04ba2e9a2bebd172e125 to your computer and use it in GitHub Desktop.
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
#!/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