srthax.pl 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use Data::Dumper;
  5. use File::Slurp;
  6. use Getopt::Mixed;
  7. use constant VERSION => '1.0.1';
  8. sub printem {
  9. my $obj = shift;
  10. return if(!defined($obj));
  11. my $reftype = ref($obj);
  12. if($reftype eq 'ARRAY') {
  13. print Dumper(@{$obj});
  14. }
  15. elsif($reftype eq 'HASH') {
  16. print Dumper(%{$obj});
  17. }
  18. else {
  19. print $obj;
  20. }
  21. print "\r\n";
  22. }
  23. sub fuckoff {
  24. my $msg = shift;
  25. printem($msg) if(defined($msg) && length($msg));
  26. exit(1);
  27. }
  28. sub printem_halp {
  29. printem_version();
  30. printem('This shit allows you to correct the timestamps within subtitle files (.srt and .ass only lol) ;;];]];');
  31. printem('As a b0nus, it also converts the subtitle files to use Linux-style line endings, cuz many are uploaded with Wind0ngs ones instead');
  32. printem('It also shits out a bunch of warnings to help ya find inconsistencies within em subs y0');
  33. printem('');
  34. printem('Usage: srthax [options] <time shift> <subtitle file(s)>');
  35. printem('');
  36. printem('Time shift is in seconds and supports a resolution of 1 ms (+0.001) as well as negative numbahs');
  37. printem('Options:');
  38. printem(' --halp, --help, -h');
  39. printem(' --version');
  40. printem('');
  41. printem(' --overwrite, -o');
  42. printem(' Update the files in-place, otherwise they\'re saved as <basename>-hax.<extension>');
  43. printem(' --start [HH:]MM:SS[,ms], -s [HH:]MM:SS[,ms]');
  44. printem(' Only modify subtitles from this point on (inclusive), hours and milliseconds are optional');
  45. printem(' Keep in mind that [,ms] is the literal amount of milliseconds, so 01:23,45 would be: 1 minute, 23 seconds, 45 milliseconds (*not* 450 ms, i.e. it\'s not a decimal)');
  46. printem(' --verbose, -v');
  47. }
  48. sub printem_version {
  49. printem('srthax v' . VERSION);
  50. }
  51. sub checkem_timestring_srt {
  52. my $timestr = shift || '';
  53. if($timestr !~ /^([+-])?(?:(\d{2}):)?(?:(\d{2}))(?::(\d{2}))(?:,(\d{1,3}))?$/) {
  54. return undef;
  55. }
  56. my $sign = $1 || '';
  57. my $h = $2;
  58. my $m = $3;
  59. my $s = $4;
  60. my $ms = $5;
  61. return gib_ms($timestr, $sign, $h, $m, $s, $ms, 0);
  62. }
  63. sub checkem_timestring_ass {
  64. my $timestr = shift || '';
  65. if($timestr !~ /^([+-])?(?:(\d{1}):)?(?:(\d{2}))(?::(\d{2}))(?:.(\d{1,2}))?$/) {
  66. return undef;
  67. }
  68. my $sign = $1 || '';
  69. my $h = $2;
  70. my $m = $3;
  71. my $s = $4;
  72. my $ms = (defined($5) ? ($5 * 10) : 0);
  73. return gib_ms($timestr, $sign, $h, $m, $s, $ms, 0);
  74. }
  75. sub gib_ms {
  76. my ($timestr, $sign, $match_h, $match_m, $match_s, $match_ms, $fatal) = @_;
  77. my $h = 0;
  78. my $m = 0;
  79. my $s = 0;
  80. my $ms = 0;
  81. $sign = '' if($sign ne '-');
  82. # No checks required, although idk how .srt and .ass files work with hours beyond 24 xd
  83. if(defined($match_h)) {
  84. $h = ($match_h * (60 * 60 * 1000));
  85. }
  86. # Even though minutes and seconds are required, let's just keep the code style consistent for all of em ;]
  87. if(defined($match_m)) {
  88. if($m >= 60) {
  89. if($fatal) {
  90. fuckoff("[ERROR] Ayy invalid timestring y0: $timestr (minutes can't be equal to or greater than 60 fam)");
  91. }
  92. printem("[WARN] Ayy invalid timestring y0: $timestr (minutes can't be equal to or greater than 60 fam)");
  93. return undef;
  94. }
  95. $m = ($match_m * (60 * 1000));
  96. }
  97. if(defined($match_s)) {
  98. if($s >= 60) {
  99. if($fatal) {
  100. fuckoff("[ERROR] Ayy invalid timestring y0: $timestr (seconds can't be equal to or greater than 60 fam)");
  101. }
  102. printem("[WARN] Ayy invalid timestring y0: $timestr (seconds can't be equal to or greater than 60 fam)");
  103. return undef;
  104. }
  105. $s = ($match_s * 1000);
  106. }
  107. if(defined($match_ms)) {
  108. if($ms >= 1000) {
  109. if($fatal) {
  110. fuckoff("[ERROR] Ayy invalid timestring y0: $timestr (milliseconds can't be equal to or greater than 1000 fam)");
  111. }
  112. printem("[WARN] Ayy invalid timestring y0: $timestr (milliseconds can't be equal to or greater than 1000 fam)");
  113. return undef;
  114. }
  115. $ms = $match_ms;
  116. }
  117. return sprintf('%s%d', $sign, ($h + $m + $s + $ms));
  118. }
  119. sub gib_timestring_srt {
  120. my ($ms, $timeshift_ms) = @_;
  121. if(!defined($ms) || !defined($timeshift_ms) || $ms !~ /^[+-]?\d+$/ || $timeshift_ms !~ /^[+-]?\d+$/) {
  122. return undef;
  123. }
  124. my $sign = '';
  125. $ms += $timeshift_ms;
  126. if($ms < 0.0) {
  127. $sign = '-';
  128. $ms = abs($ms);
  129. }
  130. my $s = int($ms / 1000);
  131. $ms = ($ms % 1000);
  132. my $m = int($s / 60);
  133. $s = ($s % 60);
  134. my $h = int($m / 60);
  135. $m = ($m % 60);
  136. return sprintf('%s%02d:%02d:%02d,%03d', $sign, $h, $m, $s, $ms);
  137. }
  138. sub gib_timestring_ass {
  139. my ($ms, $timeshift_ms) = @_;
  140. if(!defined($ms) || !defined($timeshift_ms) || $ms !~ /^[+-]?\d+$/ || $timeshift_ms !~ /^[+-]?\d+$/) {
  141. return undef;
  142. }
  143. my $sign = '';
  144. $ms += $timeshift_ms;
  145. if($ms < 0.0) {
  146. $sign = '-';
  147. $ms = abs($ms);
  148. }
  149. my $s = int($ms / 1000);
  150. $ms = ($ms % 1000);
  151. my $m = int($s / 60);
  152. $s = ($s % 60);
  153. my $h = int($m / 60);
  154. $m = ($m % 60);
  155. return sprintf('%s%01d:%02d:%02d.%02d', $sign, $h, $m, $s, ($ms / 10));
  156. }
  157. sub parseline_srt {
  158. my ($opt_verbose, $opt_start_ms, $subpath, $timeshift_ms, $i, $line, $has_linenumber_ref, $has_timestamps_ref, $lines_new_ref) = @_;
  159. # First should be the line number
  160. if($line =~ /^\d+$/) {
  161. if(${$has_linenumber_ref}) {
  162. # Duplicate number lol (without shit inbetween etc)
  163. if($opt_verbose) {
  164. printem("[WARN] Found successive line numbers (around line $i)");
  165. }
  166. else {
  167. printem("[WARN] Found successive line numbers for file: $subpath (around line $i)");
  168. }
  169. }
  170. ${$has_linenumber_ref} = 1;
  171. push(@{$lines_new_ref}, $line);
  172. return;
  173. }
  174. if($line =~ /^([+-]?[\d:,]+) --> ([+-]?[\d:,]+)$/) {
  175. my $match_timestamp_start = $1;
  176. my $match_timestamp_end = $2;
  177. if(${$has_timestamps_ref}) {
  178. if($opt_verbose) {
  179. printem("[WARN] Found successive time stamps (around line $i)");
  180. }
  181. else {
  182. printem("[WARN] Found successive time stamps for file: $subpath (around line $i)");
  183. }
  184. }
  185. ${$has_timestamps_ref} = 1;
  186. my ($timestamp_start_ms, $timestamp_end_ms);
  187. $timestamp_start_ms = checkem_timestring_srt($match_timestamp_start);
  188. if(!defined($timestamp_start_ms)) {
  189. if($opt_verbose) {
  190. printem("[WARN] Invalid time string (around line $i)");
  191. }
  192. else {
  193. printem("[WARN] Invalid time string for file: $subpath (around line $i)");
  194. }
  195. push(@{$lines_new_ref}, $line);
  196. return;
  197. }
  198. $timestamp_end_ms = checkem_timestring_srt($match_timestamp_end);
  199. if(!defined($timestamp_end_ms)) {
  200. if($opt_verbose) {
  201. printem("[WARN] Invalid time string (around line $i)");
  202. }
  203. else {
  204. printem("[WARN] Invalid time string for file: $subpath (around line $i)");
  205. }
  206. push(@{$lines_new_ref}, $line);
  207. return;
  208. }
  209. my $timestamp_start = gib_timestring_srt($timestamp_start_ms, $timeshift_ms);
  210. if(!defined($timestamp_start)) {
  211. if($opt_verbose) {
  212. printem("[WARN] Invalid resulting start timestring '$match_timestamp_start' (around line $i)");
  213. }
  214. else {
  215. printem("[WARN] Invalid resulting start timestring '$match_timestamp_start' for file: $subpath (around line $i)");
  216. }
  217. push(@{$lines_new_ref}, $line);
  218. return;
  219. }
  220. my $timestamp_end = gib_timestring_srt($timestamp_end_ms, $timeshift_ms);
  221. if(!defined($timestamp_end)) {
  222. if($opt_verbose) {
  223. printem("[WARN] Invalid resulting end timestring '$match_timestamp_end' (around line $i)");
  224. }
  225. else {
  226. printem("[WARN] Invalid resulting end timestring '$match_timestamp_end' for file: $subpath (around line $i)");
  227. }
  228. push(@{$lines_new_ref}, $line);
  229. return;
  230. }
  231. if($opt_start_ms > 0 && $timestamp_start_ms < $opt_start_ms) {
  232. push(@{$lines_new_ref}, $line);
  233. return;
  234. }
  235. push(@{$lines_new_ref}, "$timestamp_start --> $timestamp_end");
  236. return;
  237. }
  238. # Anything else should be all-text, or empty for separating lines/paragraphs
  239. if(!${$has_linenumber_ref} || !${$has_timestamps_ref}) {
  240. # Obviously we need the other 2 variables first ;]
  241. if($opt_verbose) {
  242. printem("[WARN] Apparently reached text section but found no preceeding timestamps or line number (around line $i)");
  243. }
  244. else {
  245. printem("[WARN] Apparently reached text section but found no preceeding timestamps or line number for file: $subpath (around line $i)");
  246. }
  247. }
  248. if(length($line) == 0) {
  249. ${$has_linenumber_ref} = 0;
  250. ${$has_timestamps_ref} = 0;
  251. }
  252. push(@{$lines_new_ref}, $line);
  253. }
  254. sub parseline_ass {
  255. my ($opt_verbose, $opt_start_ms, $subpath, $timeshift_ms, $i, $line, $has_events_ref, $has_eventformat_ref, $lines_new_ref) = @_;
  256. # Need to find the part where the actual lines start lmao
  257. if(!${$has_events_ref}) {
  258. if($line =~ /^\[Events\]/i) {
  259. ${$has_events_ref} = 1;
  260. }
  261. push(@{$lines_new_ref}, $line);
  262. return;
  263. }
  264. if(!${$has_eventformat_ref}) {
  265. if($line =~ /^Format:/i) {
  266. ${$has_eventformat_ref} = 1;
  267. }
  268. push(@{$lines_new_ref}, $line);
  269. return;
  270. }
  271. # Ayy here we g0 =]
  272. if($line =~ /^(?:[^:]+:\s*)?[^,]*,([\d:.]+),([\d:.]+)/) {
  273. my $match_timestamp_start = $1;
  274. my $match_timestamp_end = $2;
  275. my ($timestamp_start_ms, $timestamp_end_ms);
  276. $timestamp_start_ms = checkem_timestring_ass($match_timestamp_start);
  277. if(!defined($timestamp_start_ms)) {
  278. if($opt_verbose) {
  279. printem("[WARN] Invalid time string (around line $i)");
  280. }
  281. else {
  282. printem("[WARN] Invalid time string for file: $subpath (around line $i)");
  283. }
  284. push(@{$lines_new_ref}, $line);
  285. return;
  286. }
  287. $timestamp_end_ms = checkem_timestring_ass($match_timestamp_end);
  288. if(!defined($timestamp_end_ms)) {
  289. if($opt_verbose) {
  290. printem("[WARN] Invalid time string (around line $i)");
  291. }
  292. else {
  293. printem("[WARN] Invalid time string for file: $subpath (around line $i)");
  294. }
  295. push(@{$lines_new_ref}, $line);
  296. return;
  297. }
  298. my $timestamp_start = gib_timestring_ass($timestamp_start_ms, $timeshift_ms);
  299. if(!defined($timestamp_start)) {
  300. if($opt_verbose) {
  301. printem("[WARN] Invalid resulting start timestring '$match_timestamp_start' (around line $i)");
  302. }
  303. else {
  304. printem("[WARN] Invalid resulting start timestring '$match_timestamp_start' for file: $subpath (around line $i)");
  305. }
  306. push(@{$lines_new_ref}, $line);
  307. return;
  308. }
  309. my $timestamp_end = gib_timestring_ass($timestamp_end_ms, $timeshift_ms);
  310. if(!defined($timestamp_end)) {
  311. if($opt_verbose) {
  312. printem("[WARN] Invalid resulting end timestring '$match_timestamp_end' (around line $i)");
  313. }
  314. else {
  315. printem("[WARN] Invalid resulting end timestring '$match_timestamp_end' for file: $subpath (around line $i)");
  316. }
  317. push(@{$lines_new_ref}, $line);
  318. return;
  319. }
  320. if($opt_start_ms > 0 && $timestamp_start_ms < $opt_start_ms) {
  321. push(@{$lines_new_ref}, $line);
  322. return;
  323. }
  324. $line =~ s/$match_timestamp_start,$match_timestamp_end/$timestamp_start,$timestamp_end/;
  325. push(@{$lines_new_ref}, $line);
  326. return;
  327. }
  328. # Anything else should belong to some other section
  329. #if(length($line) == 0 || $line =~ /^\[/) {
  330. if($line =~ /^\[/) {
  331. ${$has_events_ref} = 0;
  332. ${$has_eventformat_ref} = 0;
  333. }
  334. push(@{$lines_new_ref}, $line);
  335. }
  336. sub mainlol {
  337. Getopt::Mixed::init(
  338. # *n0* leading spaec for the first wun lmao
  339. 'halp help>halp h>halp' .
  340. ' overwrite o>overwrite' .
  341. ' start=s s>start' . # Only check shit from this point on (HH:MM:SS)
  342. ' verbose v>verbose' .
  343. ' version' .
  344. ''); # Ending with '' here so every line above can end with a period ;];];];]];;]
  345. my ($opt_overwrite, $opt_start_ms, $opt_verbose);
  346. while(my($opt, $val) = Getopt::Mixed::nextOption()) {
  347. OPTION: {
  348. $opt eq 'halp' && do {
  349. printem_halp();
  350. fuckoff();
  351. };
  352. $opt eq 'version' && do {
  353. printem_version();
  354. fuckoff();
  355. };
  356. $opt eq 'overwrite' && do {
  357. $opt_overwrite = 1;
  358. last OPTION;
  359. };
  360. $opt eq 'start' && do {
  361. # The srt timestamp format seems most natural, so let's use that for parsing the user-passed start time ;]
  362. $opt_start_ms = checkem_timestring_srt($val);
  363. if(!defined($opt_start_ms)) {
  364. fuckoff("[ERROR] Ayy invalid time string y0: $val");
  365. }
  366. last OPTION;
  367. };
  368. $opt eq 'verbose' && do {
  369. $opt_verbose = 1;
  370. last OPTION;
  371. };
  372. }
  373. }
  374. Getopt::Mixed::cleanup();
  375. $opt_start_ms = 0 if(!defined($opt_start_ms));
  376. if(scalar(@ARGV) < 2) {
  377. fuckoff('[ERROR] Not enough arguments: time shift value and subtitle files are required y0 (use --halp to see the full description)');
  378. }
  379. my $arg_timeshift = shift(@ARGV);
  380. if($arg_timeshift !~ /^([+-])?(\d+)(?:\.(\d{1,3}))?$/) {
  381. fuckoff("[ERROR] Invalid time shift value: $arg_timeshift (must be of the format [+|-]SS[.ms])");
  382. }
  383. my $timeshift_sign = $1 || '';
  384. my $match_timeshift_s = $2;
  385. my $match_timeshift_ms = $3;
  386. if(defined($match_timeshift_ms)) {
  387. $match_timeshift_ms .= '0' x (3 - length($match_timeshift_ms));
  388. }
  389. my $timeshift_ms = gib_ms($arg_timeshift, $timeshift_sign, undef, undef, $match_timeshift_s, $match_timeshift_ms, 1);
  390. if($timeshift_ms == 0.0) {
  391. fuckoff("[ERROR] Invalid time shift value: $arg_timeshift (must be non-zero)");
  392. }
  393. my $sub_count = 0;
  394. CHECKEM_SUB: foreach my $subpath(@ARGV) {
  395. printem('') if($opt_verbose && $sub_count > 0);
  396. if(! -e $subpath) {
  397. printem("[ERROR] Subtitle path doesn't exist: $subpath");
  398. next;
  399. }
  400. if(! -f $subpath) {
  401. printem("[ERROR] Subtitle path is not a regular file: $subpath");
  402. next;
  403. }
  404. if($subpath !~ /\.(srt|ass)$/i) {
  405. printem("[ERROR] Ayy not een .srt or .ass file l0l: $subpath");
  406. next;
  407. }
  408. my $is_srt = ($1 eq 'srt');
  409. my $is_ass = ($1 eq 'ass');
  410. printem("Ayy checkin: $subpath") if($opt_verbose);
  411. my @lines = ();
  412. eval {
  413. my $data = read_file($subpath);
  414. @lines = split(/^/m, $data);
  415. };
  416. if($@) {
  417. if($opt_verbose) {
  418. # Already printed the path ab0ve when in verb0se m00d ;]
  419. printem("[ERROR] Got error while reading em file, skipping em: $@");
  420. next;
  421. }
  422. else {
  423. printem("[ERROR] Got error while reading em file, skipping em ($subpath): $@");
  424. next;
  425. }
  426. }
  427. my ($has_linenumber, $has_timestamps); # For .srt
  428. my ($has_events, $has_eventformat); # For .ass
  429. my @lines_new = ();
  430. for(my $i = 1; $i <= scalar(@lines); $i++) {
  431. # Not doing a foreach lewp cuz we may need to store the indices of entries to rem0ve lol
  432. # Also starting with $i = 1 for ez printing [=[[===[=[
  433. my $line = $lines[$i - 1];
  434. $line =~ s/[\r\n\x00]+//g; # Sometimes that shit may contain null bytes, fuck off pls
  435. if($is_srt) {
  436. parseline_srt($opt_verbose, $opt_start_ms, $subpath, $timeshift_ms, $i, $line, \$has_linenumber, \$has_timestamps, \@lines_new);
  437. }
  438. elsif($is_ass) {
  439. parseline_ass($opt_verbose, $opt_start_ms, $subpath, $timeshift_ms, $i, $line, \$has_events, \$has_eventformat, \@lines_new);
  440. }
  441. }
  442. if(!scalar(@lines_new)) {
  443. if($opt_verbose) {
  444. printem("[WARN] Ayy no new data to write lol, skipping em file");
  445. }
  446. else {
  447. printem("[WARN] Ayy no new data to write lol, skipping em file ($subpath)");
  448. }
  449. next;
  450. }
  451. # Always use the -hax file as tempfile for writing, then move it over the existing file if --overwrite is specified ;]
  452. my $new_subpath = $subpath;
  453. $new_subpath =~ s/(\.[A-Za-z]+)$/-hax$1/;
  454. if($opt_verbose) {
  455. if($opt_overwrite) {
  456. printem("Ayy writing tempfile: $new_subpath");
  457. }
  458. else {
  459. printem("Ayy writing new file: $new_subpath");
  460. }
  461. }
  462. eval {
  463. @lines = write_file($new_subpath, (join("\n", @lines_new) . "\n"));
  464. };
  465. if($@) {
  466. if($opt_verbose) {
  467. # Already printed the path ab0ve when in verb0se m00d ;]
  468. printem("[ERROR] Got error while writing em tempfile, skipping em: $@");
  469. next;
  470. }
  471. else {
  472. printem("[ERROR] Got error while writing em tempfile, skipping em ($subpath): $@");
  473. next;
  474. }
  475. }
  476. if($opt_overwrite) {
  477. if($opt_verbose) {
  478. printem("Ayy renaming tempfile to original");
  479. }
  480. if(!rename($new_subpath, $subpath)) {
  481. if($opt_verbose) {
  482. # Already printed the path ab0ve when in verb0se m00d ;]
  483. printem("[ERROR] Unable to rename em tempfile, original subtitle file may or may not be fucked lol");
  484. next;
  485. }
  486. else {
  487. printem("[ERROR] Unable to rename em tempfile, original subtitle file may or may not be fucked lol ($subpath)");
  488. next;
  489. }
  490. }
  491. }
  492. if($opt_verbose) {
  493. printem("Ayy dunz0");
  494. }
  495. $sub_count++;
  496. }
  497. printem('') if($opt_verbose);
  498. printem("Successfully br0cessed $sub_count subtitle file(s) l0l");
  499. }
  500. mainlol();