subsync

cli tool to synchronize srt subtitles automatically
git clone git@git.2f30.org/subsync.git
Log | Files | Refs | README | LICENSE

subsync.c (9084B)


      1 #include <stdio.h>
      2 #include <stdlib.h>
      3 #include <stdint.h>
      4 #include <inttypes.h>
      5 #include <string.h>
      6 #include <assert.h>
      7 #include <errno.h>
      8 #include <getopt.h>
      9 #include <math.h>
     10 #include "list.h"
     11 
     12 #define VERSION "0.2.0"
     13 #define SUB_MAX_BUF 1024
     14 
     15 typedef uint64_t msec_t;
     16 #define PRImsec PRIu64
     17 
     18 struct srt_sub {
     19 	msec_t start;
     20 	msec_t end;
     21 	char *position;
     22 	char text[SUB_MAX_BUF];
     23 	struct list_head list;
     24 };
     25 
     26 void init_srt_sub(struct srt_sub *sub)
     27 {
     28 	assert(sub != NULL);
     29 	sub->start = 0;
     30 	sub->end = 0;
     31 	sub->position = NULL;
     32 	sub->text[0] = '\0';
     33 	sub->list.next = NULL;
     34 	sub->list.prev = NULL;
     35 }
     36 
     37 void free_srt_sub(struct srt_sub *sub)
     38 {
     39 	assert(sub != NULL);
     40 	if (sub->position)
     41 		free(sub->position);
     42 	free(sub);
     43 }
     44 
     45 void free_srt_sub_list(struct list_head *srt_head)
     46 {
     47 	struct list_head *pos, *tmp;
     48 	struct srt_sub *sub;
     49 
     50 	assert(srt_head != NULL);
     51 
     52 	list_for_each_safe(pos, tmp, srt_head) {
     53 		sub = list_entry(pos, struct srt_sub, list);
     54 		free_srt_sub(sub);
     55 	}
     56 }
     57 
     58 void *xmalloc(size_t size)
     59 {
     60 	void *m = malloc(size);
     61 
     62 	if (m == NULL)
     63 		abort();
     64 
     65 	return m;
     66 }
     67 
     68 /* converts hh:mm:ss[,.]mss to milliseconds */
     69 int timestr_to_msec(const char *time, msec_t *msecs)
     70 {
     71 	char *tmp;
     72 	msec_t h, m, s, ms;
     73 	int res;
     74 
     75 	assert(msecs != NULL || time != NULL);
     76 
     77 	tmp = strchr(time, '.');
     78 	if (tmp != NULL)
     79 		*tmp = ',';
     80 
     81 	res = sscanf(time, "%" PRImsec ":%" PRImsec ":%" PRImsec ",%" PRImsec, &h, &m, &s, &ms);
     82 	if (res != 4 || m >= 60 || s >= 60 || ms >= 1000) {
     83 		fprintf(stderr, "Parsing error: Can not convert `%s' to milliseconds\n", time);
     84 		return -1;
     85 	}
     86 
     87 	*msecs = h * 60 * 60 * 1000;
     88 	*msecs += m * 60 * 1000;
     89 	*msecs += s * 1000;
     90 	*msecs += ms;
     91 
     92 	return 0;
     93 }
     94 
     95 /* converts milliseconds to hh:mm:ss,mss */
     96 char *msec_to_timestr(msec_t msecs, char *timestr, size_t size)
     97 {
     98 	msec_t h, m, s, ms;
     99 
    100 	assert(timestr != NULL || size == 0);
    101 
    102 	h = msecs / (60 * 60 * 1000);
    103 	msecs %= 60 * 60 * 1000;
    104 	m = msecs / (60 * 1000);
    105 	msecs %= 60 * 1000;
    106 	s = msecs / 1000;
    107 	ms = msecs % 1000;
    108 
    109 	snprintf(timestr, size, "%02" PRImsec ":%02" PRImsec ":%02" PRImsec ",%03" PRImsec,
    110 		 h, m, s, ms);
    111 
    112 	return timestr;
    113 }
    114 
    115 char *strip_eol(char *str)
    116 {
    117 	size_t i;
    118 
    119 	assert(str != NULL);
    120 
    121 	for (i = 0; str[i] != '\0'; i++) {
    122 		if (str[i] == '\n') {
    123 			str[i] = '\0';
    124 			if (i > 0 && str[i - 1] == '\r')
    125 				str[i - 1] = '\0';
    126 			return str;
    127 		}
    128 	}
    129 
    130 	return str;
    131 }
    132 
    133 /* read SubRip (srt) file */
    134 int read_srt(FILE *fin, struct list_head *srt_head)
    135 {
    136 	int state = 0;
    137 	char *s, buf[SUB_MAX_BUF];
    138 	struct srt_sub *sub = NULL;
    139 
    140 	assert(fin != NULL || srt_head != NULL);
    141 
    142 	while (1) {
    143 		s = fgets(buf, sizeof(buf), fin);
    144 		if (s == NULL)
    145 			break;
    146 
    147 		strip_eol(buf);
    148 
    149 		if (state == 0) {
    150 			/* drop empty lines */
    151 			if (buf[0] == '\0')
    152 				continue;
    153 			/* drop subtitle number */
    154 			state = 1;
    155 		} else if (state == 1) {
    156 			char start_time[20], end_time[20], position[50];
    157 			int res;
    158 
    159 			sub = xmalloc(sizeof(*sub));
    160 			init_srt_sub(sub);
    161 
    162 			/* parse start, end, and position */
    163 			res = sscanf(buf, "%19s --> %19s%49[^\n]", start_time, end_time, position);
    164 			if (res < 2) {
    165 				fprintf(stderr, "Parsing error: Wrong file format\n");
    166 				goto out_err;
    167 			}
    168 
    169 			if (res == 3)
    170 				sub->position = strdup(position);
    171 
    172 			res = timestr_to_msec(start_time, &sub->start);
    173 			if (res == -1)
    174 				goto out_err;
    175 
    176 			res = timestr_to_msec(end_time, &sub->end);
    177 			if (res == -1)
    178 				goto out_err;
    179 
    180 			state = 2;
    181 		} else if (state == 2) {
    182 			/* empty line indicates the end of the subtitle,
    183 			 * so append it to the list */
    184 			if (buf[0] == '\0') {
    185 				list_add_tail(&sub->list, srt_head);
    186 				sub = NULL;
    187 				state = 0;
    188 				continue;
    189 			}
    190 			/* save subtitle text */
    191 			strncat(sub->text, buf, sizeof(sub->text) - strlen(sub->text) - 1);
    192 			strncat(sub->text, "\r\n", sizeof(sub->text) - strlen(sub->text) - 1);
    193 		}
    194 	}
    195 
    196 	if (ferror(fin)) {
    197 		fprintf(stderr, "read: File error\n");
    198 		goto out_err;
    199 	}
    200 
    201 	return 0;
    202 
    203 out_err:
    204 	if (sub != NULL)
    205 		free_srt_sub(sub);
    206 	free_srt_sub_list(srt_head);
    207 	return -1;
    208 }
    209 
    210 /* write SubRip (srt) file */
    211 void write_srt(FILE *fout, struct list_head *srt_head)
    212 {
    213 	struct list_head *pos;
    214 	struct srt_sub *sub;
    215 	unsigned int id = 1;
    216 	char tm[20];
    217 
    218 	assert(fout != NULL || srt_head != NULL);
    219 
    220 	list_for_each(pos, srt_head) {
    221 		sub = list_entry(pos, struct srt_sub, list);
    222 		fprintf(fout, "%u\r\n", id++);
    223 		fprintf(fout, "%s", msec_to_timestr(sub->start, tm, sizeof(tm)));
    224 		fprintf(fout, " --> ");
    225 		fprintf(fout, "%s", msec_to_timestr(sub->end, tm, sizeof(tm)));
    226 		if (sub->position)
    227 			fprintf(fout, "%s", sub->position);
    228 		fprintf(fout, "\r\n%s\r\n", sub->text);
    229 	}
    230 }
    231 
    232 /* synchronize subtitles by knowing the start time of the first and the last subtitle.
    233  * to achieve this we must use the linear equation: y = mx + b */
    234 void sync_srt(struct list_head *srt_head, msec_t synced_first, msec_t synced_last)
    235 {
    236 	long double slope, yint;
    237 	msec_t desynced_first, desynced_last;
    238 	struct list_head *pos;
    239 	struct srt_sub *sub;
    240 
    241 	assert(srt_head != NULL);
    242 
    243 	desynced_first = list_first_entry(srt_head, struct srt_sub, list)->start;
    244 	desynced_last = list_last_entry(srt_head, struct srt_sub, list)->start;
    245 
    246 	/* m = (y2 - y1) / (x2 - x1)
    247 	 * m: slope
    248 	 * y2: synced_last
    249 	 * y1: synced_first
    250 	 * x2: desynced_last
    251 	 * x1: desynced_first */
    252 	slope = (long double)(synced_last - synced_first)
    253 		/ (long double)(desynced_last - desynced_first);
    254 
    255 	/* b = y - mx
    256 	 * b: yint
    257 	 * y: synced_last
    258 	 * m: slope
    259 	 * x: desynced_last */
    260 	yint = synced_last - slope * desynced_last;
    261 
    262 	list_for_each(pos, srt_head) {
    263 		sub = list_entry(pos, struct srt_sub, list);
    264 		/* y = mx + b
    265 		 * y: sub->start and sub->end
    266 		 * m: slope
    267 		 * x: sub->start and sub->end
    268 		 * b: yint */
    269 		sub->start = llroundl(slope * sub->start + yint);
    270 		sub->end = llroundl(slope * sub->end + yint);
    271 	}
    272 }
    273 
    274 void usage(void)
    275 {
    276 	fprintf(stderr, "Usage:\n");
    277 	fprintf(stderr, "  subsync [options]\n");
    278 	fprintf(stderr, "\nOptions:\n");
    279 	fprintf(stderr, "  -h, --help           Show this help\n");
    280 	fprintf(stderr, "  -f, --first-sub      Time of the first subtitle\n");
    281 	fprintf(stderr, "  -l, --last-sub       Time of the last subtitle\n");
    282 	fprintf(stderr, "  -i, --input          Input file\n");
    283 	fprintf(stderr, "  -o, --output         Output file");
    284 	fprintf(stderr, " (if not specified, it overwrites the input file)\n");
    285 	fprintf(stderr, "  -v, --version        Print version\n");
    286 	fprintf(stderr, "\nExample:\n");
    287 	fprintf(stderr, "  subsync -f 00:01:33,492 -l 01:39:23,561 -i file.srt\n");
    288 }
    289 
    290 #define FLAG_F (1 << 0)
    291 #define FLAG_L (1 << 1)
    292 
    293 int main(int argc, char *argv[])
    294 {
    295 	struct list_head subs_head;
    296 	unsigned int flags = 0;
    297 	msec_t first_ms, last_ms;
    298 	char *input_path = NULL, *output_path = NULL;
    299 	FILE *fin = stdin, *fout = stdout;
    300 	int res;
    301 
    302 	if (argc <= 1) {
    303 		usage();
    304 		return 1;
    305 	}
    306 
    307 	init_list_head(&subs_head);
    308 
    309 	while (1) {
    310 		int c, option_index;
    311 		static struct option long_options[] = {
    312 			{"help",	no_argument,		0, 'h'},
    313 			{"first-sub",	required_argument,	0, 'f'},
    314 			{"last-sub",	required_argument,	0, 'l'},
    315 			{"input",	required_argument,	0, 'i'},
    316 			{"output",	required_argument,	0, 'o'},
    317 			{"version",	required_argument,	0, 'v'},
    318 			{ 0,		0,			0,   0}
    319 		};
    320 
    321 		c = getopt_long(argc, argv, "f:l:i:o:hv", long_options, &option_index);
    322 		if (c == -1)
    323 			break;
    324 
    325 		switch (c) {
    326 		case 'h':
    327 			usage();
    328 			return 0;
    329 		case 'f':
    330 			flags |= FLAG_F;
    331 			res = timestr_to_msec(optarg, &first_ms);
    332 			if (res == -1)
    333 				return 1;
    334 			break;
    335 		case 'l':
    336 			flags |= FLAG_L;
    337 			res = timestr_to_msec(optarg, &last_ms);
    338 			if (res == -1)
    339 				return 1;
    340 			break;
    341 		case 'i':
    342 			input_path = optarg;
    343 			break;
    344 		case 'o':
    345 			output_path = optarg;
    346 			break;
    347 		case 'v':
    348 			printf("%s\n", VERSION);
    349 			return 0;
    350 		default:
    351 			return 1;
    352 		}
    353 	}
    354 
    355 	if (optind < argc) {
    356 		int i;
    357 		fprintf(stderr, "Invalid argument%s:", argc - optind > 1 ? "s" : "");
    358 		for (i = optind; i < argc; i++)
    359 			fprintf(stderr, " %s", argv[i]);
    360 		fprintf(stderr, "\n");
    361 		return 1;
    362 	}
    363 
    364 	if (input_path == NULL) {
    365 		fprintf(stderr, "You must specify an input file with -i option.\n");
    366 		return 1;
    367 	}
    368 
    369 	if (output_path == NULL)
    370 		output_path = input_path;
    371 
    372 	/* read srt file */
    373 	if (strcmp(input_path, "-") != 0)
    374 		fin = fopen(input_path, "r");
    375 
    376 	if (fin == NULL) {
    377 		fprintf(stderr, "open: %s: %s\n", input_path, strerror(errno));
    378 		return 1;
    379 	}
    380 
    381 	res = read_srt(fin, &subs_head);
    382 	fclose(fin);
    383 	if (res == -1)
    384 		return 1;
    385 
    386 	/* if user didn't pass 'f' flag, then get the time of the first subtitle */
    387 	if (!(flags & FLAG_F))
    388 		first_ms = list_first_entry(&subs_head, struct srt_sub, list)->start;
    389 
    390 	/* if user didn't pass 'l' flag, then get the time of the last subtitle */
    391 	if (!(flags & FLAG_L))
    392 		last_ms = list_last_entry(&subs_head, struct srt_sub, list)->start;
    393 
    394 	/* sync subtitles */
    395 	sync_srt(&subs_head, first_ms, last_ms);
    396 
    397 	/* write subtitles */
    398 	if (strcmp(output_path, "-") != 0)
    399 		fout = fopen(output_path, "w");
    400 
    401 	if (fout == NULL) {
    402 		fprintf(stderr, "open: %s: %s\n", output_path, strerror(errno));
    403 		free_srt_sub_list(&subs_head);
    404 		return 1;
    405 	}
    406 
    407 	write_srt(fout, &subs_head);
    408 	fclose(fout);
    409 
    410 	free_srt_sub_list(&subs_head);
    411 	return 0;
    412 }