#!/usr/bin/php [, not an important tag anyway $tmp = preg_replace('~\(([^\)]*(?:G_P|B-A|R2J|NakamaSub|720|1080|264|BD|DVD|AAC|AC3|Dual Audio|XVID|divx|[A-F0-9]{8})[^\)]*)\)~i', '[\1]', $tmp); // Work around nightmare sub group name $tmp = str_replace('[Saiyan]BrollY', 'SaiyanBrollY', $tmp); // Move tags from front to end if ($tmp[0] == '[') { $tmp = preg_replace('~^(\[.+?\])\s?([^\[]+)(.*)$~', '\2 \1 \3', $tmp); } // Cleanup $cleanup = [ '][' => ' ', '] [' => ' ', '] - [' => ' ', ' ' => ' ', ' - [' => '[', '()' => '', '( )' => '', '720x480' => '480p', '1280x720' => '720p', '1920x1080' => '1080p', 'BluRay' => 'BD', 'Blu-Ray' => 'BD', 'BDrip' => 'BD', 'bd-rip' => 'BD', 'Clean Ending' => 'NCED', 'Clean Opening' => 'NCOP', 'Creditless Opening' => 'NCOP', 'Creditless Ending' => 'NCED', 'Creditless OP' => 'NCOP', 'Creditless ED' => 'NCED', ]; $tmp = $fixed_point_iterate($tmp, function($tmp) use ($cleanup) { // Clean up tags $tmp = str_replace(array_keys($cleanup), array_values($cleanup), $tmp); // Move tags from middle to end $tmp = preg_replace('~([^\[\]]+)(\[[^\]]+?\])([^\[\]]+)~', '\1\3\2', $tmp); return $tmp; }); // Cleanup commas inside tags ( [720p,Bluray] => [720p Bluray] ) $tmp = preg_replace_callback('~\[(.+)\]~', function($matches) { return str_replace(',', ' ', $matches[0]); }, $tmp); // Remove trailing spaces before file extension $tmp = trim($tmp); //$tmp = preg_replace('~\s+(\..{3})$~', '\1', $tmp); // Episode numbers // #xx, Exx, EPxx, Ep.XX, Ep. XX, EXXv2... but not a year like 1970 or 2013 $add_episode_hyphen_if_not_year = function($matches) { $is_year = (intval($matches[1]) > 1000); if ($is_year) { return $matches[0]; // as-is } else { return ' - '.$matches[1]; } }; $tmp = preg_replace_callback('~ (?:#|E(?:P(?:\. ?)?)?)(\d[\dv]*)\b~i', $add_episode_hyphen_if_not_year, $tmp); $got_episode_number = function($tmp) { if (preg_match('~ \- S?[\d]+~', $tmp)) { return true; } // Allow "Lesson XX" (GTO format), "OVA XX", "ONA XX" if (preg_match('~ \- (?:Lesson|OVA|ONA) [\d]+~', $tmp)) { return true; } return false; }; if (! $got_episode_number($tmp)) { $tmp = preg_replace_callback('~ episode (\d+)~', function($matches) { return sprintf(' - %02d', $matches[1]); }, $tmp); } if (! $got_episode_number($tmp)) { // Last ditch - replace any lone 1+ digit number (but also support v2 and 0.5 episodes) $tmp = preg_replace_callback('~ (\d\d*(?:[v\.]\d\d?)?)\b~', $add_episode_hyphen_if_not_year, $tmp); } // Two-digit episode numbers even for 0.5 and 6.75 episodes $tmp = preg_replace('~ - (\d)\b~', ' - 0\1', $tmp); $tmp = preg_replace('~ - (\d\.\d\d?)\b~', ' - 0\1', $tmp); // Anything after the episode number but before the tag is an episode title // Enforce dash between them if it's not there already $tmp = preg_replace('~ - (\d[\dv\.]+) ([^\[\-\(])~', ' - \1 - \2', $tmp); // Fix double dashes //$tmp = str_replace('- -', '-', $tmp); // Remove unnecessary quotes $tmp = preg_replace("~ - '(.+?)' ([-\\[])~", ' - \1 \2', $tmp); // From the first [ to the last ], no other [] should appear within, and there should be no extraneous spaces $tmp = preg_replace_callback('~^([^\[]*?)\[(.+)\]~', function($matches) { return $matches[1].'['.trim(str_replace(['[', ']', ' ', ' - ', '- ', '.'], [' ', ' ', ' ', ' ', ' ', ' '], $matches[2])).']'; }, $tmp); // There's always a space before the tag $tmp = preg_replace('~([^ ])\[~', '\1 [', $tmp); // Title-case for sections (delimited by -/~) $fix_title_cases = function($str) { // First section: all words titlecase except the/and/or unless first word $titlecase = function($str) { $words = explode(" ", $str); for($i = 0; $i < count($words); ++$i) { if (strlen($words[$i]) == 0) { continue; } if ( $i == 0 || !in_array($words[$i], ['and', 'or', 'of', 'on', 'the', 'in', 'no', 'a']) ) { $words[$i][0] = strtoupper($words[$i][0]); } } return implode(" ", $words); }; // Second section: enforce first character uppercase if not already $weaktitlecase = function($str) { if (!ctype_upper($str[0])) { $str[0] = strtoupper($str[0]); } return $str; }; // Only process up to the first [ (if one exists) $spos = strpos($str, '['); $consider = substr($str, 0, $spos); $sections = preg_split('~( - | \~ )~', $consider, -1, PREG_SPLIT_DELIM_CAPTURE); #var_dump($sections); #die(); for($i = 0; $i < count($sections); $i += 2) { if (strlen($sections[$i]) == 0) { continue; } if ($i == 0) { $sections[$i] = $titlecase($sections[$i]); } else { $sections[$i] = $weaktitlecase($sections[$i]); } } return implode("", $sections).substr($str, $spos); }; $tmp = $fix_title_cases($tmp); // Special fixups: // Fixup [Saiyan]BrollY, that's a sub group $tmp = str_replace('SaiyanBrollY', '[Saiyan]BrollY', $tmp); // Fixup terrible case with [m.3.3.w] sub group being detected as an episode number // It would be preferable for this to not happen in the first place $tmp = str_replace('m 03 3 w', 'm.3.3.w', $tmp); // Fixup anime_fin, that's a sub group $tmp = str_replace('[anime fin', '[anime_fin', $tmp); // Fixup "No. 6", that's a show title $tmp = str_replace('No. - 06 - ', 'No. 6 - ', $tmp); // Fixup "Oedo 808", that's a show title $tmp = str_replace('Oedo - 808', 'Oedo 808', $tmp); // Fixup "h264-720p" $tmp = str_replace("h264-", "h264 ", $tmp); // Reinstate file extension $tmp = $tmp.$extension; // Done return $tmp; } function tests() { $tests = [ "[a-s]_you're_under_arrest_-_07_-_strike_man_~_defender_of_justice__rs2_[54AEE83C].mkv" => "You're Under Arrest - 07 - Strike man ~ Defender of justice [a-s rs2 54AEE83C].mkv" , "[a-s]_a_certain_scientific_railgun_-_17_-_tsuzuri's_summer_vacation__pball_[1080p_bd-rip][2C2AE93D].mkv" => "A Certain Scientific Railgun - 17 - Tsuzuri's summer vacation [a-s pball 1080p BD 2C2AE93D].mkv" , '[gg]_Valvrave_the_Liberator_-_12_[F9F3F5C5].mkv' => 'Valvrave the Liberator - 12 [gg F9F3F5C5].mkv' , 'Poyopoyo Kansatsu Nikki - 16 [HorribleSubs][FShahid][46B9A7B5].mkv' => 'Poyopoyo Kansatsu Nikki - 16 [HorribleSubs FShahid 46B9A7B5].mkv' , '[AHQ] Gundam Seed - 42 - Lacus Strikes.mkv' => 'Gundam Seed - 42 - Lacus Strikes [AHQ].mkv' , '[Frenchies-Mux]_True_Mazinger_Impact!_Chapter_Z_03_(720p)[7EBA0032].mkv' => 'True Mazinger Impact! Chapter Z - 03 [Frenchies-Mux 720p 7EBA0032].mkv' , '[RUELL-Next] Natsume Yuujinchou S4 EP09 (BD 1280x720 x264 AAC ASS(EN)) [BE846FE4].mkv' => 'Natsume Yuujinchou S4 - 09 [RUELL-Next BD 720p x264 AAC BE846FE4].mkv' , '[Nubles] Space Battleship Yamato 2199 (2012) episode 21 [720p 10 bit AAC][C6884514].mkv' => 'Space Battleship Yamato 2199 (2012) - 21 [Nubles 720p 10 bit AAC C6884514].mkv', '[Nubles] Space Battleship Yamato 2199 (2012) episode 8 (720p 10 bit AAC).mkv' => 'Space Battleship Yamato 2199 (2012) - 08 [Nubles 720p 10 bit AAC].mkv' , "Salaryman_Kintaro_-_15_[A-Et]_(0F83C5FF).mkv" => "Salaryman Kintaro - 15 [A-Et 0F83C5FF].mkv" , '[Elysium]Shiki.EP15(BD.720p.Hi10P.AAC)[0437F1F6].mkv' => 'Shiki - 15 [Elysium BD 720p Hi10P AAC 0437F1F6].mkv' , '[Elysium]Shiki.EP20.5(BD.720p.Hi10P.AAC)[BFE4B6BF].mkv' => 'Shiki - 20.5 [Elysium BD 720p Hi10P AAC BFE4B6BF].mkv' , '(G_P) Phoenix 03(x264)(17A173E7).mkv' => 'Phoenix - 03 [G_P x264 17A173E7].mkv' , '[Commie] Teekyuu - OVA 1 [BD 720p AAC] [58F19BF2].mkv' => 'Teekyuu - OVA 1 [Commie BD 720p AAC 58F19BF2].mkv' , '[Kametsu]_Your_Lie_in_April_11v2_[Blu-Ray][720p][Hi10][588D1B1F].mkv' => 'Your Lie in April - 11 [Kametsu v2 BD 720p Hi10 588D1B1F].mkv' , 'Patlabor.TV.38v2.(Dual.Audio).XVID.[AM].ogm' => 'Patlabor TV - 38 [v2 Dual Audio XVID AM].ogm' , 'Laputa_Castle_in_the_Sky_(1986)_[720p,BluRay,x264]_-_THORA.mkv' => 'Laputa Castle in the Sky (1986) [720p BD x264 THORA].mkv' , '[HorribleSubs] 91 Days - 7.5 [720p].mkv' => '91 Days - 07.5 [HorribleSubs 720p].mkv' , 'Code_Geass_Ep01_The_Day_the_Demon_was_Born_[720p,BluRay,x264]_-_gg-THORA.mkv' => 'Code Geass - 01 - The Day the Demon was Born [720p BD x264 gg-THORA].mkv' , '[Z-Z] Blood + Ep. 01 - First Kiss.mkv' => 'Blood + - 01 - First Kiss [Z-Z].mkv' , 'Code_Geass_Picture_Drama_6.75_[720p,BluRay,x264]_-_gg-THORA v2.mkv' => 'Code Geass Picture Drama - 06.75 [720p BD x264 gg-THORA v2].mkv' , 'Baccano - 14 (OVA).mkv' => 'Baccano - 14 (OVA).mkv' // no change expected , '[Jarzka] Cromartie High School 05 - Sentimental Bus [480p 10bit X264 DVD Dual-Audio] [A63E3F52].mkv' => 'Cromartie High School - 05 - Sentimental Bus [Jarzka 480p 10bit X264 DVD Dual-Audio A63E3F52].mkv' , 'Area 88 OVA - 01 [Blitz flawed 5EF8A0E2].mkv' => 'Area 88 OVA - 01 [Blitz flawed 5EF8A0E2].mkv' // no change expected , '[m.3.3.w]_Genshiken_Nidaime_no_Roku_-_OAD_[v0][A6EF7886].mkv' => 'Genshiken Nidaime no Roku - OAD [m.3.3.w v0 A6EF7886].mkv' , 'Gintama - 21 [Rumbel XviD 3E184082 v2].avi' => 'Gintama - 21 [Rumbel XviD 3E184082 v2].avi' // no change expected , 'Patlabor 1 (1989) [THORA].mkv' => 'Patlabor - 01 (1989) [THORA].mkv' , '[ACX]Immortal_Grand_Prix_-_01_-_Time_to_Shine_[[Saiyan]BrollY]_[7A73D794].mkv' => 'Immortal Grand Prix - 01 - Time to Shine [ACX [Saiyan]BrollY 7A73D794].mkv' , '[AnimeNOW] Danshi Koukousei no Nichijou - OP (BD 1280x720 10-bit x264 AAC) [429C5783].mkv' => 'Danshi Koukousei no Nichijou - OP [AnimeNOW BD 720p 10-bit x264 AAC 429C5783].mkv' , '(B-A)Great_Teacher_Onizuka_-_Lesson_01_(6F68CC7E).mkv' => 'Great Teacher Onizuka - Lesson 01 [B-A 6F68CC7E].mkv' , "[OZC]Mobile Suit Gundam - The 08th MS Team Blu-ray Box E01 'War for Two' [720p].mkv" => "Mobile Suit Gundam - The 08th MS Team Blu-ray Box - 01 - War for Two [OZC 720p].mkv" , 'Gundam SEED Destiny 14[AEF97E04].avi' => 'Gundam SEED Destiny - 14 [AEF97E04].avi' , 'Gundam SEED Destiny 1.avi' => 'Gundam SEED Destiny - 01.avi' , '[Exiled-Destiny]_Girls_Bravo_Ep01v3_[R2_Video]_(94D81F22).mkv' => 'Girls Bravo - 01 [Exiled-Destiny v3 R2 Video 94D81F22].mkv' , '[ACX]Immortal_Grand_Prix_-_11_-_And_Then_..._[[Saiyan]BrollY]_[50EC0CBC].mkv' => 'Immortal Grand Prix - 11 - And Then ... [ACX [Saiyan]BrollY 50EC0CBC].mkv' , '[DmonHiro] Kyousougiga #00v2 - Recap (BD, 720p) [47986C5A].mkv' => 'Kyousougiga - 00 - Recap [DmonHiro v2 BD 720p 47986C5A].mkv' , 'Legend.of.the.Galactic.Heroes.110.[x264.720p.10bit.AAC].mkv' => 'Legend of the Galactic Heroes - 110 [x264 720p 10bit AAC].mkv' , 'Minipato OVA [anime_fin 2906CE7D].avi' => 'Minipato OVA [anime_fin 2906CE7D].avi' // no change expected , '[Doki] No. 6 - NCOP (1280x720 h264 BD AAC) [0A4A8A9B].mkv' => 'No. 6 - NCOP [Doki 720p h264 BD AAC 0A4A8A9B].mkv' , '[RG Genshiken] Gintama - Creditless Ending ep.088-095, 097-099 [DVDRip 704x528 x264 PCM].mkv' => 'Gintama - NCED - 088-095, 097-099 [RG Genshiken DVDRip 704x528 x264 PCM].mkv' , 'Cyber City Oedo 808 - Art Gallery (XviD)[anibalance][4C7F6645].mkv' => 'Cyber City Oedo 808 - Art Gallery [XviD anibalance 4C7F6645].mkv' , 'Death Billiards (Anime Mirai 2013) [gg 29BE9711].mkv' => 'Death Billiards (Anime Mirai 2013) [gg 29BE9711].mkv' // no change expected , 'King of Thorn (Ibara no Ou) [Harth-QTS-v2].mkv' => 'King of Thorn (Ibara no Ou) [Harth-QTS v2].mkv' , 'Lupin III - [fong] Lupin III - Kutabare! Nostradamus [BDrip.720p.10bit.DualAudio].mkv' => 'Lupin III Lupin III - Kutabare! Nostradamus [fong BD 720p 10bit DualAudio].mkv' , "[JoJo]_Jojo's_Bizarre_Adventure_-_20_[BD][h264-720p_AAC][054C181A].mkv" => "Jojo's Bizarre Adventure - 20 [JoJo BD h264 720p AAC 054C181A].mkv" , "Ghost in the Shell Arise - 04 [THORA 1080p].mkv" => "Ghost in the Shell Arise - 04 [THORA 1080p].mkv" // no change expected , "Cowboy_Bebop_Knockin'_on_Heaven's_Door_(2001)_[1080p,BluRay,x264,flac]_-_THORA.mkv" => "Cowboy Bebop Knockin' on Heaven's Door (2001) [1080p BD x264 flac THORA].mkv" , '[EG]Turn-A_Gundam_01_V2[E8F575A6].mkv' => 'Turn-A Gundam - 01 [EG v2 E8F575A6].mkv' , 'PV2.mp4' => 'PV2.mp4' // no change expected , '[DmonHiro] Kyousougiga - Clean Ending (v2) (BD, 720p) [F1849601].mkv' => 'Kyousougiga - NCED [DmonHiro v2 BD 720p F1849601].mkv' , ]; $any_failures = false; foreach($tests as $input => $expected) { $result = fixup($input); if ($result !== $expected) { echo "Test failed\n"; echo " Input : $input \n"; echo " Got : $result \n"; echo " Expected : $expected \n"; $any_failures = true; } } if (! $any_failures) { echo "All tests passed.\n"; } return !$any_failures; } function findFilesNeedingReplacement($basedir, $recursive, $quiet) { $iterator = $recursive ? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir), RecursiveIteratorIterator::SELF_FIRST) : new DirectoryIterator($basedir) ; if (! $quiet) { echo "Scanning..."; } $ct = 0; $replacements = []; foreach ($iterator as $file) { if (! $quiet) { if (++$ct % 100 == 0) { echo "."; } } if ($file->isDir()) { continue; } $fname = $file->getFilename(); if (! preg_match("~\\.(?:mkv|mp4|avi|ogm|ogv)$~", $fname)) { continue; } $new = fixup($fname); if ($new !== $fname) { $replacements[] = [ $file->getPath(), $fname, $new ]; } } if (! $quiet) { echo "\n"; } // Alphabetic sort usort($replacements, function($a, $b) { $pc = strcasecmp($a[0], $b[0]); if ($pc !== 0) { return $pc; } return strcasecmp($a[1], $b[1]); }); return $replacements; } function displayReplacementsTable(array $replacements) { $max_length = 10; foreach($replacements as $r) { $display_width = mb_strwidth($r[1], 'UTF-8'); // strlen($r[1]) $max_length = max($max_length, $display_width); } foreach($replacements as $r) { echo sprintf("%-{$max_length}s %s\n", $r[1], $r[2]); } } function applyReplacements(array $replacements, $echo_undo_script) { $any_errors = false; if ($echo_undo_script) { echo "#!/bin/bash\nset -eu\n\n"; } foreach($replacements as $r) { $src_path = $r[0].'/'.$r[1]; $dest_path = $r[0].'/'.$r[2]; if (file_exists($dest_path)) { if ($echo_undo_script) { echo "# "; } echo "Skipping {$r[1]} (destination path already exists)\n"; $any_errors = true; continue; } rename($src_path, $dest_path); if ($echo_undo_script) { echo "mv ".escapeshellarg($dest_path)." ".escapeshellarg($src_path)."\n"; } } return $any_errors; } function usage() { echo << "; $line = trim(fgets(STDIN)); if ($line !== "y") { echo "Not applying changes.\n"; die(0); } } $any_errors = applyReplacements($replacements, $echo_undo_script); die($any_errors ? 1 : 0); } main(array_slice($_SERVER['argv'], 1));