diff --git a/delete.py b/delete.py new file mode 100644 index 0000000..d466cac --- /dev/null +++ b/delete.py @@ -0,0 +1,272 @@ +#!/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') + os.remove(index_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) diff --git a/original.py b/original.py new file mode 100644 index 0000000..161cd47 --- /dev/null +++ b/original.py @@ -0,0 +1,271 @@ +#!/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)