Skip to content

Instantly share code, notes, and snippets.

@matthewmorrone
Created August 8, 2025 17:58
Show Gist options
  • Select an option

  • Save matthewmorrone/6f0330a8bfe99683c93ee1f336c55617 to your computer and use it in GitHub Desktop.

Select an option

Save matthewmorrone/6f0330a8bfe99683c93ee1f336c55617 to your computer and use it in GitHub Desktop.
helpers for working with MKVs
#!/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