Created
August 8, 2025 17:58
-
-
Save matthewmorrone/6f0330a8bfe99683c93ee1f336c55617 to your computer and use it in GitHub Desktop.
helpers for working with MKVs
This file contains hidden or 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
| #!/bin/bash | |
| # Map 3-letter language codes to full language names | |
| # Usage: get_language_name <code> - returns full language name | |
| # Usage: list_language_codes - displays all supported codes and names | |
| get_language_name() { | |
| local code="$1" | |
| # Convert to lowercase for consistent matching | |
| code=$(echo "$code" | tr '[:upper:]' '[:lower:]') | |
| # ISO 639-2/T and common video language codes | |
| case "$code" in | |
| "aar") echo "Afar" ;; | |
| "abk") echo "Abkhazian" ;; | |
| "ace") echo "Achinese" ;; | |
| "ach") echo "Acoli" ;; | |
| "ada") echo "Adangme" ;; | |
| "ady") echo "Adyghe" ;; | |
| "afa") echo "Afro-Asiatic (Other)" ;; | |
| "afh") echo "Afrihili" ;; | |
| "afr") echo "Afrikaans" ;; | |
| "ain") echo "Ainu" ;; | |
| "aka") echo "Akan" ;; | |
| "akk") echo "Akkadian" ;; | |
| "alb"|"sqi") echo "Albanian" ;; | |
| "ale") echo "Aleut" ;; | |
| "alg") echo "Algonquian" ;; | |
| "alt") echo "Southern Altai" ;; | |
| "amh") echo "Amharic" ;; | |
| "ang") echo "English, Old" ;; | |
| "anp") echo "Angika" ;; | |
| "apa") echo "Apache" ;; | |
| "ara") echo "Arabic" ;; | |
| "arc") echo "Aramaic" ;; | |
| "arg") echo "Aragonese" ;; | |
| "arm"|"hye") echo "Armenian" ;; | |
| "arn") echo "Mapudungun" ;; | |
| "arp") echo "Arapaho" ;; | |
| "art") echo "Artificial (Other)" ;; | |
| "arw") echo "Arawak" ;; | |
| "asm") echo "Assamese" ;; | |
| "ast") echo "Asturian" ;; | |
| "ath") echo "Athapascan" ;; | |
| "aus") echo "Australian (Other)" ;; | |
| "ava") echo "Avaric" ;; | |
| "ave") echo "Avestan" ;; | |
| "awa") echo "Awadhi" ;; | |
| "aym") echo "Aymara" ;; | |
| "aze") echo "Azerbaijani" ;; | |
| "bad") echo "Banda" ;; | |
| "bai") echo "Bamileke" ;; | |
| "bak") echo "Bashkir" ;; | |
| "bal") echo "Baluchi" ;; | |
| "bam") echo "Bambara" ;; | |
| "ban") echo "Balinese" ;; | |
| "baq"|"eus") echo "Basque" ;; | |
| "bas") echo "Basa" ;; | |
| "bat") echo "Baltic (Other)" ;; | |
| "bej") echo "Beja" ;; | |
| "bel") echo "Belarusian" ;; | |
| "bem") echo "Bemba" ;; | |
| "ben") echo "Bengali" ;; | |
| "ber") echo "Berber (Other)" ;; | |
| "bho") echo "Bhojpuri" ;; | |
| "bih") echo "Bihari" ;; | |
| "bik") echo "Bikol" ;; | |
| "bin") echo "Bini" ;; | |
| "bis") echo "Bislama" ;; | |
| "bla") echo "Siksika" ;; | |
| "bnt") echo "Bantu (Other)" ;; | |
| "bos") echo "Bosnian" ;; | |
| "bra") echo "Braj" ;; | |
| "bre") echo "Breton" ;; | |
| "btk") echo "Batak" ;; | |
| "bua") echo "Buriat" ;; | |
| "bug") echo "Buginese" ;; | |
| "bul") echo "Bulgarian" ;; | |
| "bur"|"mya") echo "Burmese" ;; | |
| "byn") echo "Blin" ;; | |
| "cad") echo "Caddo" ;; | |
| "cai") echo "Central American Indian (Other)" ;; | |
| "car") echo "Galibi Carib" ;; | |
| "cat") echo "Catalan" ;; | |
| "cau") echo "Caucasian (Other)" ;; | |
| "ceb") echo "Cebuano" ;; | |
| "cel") echo "Celtic (Other)" ;; | |
| "cha") echo "Chamorro" ;; | |
| "chb") echo "Chibcha" ;; | |
| "che") echo "Chechen" ;; | |
| "chg") echo "Chagatai" ;; | |
| "chi"|"zho") echo "Chinese" ;; | |
| "chk") echo "Chuukese" ;; | |
| "chm") echo "Mari" ;; | |
| "chn") echo "Chinook jargon" ;; | |
| "cho") echo "Choctaw" ;; | |
| "chp") echo "Chipewyan" ;; | |
| "chr") echo "Cherokee" ;; | |
| "chu") echo "Church Slavic" ;; | |
| "chv") echo "Chuvash" ;; | |
| "chy") echo "Cheyenne" ;; | |
| "cmc") echo "Chamic" ;; | |
| "cop") echo "Coptic" ;; | |
| "cor") echo "Cornish" ;; | |
| "cos") echo "Corsican" ;; | |
| "cpe") echo "Creoles and pidgins, English based (Other)" ;; | |
| "cpf") echo "Creoles and pidgins, French-based (Other)" ;; | |
| "cpp") echo "Creoles and pidgins, Portuguese-based (Other)" ;; | |
| "cre") echo "Cree" ;; | |
| "crh") echo "Crimean Tatar" ;; | |
| "crp") echo "Creoles and pidgins (Other)" ;; | |
| "csb") echo "Kashubian" ;; | |
| "cus") echo "Cushitic (Other)" ;; | |
| "cze"|"ces") echo "Czech" ;; | |
| "dak") echo "Dakota" ;; | |
| "dan") echo "Danish" ;; | |
| "dar") echo "Dargwa" ;; | |
| "day") echo "Land Dayak" ;; | |
| "del") echo "Delaware" ;; | |
| "den") echo "Slave (Athapascan)" ;; | |
| "dgr") echo "Dogrib" ;; | |
| "din") echo "Dinka" ;; | |
| "div") echo "Divehi" ;; | |
| "doi") echo "Dogri" ;; | |
| "dra") echo "Dravidian (Other)" ;; | |
| "dsb") echo "Lower Sorbian" ;; | |
| "dua") echo "Duala" ;; | |
| "dum") echo "Dutch, Middle" ;; | |
| "dut"|"nld") echo "Dutch" ;; | |
| "dyu") echo "Dyula" ;; | |
| "dzo") echo "Dzongkha" ;; | |
| "efi") echo "Efik" ;; | |
| "egy") echo "Egyptian (Ancient)" ;; | |
| "eka") echo "Ekajuk" ;; | |
| "elx") echo "Elamite" ;; | |
| "eng") echo "English" ;; | |
| "enm") echo "English, Middle" ;; | |
| "epo") echo "Esperanto" ;; | |
| "est") echo "Estonian" ;; | |
| "ewe") echo "Ewe" ;; | |
| "ewo") echo "Ewondo" ;; | |
| "fan") echo "Fang" ;; | |
| "fao") echo "Faroese" ;; | |
| "fat") echo "Fanti" ;; | |
| "fij") echo "Fijian" ;; | |
| "fil") echo "Filipino" ;; | |
| "fin") echo "Finnish" ;; | |
| "fiu") echo "Finno-Ugrian (Other)" ;; | |
| "fon") echo "Fon" ;; | |
| "fre"|"fra") echo "French" ;; | |
| "frm") echo "French, Middle" ;; | |
| "fro") echo "French, Old" ;; | |
| "frr") echo "Northern Frisian" ;; | |
| "frs") echo "Eastern Frisian" ;; | |
| "fry") echo "Western Frisian" ;; | |
| "ful") echo "Fulah" ;; | |
| "fur") echo "Friulian" ;; | |
| "gaa") echo "Ga" ;; | |
| "gay") echo "Gayo" ;; | |
| "gba") echo "Gbaya" ;; | |
| "gem") echo "Germanic (Other)" ;; | |
| "geo"|"kat") echo "Georgian" ;; | |
| "ger"|"deu") echo "German" ;; | |
| "gez") echo "Geez" ;; | |
| "gil") echo "Gilbertese" ;; | |
| "gla") echo "Gaelic" ;; | |
| "gle") echo "Irish" ;; | |
| "glg") echo "Galician" ;; | |
| "glv") echo "Manx" ;; | |
| "gmh") echo "German, Middle High" ;; | |
| "goh") echo "German, Old High" ;; | |
| "gon") echo "Gondi" ;; | |
| "gor") echo "Gorontalo" ;; | |
| "got") echo "Gothic" ;; | |
| "grb") echo "Grebo" ;; | |
| "grc") echo "Greek, Ancient" ;; | |
| "gre"|"ell") echo "Greek, Modern" ;; | |
| "grn") echo "Guarani" ;; | |
| "gsw") echo "Swiss German" ;; | |
| "guj") echo "Gujarati" ;; | |
| "gwi") echo "Gwich'in" ;; | |
| "hai") echo "Haida" ;; | |
| "hat") echo "Haitian" ;; | |
| "hau") echo "Hausa" ;; | |
| "haw") echo "Hawaiian" ;; | |
| "heb") echo "Hebrew" ;; | |
| "her") echo "Herero" ;; | |
| "hil") echo "Hiligaynon" ;; | |
| "him") echo "Himachali" ;; | |
| "hin") echo "Hindi" ;; | |
| "hit") echo "Hittite" ;; | |
| "hmn") echo "Hmong" ;; | |
| "hmo") echo "Hiri Motu" ;; | |
| "hrv") echo "Croatian" ;; | |
| "hsb") echo "Upper Sorbian" ;; | |
| "hun") echo "Hungarian" ;; | |
| "hup") echo "Hupa" ;; | |
| "iba") echo "Iban" ;; | |
| "ibo") echo "Igbo" ;; | |
| "ice"|"isl") echo "Icelandic" ;; | |
| "ido") echo "Ido" ;; | |
| "iii") echo "Sichuan Yi" ;; | |
| "ijo") echo "Ijo" ;; | |
| "iku") echo "Inuktitut" ;; | |
| "ile") echo "Interlingue" ;; | |
| "ilo") echo "Iloko" ;; | |
| "ina") echo "Interlingua" ;; | |
| "inc") echo "Indic (Other)" ;; | |
| "ind") echo "Indonesian" ;; | |
| "ine") echo "Indo-European (Other)" ;; | |
| "inh") echo "Ingush" ;; | |
| "ipk") echo "Inupiaq" ;; | |
| "ira") echo "Iranian (Other)" ;; | |
| "iro") echo "Iroquoian" ;; | |
| "ita") echo "Italian" ;; | |
| "jav") echo "Javanese" ;; | |
| "jbo") echo "Lojban" ;; | |
| "jpn") echo "Japanese" ;; | |
| "jpr") echo "Judeo-Persian" ;; | |
| "jrb") echo "Judeo-Arabic" ;; | |
| "kaa") echo "Kara-Kalpak" ;; | |
| "kab") echo "Kabyle" ;; | |
| "kac") echo "Kachin" ;; | |
| "kal") echo "Kalaallisut" ;; | |
| "kam") echo "Kamba" ;; | |
| "kan") echo "Kannada" ;; | |
| "kar") echo "Karen" ;; | |
| "kas") echo "Kashmiri" ;; | |
| "kau") echo "Kanuri" ;; | |
| "kaw") echo "Kawi" ;; | |
| "kaz") echo "Kazakh" ;; | |
| "kbd") echo "Kabardian" ;; | |
| "kha") echo "Khasi" ;; | |
| "khi") echo "Khoisan (Other)" ;; | |
| "khm") echo "Central Khmer" ;; | |
| "kho") echo "Khotanese" ;; | |
| "kik") echo "Kikuyu" ;; | |
| "kin") echo "Kinyarwanda" ;; | |
| "kir") echo "Kirghiz" ;; | |
| "kmb") echo "Kimbundu" ;; | |
| "kok") echo "Konkani" ;; | |
| "kom") echo "Komi" ;; | |
| "kon") echo "Kongo" ;; | |
| "kor") echo "Korean" ;; | |
| "kos") echo "Kosraean" ;; | |
| "kpe") echo "Kpelle" ;; | |
| "krc") echo "Karachay-Balkar" ;; | |
| "krl") echo "Karelian" ;; | |
| "kro") echo "Kru" ;; | |
| "kru") echo "Kurukh" ;; | |
| "kua") echo "Kuanyama" ;; | |
| "kum") echo "Kumyk" ;; | |
| "kur") echo "Kurdish" ;; | |
| "kut") echo "Kutenai" ;; | |
| "lad") echo "Ladino" ;; | |
| "lah") echo "Lahnda" ;; | |
| "lam") echo "Lamba" ;; | |
| "lao") echo "Lao" ;; | |
| "lat") echo "Latin" ;; | |
| "lav") echo "Latvian" ;; | |
| "lez") echo "Lezghian" ;; | |
| "lim") echo "Limburgan" ;; | |
| "lin") echo "Lingala" ;; | |
| "lit") echo "Lithuanian" ;; | |
| "lol") echo "Mongo" ;; | |
| "loz") echo "Lozi" ;; | |
| "ltz") echo "Luxembourgish" ;; | |
| "lua") echo "Luba-Lulua" ;; | |
| "lub") echo "Luba-Katanga" ;; | |
| "lug") echo "Ganda" ;; | |
| "lui") echo "Luiseno" ;; | |
| "lun") echo "Lunda" ;; | |
| "luo") echo "Luo (Kenya and Tanzania)" ;; | |
| "lus") echo "Lushai" ;; | |
| "mac"|"mkd") echo "Macedonian" ;; | |
| "mad") echo "Madurese" ;; | |
| "mag") echo "Magahi" ;; | |
| "mah") echo "Marshallese" ;; | |
| "mai") echo "Maithili" ;; | |
| "mak") echo "Makasar" ;; | |
| "mal") echo "Malayalam" ;; | |
| "man") echo "Mandingo" ;; | |
| "mao"|"mri") echo "Maori" ;; | |
| "map") echo "Austronesian (Other)" ;; | |
| "mar") echo "Marathi" ;; | |
| "mas") echo "Masai" ;; | |
| "may"|"msa") echo "Malay" ;; | |
| "mdf") echo "Moksha" ;; | |
| "mdr") echo "Mandar" ;; | |
| "men") echo "Mende" ;; | |
| "mga") echo "Irish, Middle" ;; | |
| "mic") echo "Mi'kmaq" ;; | |
| "min") echo "Minangkabau" ;; | |
| "mis") echo "Uncoded languages" ;; | |
| "mkh") echo "Mon-Khmer (Other)" ;; | |
| "mlg") echo "Malagasy" ;; | |
| "mlt") echo "Maltese" ;; | |
| "mnc") echo "Manchu" ;; | |
| "mni") echo "Manipuri" ;; | |
| "mno") echo "Manobo" ;; | |
| "moh") echo "Mohawk" ;; | |
| "mon") echo "Mongolian" ;; | |
| "mos") echo "Mossi" ;; | |
| "mul") echo "Multiple languages" ;; | |
| "mun") echo "Munda" ;; | |
| "mus") echo "Creek" ;; | |
| "mwl") echo "Mirandese" ;; | |
| "mwr") echo "Marwari" ;; | |
| "myn") echo "Mayan" ;; | |
| "myv") echo "Erzya" ;; | |
| "nah") echo "Nahuatl" ;; | |
| "nai") echo "North American Indian (Other)" ;; | |
| "nap") echo "Neapolitan" ;; | |
| "nau") echo "Nauru" ;; | |
| "nav") echo "Navajo" ;; | |
| "nbl") echo "Ndebele, South" ;; | |
| "nde") echo "Ndebele, North" ;; | |
| "ndo") echo "Ndonga" ;; | |
| "nds") echo "Low German" ;; | |
| "nep") echo "Nepali" ;; | |
| "new") echo "Nepal Bhasa" ;; | |
| "nia") echo "Nias" ;; | |
| "nic") echo "Niger-Kordofanian (Other)" ;; | |
| "niu") echo "Niuean" ;; | |
| "nno") echo "Norwegian Nynorsk" ;; | |
| "nob") echo "Norwegian Bokmål" ;; | |
| "nog") echo "Nogai" ;; | |
| "non") echo "Norse, Old" ;; | |
| "nor") echo "Norwegian" ;; | |
| "nqo") echo "N'Ko" ;; | |
| "nso") echo "Pedi" ;; | |
| "nub") echo "Nubian" ;; | |
| "nwc") echo "Classical Newari" ;; | |
| "nya") echo "Chichewa" ;; | |
| "nym") echo "Nyamwezi" ;; | |
| "nyn") echo "Nyankole" ;; | |
| "nyo") echo "Nyoro" ;; | |
| "nzi") echo "Nzima" ;; | |
| "oci") echo "Occitan" ;; | |
| "oji") echo "Ojibwa" ;; | |
| "ori") echo "Oriya" ;; | |
| "orm") echo "Oromo" ;; | |
| "osa") echo "Osage" ;; | |
| "oss") echo "Ossetian" ;; | |
| "ota") echo "Turkish, Ottoman" ;; | |
| "oto") echo "Otomian" ;; | |
| "paa") echo "Papuan (Other)" ;; | |
| "pag") echo "Pangasinan" ;; | |
| "pal") echo "Pahlavi" ;; | |
| "pam") echo "Pampanga" ;; | |
| "pan") echo "Panjabi" ;; | |
| "pap") echo "Papiamento" ;; | |
| "pau") echo "Palauan" ;; | |
| "pcc") echo "Bouyei" ;; | |
| "peo") echo "Persian, Old" ;; | |
| "per"|"fas") echo "Persian" ;; | |
| "phi") echo "Philippine (Other)" ;; | |
| "phn") echo "Phoenician" ;; | |
| "pli") echo "Pali" ;; | |
| "pol") echo "Polish" ;; | |
| "pon") echo "Pohnpeian" ;; | |
| "por") echo "Portuguese" ;; | |
| "pra") echo "Prakrit" ;; | |
| "pro") echo "Provençal, Old" ;; | |
| "pus") echo "Pushto" ;; | |
| "que") echo "Quechua" ;; | |
| "raj") echo "Rajasthani" ;; | |
| "rap") echo "Rapanui" ;; | |
| "rar") echo "Rarotongan" ;; | |
| "roa") echo "Romance (Other)" ;; | |
| "roh") echo "Romansh" ;; | |
| "rom") echo "Romany" ;; | |
| "rum"|"ron") echo "Romanian" ;; | |
| "run") echo "Rundi" ;; | |
| "rup") echo "Aromanian" ;; | |
| "rus") echo "Russian" ;; | |
| "sad") echo "Sandawe" ;; | |
| "sag") echo "Sango" ;; | |
| "sah") echo "Yakut" ;; | |
| "sai") echo "South American Indian (Other)" ;; | |
| "sal") echo "Salishan" ;; | |
| "sam") echo "Samaritan Aramaic" ;; | |
| "san") echo "Sanskrit" ;; | |
| "sas") echo "Sasak" ;; | |
| "sat") echo "Santali" ;; | |
| "scn") echo "Sicilian" ;; | |
| "sco") echo "Scots" ;; | |
| "sel") echo "Selkup" ;; | |
| "sem") echo "Semitic (Other)" ;; | |
| "sga") echo "Irish, Old" ;; | |
| "sgn") echo "Sign Languages" ;; | |
| "shn") echo "Shan" ;; | |
| "sid") echo "Sidamo" ;; | |
| "sin") echo "Sinhala" ;; | |
| "sio") echo "Siouan" ;; | |
| "sit") echo "Sino-Tibetan (Other)" ;; | |
| "sla") echo "Slavic (Other)" ;; | |
| "slo"|"slk") echo "Slovak" ;; | |
| "slv") echo "Slovenian" ;; | |
| "sma") echo "Southern Sami" ;; | |
| "sme") echo "Northern Sami" ;; | |
| "smi") echo "Sami (Other)" ;; | |
| "smj") echo "Lule Sami" ;; | |
| "smn") echo "Inari Sami" ;; | |
| "smo") echo "Samoan" ;; | |
| "sms") echo "Skolt Sami" ;; | |
| "sna") echo "Shona" ;; | |
| "snd") echo "Sindhi" ;; | |
| "snk") echo "Soninke" ;; | |
| "sog") echo "Sogdian" ;; | |
| "som") echo "Somali" ;; | |
| "son") echo "Songhai" ;; | |
| "sot") echo "Sotho, Southern" ;; | |
| "spa") echo "Spanish" ;; | |
| "srd") echo "Sardinian" ;; | |
| "srn") echo "Sranan Tongo" ;; | |
| "srp") echo "Serbian" ;; | |
| "srr") echo "Serer" ;; | |
| "ssa") echo "Nilo-Saharan (Other)" ;; | |
| "ssw") echo "Swati" ;; | |
| "suk") echo "Sukuma" ;; | |
| "sun") echo "Sundanese" ;; | |
| "sus") echo "Susu" ;; | |
| "sux") echo "Sumerian" ;; | |
| "swa") echo "Swahili" ;; | |
| "swe") echo "Swedish" ;; | |
| "syc") echo "Classical Syriac" ;; | |
| "syr") echo "Syriac" ;; | |
| "tah") echo "Tahitian" ;; | |
| "tai") echo "Tai (Other)" ;; | |
| "tam") echo "Tamil" ;; | |
| "tat") echo "Tatar" ;; | |
| "tel") echo "Telugu" ;; | |
| "tem") echo "Timne" ;; | |
| "ter") echo "Tereno" ;; | |
| "tet") echo "Tetum" ;; | |
| "tgk") echo "Tajik" ;; | |
| "tgl") echo "Tagalog" ;; | |
| "tha") echo "Thai" ;; | |
| "tib"|"bod") echo "Tibetan" ;; | |
| "tig") echo "Tigre" ;; | |
| "tir") echo "Tigrinya" ;; | |
| "tiv") echo "Tiv" ;; | |
| "tkl") echo "Tokelau" ;; | |
| "tlh") echo "Klingon" ;; | |
| "tli") echo "Tlingit" ;; | |
| "tmh") echo "Tamashek" ;; | |
| "tog") echo "Tonga (Nyasa)" ;; | |
| "ton") echo "Tonga (Tonga Islands)" ;; | |
| "tpi") echo "Tok Pisin" ;; | |
| "tsi") echo "Tsimshian" ;; | |
| "tsn") echo "Tswana" ;; | |
| "tso") echo "Tsonga" ;; | |
| "tuk") echo "Turkmen" ;; | |
| "tum") echo "Tumbuka" ;; | |
| "tup") echo "Tupi" ;; | |
| "tur") echo "Turkish" ;; | |
| "tut") echo "Altaic (Other)" ;; | |
| "tvl") echo "Tuvalu" ;; | |
| "twi") echo "Twi" ;; | |
| "tyv") echo "Tuvinian" ;; | |
| "udm") echo "Udmurt" ;; | |
| "uga") echo "Ugaritic" ;; | |
| "uig") echo "Uighur" ;; | |
| "ukr") echo "Ukrainian" ;; | |
| "umb") echo "Umbundu" ;; | |
| "und") echo "Undetermined" ;; | |
| "urd") echo "Urdu" ;; | |
| "uzb") echo "Uzbek" ;; | |
| "vai") echo "Vai" ;; | |
| "ven") echo "Venda" ;; | |
| "vie") echo "Vietnamese" ;; | |
| "vol") echo "Volapük" ;; | |
| "vot") echo "Votic" ;; | |
| "wak") echo "Wakashan" ;; | |
| "wal") echo "Walamo" ;; | |
| "war") echo "Waray" ;; | |
| "was") echo "Washo" ;; | |
| "wel"|"cym") echo "Welsh" ;; | |
| "wen") echo "Sorbian" ;; | |
| "wln") echo "Walloon" ;; | |
| "wol") echo "Wolof" ;; | |
| "xal") echo "Kalmyk" ;; | |
| "xho") echo "Xhosa" ;; | |
| "yao") echo "Yao" ;; | |
| "yap") echo "Yapese" ;; | |
| "yid") echo "Yiddish" ;; | |
| "yor") echo "Yoruba" ;; | |
| "ypk") echo "Yupik" ;; | |
| "zap") echo "Zapotec" ;; | |
| "zbl") echo "Blissymbols" ;; | |
| "zen") echo "Zenaga" ;; | |
| "zgh") echo "Standard Moroccan Tamazight" ;; | |
| "zha") echo "Zhuang" ;; | |
| "znd") echo "Zande" ;; | |
| "zul") echo "Zulu" ;; | |
| "zun") echo "Zuni" ;; | |
| "zxx") echo "No linguistic content" ;; | |
| "zza") echo "Zaza" ;; | |
| # Common 2-letter codes used in video files | |
| "en") echo "English" ;; | |
| "es") echo "Spanish" ;; | |
| "fr") echo "French" ;; | |
| "de") echo "German" ;; | |
| "it") echo "Italian" ;; | |
| "pt") echo "Portuguese" ;; | |
| "ru") echo "Russian" ;; | |
| "ja") echo "Japanese" ;; | |
| "ko") echo "Korean" ;; | |
| "zh") echo "Chinese" ;; | |
| "ar") echo "Arabic" ;; | |
| "hi") echo "Hindi" ;; | |
| "th") echo "Thai" ;; | |
| "vi") echo "Vietnamese" ;; | |
| "tr") echo "Turkish" ;; | |
| "pl") echo "Polish" ;; | |
| "nl") echo "Dutch" ;; | |
| "sv") echo "Swedish" ;; | |
| "da") echo "Danish" ;; | |
| "no") echo "Norwegian" ;; | |
| "fi") echo "Finnish" ;; | |
| "cs") echo "Czech" ;; | |
| "hu") echo "Hungarian" ;; | |
| "ro") echo "Romanian" ;; | |
| "bg") echo "Bulgarian" ;; | |
| "hr") echo "Croatian" ;; | |
| "sr") echo "Serbian" ;; | |
| "sk") echo "Slovak" ;; | |
| "sl") echo "Slovenian" ;; | |
| "et") echo "Estonian" ;; | |
| "lv") echo "Latvian" ;; | |
| "lt") echo "Lithuanian" ;; | |
| "el") echo "Greek" ;; | |
| "he") echo "Hebrew" ;; | |
| "fa") echo "Persian" ;; | |
| "ur") echo "Urdu" ;; | |
| "bn") echo "Bengali" ;; | |
| "ta") echo "Tamil" ;; | |
| "te") echo "Telugu" ;; | |
| "ml") echo "Malayalam" ;; | |
| "kn") echo "Kannada" ;; | |
| "gu") echo "Gujarati" ;; | |
| "pa") echo "Punjabi" ;; | |
| "mr") echo "Marathi" ;; | |
| "ne") echo "Nepali" ;; | |
| "si") echo "Sinhala" ;; | |
| "my") echo "Burmese" ;; | |
| "km") echo "Khmer" ;; | |
| "lo") echo "Lao" ;; | |
| "ka") echo "Georgian" ;; | |
| "am") echo "Amharic" ;; | |
| "sw") echo "Swahili" ;; | |
| "zu") echo "Zulu" ;; | |
| "af") echo "Afrikaans" ;; | |
| "sq") echo "Albanian" ;; | |
| "eu") echo "Basque" ;; | |
| "be") echo "Belarusian" ;; | |
| "bs") echo "Bosnian" ;; | |
| "ca") echo "Catalan" ;; | |
| "cy") echo "Welsh" ;; | |
| "ga") echo "Irish" ;; | |
| "is") echo "Icelandic" ;; | |
| "mk") echo "Macedonian" ;; | |
| "mt") echo "Maltese" ;; | |
| "lb") echo "Luxembourgish" ;; | |
| *) echo "Unknown ($code)" ;; | |
| esac | |
| } | |
| # Display all supported language codes and their names | |
| list_language_codes() { | |
| echo "* Supported Language Codes and Names:" | |
| echo "======================================" | |
| echo "* Common 2-letter codes:" | |
| echo " en - English es - Spanish fr - French de - German" | |
| echo " it - Italian pt - Portuguese ru - Russian ja - Japanese" | |
| echo " ko - Korean zh - Chinese ar - Arabic hi - Hindi" | |
| echo " th - Thai vi - Vietnamese tr - Turkish pl - Polish" | |
| echo "* Common 3-letter codes:" | |
| echo " eng - English spa - Spanish fra - French deu - German" | |
| echo " ita - Italian por - Portuguese rus - Russian jpn - Japanese" | |
| echo " kor - Korean zho - Chinese ara - Arabic hin - Hindi" | |
| echo " tha - Thai vie - Vietnamese tur - Turkish pol - Polish" | |
| echo "* Usage examples:" | |
| echo " get_language_name eng # Returns: English" | |
| echo " get_language_name spa # Returns: Spanish" | |
| echo " get_language_name und # Returns: Undetermined" | |
| echo "* This function supports ISO 639-1 (2-letter) and ISO 639-2 (3-letter) codes" | |
| echo " plus common variants used in video files and mkvmerge output." | |
| } | |
| # Analyze MKV files and display detailed track information including duration, | |
| # languages, codecs, and track properties (default/forced/enabled status) | |
| # Usage: mkv_scan [--verbose] [--help] [path...] | |
| # Flags: --verbose (verbose output), --help (show usage) | |
| mkv_scan() { | |
| local verbose=false | |
| local args=() | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --help) | |
| echo "* USAGE: mkv_scan [--verbose] [--help] [path...]" | |
| echo "* PURPOSE:" | |
| echo " Analyze MKV files and display detailed track information including" | |
| echo " duration, languages, codecs, and track properties." | |
| echo "* FLAGS:" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --help Show this help message" | |
| echo "* EXAMPLES:" | |
| echo " mkv_scan # Scan current directory" | |
| echo " mkv_scan /path/to/videos # Scan specific directory" | |
| echo " mkv_scan --verbose Season1/ # Verbose scan of Season1 folder" | |
| echo " mkv_scan file.mkv # Scan single file" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| # Set default to current directory if no args | |
| [[ ${#args[@]} -eq 0 ]] && args=(".") | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "=========================================" | |
| fi | |
| # Collect all files | |
| files=() | |
| for arg in "${args[@]}"; do | |
| if [[ -d "$arg" ]]; then | |
| while IFS= read -r f; do | |
| if [[ "$verbose" == true ]]; then | |
| echo "VERBOSE: Found directory $arg, searching for mkvs..." | |
| echo "VERBOSE: Adding $f" | |
| fi | |
| files+=("$f") | |
| done < <(find "$arg" -type f -name "*.mkv" ! -name "._*" | sort -V) | |
| else | |
| files+=("$arg") | |
| fi | |
| done | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No MKV files found" | |
| echo "* USAGE HINT:" | |
| echo " mkv_scan [--verbose] [--help] [path...]" | |
| echo " Try: mkv_scan --help" | |
| return 1 | |
| fi | |
| total=${#files[@]} | |
| count=0 | |
| echo "* Analyzing $total MKV file(s)..." | |
| for f in "${files[@]}"; do | |
| count=$((count + 1)) | |
| echo "==> [$count/$total] $f" | |
| json=$(mkvmerge -J "$f") | |
| # Get duration in milliseconds and convert to readable format | |
| duration_ms=$(echo "$json" | jq -r '.container.properties.duration // 0') | |
| if [[ "$duration_ms" != "0" && "$duration_ms" != "null" ]]; then | |
| # Check if duration seems to be in nanoseconds (unreasonably large) | |
| if [[ $duration_ms -gt 86400000000 ]]; then # More than 24 hours in milliseconds | |
| # Likely nanoseconds, convert to milliseconds | |
| duration_sec=$((duration_ms / 1000000000)) | |
| else | |
| # Normal milliseconds | |
| duration_sec=$((duration_ms / 1000)) | |
| fi | |
| # Sanity check - if still unreasonable, mark as unknown | |
| if [[ $duration_sec -gt 86400 ]]; then # More than 24 hours | |
| duration_str="Unknown (${duration_ms}ms)" | |
| else | |
| hours=$((duration_sec / 3600)) | |
| minutes=$(((duration_sec % 3600) / 60)) | |
| seconds=$((duration_sec % 60)) | |
| # Format as "X hours, Y minutes, Z seconds" | |
| duration_parts=() | |
| if [[ $hours -gt 0 ]]; then | |
| if [[ $hours -eq 1 ]]; then | |
| duration_parts+=("$hours hour") | |
| else | |
| duration_parts+=("$hours hours") | |
| fi | |
| fi | |
| if [[ $minutes -gt 0 ]]; then | |
| if [[ $minutes -eq 1 ]]; then | |
| duration_parts+=("$minutes minute") | |
| else | |
| duration_parts+=("$minutes minutes") | |
| fi | |
| fi | |
| # Always show seconds if no other parts, or if seconds > 0 | |
| if [[ $seconds -gt 0 ]] || [[ ${#duration_parts[@]} -eq 0 ]]; then | |
| if [[ $seconds -eq 1 ]]; then | |
| duration_parts+=("$seconds second") | |
| else | |
| duration_parts+=("$seconds seconds") | |
| fi | |
| fi | |
| # Join with commas and "and" for the last part | |
| if [[ ${#duration_parts[@]} -eq 1 ]]; then | |
| duration_str="${duration_parts[0]}" | |
| elif [[ ${#duration_parts[@]} -eq 2 ]]; then | |
| duration_str="${duration_parts[0]} and ${duration_parts[1]}" | |
| elif [[ ${#duration_parts[@]} -eq 3 ]]; then | |
| duration_str="${duration_parts[0]}, ${duration_parts[1]}, and ${duration_parts[2]}" | |
| else | |
| duration_str="Error: ${#duration_parts[@]} parts: ${duration_parts[*]}" | |
| fi | |
| fi | |
| else | |
| duration_str="Unknown" | |
| fi | |
| echo " Duration: $duration_str" | |
| # Process track data and add language names | |
| printf " %-10s %-4s %-8s %-25s %-20s %-8s %-8s %-8s\n" "Type" "ID" "Lang" "Language" "Codec" "Default" "Forced" "Enabled" | |
| echo " ----------------------------------------------------------------------------------------------" | |
| echo "$json" | jq -r ' | |
| .tracks[] | |
| | { | |
| id, | |
| type, | |
| lang: (.properties.language // "und"), | |
| codec, | |
| default: (.properties.default_track // false), | |
| forced: (.properties.forced_track // false), | |
| enabled: (.properties.enabled_track // false) | |
| } | |
| | [.type, (.id | tostring), .lang, .codec, (.default | tostring), (.forced | tostring), (.enabled | tostring)] | |
| | @tsv | |
| ' | while IFS=$'\t' read -r track_type track_id lang_code codec default_flag forced_flag enabled_flag; do | |
| # Get the language name using our function | |
| lang_name=$(get_language_name "$lang_code") | |
| printf " %-10s %-4s %-8s %-25s %-20s %-8s %-8s %-8s\n" "$track_type" "$track_id" "$lang_code" "$lang_name" "$codec" "$default_flag" "$forced_flag" "$enabled_flag" | |
| done | |
| echo | |
| done | |
| echo "* Analysis complete: $count/$total files processed" | |
| } | |
| # Find and report problematic subtitle tracks in MKV files | |
| # Flags non-SRT subtitle formats and undetermined language codes | |
| # Usage: mkv_review [--verbose] [--help] [path...] | |
| # Flags: --verbose (verbose output), --help (show usage) | |
| mkv_review() { | |
| local verbose=false | |
| local args=() | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --help) | |
| echo "* USAGE: mkv_review [--verbose] [--help] [path...]" | |
| echo "* PURPOSE:" | |
| echo " Find and report problematic subtitle tracks in MKV files." | |
| echo " Flags non-SRT subtitle formats and undetermined language codes." | |
| echo "* FLAGS:" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --help Show this help message" | |
| echo "* EXAMPLES:" | |
| echo " mkv_review # Review current directory" | |
| echo " mkv_review /path/to/videos # Review specific directory" | |
| echo " mkv_review --verbose Season1/ # Verbose review of Season1" | |
| echo " mkv_review file.mkv # Review single file" | |
| echo "* WHAT IT CHECKS:" | |
| echo " • Non-SRT subtitle formats (ASS, SSA, etc.)" | |
| echo " • Undetermined language codes (und)" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| # Set default to current directory if no args | |
| [[ ${#args[@]} -eq 0 ]] && args=(".") | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "=========================================" | |
| fi | |
| # Collect all files | |
| files=() | |
| for input in "${args[@]}"; do | |
| if [[ -d "$input" ]]; then | |
| while IFS= read -r f; do | |
| files+=("$f") | |
| done < <(find "$input" -type f -name "*.mkv" ! -name "._*" | sort -V) | |
| elif [[ "$input" == *.mkv ]]; then | |
| files+=("$input") | |
| fi | |
| done | |
| # Sort the final array naturally for deterministic processing | |
| IFS=$'\n' files=($(printf '%s\n' "${files[@]}" | sort -V)) | |
| unset IFS | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No MKV files found" | |
| echo "* USAGE HINT:" | |
| echo " mkv_review [--verbose] [--help] [path...]" | |
| echo " Try: mkv_review --help" | |
| return 1 | |
| fi | |
| total=${#files[@]} | |
| count=0 | |
| issues_found=0 | |
| echo "* Reviewing $total MKV file(s) for subtitle issues..." | |
| for file in "${files[@]}"; do | |
| count=$((count + 1)) | |
| filename=$(basename "$file") | |
| if [[ "$verbose" == true ]]; then | |
| echo "* [$count/$total] Checking: $file" | |
| fi | |
| file_has_issues=false | |
| # Check subtitle tracks | |
| mkvmerge -i "$file" | grep -i subtitles | while read -r line; do | |
| if ! echo "$line" | grep -iq 'subrip'; then | |
| if [[ "$file_has_issues" == false ]]; then | |
| echo "= [$count/$total] $filename" | |
| file_has_issues=true | |
| fi | |
| echo "! Non-SRT or malformed subtitle: $line" | |
| ((issues_found++)) | |
| break | |
| fi | |
| if echo "$line" | grep -iq 'und'; then | |
| if [[ "$file_has_issues" == false ]]; then | |
| echo "= [$count/$total] $filename" | |
| file_has_issues=true | |
| fi | |
| echo "! Undetermined language: $line" | |
| ((issues_found++)) | |
| break | |
| fi | |
| done | |
| if [[ "$verbose" == true && "$file_has_issues" == false ]]; then | |
| echo "+ No issues found" | |
| fi | |
| done | |
| if [[ $issues_found -eq 0 ]]; then | |
| echo "* Review complete: No subtitle issues found in $total file(s)" | |
| else | |
| echo "* Review complete: Found issues in $issues_found/$total file(s)" | |
| echo "* Consider using mkv_reformat_srt for format issues" | |
| fi | |
| } | |
| # Set a specific audio language as default across MKV files | |
| # Creates new files in "dirname-LangAudioDefault" directory | |
| # Usage: mkv_default_audio [--execute] [--force] [--verbose] [--lang=<code>] [--help] [path...] | |
| # Default behavior (no flags): Shows available audio languages | |
| mkv_default_audio() { | |
| local dry_run=true | |
| local force_overwrite=false | |
| local show_only=false | |
| local verbose=false | |
| local specified_language="" | |
| local args=() | |
| # Check if no flags provided (show-only mode) | |
| local has_flags=false | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute|--force|--verbose|--lang=*) | |
| has_flags=true | |
| break | |
| ;; | |
| esac | |
| done | |
| [[ "$has_flags" == false ]] && show_only=true | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute) | |
| dry_run=false | |
| ;; | |
| --force) | |
| force_overwrite=true | |
| ;; | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --lang=*) | |
| specified_language="${arg#--lang=}" | |
| ;; | |
| --help) | |
| echo "= USAGE: mkv_default_audio [--execute] [--force] [--verbose] [--lang=<code>] [--help] [path...]" | |
| echo "> PURPOSE:" | |
| echo " Set a specific audio language as default across MKV files." | |
| echo " Creates new files in 'dirname-LangAudioDefault' directory." | |
| echo "* FLAGS:" | |
| echo " --execute Apply changes (default: show available languages)" | |
| echo " --force Force overwrite existing output files" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --lang=<code> Specify language code directly (eng, jpn, spa, etc.)" | |
| echo " --help Show this help message" | |
| echo "= EXAMPLES:" | |
| echo " mkv_default_audio # Show available audio languages" | |
| echo " mkv_default_audio --execute # Set common language as default (interactive)" | |
| echo " mkv_default_audio --lang=eng --execute # Set English as default" | |
| echo " mkv_default_audio --lang=jpn --execute # Set Japanese as default" | |
| echo " mkv_default_audio --force --execute # Overwrite existing outputs" | |
| echo " mkv_default_audio --verbose Season1/ # Verbose language analysis" | |
| echo "* MODES:" | |
| echo " • Show-only: Display common audio languages (default)" | |
| echo " • Execute: Create new files with default audio set" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| # Set default to current directory if no args | |
| [[ ${#args[@]} -eq 0 ]] && args=(".") | |
| # Collect all files | |
| files=() | |
| for input in "${args[@]}"; do | |
| if [[ -d "$input" ]]; then | |
| while IFS= read -r f; do | |
| [[ "$(basename "$f")" == ._* ]] && continue | |
| files+=("$f") | |
| done < <(find "$input" -type f -name "*.mkv" ! -name "._*" | sort -V) | |
| elif [[ "$input" == *.mkv ]]; then | |
| files+=("$input") | |
| fi | |
| done | |
| # Sort the final array naturally for deterministic processing | |
| IFS=$'\n' files=($(printf '%s\n' "${files[@]}" | sort -V)) | |
| unset IFS | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No MKV files found" | |
| echo "* USAGE HINT:" | |
| echo " mkv_default_audio [--execute] [--force] [--verbose] [--help] [path...]" | |
| echo " Try: mkv_default_audio --help" | |
| return 1 | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "=======================================" | |
| fi | |
| echo "* Analyzing ${#files[@]} MKV files for audio tracks..." | |
| # Simple approach for bash 3.2 - collect all languages first | |
| all_languages="" | |
| total_files=0 | |
| for file in "${files[@]}"; do | |
| if [[ "$verbose" == true ]]; then | |
| echo "Processing: $(basename "$file")" | |
| fi | |
| json=$(mkvmerge -J "$file") | |
| file_langs=$(echo "$json" | jq -r '.tracks[] | select(.type=="audio") | .properties.language // "und"' | tr '\n' ' ') | |
| if [[ -n "$file_langs" && "$file_langs" != " " ]]; then | |
| total_files=$((total_files + 1)) | |
| all_languages="$all_languages$file_langs" | |
| fi | |
| done | |
| if [[ $total_files -eq 0 ]]; then | |
| echo "x No files found with audio tracks" | |
| return 1 | |
| fi | |
| # Find common languages by checking each unique language | |
| # Trim leading/trailing whitespace from all_languages | |
| all_languages=$(echo "$all_languages" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
| common_languages=() | |
| for lang in eng jpn spa fre deu ita por rus chi kor und; do | |
| count=$(echo "$all_languages" | tr ' ' '\n' | grep -c "^$lang$" 2>/dev/null) | |
| # Ensure count is a number (default to 0 if empty or invalid) | |
| case "$count" in | |
| ''|*[!0-9]*) count=0 ;; | |
| esac | |
| if [[ $count -ge $total_files ]]; then | |
| common_languages+=("$lang") | |
| fi | |
| done | |
| if [[ ${#common_languages[@]} -eq 0 ]]; then | |
| echo "x No common audio languages found across all files" | |
| return 1 | |
| fi | |
| echo "+ Found ${#common_languages[@]} common audio language(s): ${common_languages[*]}" | |
| # Debug: show what's actually in the array | |
| if [[ "$verbose" == true ]]; then | |
| echo "Debug: Array contents:" | |
| for ((i=0; i<${#common_languages[@]}; i++)); do | |
| echo " [$i] = '${common_languages[i]}'" | |
| done | |
| fi | |
| # If show-only mode, just display available languages and exit | |
| if [[ "$show_only" == true ]]; then | |
| echo "* Available audio languages (present in all files):" | |
| display_num=1 | |
| for ((i=1; i<=${#common_languages[@]}; i++)); do | |
| # Skip empty entries | |
| if [[ -n "${common_languages[i]}" ]]; then | |
| echo " $display_num) ${common_languages[i]}" | |
| display_num=$((display_num + 1)) | |
| fi | |
| done | |
| echo "* Usage:" | |
| echo " • Add --execute to set defaults (dry run first)" | |
| echo " • Add --force to overwrite existing files" | |
| echo " • Example: set_default_audio --execute /path/to/files" | |
| return 0 | |
| fi | |
| # Language selection logic | |
| if [[ -n "$specified_language" ]]; then | |
| # Check if specified language is available in common languages | |
| language_found=false | |
| for lang in "${common_languages[@]}"; do | |
| if [[ "$lang" == "$specified_language" ]]; then | |
| language_found=true | |
| chosen_language="$specified_language" | |
| echo "> Using specified language: $chosen_language" | |
| break | |
| fi | |
| done | |
| if [[ "$language_found" == false ]]; then | |
| echo "x Error: Language '$specified_language' is not available in all files" | |
| echo "* Available languages: ${common_languages[*]}" | |
| return 1 | |
| fi | |
| elif [[ ${#common_languages[@]} -eq 1 ]]; then | |
| chosen_language="${common_languages[0]}" | |
| echo "> Automatically selecting: $chosen_language" | |
| else | |
| # Ask user to choose | |
| echo "* Available audio languages (present in all files):" | |
| display_num=1 | |
| for ((i=0; i<=${#common_languages[@]}; i++)); do | |
| # Skip empty entries | |
| if [[ -n "${common_languages[i]}" ]]; then | |
| echo " $display_num) ${common_languages[i]}" | |
| display_num=$((display_num + 1)) | |
| fi | |
| done | |
| while true; do | |
| printf "Choose language to set as default [1-${#common_languages[@]}]: " | |
| read -r choice | |
| if [[ "$choice" =~ ^[0-9]+$ ]] && [[ $choice -ge 1 ]] && [[ $choice -le ${#common_languages[@]} ]]; then | |
| chosen_language="${common_languages[$((choice-1))]}" | |
| break | |
| else | |
| echo "x Invalid choice. Please enter a number between 1 and ${#common_languages[@]}" | |
| fi | |
| done | |
| fi | |
| echo "> Will set '$chosen_language' audio as default in ${#files[@]} file(s)" | |
| if [[ "$dry_run" == true ]]; then | |
| echo "* DRY RUN MODE - Preview of changes:" | |
| echo "==================================" | |
| for file in "${files[@]}"; do | |
| echo "= $(basename "$file")" | |
| json=$(mkvmerge -J "$file") | |
| # Show current track info and what will change | |
| echo "$json" | jq -r ' | |
| .tracks[] | | |
| select(.type=="audio") | | |
| " Track \(.id): \(.properties.language // "und") [\(.properties.default_track // false | if . then "DEFAULT" else "not default" end)]" | |
| ' | |
| # Find which track will become default | |
| new_default_id=$(echo "$json" | jq -r " | |
| .tracks[] | | |
| select(.type==\"audio\" and (.properties.language // \"und\") == \"$chosen_language\") | | |
| .id" | head -n1) | |
| if [[ -n "$new_default_id" ]]; then | |
| echo "> Track $new_default_id ($chosen_language) will become DEFAULT" | |
| fi | |
| done | |
| echo "+ Run with --execute to apply changes" | |
| echo "======================================" | |
| return 0 | |
| fi | |
| # Execute mode | |
| echo "* EXECUTE MODE - Applying changes..." | |
| echo "====================================" | |
| success_count=0 | |
| for file in "${files[@]}"; do | |
| filename=$(basename "$file") | |
| base_dir=$(basename "$(dirname "$file")") | |
| # Capitalize first letter of language for directory name (bash 3.2 compatible) | |
| first_char=$(echo "$chosen_language" | cut -c1 | tr '[:lower:]' '[:upper:]') | |
| rest_chars=$(echo "$chosen_language" | cut -c2-) | |
| lang_cap="${first_char}${rest_chars}" | |
| # Create output directory | |
| if [[ $(dirname "$file") == */* ]]; then | |
| output_dir="$(dirname "$(dirname "$file")")/${lang_cap}AudioDefault" | |
| else | |
| output_dir="${lang_cap}AudioDefault" | |
| fi | |
| mkdir -p "$output_dir" | |
| output_file="$output_dir/$filename" | |
| # Check if output already exists | |
| if [[ -f "$output_file" && "$force_overwrite" == false ]]; then | |
| echo "> Skipping $filename: output already exists (use --force to overwrite)" | |
| continue | |
| fi | |
| echo "> Processing: $filename" | |
| # Get track information | |
| json=$(mkvmerge -J "$file") | |
| # Build mkvmerge command to set defaults | |
| cmd_args=("-o" "$output_file") | |
| # Process each audio track and set default flags | |
| while IFS='|' read -r track_id track_type_found track_lang; do | |
| if [[ "$track_type_found" == "audio" ]]; then | |
| if [[ "$track_lang" == "$chosen_language" ]]; then | |
| cmd_args+=("--default-track" "${track_id}:yes") | |
| else | |
| cmd_args+=("--default-track" "${track_id}:no") | |
| fi | |
| fi | |
| done < <(echo "$json" | jq -r '.tracks[] | "\(.id)|\(.type)|\(.properties.language // "und")"') | |
| cmd_args+=("$file") | |
| # Execute mkvmerge | |
| if mkvmerge "${cmd_args[@]}" >/dev/null 2>&1; then | |
| echo "+ Created: $output_file" | |
| success_count=$((success_count + 1)) | |
| else | |
| echo "x Failed to process: $filename" | |
| fi | |
| done | |
| echo "* Summary: $success_count/${#files[@]} files processed successfully" | |
| echo "> Set '$chosen_language' audio as default" | |
| } | |
| # Set a specific subtitle language as default across MKV files | |
| # Creates new files in "dirname-LangSubsDefault" directory | |
| # Usage: mkv_default_subtitles [--execute] [--force] [--verbose] [--lang=<code>] [--help] [path...] | |
| # Default behavior (no flags): Shows available subtitle languages | |
| mkv_default_subtitles() { | |
| local dry_run=true | |
| local force_overwrite=false | |
| local show_only=false | |
| local verbose=false | |
| local specified_language="" | |
| local args=() | |
| # Check if no flags provided (show-only mode) | |
| local has_flags=false | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute|--force|--verbose|--lang=*) | |
| has_flags=true | |
| break | |
| ;; | |
| esac | |
| done | |
| [[ "$has_flags" == false ]] && show_only=true | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute) | |
| dry_run=false | |
| ;; | |
| --force) | |
| force_overwrite=true | |
| ;; | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --lang=*) | |
| specified_language="${arg#--lang=}" | |
| ;; | |
| --help) | |
| echo "= USAGE: mkv_default_subtitles [--execute] [--force] [--verbose] [--lang=<code>] [--help] [path...]" | |
| echo "> PURPOSE:" | |
| echo " Set a specific subtitle language as default across MKV files." | |
| echo " Creates new files in 'dirname-LangSubsDefault' directory." | |
| echo "* FLAGS:" | |
| echo " --execute Apply changes (default: show available languages)" | |
| echo " --force Force overwrite existing output files" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --lang=<code> Specify language code directly (eng, jpn, spa, etc.)" | |
| echo " --help Show this help message" | |
| echo "= EXAMPLES:" | |
| echo " mkv_default_subtitles # Show available subtitle languages" | |
| echo " mkv_default_subtitles --execute # Set common language as default (interactive)" | |
| echo " mkv_default_subtitles --lang=eng --execute # Set English as default" | |
| echo " mkv_default_subtitles --lang=jpn --execute # Set Japanese as default" | |
| echo " mkv_default_subtitles --force --execute # Overwrite existing outputs" | |
| echo " mkv_default_subtitles --verbose Season1/ # Verbose language analysis" | |
| echo "* MODES:" | |
| echo " • Show-only: Display common subtitle languages (default)" | |
| echo " • Execute: Create new files with default subtitles set" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| # Set default to current directory if no args | |
| [[ ${#args[@]} -eq 0 ]] && args=(".") | |
| # Collect all files | |
| files=() | |
| for input in "${args[@]}"; do | |
| if [[ -d "$input" ]]; then | |
| while IFS= read -r f; do | |
| [[ "$(basename "$f")" == ._* ]] && continue | |
| files+=("$f") | |
| done < <(find "$input" -type f -name "*.mkv" ! -name "._*" | sort -V) | |
| elif [[ "$input" == *.mkv ]]; then | |
| files+=("$input") | |
| fi | |
| done | |
| # Sort the final array naturally for deterministic processing | |
| IFS=$'\n' files=($(printf '%s\n' "${files[@]}" | sort -V)) | |
| unset IFS | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No MKV files found" | |
| echo "* USAGE HINT:" | |
| echo " mkv_default_subtitles [--execute] [--force] [--verbose] [--help] [path...]" | |
| echo " Try: mkv_default_subtitles --help" | |
| return 1 | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "========================================" | |
| fi | |
| echo "* Analyzing ${#files[@]} MKV files for subtitle tracks..." | |
| # Simple approach for bash 3.2 - collect all languages first | |
| all_languages="" | |
| total_files=0 | |
| for file in "${files[@]}"; do | |
| if [[ "$verbose" == true ]]; then | |
| echo "Processing: $(basename "$file")" | |
| fi | |
| json=$(mkvmerge -J "$file") | |
| file_langs=$(echo "$json" | jq -r '.tracks[] | select(.type=="subtitles") | .properties.language // "und"' | tr '\n' ' ') | |
| if [[ -n "$file_langs" && "$file_langs" != " " ]]; then | |
| total_files=$((total_files + 1)) | |
| all_languages="$all_languages$file_langs" | |
| fi | |
| done | |
| if [[ $total_files -eq 0 ]]; then | |
| echo "x No files found with subtitle tracks" | |
| return 1 | |
| fi | |
| # Find common languages by checking each unique language | |
| # Trim leading/trailing whitespace from all_languages | |
| all_languages=$(echo "$all_languages" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
| common_languages=() | |
| for lang in eng jpn spa fre deu ita por rus chi kor und; do | |
| count=$(echo "$all_languages" | tr ' ' '\n' | grep -c "^$lang$" 2>/dev/null) | |
| # Ensure count is a number (default to 0 if empty or invalid) | |
| case "$count" in | |
| ''|*[!0-9]*) count=0 ;; | |
| esac | |
| if [[ $count -ge $total_files ]]; then | |
| common_languages+=("$lang") | |
| fi | |
| done | |
| if [[ ${#common_languages[@]} -eq 0 ]]; then | |
| echo "x No common subtitle languages found across all files" | |
| return 1 | |
| fi | |
| echo "+ Found ${#common_languages[@]} common subtitle language(s): ${common_languages[*]}" | |
| # Debug: show what's actually in the array | |
| if [[ "$verbose" == true ]]; then | |
| echo "Debug: Array contents:" | |
| for ((i=0; i<${#common_languages[@]}; i++)); do | |
| echo " [$i] = '${common_languages[i]}'" | |
| done | |
| fi | |
| # If show-only mode, just display available languages and exit | |
| if [[ "$show_only" == true ]]; then | |
| echo "* Available subtitle languages (present in all files):" | |
| display_num=1 | |
| for ((i=1; i<=${#common_languages[@]}; i++)); do | |
| # Skip empty entries | |
| if [[ -n "${common_languages[i]}" ]]; then | |
| echo " $display_num) ${common_languages[i]}" | |
| display_num=$((display_num + 1)) | |
| fi | |
| done | |
| echo "* Usage:" | |
| echo " • Add --execute to set defaults (dry run first)" | |
| echo " • Add --force to overwrite existing files" | |
| echo " • Example: mkv_default_subtitles --execute /path/to/files" | |
| return 0 | |
| fi | |
| # Language selection logic | |
| if [[ -n "$specified_language" ]]; then | |
| # Check if specified language is available in common languages | |
| language_found=false | |
| for lang in "${common_languages[@]}"; do | |
| if [[ "$lang" == "$specified_language" ]]; then | |
| language_found=true | |
| chosen_language="$specified_language" | |
| echo "> Using specified language: $chosen_language" | |
| break | |
| fi | |
| done | |
| if [[ "$language_found" == false ]]; then | |
| echo "x Error: Language '$specified_language' is not available in all files" | |
| echo "* Available languages: ${common_languages[*]}" | |
| return 1 | |
| fi | |
| elif [[ ${#common_languages[@]} -eq 1 ]]; then | |
| chosen_language="${common_languages[0]}" | |
| echo "> Automatically selecting: $chosen_language" | |
| else | |
| # Ask user to choose | |
| echo "* Available subtitle languages (present in all files):" | |
| display_num=1 | |
| for ((i=0; i<=${#common_languages[@]}; i++)); do | |
| # Skip empty entries | |
| if [[ -n "${common_languages[i]}" ]]; then | |
| echo " $display_num) ${common_languages[i]}" | |
| display_num=$((display_num + 1)) | |
| fi | |
| done | |
| while true; do | |
| printf "Choose language to set as default [1-${#common_languages[@]}]: " | |
| read -r choice | |
| if [[ "$choice" =~ ^[0-9]+$ ]] && [[ $choice -ge 1 ]] && [[ $choice -le ${#common_languages[@]} ]]; then | |
| chosen_language="${common_languages[$((choice-1))]}" | |
| break | |
| else | |
| echo "x Invalid choice. Please enter a number between 1 and ${#common_languages[@]}" | |
| fi | |
| done | |
| fi | |
| echo "> Will set '$chosen_language' subtitles as default in ${#files[@]} file(s)" | |
| if [[ "$dry_run" == true ]]; then | |
| echo "* DRY RUN MODE - Preview of changes:" | |
| echo "=====================================" | |
| for file in "${files[@]}"; do | |
| echo "= $(basename "$file")" | |
| json=$(mkvmerge -J "$file") | |
| # Show current track info and what will change | |
| echo "$json" | jq -r ' | |
| .tracks[] | | |
| select(.type=="subtitles") | | |
| " Track \(.id): \(.properties.language // "und") [\(.properties.default_track // false | if . then "DEFAULT" else "not default" end)]" | |
| ' | |
| # Find which track will become default | |
| new_default_id=$(echo "$json" | jq -r " | |
| .tracks[] | | |
| select(.type==\"subtitles\" and (.properties.language // \"und\") == \"$chosen_language\") | | |
| .id" | head -n1) | |
| if [[ -n "$new_default_id" ]]; then | |
| echo "> Track $new_default_id ($chosen_language) will become DEFAULT" | |
| fi | |
| done | |
| echo "+ Run with --execute to apply changes" | |
| echo "==================================" | |
| return 0 | |
| fi | |
| # Execute mode | |
| echo "* EXECUTE MODE - Applying changes..." | |
| echo "====================================" | |
| success_count=0 | |
| for file in "${files[@]}"; do | |
| filename=$(basename "$file") | |
| base_dir=$(basename "$(dirname "$file")") | |
| # Capitalize first letter of language for directory name (bash 3.2 compatible) | |
| first_char=$(echo "$chosen_language" | cut -c1 | tr '[:lower:]' '[:upper:]') | |
| rest_chars=$(echo "$chosen_language" | cut -c2-) | |
| lang_cap="${first_char}${rest_chars}" | |
| # Create output directory | |
| if [[ $(dirname "$file") == */* ]]; then | |
| output_dir="$(dirname "$(dirname "$file")")/${lang_cap}SubsDefault" | |
| else | |
| output_dir="${lang_cap}SubsDefault" | |
| fi | |
| mkdir -p "$output_dir" | |
| output_file="$output_dir/$filename" | |
| # Check if output already exists | |
| if [[ -f "$output_file" && "$force_overwrite" == false ]]; then | |
| echo "> Skipping $filename: output already exists (use --force to overwrite)" | |
| continue | |
| fi | |
| echo "> Processing: $filename" | |
| # Get track information | |
| json=$(mkvmerge -J "$file") | |
| # Build mkvmerge command to set defaults | |
| cmd_args=("-o" "$output_file") | |
| # Process each subtitle track and set default flags | |
| while IFS='|' read -r track_id track_type_found track_lang; do | |
| if [[ "$track_type_found" == "subtitles" ]]; then | |
| if [[ "$track_lang" == "$chosen_language" ]]; then | |
| cmd_args+=("--default-track" "${track_id}:yes") | |
| else | |
| cmd_args+=("--default-track" "${track_id}:no") | |
| fi | |
| fi | |
| done < <(echo "$json" | jq -r '.tracks[] | "\(.id)|\(.type)|\(.properties.language // "und")"') | |
| cmd_args+=("$file") | |
| # Execute mkvmerge | |
| if mkvmerge "${cmd_args[@]}" >/dev/null 2>&1; then | |
| echo "+ Created: $output_file" | |
| success_count=$((success_count + 1)) | |
| else | |
| echo "x Failed to process: $filename" | |
| fi | |
| done | |
| echo "* Summary: $success_count/${#files[@]} files processed successfully" | |
| echo "> Set '$chosen_language' subtitles as default" | |
| } | |
| # Embed external SRT subtitle files into MKV/MP4 videos | |
| # Looks for subtitle files with pattern: basename.LANG.srt (e.g., movie.eng.srt) | |
| # Creates new files in "dirname-Subbed" directory | |
| # Usage: mkv_embed_srt [--execute] [--force] [--verbose] [--help] [path...] | |
| # Flags: --execute (apply changes), --force (overwrite existing subtitles), --verbose (verbose) | |
| mkv_embed_srt() { | |
| local dry_run=true | |
| local force_overwrite=false | |
| local verbose=false | |
| local issues=() | |
| local ready_count=0 | |
| local skip_count=0 | |
| local args=() | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute) | |
| dry_run=false | |
| ;; | |
| --force) | |
| force_overwrite=true | |
| ;; | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --help) | |
| echo "= USAGE: mkv_embed_srt [--execute] [--force] [--verbose] [--help] [path...]" | |
| echo "> PURPOSE:" | |
| echo " Embed external SRT subtitle files into MKV/MP4 videos." | |
| echo " Looks for pattern: basename.LANG.srt (e.g., movie.eng.srt)." | |
| echo " Creates new files in 'dirname-Subbed' directory." | |
| echo "* FLAGS:" | |
| echo " --execute Apply changes (default: dry run mode)" | |
| echo " --force Overwrite files with existing subtitles" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --help Show this help message" | |
| echo "= EXAMPLES:" | |
| echo " mkv_embed_srt # Dry run in current directory" | |
| echo " mkv_embed_srt --execute # Embed subtitles" | |
| echo " mkv_embed_srt --force --execute # Overwrite existing subtitles" | |
| echo " mkv_embed_srt --verbose Season1/ # Verbose dry run analysis" | |
| echo "* SUBTITLE PATTERN:" | |
| echo " movie.mkv + movie.eng.srt → Embedded English subtitles" | |
| echo " show.mp4 + show.spa.srt → Embedded Spanish subtitles" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| if [[ "$dry_run" == false ]]; then | |
| echo "* EXECUTE MODE - Files will be modified" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will overwrite files with existing subtitles" | |
| fi | |
| echo "=======================================" | |
| else | |
| echo "* DRY RUN MODE - No files will be modified" | |
| echo "Use --execute to actually perform operations" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will overwrite files with existing subtitles" | |
| fi | |
| echo "==========================================" | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "=========================================" | |
| fi | |
| trap 'echo; echo "Aborted by user."; exit 130' INT | |
| for input in "${args[@]}"; do | |
| if [[ -d "$input" ]]; then | |
| IFS=$'\n' files=($(find "$input" -type f \( -name "*.mkv" -o -name "*.mp4" \) ! -name "._*" | sort -V)) | |
| unset IFS | |
| elif [[ "$input" == *.mkv ]] || [[ "$input" == *.mp4 ]]; then | |
| files=("$input") | |
| else | |
| continue | |
| fi | |
| total=${#files[@]} | |
| count=0 | |
| for file in "${files[@]}"; do | |
| count=$((count + 1)) | |
| base="${file%.*}" | |
| # Find all subtitle files with pattern: basename.LANG.srt | |
| subtitle_files=() | |
| for srt_file in "${base}".*.srt; do | |
| if [[ -f "$srt_file" ]]; then | |
| # Extract language code from filename (e.g., movie.eng.srt -> eng) | |
| lang=$(basename "$srt_file" .srt | sed "s/$(basename "$base")\.//" ) | |
| # Validate language code (2-3 characters, letters only) | |
| if [[ "$lang" =~ ^[a-z]{2,3}$ ]]; then | |
| subtitle_files+=("$srt_file:$lang") | |
| fi | |
| fi | |
| done | |
| if [[ ${#subtitle_files[@]} -eq 0 ]]; then | |
| echo "! [$count/$total] Skipping $(basename "$file"): no subtitle files found (format: basename.LANG.srt)" | |
| if [[ "$dry_run" == true ]]; then | |
| skip_count=$((skip_count + 1)) | |
| issues+=("No subtitle files found for: $file") | |
| fi | |
| continue | |
| fi | |
| filename=$(basename "$file") | |
| base_dir="$(dirname "$file")" | |
| output_dir="$(dirname "$base_dir")/$(basename "$base_dir")-Subbed" | |
| output_file="$output_dir/$filename" | |
| # Check if file already has embedded subtitles | |
| json=$(mkvmerge -J "$file") | |
| subtitle_count=$(echo "$json" | jq '.tracks[] | select(.type=="subtitles") | .id' | wc -l) | |
| if [[ $subtitle_count -gt 0 && "$force_overwrite" == false ]]; then | |
| echo "> [$count/$total] Skipping $(basename "$file"): already has $subtitle_count subtitle track(s) (use --force to overwrite)" | |
| if [[ "$dry_run" == true ]]; then | |
| skip_count=$((skip_count + 1)) | |
| issues+=("Already has subtitles: $file") | |
| fi | |
| continue | |
| elif [[ $subtitle_count -gt 0 && "$force_overwrite" == true ]]; then | |
| echo "* [$count/$total] Overwriting $(basename "$file"): removing $subtitle_count existing subtitle track(s)" | |
| fi | |
| if [[ "$dry_run" == true ]]; then | |
| echo "= [$count/$total] Input: $file" | |
| echo "* Output: $output_file" | |
| for sub_entry in "${subtitle_files[@]}"; do | |
| srt_file="${sub_entry%%:*}" | |
| lang="${sub_entry##*:}" | |
| echo "* Subtitle ($lang): $(basename "$srt_file")" | |
| done | |
| echo "* Will preserve all video and audio tracks" | |
| if [[ $subtitle_count -gt 0 && "$force_overwrite" == true ]]; then | |
| echo "* Will replace $subtitle_count existing subtitle track(s)" | |
| fi | |
| echo "✓ Ready to process" | |
| ready_count=$((ready_count + 1)) | |
| continue | |
| fi | |
| echo "[$count/$total] $filename" | |
| mkdir -p "$output_dir" | |
| # Sanity check: ensure input exists | |
| if [[ ! -f "$file" ]]; then | |
| echo "x Input file not found: $file" | |
| continue | |
| fi | |
| # Build ffmpeg command dynamically | |
| ffmpeg_cmd=("ffmpeg" "-i" "$file") | |
| map_args=("-map" "0") # Map all streams from input video | |
| metadata_args=() | |
| # Add subtitle inputs and mappings | |
| input_index=1 | |
| for sub_entry in "${subtitle_files[@]}"; do | |
| srt_file="${sub_entry%%:*}" | |
| lang="${sub_entry##*:}" | |
| if [[ ! -f "$srt_file" ]]; then | |
| echo "x Subtitle file not found: $srt_file" | |
| continue | |
| fi | |
| ffmpeg_cmd+=("-i" "$srt_file") | |
| map_args+=("-map" "$input_index") | |
| metadata_args+=("-metadata:s:s:$((input_index-1))" "language=$lang") | |
| input_index=$((input_index + 1)) | |
| done | |
| # Execute ffmpeg with all subtitles | |
| "${ffmpeg_cmd[@]}" "${map_args[@]}" \ | |
| -c copy \ | |
| "${metadata_args[@]}" \ | |
| -y "${output_file%.*}.new.${output_file##*.}" | |
| # Replace original file if successful | |
| if [[ -f "${output_file%.*}.new.${output_file##*.}" ]]; then | |
| mv "${output_file%.*}.new.${output_file##*.}" "$output_file" | |
| echo "+ Success - embedded ${#subtitle_files[@]} subtitle track(s)" | |
| else | |
| echo "x Failed to mux subtitles for $file" | |
| continue | |
| fi | |
| done | |
| done | |
| # Show summary for dry run | |
| if [[ "$dry_run" == true ]]; then | |
| echo "==========================================" | |
| echo "* DRY RUN SUMMARY" | |
| echo "==========================================" | |
| echo "+ Ready to process: $ready_count files" | |
| echo "! Will skip: $skip_count files" | |
| echo "= Total files found: $total" | |
| if [[ ${#issues[@]} -gt 0 ]]; then | |
| echo "! DETECTED ISSUES:" | |
| for issue in "${issues[@]}"; do | |
| echo "• $issue" | |
| done | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* Some issues may be resolved by --force mode" | |
| else | |
| echo "* Fix these issues before running with --execute, or use --force to overwrite existing subtitles" | |
| fi | |
| else | |
| echo "+ No issues detected! Ready to run with --execute" | |
| fi | |
| echo "==========================================" | |
| fi | |
| trap - INT | |
| } | |
| # Remove all subtitle tracks from MKV files while preserving video and audio | |
| # Creates new files in "dirname-NoSubs" directory | |
| # Useful for cleaning files before re-embedding subtitles | |
| # Usage: mkv_remove_srt [--execute] [--force] [--verbose] [--help] [path...] | |
| # Flags: --execute (apply changes), --force (overwrite existing files), --verbose (verbose output) | |
| mkv_remove_srt() { | |
| local dry_run=true | |
| local force_overwrite=false | |
| local verbose=false | |
| local args=() | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute) | |
| dry_run=false | |
| ;; | |
| --force) | |
| force_overwrite=true | |
| ;; | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --help) | |
| echo "= USAGE: mkv_remove_srt [--execute] [--force] [--verbose] [--help] [path...]" | |
| echo "> PURPOSE:" | |
| echo " Remove all subtitle tracks from MKV files while preserving video and audio." | |
| echo " Creates new files in 'dirname-NoSubs' directory." | |
| echo " Useful for cleaning files before re-embedding subtitles." | |
| echo "* FLAGS:" | |
| echo " --execute Apply changes (default: dry run mode)" | |
| echo " --force Overwrite existing output files" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --help Show this help message" | |
| echo "= EXAMPLES:" | |
| echo " mkv_remove_srt # Dry run in current directory" | |
| echo " mkv_remove_srt --execute # Remove all subtitles" | |
| echo " mkv_remove_srt --force --execute # Overwrite existing clean files" | |
| echo " mkv_remove_srt --verbose Season1/ # Verbose dry run analysis" | |
| echo "* WORKFLOW:" | |
| echo " • Useful for cleaning before re-embedding subtitles" | |
| echo " • Preserves all video and audio tracks" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| # Set default to current directory if no args | |
| [[ ${#args[@]} -eq 0 ]] && args=(".") | |
| if [[ "$dry_run" == false ]]; then | |
| echo "* EXECUTE MODE - Files will be created" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will overwrite existing files" | |
| fi | |
| echo "=====================================" | |
| else | |
| echo "* DRY RUN MODE - No files will be created" | |
| echo "Use --execute to actually perform operations" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will overwrite existing files" | |
| fi | |
| echo "==========================================" | |
| fi | |
| # Collect all files | |
| files=() | |
| for input in "${args[@]}"; do | |
| if [[ -d "$input" ]]; then | |
| while IFS= read -r f; do | |
| [[ "$(basename "$f")" == ._* ]] && continue | |
| files+=("$f") | |
| done < <(find "$input" -type f -name "*.mkv" ! -name "._*" | sort -V) | |
| elif [[ "$input" == *.mkv ]]; then | |
| files+=("$input") | |
| fi | |
| done | |
| # Sort the final array naturally for deterministic processing | |
| IFS=$'\n' files=($(printf '%s\n' "${files[@]}" | sort -V)) | |
| unset IFS | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No MKV files found" | |
| echo "* USAGE HINT:" | |
| echo " mkv_remove_srt [--execute] [--force] [--verbose] [--help] [path...]" | |
| echo " Try: mkv_remove_srt --help" | |
| return 1 | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "=======================================" | |
| fi | |
| total=${#files[@]} | |
| count=0 | |
| success_count=0 | |
| for file in "${files[@]}"; do | |
| count=$((count + 1)) | |
| filename=$(basename "$file") | |
| # Check if file has any subtitle tracks first | |
| json=$(mkvmerge -J "$file") | |
| subtitle_count=$(echo "$json" | jq '.tracks[] | select(.type=="subtitles") | .id' | wc -l) | |
| if [[ $subtitle_count -eq 0 ]]; then | |
| echo "> [$count/$total] Skipping $filename: no subtitle tracks found" | |
| continue | |
| fi | |
| echo "> [$count/$total] $filename: found $subtitle_count subtitle track(s) to remove" | |
| # Create output path | |
| base_dir="$(basename "$(dirname "$file")")" | |
| output_dir="$(dirname "$(dirname "$file")")/${base_dir}-NoSubs" | |
| output_file="$output_dir/$filename" | |
| # Check if output already exists | |
| if [[ -f "$output_file" && "$force_overwrite" == false ]]; then | |
| echo "> Skipping: output already exists (use --force to overwrite)" | |
| continue | |
| fi | |
| # Get all audio track IDs | |
| audio_ids=$(echo "$json" | jq -r '.tracks[] | select(.type=="audio") | .id' | tr '\n' ',' | sed 's/,$//') | |
| if [[ "$dry_run" == true ]]; then | |
| echo "* Would create: $output_file" | |
| if [[ -n "$audio_ids" ]]; then | |
| echo "* Would preserve audio tracks: $audio_ids" | |
| else | |
| echo "! No audio tracks found, would copy video only" | |
| fi | |
| if [[ -f "$output_file" && "$force_overwrite" == true ]]; then | |
| echo "* Will overwrite existing file" | |
| fi | |
| else | |
| mkdir -p "$output_dir" | |
| if [[ -n "$audio_ids" ]]; then | |
| if [[ "$verbose" == true ]]; then | |
| echo "* Preserving audio tracks: $audio_ids" | |
| fi | |
| mkvmerge -o "$output_file" \ | |
| --video-tracks 0 \ | |
| --audio-tracks "$audio_ids" \ | |
| --no-subtitles \ | |
| "$file" | |
| else | |
| if [[ "$verbose" == true ]]; then | |
| echo "! No audio tracks found, copying video only" | |
| fi | |
| mkvmerge -o "$output_file" \ | |
| --video-tracks 0 \ | |
| --no-audio \ | |
| --no-subtitles \ | |
| "$file" | |
| fi | |
| if [[ -f "$output_file" ]]; then | |
| echo "+ Created: $(basename "$output_file")" | |
| success_count=$((success_count + 1)) | |
| else | |
| echo "x Failed to create: $(basename "$output_file")" | |
| fi | |
| fi | |
| done | |
| if [[ "$dry_run" == true ]]; then | |
| echo "==========================================" | |
| echo "* DRY RUN SUMMARY" | |
| echo "==========================================" | |
| echo "= Total files found: $total" | |
| echo "+ Run with --execute to remove subtitle tracks" | |
| echo "===============================================" | |
| else | |
| echo "* Summary: $success_count/$total files processed successfully" | |
| fi | |
| } | |
| # Convert ASS/SSA subtitle format to proper SRT format | |
| # Specifically processes .eng.srt files that are actually in ASS format | |
| # Creates .fixed.srt files and optionally overwrites originals | |
| # Usage: mkv_reformat_srt [--execute] [--force] [--verbose] [--help] [path] | |
| # Flags: --execute (apply changes), --force (auto-overwrite originals), --verbose (verbose output) | |
| mkv_reformat_srt() { | |
| local dry_run=true | |
| local force_overwrite=false | |
| local verbose=false | |
| local args=() | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute) | |
| dry_run=false | |
| ;; | |
| --force) | |
| force_overwrite=true | |
| ;; | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --help) | |
| echo "= USAGE: mkv_reformat_srt [--execute] [--force] [--verbose] [--help] <directory>" | |
| echo "> PURPOSE:" | |
| echo " Convert ASS/SSA subtitle format to proper SRT format." | |
| echo " Specifically processes .eng.srt files that are actually in ASS format." | |
| echo " Creates .fixed.srt files and optionally overwrites originals." | |
| echo "* FLAGS:" | |
| echo " --execute Apply changes (default: dry run mode)" | |
| echo " --force Auto-overwrite original files after conversion" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --help Show this help message" | |
| echo "= EXAMPLES:" | |
| echo " mkv_reformat_srt Season1/ # Dry run analysis" | |
| echo " mkv_reformat_srt --execute Season1/ # Convert ASS to SRT" | |
| echo " mkv_reformat_srt --force --execute Season1/ # Convert and overwrite" | |
| echo " mkv_reformat_srt --verbose Season1/ # Verbose dry run" | |
| echo "* CONVERSION:" | |
| echo " • Detects ASS/SSA format in .eng.srt files" | |
| echo " • Creates .fixed.srt with proper SRT format" | |
| echo " • Optionally replaces originals with --force" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| # Need at least one directory argument | |
| if [[ ${#args[@]} -eq 0 ]]; then | |
| echo "x Error: Please specify a directory to process" | |
| echo "* USAGE HINT:" | |
| echo " mkv_reformat_srt [--execute] [--force] [--verbose] [--help] <directory>" | |
| echo " Try: mkv_reformat_srt --help" | |
| return 1 | |
| fi | |
| season_dir="$(cd "${args[0]}" && pwd)" 2>/dev/null | |
| if [[ ! -d "$season_dir" ]]; then | |
| echo "x Error: Directory '${args[0]}' not found" | |
| return 1 | |
| fi | |
| if [[ "$dry_run" == false ]]; then | |
| echo "* EXECUTE MODE - Files will be created/modified" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will automatically overwrite original files" | |
| fi | |
| echo "===========================================" | |
| else | |
| echo "* DRY RUN MODE - No files will be modified" | |
| echo "Use --execute to actually perform operations" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will automatically overwrite original files" | |
| fi | |
| echo "==========================================" | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "==========================================" | |
| fi | |
| # Find all .eng.srt files | |
| files=() | |
| while IFS= read -r file; do | |
| files+=("$file") | |
| done < <(find "$season_dir" -type f -name "*.eng.srt" ! -name "._*") | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No .eng.srt files found in: $season_dir" | |
| return 1 | |
| fi | |
| total=${#files[@]} | |
| count=0 | |
| success_count=0 | |
| echo "� Found $total .eng.srt file(s) to process" | |
| for file in "${files[@]}"; do | |
| count=$((count + 1)) | |
| filename=$(basename "$file") | |
| outfile="${file%.srt}.fixed.srt" | |
| if [[ "$verbose" == true ]]; then | |
| echo "* [$count/$total] Processing: $file" | |
| fi | |
| echo "* [$count/$total] $filename" | |
| if [[ "$dry_run" == true ]]; then | |
| echo "* Would create: $(basename "$outfile")" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* Would automatically overwrite original after processing" | |
| else | |
| echo "* Would prompt to overwrite original files" | |
| fi | |
| else | |
| # Process the file | |
| awk ' | |
| BEGIN { count = 0 } | |
| /^\[Events\]/ { in_events = 1; next } | |
| in_events && /^Dialogue:/ { | |
| count++ | |
| n = split($0, a, ",") | |
| start = a[2]; gsub(/\./, ",", start) | |
| end = a[3]; gsub(/\./, ",", end) | |
| text = a[10] | |
| for (i = 11; i <= n; i++) text = text "," a[i] | |
| gsub(/\\N/, "\n", text) | |
| printf "%d\n%s --> %s\n%s\n\n", count, start, end, text | |
| } | |
| ' "$file" > "$outfile" | |
| if [[ -f "$outfile" ]]; then | |
| echo "+ Created: $(basename "$outfile")" | |
| success_count=$((success_count + 1)) | |
| else | |
| echo "x Failed to create: $(basename "$outfile")" | |
| fi | |
| fi | |
| done | |
| if [[ "$dry_run" == true ]]; then | |
| echo "==========================================" | |
| echo "* DRY RUN SUMMARY" | |
| echo "==========================================" | |
| echo "= Total files found: $total" | |
| echo "+ Run with --execute to reformat subtitle files" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* Force mode: originals will be automatically overwritten" | |
| else | |
| echo "* Interactive mode: will prompt to overwrite originals" | |
| fi | |
| echo "==========================================" | |
| return 0 | |
| fi | |
| echo "* Conversion Summary: $success_count/$total files processed successfully" | |
| # Handle overwriting originals | |
| if [[ $success_count -gt 0 ]]; then | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* Auto-overwriting original files..." | |
| find "$season_dir" -type f -name "*.eng.fixed.srt" | while read -r fixed; do | |
| original="${fixed%.fixed.srt}.srt" | |
| if mv "$fixed" "$original"; then | |
| echo "+ Replaced: $(basename "$original")" | |
| else | |
| echo "x Failed to replace: $(basename "$original")" | |
| fi | |
| done | |
| else | |
| printf "* Overwrite all original .eng.srt files with the .fixed.srt versions? [y/N] " | |
| read -r answer | |
| if [[ "$answer" =~ ^[Yy]$ ]]; then | |
| find "$season_dir" -type f -name "*.eng.fixed.srt" | while read -r fixed; do | |
| original="${fixed%.fixed.srt}.srt" | |
| if mv "$fixed" "$original"; then | |
| echo "+ Replaced: $(basename "$original")" | |
| else | |
| echo "x Failed to replace: $(basename "$original")" | |
| fi | |
| done | |
| else | |
| echo "Original .eng.srt files not overwritten." | |
| fi | |
| fi | |
| fi | |
| } | |
| # Extract subtitle tracks from MKV files to separate SRT files | |
| # Creates files with pattern: basename.LANG.srt | |
| # Uses mkvextract for clean subtitle extraction | |
| # Usage: mkv_extract_srt [--execute] [--force] [--verbose] [--help] [path...] | |
| # Flags: --execute (apply changes), --force (overwrite existing files), --verbose (verbose output) | |
| mkv_extract_srt() { | |
| local dry_run=true | |
| local force_overwrite=false | |
| local verbose=false | |
| local args=() | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute) | |
| dry_run=false | |
| ;; | |
| --force) | |
| force_overwrite=true | |
| ;; | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --help) | |
| echo "= USAGE: mkv_extract_srt [--execute] [--force] [--verbose] [--help] [path...]" | |
| echo "> PURPOSE:" | |
| echo " Extract subtitle tracks from MKV files to separate SRT files." | |
| echo " Creates files with pattern: basename.LANG.srt" | |
| echo " Uses mkvextract for clean subtitle extraction." | |
| echo "* FLAGS:" | |
| echo " --execute Apply changes (default: dry run mode)" | |
| echo " --force Overwrite existing SRT files" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --help Show this help message" | |
| echo "= EXAMPLES:" | |
| echo " mkv_extract_srt # Dry run in current directory" | |
| echo " mkv_extract_srt --execute # Extract all subtitles" | |
| echo " mkv_extract_srt --force --execute # Overwrite existing SRT files" | |
| echo " mkv_extract_srt --verbose Season1/ # Verbose dry run analysis" | |
| echo "* OUTPUT PATTERN:" | |
| echo " movie.mkv → movie.eng.srt, movie.spa.srt" | |
| echo " show.mkv → show.eng.srt, show.fre.srt" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| # Set default to current directory if no args | |
| [[ ${#args[@]} -eq 0 ]] && args=(".") | |
| if [[ "$dry_run" == false ]]; then | |
| echo "* EXECUTE MODE - Files will be created" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will overwrite existing SRT files" | |
| fi | |
| echo "======================================" | |
| else | |
| echo "* DRY RUN MODE - No files will be created" | |
| echo "Use --execute to actually perform operations" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will overwrite existing SRT files" | |
| fi | |
| echo "==========================================" | |
| fi | |
| # Collect all files and count | |
| files=() | |
| for input in "${args[@]}"; do | |
| if [[ -d "$input" ]]; then | |
| while IFS= read -r f; do | |
| [[ "$(basename "$f")" == ._* ]] && continue | |
| files+=("$f") | |
| done < <(find "$input" -type f -name "*.mkv" ! -name "._*" | sort -V) | |
| elif [[ "$input" == *.mkv ]]; then | |
| files+=("$input") | |
| fi | |
| done | |
| # Sort the final array naturally for deterministic processing | |
| IFS=$'\n' files=($(printf '%s\n' "${files[@]}" | sort -V)) | |
| unset IFS | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No MKV files found" | |
| echo "* USAGE HINT:" | |
| echo " mkv_extract_srt [--execute] [--force] [--verbose] [--help] [path...]" | |
| echo " Try: mkv_extract_srt --help" | |
| return 1 | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "=========================================" | |
| fi | |
| total=${#files[@]} | |
| count=0 | |
| success_count=0 | |
| for file in "${files[@]}"; do | |
| count=$((count + 1)) | |
| filename=$(basename "$file") | |
| base_name="${file%.mkv}" | |
| if [[ "$verbose" == true ]]; then | |
| echo "* [$count/$total] Processing: $file" | |
| fi | |
| json=$(mkvmerge -J "$file") | |
| # Get subtitle track info | |
| sub_ids=() | |
| while IFS= read -r line; do | |
| sub_ids+=("$line") | |
| done < <(echo "$json" | jq -r '.tracks[] | select(.type=="subtitles") | "\(.id):\(.properties.language // "und")"') | |
| if [[ ${#sub_ids[@]} -eq 0 ]]; then | |
| echo "> [$count/$total] Skipping $filename: no subtitle tracks found" | |
| continue | |
| fi | |
| echo "* [$count/$total] $filename: found ${#sub_ids[@]} subtitle track(s)" | |
| for entry in "${sub_ids[@]}"; do | |
| id="${entry%%:*}" | |
| lang="${entry##*:}" | |
| output_path="${base_name}.${lang}.srt" | |
| # Check if output already exists | |
| if [[ -f "$output_path" && "$force_overwrite" == false ]]; then | |
| echo "> Skipping track $id ($lang): output already exists (use --force to overwrite)" | |
| continue | |
| fi | |
| if [[ "$dry_run" == true ]]; then | |
| echo "* Would extract track $id ($lang) -> $(basename "$output_path")" | |
| if [[ -f "$output_path" && "$force_overwrite" == true ]]; then | |
| echo "* Will overwrite existing file" | |
| fi | |
| else | |
| echo "* Extracting track $id ($lang) -> $(basename "$output_path")" | |
| if mkvextract tracks "$file" "${id}:${output_path}" >/dev/null 2>&1; then | |
| echo "+ Success" | |
| else | |
| echo "x Failed" | |
| fi | |
| fi | |
| done | |
| if [[ "$dry_run" == false ]]; then | |
| success_count=$((success_count + 1)) | |
| fi | |
| done | |
| if [[ "$dry_run" == true ]]; then | |
| echo "==========================================" | |
| echo "* DRY RUN SUMMARY" | |
| echo "==========================================" | |
| echo "= Total files found: $total" | |
| echo "+ Run with --execute to extract subtitle tracks" | |
| echo "==========================================" | |
| else | |
| echo "* Summary: $success_count/$total files processed successfully" | |
| fi | |
| } | |
| # Extract all tracks from MKV files into separate files | |
| # Creates: basename.LANG.mp3 (audio), basename.LANG.srt (subtitles), basename.mp4 (video) | |
| # Usage: mkv_split [--execute] [--cleanup] [--verbose] [--help] [path...] | |
| # Flags: --execute (apply changes), --cleanup (remove previous outputs), --verbose (verbose output), --help (show usage) | |
| mkv_split() { | |
| local dry_run=true | |
| local cleanup=false | |
| local verbose=false | |
| local args=() | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute) | |
| dry_run=false | |
| ;; | |
| --cleanup) | |
| cleanup=true | |
| ;; | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --help) | |
| echo "= USAGE: mkv_split [--execute] [--cleanup] [--verbose] [--help] [path...]" | |
| echo "> PURPOSE:" | |
| echo " Extract all tracks from MKV files into separate files." | |
| echo " Creates: basename.LANG.mp3 (audio), basename.LANG.srt (subtitles), basename.mp4 (video)" | |
| echo "* FLAGS:" | |
| echo " --execute Apply changes (default: dry run mode)" | |
| echo " --cleanup Remove previous output files before processing" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --help Show this help message" | |
| echo "= EXAMPLES:" | |
| echo " mkv_split # Dry run in current directory" | |
| echo " mkv_split --execute # Split files" | |
| echo " mkv_split --cleanup --execute # Clean previous outputs and split" | |
| echo " mkv_split --verbose file.mkv # Verbose dry run of single file" | |
| echo "* OUTPUT PATTERN:" | |
| echo " movie.mkv → movie.eng.mp3, movie.spa.srt, movie.mp4" | |
| echo " show.mkv → show.eng.mp3, show.fre.srt, show.mp4" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| # Set default to current directory if no args | |
| [[ ${#args[@]} -eq 0 ]] && args=(".") | |
| if [[ "$dry_run" == false ]]; then | |
| echo "* EXECUTE MODE - Files will be created" | |
| if [[ "$cleanup" == true ]]; then | |
| echo "* CLEANUP MODE - Will remove previous output files" | |
| fi | |
| echo "=====================================" | |
| else | |
| echo "* DRY RUN MODE - No files will be created" | |
| echo "Use --execute to actually perform operations" | |
| if [[ "$cleanup" == true ]]; then | |
| echo "* CLEANUP MODE - Will remove previous output files" | |
| fi | |
| echo "==========================================" | |
| fi | |
| inputs=("${args[@]}") | |
| files=() | |
| for input in "${inputs[@]}"; do | |
| if [[ -d "$input" ]]; then | |
| # Recursively find all .mkv files, ignore ._* files, sort naturally | |
| while IFS= read -r f; do | |
| [[ "$(basename "$f")" == ._* ]] && continue | |
| files+=("$f") | |
| done < <(find "$input" -type f -name "*.mkv" ! -name "._*" | sort -V) | |
| elif [[ "$input" == *.mkv ]]; then | |
| files+=("$input") | |
| fi | |
| done | |
| # Sort the final array naturally for deterministic processing | |
| IFS=$'\n' files=($(printf '%s\n' "${files[@]}" | sort -V)) | |
| unset IFS | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No MKV files found" | |
| echo "* USAGE HINT:" | |
| echo " mkv_split [--execute] [--cleanup] [--verbose] [--help] [path...]" | |
| echo " Try: mkv_split --help" | |
| return 1 | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "=======================================" | |
| fi | |
| total=${#files[@]} | |
| count=0 | |
| success_count=0 | |
| for file in "${files[@]}"; do | |
| count=$((count + 1)) | |
| base="${file%.*}" | |
| # Check if file exists before processing | |
| if [[ ! -f "$file" ]]; then | |
| echo "x [$count/$total] Error: File not found: $(basename "$file")" | |
| echo " Path: $file" | |
| continue | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* [$count/$total] Processing: $file" | |
| fi | |
| echo "= [$count/$total] $(basename "$file")" | |
| # Clean up any previous outputs if requested | |
| if [[ "$cleanup" == true ]]; then | |
| if [[ "$verbose" == true ]]; then | |
| echo "* Cleaning up previous outputs for $(basename "$base")" | |
| fi | |
| if [[ "$dry_run" == false ]]; then | |
| # Set shell options for safe globbing (zsh compatibility) | |
| setopt +o nomatch 2>/dev/null || true | |
| rm -f "${base}".*.mp3 "${base}".*.srt "${base}.mp4" 2>/dev/null | |
| setopt -o nomatch 2>/dev/null || true | |
| else | |
| echo "* Would clean: ${base}.*.mp3, ${base}.*.srt, ${base}.mp4" | |
| fi | |
| fi | |
| # Get track information using mkvmerge - check if command succeeds | |
| if ! json=$(mkvmerge -J "$file" 2>&1); then | |
| echo "x Failed to read file metadata: $(basename "$file")" | |
| if [[ "$verbose" == true ]]; then | |
| echo " mkvmerge error: $json" | |
| fi | |
| continue | |
| fi | |
| # Validate JSON output | |
| if ! count_tracks=$(echo "$json" | jq '.tracks | length' 2>/dev/null); then | |
| echo "x Failed to parse metadata for: $(basename "$file")" | |
| if [[ "$verbose" == true ]]; then | |
| echo " Invalid JSON output from mkvmerge" | |
| fi | |
| continue | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* Found $count_tracks tracks in $(basename "$file")" | |
| fi | |
| for i in $(seq 0 $((count_tracks-1))); do | |
| track=$(echo "$json" | jq -c ".tracks[$i]") | |
| id=$(echo "$track" | jq -r '.id') | |
| type=$(echo "$track" | jq -r '.type') | |
| lang=$(echo "$track" | jq -r '.properties.language // "und"') | |
| if [[ "$verbose" == true ]]; then | |
| echo "> Processing track $i: id=$id type=$type lang=$lang" | |
| fi | |
| if [ "$type" = "audio" ]; then | |
| out="${base}.${lang}.mp3" | |
| if [[ "$dry_run" == true ]]; then | |
| echo "* Would extract audio track $id ($lang) -> $(basename "$out")" | |
| else | |
| if [[ "$verbose" == true ]]; then | |
| echo "* Extracting audio track $id ($lang) -> $(basename "$out")" | |
| else | |
| echo "* Extracting audio track $id ($lang) -> $(basename "$out")" | |
| fi | |
| ffmpeg -y -loglevel error -i "$file" -map 0:"$id" -vn -acodec libmp3lame "$out" 2>fferr.txt | |
| fi | |
| elif [ "$type" = "subtitles" ]; then | |
| out="${base}.${lang}.srt" | |
| if [[ "$dry_run" == true ]]; then | |
| echo "* Would extract subtitle track $id ($lang) -> $(basename "$out")" | |
| else | |
| if [[ "$verbose" == true ]]; then | |
| echo "* Extracting subtitle track $id ($lang) -> $(basename "$out")" | |
| else | |
| echo "* Extracting subtitle track $id ($lang) -> $(basename "$out")" | |
| fi | |
| ffmpeg -y -loglevel error -i "$file" -map 0:"$id" "$out" 2>fferr.txt | |
| fi | |
| elif [ "$type" = "video" ]; then | |
| out="${base}.mp4" | |
| if [[ "$dry_run" == true ]]; then | |
| echo "* Would extract video track -> $(basename "$out")" | |
| else | |
| if [[ "$verbose" == true ]]; then | |
| echo "* Extracting video track -> $(basename "$out")" | |
| else | |
| echo "* Extracting video track -> $(basename "$out")" | |
| fi | |
| ffmpeg -y -loglevel error -i "$file" -map 0:v:0 -c copy "$out" 2>fferr.txt | |
| fi | |
| else | |
| echo "! Unknown track type: $type (track $id)" | |
| continue | |
| fi | |
| if [[ "$dry_run" == false ]]; then | |
| if [[ $? -eq 0 && -f "$out" ]]; then | |
| echo "+ Success" | |
| else | |
| echo "x Failed" | |
| if [[ -f "fferr.txt" && -s "fferr.txt" ]]; then | |
| echo " Error details:" | |
| sed 's/^/ /' fferr.txt | |
| fi | |
| fi | |
| fi | |
| done | |
| if [[ "$dry_run" == false ]]; then | |
| success_count=$((success_count + 1)) | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* Completed processing: $(basename "$file")" | |
| fi | |
| done | |
| if [[ "$dry_run" == true ]]; then | |
| echo "==========================================" | |
| echo "* DRY RUN SUMMARY" | |
| echo "==========================================" | |
| echo "= Total files found: $total" | |
| echo "+ Run with --execute to extract track files" | |
| echo "==========================================" | |
| else | |
| echo "* Summary: $success_count/$total files processed successfully" | |
| fi | |
| # Clean up error log | |
| rm -f fferr.txt 2>/dev/null | |
| } | |
| # Merge separated track files back into a single MKV file | |
| # Combines basename.mp4 (video) + basename.LANG.mp3 (audio) + basename.LANG.srt (subtitles) | |
| # Creates new MKV files with proper language metadata | |
| # Usage: mkv_merge [--execute] [--force] [--verbose] [--language=LANG] [--help] [path...] | |
| # Flags: --execute (apply changes), --force (overwrite existing MKV files), --verbose (verbose), --language (set custom default) | |
| mkv_merge() { | |
| local dry_run=true | |
| local force_overwrite=false | |
| local verbose=false | |
| local default_lang="" | |
| local args=() | |
| # Parse arguments to find flags - flexible order | |
| for arg in "$@"; do | |
| case "$arg" in | |
| --execute) | |
| dry_run=false | |
| ;; | |
| --force) | |
| force_overwrite=true | |
| ;; | |
| --verbose) | |
| verbose=true | |
| ;; | |
| --language=*) | |
| default_lang="${arg#--language=}" | |
| ;; | |
| --help) | |
| echo "= USAGE: mkv_merge [--execute] [--force] [--verbose] [--language=LANG] [--help] [path...]" | |
| echo "> PURPOSE:" | |
| echo " Merge separated track files back into a single MKV file." | |
| echo " Combines basename.mp4 (video) + basename.LANG.mp3 (audio) + basename.LANG.srt (subtitles)." | |
| echo " Creates new MKV files with proper language metadata and correct default track flags." | |
| echo "* FLAGS:" | |
| echo " --execute Apply changes (default: dry run mode)" | |
| echo " --force Overwrite existing MKV files" | |
| echo " --verbose Enable verbose output with detailed processing info" | |
| echo " --language=LANG Set specific language as default (e.g., --language=jpn)" | |
| echo " --help Show this help message" | |
| echo "= EXAMPLES:" | |
| echo " mkv_merge # Dry run in current directory" | |
| echo " mkv_merge --execute # Merge with English-preferred defaults" | |
| echo " mkv_merge --language=jpn --execute # Set Japanese as default" | |
| echo " mkv_merge --force --execute # Overwrite existing MKV files" | |
| echo " mkv_merge --verbose Season1/ # Verbose dry run analysis" | |
| echo "* INPUT PATTERN:" | |
| echo " movie.mp4 + movie.eng.mp3 + movie.spa.srt → movie.mkv" | |
| echo " show.mp4 + show.eng.mp3 + show.fre.srt → show.mkv" | |
| echo "> DEFAULT TRACK LOGIC:" | |
| echo " • Video: Always marked as default" | |
| echo " • Audio: Custom language preferred, fallback to English, then first track" | |
| echo " • Subtitles: Custom language preferred, fallback to English, then first track" | |
| echo " • Ensures exactly one default track per type (MKV compliance)" | |
| return 0 | |
| ;; | |
| *) | |
| args+=("$arg") | |
| ;; | |
| esac | |
| done | |
| if [[ "$dry_run" == false ]]; then | |
| echo "* EXECUTE MODE - Files will be created" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will overwrite existing mkv files" | |
| fi | |
| echo "=====================================" | |
| else | |
| echo "* DRY RUN MODE - No files will be created" | |
| echo "Use --execute to actually perform operations" | |
| if [[ "$force_overwrite" == true ]]; then | |
| echo "* FORCE MODE - Will overwrite existing mkv files" | |
| fi | |
| echo "==========================================" | |
| fi | |
| if [[ "$verbose" == true ]]; then | |
| echo "* VERBOSE MODE - Detailed output enabled" | |
| echo "=======================================" | |
| fi | |
| # Show default language preference | |
| if [[ -n "$default_lang" ]]; then | |
| echo "> Default language preference: $default_lang" | |
| else | |
| echo "> Default language preference: English (eng/en), fallback to first track" | |
| fi | |
| inputs=("${args[@]}") | |
| [[ ${#inputs[@]} -eq 0 ]] && inputs=(".") | |
| files=() | |
| for input in "${inputs[@]}"; do | |
| if [[ -d "$input" ]]; then | |
| # Find all .mp4 files (video base files) | |
| while IFS= read -r f; do | |
| [[ "$(basename "$f")" == ._* ]] && continue | |
| files+=("$f") | |
| done < <(find "$input" -type f -name "*.mp4" ! -name "._*" | sort -V) | |
| elif [[ "$input" == *.mp4 ]]; then | |
| files+=("$input") | |
| fi | |
| done | |
| # Sort the final array naturally for deterministic processing | |
| IFS=$'\n' files=($(printf '%s\n' "${files[@]}" | sort -V)) | |
| unset IFS | |
| if [[ ${#files[@]} -eq 0 ]]; then | |
| echo "x No MP4 video files found" | |
| echo "* USAGE HINT:" | |
| echo " mkv_merge [--execute] [--force] [--verbose] [--help] [path...]" | |
| echo " Try: mkv_merge --help" | |
| return 1 | |
| fi | |
| for video_file in "${files[@]}"; do | |
| base="${video_file%.*}" | |
| output_mkv="${base}.mkv" | |
| # Check if output file already exists | |
| if [[ -f "$output_mkv" && "$force_overwrite" == false ]]; then | |
| echo "> Skipping $(basename "$base"): $output_mkv already exists (use --force to overwrite)" | |
| continue | |
| fi | |
| # Find all associated files | |
| audio_files=() | |
| subtitle_files=() | |
| # Find audio files (.mp3) | |
| for mp3_file in "${base}".*.mp3; do | |
| [[ -f "$mp3_file" ]] && audio_files+=("$mp3_file") | |
| done | |
| # Find subtitle files (.srt) | |
| for srt_file in "${base}".*.srt; do | |
| [[ -f "$srt_file" ]] && subtitle_files+=("$srt_file") | |
| done | |
| # Check if we have at least the video file | |
| if [[ ! -f "$video_file" ]]; then | |
| echo "x Video file not found: $video_file" | |
| continue | |
| fi | |
| if [[ "$dry_run" == true ]]; then | |
| echo "= Processing: $(basename "$base")" | |
| echo "* Video: $(basename "$video_file") [DEFAULT]" | |
| # Show audio tracks with smart default selection | |
| audio_default_set=false | |
| for audio in "${audio_files[@]}"; do | |
| lang=$(basename "$audio" .mp3 | sed "s/$(basename "$base")\.//" ) | |
| # Check if this track will be default (prefer custom, then English, then first) | |
| if [[ -n "$default_lang" && "$lang" == "$default_lang" && "$audio_default_set" == false ]] || | |
| [[ -z "$default_lang" && ("$lang" == "eng" || "$lang" == "en") && "$audio_default_set" == false ]] || | |
| [[ "$audio_default_set" == false && "$audio" == "${audio_files[0]}" ]]; then | |
| echo "* Audio ($lang): $(basename "$audio") [DEFAULT]" | |
| audio_default_set=true | |
| else | |
| echo "* Audio ($lang): $(basename "$audio")" | |
| fi | |
| done | |
| # Show subtitle tracks with smart default selection | |
| subtitle_default_set=false | |
| for subtitle in "${subtitle_files[@]}"; do | |
| lang=$(basename "$subtitle" .srt | sed "s/$(basename "$base")\.//" ) | |
| # Check if this track will be default (prefer custom, then English, then first) | |
| if [[ -n "$default_lang" && "$lang" == "$default_lang" && "$subtitle_default_set" == false ]] || | |
| [[ -z "$default_lang" && ("$lang" == "eng" || "$lang" == "en") && "$subtitle_default_set" == false ]] || | |
| [[ "$subtitle_default_set" == false && "$subtitle" == "${subtitle_files[0]}" ]]; then | |
| echo "* Subtitle ($lang): $(basename "$subtitle") [DEFAULT]" | |
| subtitle_default_set=true | |
| else | |
| echo "* Subtitle ($lang): $(basename "$subtitle")" | |
| fi | |
| done | |
| echo "* Output: $(basename "$output_mkv")" | |
| if [[ -f "$output_mkv" && "$force_overwrite" == true ]]; then | |
| echo "* Will overwrite existing file" | |
| fi | |
| continue | |
| fi | |
| echo "> Merging: $(basename "$base")" | |
| # Build mkvmerge command | |
| cmd_args=("-o" "$output_mkv") | |
| # Add video (always default for video tracks) | |
| cmd_args+=("--default-track" "0:yes" "$video_file") | |
| # Add audio files with language metadata and default flags | |
| # Prefer custom language, fallback to English, then first track | |
| audio_default_set=false | |
| for audio in "${audio_files[@]}"; do | |
| lang=$(basename "$audio" .mp3 | sed "s/$(basename "$base")\.//" ) | |
| # Set custom language as default if specified, otherwise English, otherwise first track | |
| if [[ -n "$default_lang" && "$lang" == "$default_lang" && "$audio_default_set" == false ]] || | |
| [[ -z "$default_lang" && ("$lang" == "eng" || "$lang" == "en") && "$audio_default_set" == false ]] || | |
| [[ "$audio_default_set" == false && "$audio" == "${audio_files[0]}" ]]; then | |
| cmd_args+=("--language" "0:$lang" "--default-track" "0:yes" "$audio") | |
| audio_default_set=true | |
| else | |
| cmd_args+=("--language" "0:$lang" "--default-track" "0:no" "$audio") | |
| fi | |
| done | |
| # Add subtitle files with language metadata and default flags | |
| # Prefer custom language, fallback to English, then first track | |
| subtitle_default_set=false | |
| for subtitle in "${subtitle_files[@]}"; do | |
| lang=$(basename "$subtitle" .srt | sed "s/$(basename "$base")\.//" ) | |
| # Set custom language as default if specified, otherwise English, otherwise first track | |
| if [[ -n "$default_lang" && "$lang" == "$default_lang" && "$subtitle_default_set" == false ]] || | |
| [[ -z "$default_lang" && ("$lang" == "eng" || "$lang" == "en") && "$subtitle_default_set" == false ]] || | |
| [[ "$subtitle_default_set" == false && "$subtitle" == "${subtitle_files[0]}" ]]; then | |
| cmd_args+=("--language" "0:$lang" "--default-track" "0:yes" "$subtitle") | |
| subtitle_default_set=true | |
| else | |
| cmd_args+=("--language" "0:$lang" "--default-track" "0:no" "$subtitle") | |
| fi | |
| done | |
| # Execute mkvmerge | |
| if mkvmerge "${cmd_args[@]}" >/dev/null 2>&1; then | |
| echo "+ Created: $(basename "$output_mkv")" | |
| else | |
| echo "x Failed to create: $(basename "$output_mkv")" | |
| if [[ "$verbose" == true ]]; then | |
| error_output=$(mkvmerge "${cmd_args[@]}" 2>&1) | |
| echo " Error: $error_output" | |
| fi | |
| fi | |
| done | |
| if [[ "$dry_run" == true ]]; then | |
| echo "===========================================" | |
| echo "* DRY RUN SUMMARY" | |
| echo "===========================================" | |
| echo "Found ${#files[@]} video file(s) to process" | |
| echo "+ Run with --execute to create mkv files" | |
| echo "===========================================" | |
| fi | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment