From 4dd8efd67bcc9d8ab7363776e0645b6182b0696e Mon Sep 17 00:00:00 2001 From: Hannes Date: Fri, 6 Feb 2026 14:26:09 +0100 Subject: [PATCH] new scripts --- ff | 570 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ show_path | 3 + 2 files changed, 573 insertions(+) create mode 100755 ff create mode 100755 show_path diff --git a/ff b/ff new file mode 100755 index 0000000..ae4dd05 --- /dev/null +++ b/ff @@ -0,0 +1,570 @@ +#!/bin/python +import sys +import os +import subprocess +import json + +# cmd = "ls ../Videos/OBS/*.mkv" +# result = subprocess.run(cmd, shell=True, capture_output=True, text=True) +# print(result.stdout) + +color = True +try: + from termcolor import colored +except ImportError: + if os.name == "posix": + print("For nicer output install termcolor:\nsudo \'your installer\' python-termcolor") + else: + print("For nicer output install termcolor:\npip install termcolor") + color = False + +EXT=[".mp4", ".mkv", ".avi", ".mov", ".wmv", ".flv", ".webm", ".lrv", ".gif"] + +NORMAL_STYLE = ("white", None, []) +ERROR_STYLE = ("red", None, ["bold"]) +WARN_STYLE = ("yellow", None, ["bold"]) +INFO_STYLE = ("cyan", None, []) +SUCCESS_STYLE = ("green", None, ["bold"]) +DEBUG_STYLE = ("magenta", None, ["dark"]) + +def np(string, style, end = "\n"): + if color: + print(colored(string, *style), end=end) + else: + print(string, end=end) + +def human_readable_size(size, decimal_places=2): + for unit in ['B','KB','MB','GB','TB']: + if size < 1024: + return f"{size:.{decimal_places}f} {unit}" + size /= 1024 + +def calculate_aspect(width: int, height: int) -> str: + temp = 0 + + def gcd(a, b): + """The GCD (greatest common divisor) is the highest number that evenly divides both width and height.""" + return a if b == 0 else gcd(b, a % b) + + if width == height: + return "1:1" + + if width < height: + temp = width + width = height + height = temp + + divisor = gcd(width, height) + + x = int(width / divisor) if not temp else int(height / divisor) + y = int(height / divisor) if not temp else int(width / divisor) + + return f"{x}:{y}" + +def get_interlace_label(fo): + if not fo: + return "Progressive" + + fo = str(fo).lower() + + # Map ffprobe codes to standard labels + if fo in ["tt", "tff", "tb"]: + return "Interlaced (TFF)" + elif fo in ["bb", "bff", "bt"]: + return "Interlaced (BFF)" + elif "progressive" in fo: + return "Progressive" + + return "Progressive" # Default assumption for modern web video + +class video_lines: + def __init__(self, stream, duration): + if stream.get("index"): + self.id = stream.get("index") + else: + self.id = None + + if stream.get("name"): + self.name = stream.get("name") + else: + self.name = "" + + if stream.get("name"): + self.duration = seconds_to_hms(stream.get("duration")) + else: + self.duration = duration + + if stream.get("codec_name"): + self.codec = stream.get("codec_name") + else: + self.codec = "" + + if stream.get("width"): + self.width = stream.get("width") + else: + self.width = "" + + if stream.get("height"): + self.height = stream.get("height") + else: + self.height = "" + + self.resolution = f"{self.width}x{self.height}" + + if stream.get("r_frame_rate"): + num, den = map(int, stream.get("r_frame_rate").split("/")) + self.framerate = round(num / den, 2) + else: + self.framerate = "" + + if stream.get("display_aspect_ratio"): + self.aspect_ratio = stream.get("display_aspect_ratio") + elif self.resolution != "x": + self.aspect_ratio = calculate_aspect(self.width, self.height) + else: + self.aspect_ratio = "" + + if stream.get("pix_fmt"): + self.pix_fmt = stream.get("pix_fmt") + else: + self.pix_fmt = "" + + if stream.get("color_space"): + self.color_space = stream.get("color_space") + else: + self.color_space = "" + + if stream.get("field_order"): + self.field_order = get_interlace_label(stream.get("field_order")) + else: + self.field_order = "" + + def __str__(self): + string = "Video" + if self.id != None: + string += f" {self.id}: " + else: + string += f": " + string += f"{self.codec}" + if self.duration: + string += f" {self.duration}s" + if self.resolution != "x": + string += f"({self.resolution}" + if self.framerate != "x": + string += f"@{self.framerate})" + if self.aspect_ratio: + string += f" [{self.aspect_ratio}]" + if self.pix_fmt and self.color_space: + string += f" [{self.pix_fmt}, {self.color_space}]" + if self.field_order: + string += f" [{self.field_order}]" + return string + +LANG_CODES = { + "eng": "English", "en": "English", "spa": "Spanish", "es": "Spanish", + "fra": "French", "fr": "French", "deu": "German", "ger": "German", "de": "German", + "jpn": "Japanese", "ja": "Japanese", "ita": "Italian", "it": "Italian", + "por": "Portuguese", "pt": "Portuguese", "rus": "Russian", "ru": "Russian", + "chi": "Chinese", "zho": "Chinese", "zh": "Chinese", "kor": "Korean", "ko": "Korean", + "dut": "Dutch", "nl": "Dutch", "swe": "Swedish", "sv": "Swedish", + "fin": "Finnish", "fi": "Finnish", "pol": "Polish", "pl": "Polish", + "ara": "Arabic", "ar": "Arabic", "hin": "Hindi", "hi": "Hindi", + "tur": "Turkish", "tr": "Turkish", "und": "Undefined", " ": "Undefined", + "ab": "Abkhazian", "abk": "Abkhazian", "aa": "Afar", "aar": "Afar", + "af": "Afrikaans", "afr": "Afrikaans", "ak": "Akan", "aka": "Akan", + "twi": "Twi", "fat": "Fanti", "sq": "Albanian", "sqi": "Albanian", "alb": "Albanian", + "am": "Amharic", "amh": "Amharic", "arb": "Arabic", "an": "Aragonese", "arg": "Aragonese", + "hy": "Armenian", "hye": "Armenian", "arm": "Armenian", "as": "Assamese", "asm": "Assamese", + "av": "Avaric", "ava": "Avaric", "ae": "Avestan", "ave": "Avestan", "ay": "Aymara", "aym": "Aymara", + "az": "Azerbaijani", "aze": "Azerbaijani", "bm": "Bambara", "bam": "Bambara", + "ba": "Bashkir", "bak": "Bashkir", "eu": "Basque", "eus": "Basque", "baq": "Basque", + "be": "Belarusian", "bel": "Belarusian", "bn": "Bengali", "ben": "Bengali", + "bi": "Bislama", "bis": "Bislama", "bs": "Bosnian", "bos": "Bosnian", + "br": "Breton", "bre": "Breton", "bg": "Bulgarian", "bul": "Bulgarian", + "my": "Burmese", "mya": "Burmese", "ca": "Catalan", "cat": "Catalan", + "ch": "Chamorro", "cha": "Chamorro", "ce": "Chechen", "che": "Chechen", + "ny": "Chichewa", "nya": "Chichewa", "cu": "Church Slavonic", "chu": "Church Slavonic", + "cv": "Chuvash", "chv": "Chuvash", "kw": "Cornish", "cor": "Cornish", + "co": "Corsican", "cos": "Corsican", "cr": "Cree", "cre": "Cree", + "hr": "Croatian", "hrv": "Croatian", "cs": "Czech", "ces": "Czech", "cze": "Czech", + "da": "Danish", "dan": "Danish", "dv": "Divehi", "div": "Divehi", "dz": "Dzongkha", "dzo": "Dzongkha", + "eo": "Esperanto", "epo": "Esperanto", "et": "Estonian", "est": "Estonian", + "ee": "Ewe", "ewe": "Ewe", "fo": "Faroese", "fao": "Faroese", "fj": "Fijian", "fij": "Fijian", + "fre": "French", "fy": "Western Frisian", "fry": "Western Frisian", "ff": "Fulah", "ful": "Fulah", + "gd": "Gaelic, Scottish Gaelic", "gla": "Gaelic", "gl": "Galician", "glg": "Galician", + "lg": "Ganda", "lug": "Ganda", "ka": "Georgian", "kat": "Georgian", "geo": "Georgian", + "el": "Greek", "ell": "Greek", "gre": "Greek", "kl": "Kalaallisut", "kal": "Kalaallisut", + "gn": "Guarani", "grn": "Guarani", "gu": "Gujarati", "guj": "Gujarati", + "ht": "Haitian Creole", "hat": "Haitian Creole", "ha": "Hausa", "hau": "Hausa", + "he": "Hebrew", "heb": "Hebrew", "hz": "Herero", "her": "Herero", "ho": "Hiri Motu", "hmo": "Hiri Motu", + "hu": "Hungarian", "hun": "Hungarian", "is": "Icelandic", "isl": "Icelandic", "ice": "Icelandic", + "io": "Ido", "ido": "Ido", "ig": "Igbo", "ibo": "Igbo", "id": "Indonesian", "ind": "Indonesian", + "ia": "Interlingua", "ina": "Interlingua", "ie": "Interlingue", "ile": "Interlingue", + "iu": "Inuktitut", "iku": "Inuktitut", "ik": "Inupiaq", "ipk": "Inupiaq", + "ga": "Irish", "gle": "Irish", "jv": "Javanese", "jav": "Javanese", + "kn": "Kannada", "kan": "Kannada", "kr": "Kanuri", "kau": "Kanuri", + "ks": "Kashmiri", "kas": "Kashmiri", "kk": "Kazakh", "kaz": "Kazakh", + "km": "Central Khmer", "khm": "Central Khmer", "ki": "Kikuyu", "kik": "Kikuyu", + "rw": "Kinyarwanda", "kin": "Kinyarwanda", "ky": "Kyrgyz", "kir": "Kyrgyz", + "kv": "Komi", "kom": "Komi", "kg": "Kongo", "kon": "Kongo", "kj": "Kuanyama", "kua": "Kuanyama", + "ku": "Kurdish", "kur": "Kurdish", "lo": "Lao", "lao": "Lao", "la": "Latin", "lat": "Latin", + "lv": "Latvian", "lav": "Latvian", "li": "Limburgan", "lim": "Limburgan", + "ln": "Lingala", "lin": "Lingala", "lt": "Lithuanian", "lit": "Lithuanian", + "lu": "Luba-Katanga", "lub": "Luba-Katanga", "lb": "Luxembourgish", "ltz": "Luxembourgish", + "mk": "Macedonian", "mkd": "Macedonian", "mac": "Macedonian", "mg": "Malagasy", "mlg": "Malagasy", + "ms": "Malay", "msa": "Malay", "ml": "Malayalam", "mal": "Malayalam", "mt": "Maltese", "mlt": "Maltese", + "gv": "Manx", "glv": "Manx", "mi": "Maori", "mri": "Maori", "mao": "Maori", + "mr": "Marathi", "mar": "Marathi", "mh": "Marshallese", "mah": "Marshallese", + "mn": "Mongolian", "mon": "Mongolian", "na": "Nauru", "nau": "Nauru", "nv": "Navajo", "nav": "Navajo", + "nd": "North Ndebele", "nde": "North Ndebele", "nr": "South Ndebele", "nbl": "South Ndebele", + "ng": "Ndonga", "ndo": "Ndonga", "ne": "Nepali", "nep": "Nepali", "no": "Norwegian", "nor": "Norwegian", + "nb": "Norwegian Bokmål", "nob": "Norwegian Bokmål", "nn": "Norwegian Nynorsk", "nno": "Norwegian Nynorsk", + "oc": "Occitan", "oci": "Occitan", "oj": "Ojibwa", "oji": "Ojibwa", "or": "Oriya", "ori": "Oriya", + "om": "Oromo", "orm": "Oromo", "os": "Ossetian", "oss": "Ossetian", "pi": "Pali", "pli": "Pali", + "ps": "Pashto", "pus": "Pashto", "fa": "Persian", "fas": "Persian", "per": "Persian", + "pa": "Punjabi", "pan": "Punjabi", "qu": "Quechua", "que": "Quechua", "ro": "Romanian", "ron": "Romanian", "rum": "Romanian", + "rm": "Romansh", "roh": "Romansh", "rn": "Rundi", "run": "Rundi", "se": "Northern Sami", "sme": "Northern Sami", + "sm": "Samoan", "smo": "Samoan", "sg": "Sango", "sag": "Sango", "sa": "Sanskrit", "san": "Sanskrit", + "sc": "Sardinian", "srd": "Sardinian", "sr": "Serbian", "srp": "Serbian", "sn": "Shona", "sna": "Shona", + "sd": "Sindhi", "snd": "Sindhi", "si": "Sinhala", "sin": "Sinhala", "sk": "Slovak", "slk": "Slovak", "slo": "Slovak", + "sl": "Slovenian", "slv": "Slovenian", "so": "Somali", "som": "Somali", "st": "Southern Sotho", "sot": "Southern Sotho", + "su": "Sundanese", "sun": "Sundanese", "sw": "Swahili", "swa": "Swahili", "ss": "Swati", "ssw": "Swati", + "tl": "Tagalog", "tgl": "Tagalog", "ty": "Tahitian", "tah": "Tahitian", "tg": "Tajik", "tgk": "Tajik", + "ta": "Tamil", "tam": "Tamil", "tt": "Tatar", "tat": "Tatar", "te": "Telugu", "tel": "Telugu", + "th": "Thai", "tha": "Thai", "bo": "Tibetan", "bod": "Tibetan", "tib": "Tibetan", + "ti": "Tigrinya", "tir": "Tigrinya", "to": "Tongan", "ton": "Tongan", "ts": "Tsonga", "tso": "Tsonga", + "tn": "Tswana", "tsn": "Tswana", "tk": "Turkmen", "tuk": "Turkmen", "ug": "Uighur", "uig": "Uighur", + "uk": "Ukrainian", "ukr": "Ukrainian", "ur": "Urdu", "urd": "Urdu", "uz": "Uzbek", "uzb": "Uzbek", + "ve": "Venda", "ven": "Venda", "vi": "Vietnamese", "vie": "Vietnamese", "vo": "Volapük", "vol": "Volapük", + "wa": "Walloon", "wln": "Walloon", "cy": "Welsh", "cym": "Welsh", "wel": "Welsh", "wo": "Wolof", "wol": "Wolof", + "xh": "Xhosa", "xho": "Xhosa", "ii": "Sichuan Yi", "iii": "Sichuan Yi", "yi": "Yiddish", "yid": "Yiddish", + "yo": "Yoruba", "yor": "Yoruba", "za": "Zhuang", "zha": "Zhuang", "zu": "Zulu", "zul": "Zulu" +} + +class audio_lines: + def __init__(self, stream, file_duration): + # 1. Basic ID + self.id = stream.get("index") + + # 2. Name (usually in tags as 'title') + self.name = stream.get("tags", {}).get("title", "") + + # 3. Language (usually in tags) + raw_lang = stream.get("tags", {}).get("language", "und").lower() + self.language = LANG_CODES.get(raw_lang, raw_lang.capitalize()) + + # 4. Duration (fallback to file duration if stream duration is missing) + stream_dur = stream.get("duration") + if stream_dur: + self.duration = seconds_to_hms(float(stream_dur)) + else: + self.duration = file_duration + + # 5. Codec + self.codec = stream.get("codec_name", "") + + # 6. Sample Rate (converted to kHz for readability, e.g., 48000 -> 48.0) + sr = stream.get("sample_rate") + self.sample_rate = f"{int(sr) / 1000} kHz" if sr else "" + + # 7. Channels + self.channels = stream.get("channels", "") + + # 8. Bit Depth + # PCM uses bits_per_sample; lossy like AAC/MP3 might use bits_per_raw_sample + depth = stream.get("bits_per_sample") or stream.get("bits_per_raw_sample") + self.bit_depth = f"{depth}-bit" if depth else "" + + # 9. Bitrate (converted to kbps) + br = stream.get("bit_rate") + self.bitrate = f"{int(br) // 1000} kbps" if br else "" + + def __str__(self): + string = "Audio" + if self.id is not None: + string += f" {self.id}: " + else: + string += ": " + + string += f"{self.codec}" + + if self.language: + string += f" [{self.language}]" + + if self.duration: + string += f" {self.duration}s" + + # Grouping audio specs: Channels, Sample Rate, and Bit Depth + specs = [] + if self.channels: + specs.append(f"{self.channels}ch") + if self.sample_rate: + specs.append(self.sample_rate) + if self.bit_depth: + specs.append(self.bit_depth) + + if specs: + string += f" ({', '.join(specs)})" + + if self.bitrate: + string += f" @{self.bitrate}" + + if self.name: + string += f" [{self.name}]" + + return string + +class subtitles: + def __init__(self, stream, file_duration): + self.id = stream.get("index") + self.name = stream.get("tags", {}).get("title", "") + + # Language translation + raw_lang = stream.get("tags", {}).get("language", "und") + self.language = LANG_CODES.get(raw_lang, raw_lang.capitalize()) + + # Duration logic + stream_dur = stream.get("duration") + self.duration = seconds_to_hms(float(stream_dur)) if stream_dur else file_duration + + # Codec (e.g., srt, ass, subrip) + self.codec = stream.get("codec_name", "") + + # Disposition (Extra helpful info for subs) + dispo = stream.get("disposition", {}) + self.is_forced = dispo.get("forced") == 1 + self.is_default = dispo.get("default") == 1 + + def __str__(self): + parts = [f"Subtitle {self.id}:" if self.id is not None else "Subtitle:"] + + if self.codec: + parts.append(self.codec.upper()) + + if self.language: + parts.append(f"[{self.language}]") + + if self.duration: + parts.append(f"{self.duration}s") + + # Add flags for Forced/Default + flags = [] + if self.is_forced: flags.append("FORCED") + if self.is_default: flags.append("Default") + if flags: + parts.append(f"({'/'.join(flags)})") + + if self.name: + parts.append(f"[{self.name}]") + + return " ".join(parts) + +def get_video_lines(file): + cmd = ["ffprobe", + "-v", "error", + "-select_streams", "v", # video streams only + "-show_entries", + "stream=index,codec_name,width,height,r_frame_rate,bit_rate,duration,nb_frames,pix_fmt,field_order,time_base,display_aspect_ratio,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_tags=title", + "-of", "json", + file + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + info = json.loads(result.stdout) + streams = info.get("streams", []) + return streams + +def get_audio_lines(file): + cmd = [ + "ffprobe", + "-v", "error", + "-select_streams", "a", # Select audio streams only + "-show_entries", + # Entries mapped to your requirements: + "stream=index,codec_name,sample_rate,channels,bits_per_sample,bits_per_raw_sample,bit_rate,duration" + + ":stream_tags=language,title", + "-of", "json", + file + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + info = json.loads(result.stdout) + return info.get("streams", []) + except subprocess.CalledProcessError as e: + print(f"Error running ffprobe: {e.stderr}") + return [] + +def get_subtitle_lines(file): + cmd = [ + "ffprobe", + "-v", "error", + "-select_streams", "s", # Subtitle streams only + "-show_entries", + "stream=index,codec_name,duration:stream_tags=language,title:stream_disposition=forced,default", + "-of", "json", + file + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + info = json.loads(result.stdout) + return info.get("streams", []) + except subprocess.CalledProcessError: + return [] + +def get_video_duration(file_path): + cmd = [ + "ffprobe", + "-v", "error", + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", + file_path + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + duration = float(result.stdout.strip()) + return duration + +def seconds_to_hms(seconds): + h = int(seconds // 3600) + m = int((seconds % 3600) // 60) + s = int(seconds % 60) + return f"{h:02}:{m:02}:{s:02}" + + +def get_stream_bitrate(file_path, stream=None): + # Get duration in seconds + duration = get_video_duration(file_path) + + # Get file size in bits + size_bits = os.path.getsize(file_path) * 8 + + # Approximate average bitrate + avg_bitrate = size_bits / duration if duration > 0 else 0 + return avg_bitrate # bits per second + + +class video_file: + def __init__(self, path, base_tab=""): + self.base_tab = base_tab # \t + self.path = path # folder/25.mkv + self.name = os.path.basename(path) # 25.mkv + self.size = human_readable_size(os.path.getsize(path)) # 198MB + self.duration = seconds_to_hms(get_video_duration(path)) + self.bitrate = int((os.path.getsize(path) * 8)/get_video_duration(path))/1000000 if get_video_duration(path) > 0 else 0 + # self. = get_video_lines(path) + self.videos = [] + self.audios = [] + self.subtitles = [] + for vl in get_video_lines(path): + video_line = video_lines(vl, self.duration) + self.videos.append(video_line) + for al in get_audio_lines(path): + audio_line = audio_lines(al, self.duration) + self.videos.append(audio_line) + for st in get_subtitle_lines(path): + subtitle = subtitles(st, self.duration) + self.videos.append(subtitle) + + + + def print(self): + if self.base_tab == "\t": + np(f"{self.base_tab}{self.name} ({self.size}, {self.duration}, {self.bitrate} MB/s):", NORMAL_STYLE) + else: + np(f"{os.path.dirname(self.path)}/", INFO_STYLE) + np(f"\t{self.name} ({self.size}, {self.duration}, {self.bitrate} MB/s):", NORMAL_STYLE) + for video in self.videos: + np(f"\t\t{video}", NORMAL_STYLE) + for audio in self.audios: + np(f"\t\t{audio}", NORMAL_STYLE) + for subtitle in self.subtitles: + np(f"\t\t{subtitle}", NORMAL_STYLE) + print() + + +def get_folder_info(files): + np(f"Videos in {os.path.dirname(files[0])}/", INFO_STYLE) + for file in files: + file = video_file(file, "\t") + file.print() + +def get_file_info(file, file_name): + file = video_file(file, "") + file.print() + +def handle_files(files, all_files): + if(files != []): + files.sort() + + grouped = [] + current_dir = None + current_group = [] + + for f in files: + dir_path = os.path.dirname(f) + if dir_path != current_dir: + if current_group: + if len(current_group) == 1: + grouped.append(current_group[0]) # singleton as string + else: + grouped.append(current_group) # multiple files as list + current_dir = dir_path + current_group = [f] + else: + current_group.append(f) + + # Add the last group + if current_group: + if len(current_group) == 1: + grouped.append(current_group[0]) + else: + grouped.append(current_group) + + + all_files.extend(grouped) + +def handle_folders(dirs, all_files): + if(dirs != []): + dirs.sort(key=lambda f: os.path.dirname(f)) + for dir in dirs: + dir_files = [] + for file in os.scandir(dir): + if file.is_file(): + file = file.path + if os.path.splitext(file)[1].lower() in EXT: + dir_files.append(file) + else: + np(f"{file} is not a compatabile Video file", WARN_STYLE) + if(dir_files != []): + dir_files.sort() + all_files.append(dir_files) + +if __name__ == "__main__": + if len(sys.argv) == 0: + print("Something went horribly wrong!") + if len(sys.argv) == 1: + current_dir = os.path.dirname(os.path.realpath(__file__)) + print(current_dir) + else: + files = [] + dirs = [] + for argv in sys.argv[1:]: + if os.path.isfile(argv): + files.append(os.path.abspath(argv)) + elif os.path.isdir(argv): + dirs.append(os.path.abspath(argv)) + else: + np(f"This is not a file or directory: {argv}\nNow canceling!", ERROR_STYLE) + sys.exit() + file_dir_array = [] + handle_folders(dirs, file_dir_array) + handle_files(files, file_dir_array) + + for element in file_dir_array: + if type(element) == list: + get_folder_info(element) + else: + file = element + get_file_info(file, file) \ No newline at end of file diff --git a/show_path b/show_path new file mode 100755 index 0000000..05032e1 --- /dev/null +++ b/show_path @@ -0,0 +1,3 @@ +#!/bin/bash + +echo $PATH | tr " " "\n" | nl