#!/usr/bin/env python3 # EDIT These Vars # PLEX_URL = 'https://xxxxxx.plex.direct:32400/' # If running locally, can also enter IP directly "https://127.0.0.1:32400/" PLEX_TOKEN = 'xxxxxx' PLEX_BIF_FRAME_INTERVAL = 5 THUMBNAIL_QUALITY = 4 # Allowed range is 2 - 6 with 2 being highest quality and largest file size and 6 being lowest quality and smallest file size. # PLEX_LOCAL_MEDIA_PATH = '/path_to/plex/Library/Application Support/Plex Media Server/Media' TMP_FOLDER = '/dev/shm/plex_generate_previews' PLEX_LOCAL_VIDEOS_PATH_MAPPING = '/path/this/script/sees/to/video/library' PLEX_VIDEOS_PATH_MAPPING = '/path/plex/sees/to/video/library' GPU_THREADS = 4 CPU_THREADS = 4 # DO NOT EDIT BELOW HERE # import os import sys from concurrent.futures import ProcessPoolExecutor import re import subprocess import shutil import multiprocessing import glob import os import struct if not shutil.which("mediainfo"): print('MediaInfo not found. MediaInfo must be installed and available in PATH.') sys.exit(1) try: from pymediainfo import MediaInfo except ImportError: print('Dependencies Missing! Please run "pip3 install pymediainfo".') sys.exit(1) try: import gpustat except ImportError: print('Dependencies Missing! Please run "pip3 install gpustat".') sys.exit(1) import time try: import requests except ImportError: print('Dependencies Missing! Please run "pip3 install requests".') sys.exit(1) import array try: from plexapi.server import PlexServer except ImportError: print('Dependencies Missing! Please run "pip3 install plexapi".') sys.exit(1) try: from loguru import logger except ImportError: print('Dependencies Missing! Please run "pip3 install loguru".') sys.exit(1) try: from rich.console import Console except ImportError: print('Dependencies Missing! Please run "pip3 install rich".') sys.exit(1) try: from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn except ImportError: print('Dependencies Missing! Please run "pip3 install rich".') sys.exit(1) FFMPEG_PATH = shutil.which("ffmpeg") if not FFMPEG_PATH: print('FFmpeg not found. FFmpeg must be installed and available in PATH.') sys.exit(1) console = Console(color_system=None, stderr=True) import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) def generate_images(video_file_param, output_folder, lock): video_file = video_file_param.replace(PLEX_VIDEOS_PATH_MAPPING, PLEX_LOCAL_VIDEOS_PATH_MAPPING) media_info = MediaInfo.parse(video_file) vf_parameters = "fps=fps={}:round=up,scale=w=320:h=240:force_original_aspect_ratio=decrease".format(round(1 / PLEX_BIF_FRAME_INTERVAL, 6)) # Check if we have a HDR Format. Note: Sometimes it can be returned as "None" (string) hence the check for None type or "None" (String) if media_info.video_tracks[0].hdr_format != "None" and media_info.video_tracks[0].hdr_format is not None: vf_parameters = "fps=fps={}:round=up,zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p,scale=w=320:h=240:force_original_aspect_ratio=decrease".format(round(1 / PLEX_BIF_FRAME_INTERVAL, 6)) args = [ FFMPEG_PATH, "-loglevel", "info", "-skip_frame:v", "nokey", "-threads:0", "1", "-i", video_file, "-an", "-sn", "-dn", "-q:v", str(THUMBNAIL_QUALITY), "-vf", vf_parameters, '{}/img-%06d.jpg'.format(output_folder) ] start = time.time() hw = False with lock: gpu = gpustat.core.new_query()[0] gpu_ffmpeg = [c for c in gpu.processes if c["command"].lower() == 'ffmpeg'] if len(gpu_ffmpeg) < GPU_THREADS or CPU_THREADS == 0: hw = True args.insert(5, "-hwaccel") args.insert(6, "cuda") proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Allow time for it to start time.sleep(1) out, err = proc.communicate() if proc.returncode != 0: err_lines = err.decode('utf-8').split('\n')[-5:] logger.error(err_lines) raise Exception('Problem trying to ffmpeg images for {}'.format(video_file)) # Speed end = time.time() seconds = round(end - start, 1) speed = re.findall('speed= ?([0-9]+\.?[0-9]*|\.[0-9]+)x', err.decode('utf-8')) if speed: speed = speed[-1] logger.info('Generated Video Preview for {} HW={} TIME={}seconds SPEED={}x '.format(video_file, hw, seconds, speed)) # Optimize and Rename Images for image in glob.glob('{}/img*.jpg'.format(output_folder)): frame_no = int(os.path.basename(image).strip('-img').strip('.jpg')) - 1 frame_second = frame_no * PLEX_BIF_FRAME_INTERVAL os.rename(image, os.path.join(output_folder, '{:010d}.jpg'.format(frame_second))) def generate_bif(bif_filename, images_path): """ Build a .bif file @param bif_filename name of .bif file to create @param images_path Directory of image files 00000001.jpg """ magic = [0x89, 0x42, 0x49, 0x46, 0x0d, 0x0a, 0x1a, 0x0a] version = 0 images = [img for img in os.listdir(images_path) if os.path.splitext(img)[1] == '.jpg'] images.sort() f = open(bif_filename, "wb") array.array('B', magic).tofile(f) f.write(struct.pack(" 1: if sys.argv[1] not in media_part.attrib['file']: return bundle_hash = media_part.attrib['hash'] bundle_file = '{}/{}{}'.format(bundle_hash[0], bundle_hash[1::1], '.bundle') bundle_path = os.path.join(PLEX_LOCAL_MEDIA_PATH, bundle_file) indexes_path = os.path.join(bundle_path, 'Contents', 'Indexes') index_bif = os.path.join(indexes_path, 'index-sd.bif') tmp_path = os.path.join(TMP_FOLDER, bundle_hash) if (not os.path.isfile(index_bif)) and (not os.path.isdir(tmp_path)): if not os.path.isdir(indexes_path): os.mkdir(indexes_path) try: os.mkdir(tmp_path) generate_images(media_part.attrib['file'], tmp_path, lock) generate_bif(index_bif, tmp_path) except Exception as e: # Remove bif, as it prob failed to generate if os.path.exists(index_bif): os.remove(index_bif) logger.error(e) finally: if os.path.exists(tmp_path): shutil.rmtree(tmp_path) def run(): process_pool = ProcessPoolExecutor(max_workers=CPU_THREADS + GPU_THREADS) # Ignore SSL Errors sess = requests.Session() sess.verify = False plex = PlexServer(PLEX_URL, PLEX_TOKEN, session=sess) # Get all Movies logger.info('Getting Movies from Plex') movies = [m.key for m in plex.library.search(libtype='movie')] logger.info('Got {} Movies from Plex', len(movies)) m = multiprocessing.Manager() lock = m.Lock() futures = [process_pool.submit(process_item, key, lock) for key in movies] with Progress(SpinnerColumn(), *Progress.get_default_columns(), MofNCompleteColumn(), console=console) as progress: for future in progress.track(futures): future.result() # Get all Episodes logger.info('Getting Episodes from Plex') episodes = [m.key for m in plex.library.search(libtype='episode')] logger.info('Got {} Episodes from Plex', len(episodes)) futures = [process_pool.submit(process_item, key, lock) for key in episodes] with Progress(SpinnerColumn(), *Progress.get_default_columns(), MofNCompleteColumn(), console=console) as progress: for future in progress.track(futures): future.result() process_pool.shutdown() if __name__ == '__main__': logger.remove() # Remove default 'stderr' handler # We need to specify end=''" as log message already ends with \n (thus the lambda function) # Also forcing 'colorize=True' otherwise Loguru won't recognize that the sink support colors logger.add(lambda m: console.print('\n%s' % m, end=""), colorize=True) if not os.path.exists(PLEX_LOCAL_MEDIA_PATH): logger.error('%s does not exist, please edit PLEX_LOCAL_MEDIA_PATH variable' % PLEX_LOCAL_MEDIA_PATH) exit(1) if 'xxxxxx' in PLEX_URL: logger.error('Please update the PLEX_URL variable within this script') exit(1) if 'xxxxxx' in PLEX_TOKEN: logger.error('Please update the PLEX_TOKEN variable within this script') exit(1) try: # Clean TMP Folder if os.path.isdir(TMP_FOLDER): shutil.rmtree(TMP_FOLDER) os.mkdir(TMP_FOLDER) run() finally: if os.path.isdir(TMP_FOLDER): shutil.rmtree(TMP_FOLDER)