1 /*
2  * MOC - music on console
3  * Copyright (C) 2004 - 2006 Damian Pietras <daper@daper.net>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  */
11 
12 #ifdef HAVE_CONFIG_H
13 # include "config.h"
14 #endif
15 
16 #define DEBUG
17 
18 #include <sys/file.h>
19 #include <stdio.h>
20 #include <string.h>
21 #include <strings.h>
22 #include <errno.h>
23 #include <assert.h>
24 
25 #include "common.h"
26 #include "playlist.h"
27 #include "playlist_file.h"
28 #include "log.h"
29 #include "files.h"
30 #include "options.h"
31 #include "interface.h"
32 #include "decoder.h"
33 
is_plist_file(const char * name)34 int is_plist_file (const char *name)
35 {
36 	const char *ext = ext_pos (name);
37 
38 	if (ext && (!strcasecmp(ext, "m3u") || !strcasecmp(ext, "pls")))
39 		return 1;
40 
41 	return 0;
42 }
43 
make_path(char * buf,const int buf_size,const char * cwd,char * path)44 static void make_path (char *buf, const int buf_size,
45 		const char *cwd, char *path)
46 {
47 	if (file_type(path) == F_URL) {
48 		strncpy (buf, path, buf_size);
49 		buf[buf_size-1] = 0;
50 		return;
51 	}
52 
53 	if (path[0] != '/')
54 		strcpy (buf, cwd);
55 	else
56 		strcpy (buf, "/");
57 
58 	resolve_path (buf, buf_size, path);
59 }
60 
61 /* Strip white chars from the end of a string. */
strip_string(char * str)62 static void strip_string (char *str)
63 {
64 	char *c = str;
65 	char *last_non_white = str;
66 
67 	while (*c) {
68 		if (!isblank(*c))
69 			last_non_white = c;
70 		c++;
71 	}
72 
73 	if (c > last_non_white)
74 		*(last_non_white + 1) = 0;
75 }
76 
77 /* Load M3U file into plist.  Return the number of items read. */
plist_load_m3u(struct plist * plist,const char * fname,const char * cwd,const int load_serial)78 static int plist_load_m3u (struct plist *plist, const char *fname,
79 		const char *cwd, const int load_serial)
80 {
81 	FILE *file;
82 	char *line = NULL;
83 	int last_added = -1;
84 	int after_extinf = 0;
85 	int added = 0;
86 
87 	file = fopen (fname, "r");
88 	if (!file) {
89 		error ("Can't open playlist file: %s", strerror (errno));
90 		return 0;
91 	}
92 
93 	/* Lock gets released by fclose(). */
94 	if (flock (fileno (file), LOCK_SH) == -1)
95 		logit ("Can't flock() the playlist file: %s", strerror (errno));
96 
97 	while ((line = read_line (file))) {
98 		if (!strncmp (line, "#EXTINF:", sizeof("#EXTINF:") - 1)) {
99 			char *comma, *num_err;
100 			char time_text[10] = "";
101 			int time_sec;
102 
103 			if (after_extinf) {
104 				error ("Broken M3U file: double #EXTINF!");
105 				plist_delete (plist, last_added);
106 				goto err;
107 			}
108 
109 			/* Find the comma */
110 			comma = strchr (line + (sizeof("#EXTINF:") - 1), ',');
111 			if (!comma) {
112 				error ("Broken M3U file: no comma in #EXTINF!");
113 				goto err;
114 			}
115 
116 			/* Get the time string */
117 			time_text[sizeof(time_text) - 1] = 0;
118 			strncpy (time_text, line + sizeof("#EXTINF:") - 1,
119 			         MIN(comma - line - (sizeof("#EXTINF:") - 1),
120 			         sizeof(time_text)));
121 			if (time_text[sizeof(time_text) - 1]) {
122 				error ("Broken M3U file: wrong time!");
123 				goto err;
124 			}
125 
126 			/* Extract the time. */
127 			time_sec = strtol (time_text, &num_err, 10);
128 			if (*num_err) {
129 				error ("Broken M3U file: time is not a number!");
130 				goto err;
131 			}
132 
133 			after_extinf = 1;
134 			last_added = plist_add (plist, NULL);
135 			plist_set_title_tags (plist, last_added, comma + 1);
136 
137 			if (*time_text)
138 				plist_set_item_time (plist, last_added, time_sec);
139 		}
140 		else if (line[0] != '#') {
141 			char path[2 * PATH_MAX];
142 
143 			strip_string (line);
144 			if (strlen (line) <= PATH_MAX) {
145 				make_path (path, sizeof(path), cwd, line);
146 
147 				if (plist_find_fname (plist, path) == -1) {
148 					if (after_extinf)
149 						plist_set_file (plist, last_added, path);
150 					else
151 						plist_add (plist, path);
152 					added += 1;
153 				}
154 				else if (after_extinf)
155 					plist_delete (plist, last_added);
156 			}
157 			else if (after_extinf)
158 				plist_delete (plist, last_added);
159 
160 			after_extinf = 0;
161 		}
162 		else if (load_serial &&
163 		         !strncmp (line, "#MOCSERIAL: ", sizeof("#MOCSERIAL: ") - 1)) {
164 			char *serial_str = line + sizeof("#MOCSERIAL: ") - 1;
165 
166 			if (serial_str[0]) {
167 				char *err;
168 				long serial;
169 
170 				serial = strtol (serial_str, &err, 0);
171 				if (!*err) {
172 					plist_set_serial (plist, serial);
173 					logit ("Got MOCSERIAL tag with serial %ld", serial);
174 				}
175 			}
176 		}
177 		free (line);
178 	}
179 
180 err:
181 	free (line);
182 	fclose (file);
183 	return added;
184 }
185 
186 /* Return 1 if the line contains only blank characters, 0 otherwise. */
is_blank_line(const char * l)187 static int is_blank_line (const char *l)
188 {
189 	while (*l && isblank(*l))
190 		l++;
191 
192 	if (*l)
193 		return 0;
194 	return 1;
195 }
196 
197 /* Read a value from the given section from .INI file.  File should be opened
198  * and seeking will be performed on it.  Return the malloc()ed value or NULL
199  * if not present or error occurred. */
read_ini_value(FILE * file,const char * section,const char * key)200 static char *read_ini_value (FILE *file, const char *section, const char *key)
201 {
202 	char *line = NULL;
203 	int in_section = 0;
204 	char *value = NULL;
205 	int key_len;
206 
207 	if (fseek(file, 0, SEEK_SET)) {
208 		error ("File fseek() error: %s", strerror(errno));
209 		return NULL;
210 	}
211 
212 	key_len = strlen (key);
213 
214 	while ((line = read_line(file))) {
215 		if (line[0] == '[') {
216 			if (in_section) {
217 
218 				/* we are outside of the interesting section */
219 				free (line);
220 				break;
221 			}
222 			else {
223 				char *close = strchr (line, ']');
224 
225 				if (!close) {
226 					error ("Parse error in the INI file");
227 					free (line);
228 					break;
229 				}
230 
231 				if (!strncasecmp(line + 1, section,
232 							close - line - 1))
233 					in_section = 1;
234 			}
235 		}
236 		else if (in_section && line[0] != '#' && !is_blank_line(line)) {
237 			char *t, *t2;
238 
239 			t2 = t = strchr (line, '=');
240 
241 			if (!t) {
242 				error ("Parse error in the INI file");
243 				free (line);
244 				break;
245 			}
246 
247 			/* go back to the last char in the name */
248 			while (t2 >= t && (isblank(*t2) || *t2 == '='))
249 				t2--;
250 
251 			if (t2 == t) {
252 				error ("Parse error in the INI file");
253 				free (line);
254 				break;
255 			}
256 
257 			if (!strncasecmp(line, key,
258 						MAX(t2 - line + 1, key_len))) {
259 				value = t + 1;
260 
261 				while (isblank(value[0]))
262 					value++;
263 
264 				if (value[0] == '"') {
265 					char *q = strchr (value + 1, '"');
266 
267 					if (!q) {
268 						error ("Parse error in the INI file");
269 						free (line);
270 						break;
271 					}
272 
273 					*q = 0;
274 				}
275 
276 				value = xstrdup (value);
277 				free (line);
278 				break;
279 			}
280 		}
281 
282 		free (line);
283 	}
284 
285 	return value;
286 }
287 
288 /* Load PLS file into plist. Return the number of items read. */
plist_load_pls(struct plist * plist,const char * fname,const char * cwd)289 static int plist_load_pls (struct plist *plist, const char *fname,
290 		const char *cwd)
291 {
292 	FILE *file;
293 	char *e, *line = NULL;
294 	long i, nitems, added = 0;
295 
296 	file = fopen (fname, "r");
297 	if (!file) {
298 		error ("Can't open playlist file: %s", strerror (errno));
299 		return 0;
300 	}
301 
302 	line = read_ini_value (file, "playlist", "NumberOfEntries");
303 	if (!line) {
304 
305 		/* Assume that it is a pls file version 1 - plist_load_m3u()
306 		 * should handle it like an m3u file without the m3u extensions. */
307 		fclose (file);
308 		return plist_load_m3u (plist, fname, cwd, 0);
309 	}
310 
311 	nitems = strtol (line, &e, 10);
312 	if (*e) {
313 		error ("Broken PLS file");
314 		goto err;
315 	}
316 
317 	for (i = 1; i <= nitems; i++) {
318 		int time, last_added;
319 		char *pls_file, *pls_title, *pls_length;
320 		char key[16], path[2 * PATH_MAX];
321 
322 		sprintf (key, "File%ld", i);
323 		pls_file = read_ini_value (file, "playlist", key);
324 		if (!pls_file) {
325 			error ("Broken PLS file");
326 			goto err;
327 		}
328 
329 		sprintf (key, "Title%ld", i);
330 		pls_title = read_ini_value (file, "playlist", key);
331 
332 		sprintf (key, "Length%ld", i);
333 		pls_length = read_ini_value (file, "playlist", key);
334 
335 		if (pls_length) {
336 			time = strtol (pls_length, &e, 10);
337 			if (*e)
338 				time = -1;
339 		}
340 		else
341 			time = -1;
342 
343 		if (strlen (pls_file) <= PATH_MAX) {
344 			make_path (path, sizeof(path), cwd, pls_file);
345 			if (plist_find_fname (plist, path) == -1) {
346 				last_added = plist_add (plist, path);
347 
348 				if (pls_title && pls_title[0])
349 					plist_set_title_tags (plist, last_added, pls_title);
350 
351 				if (time > 0) {
352 					plist->items[last_added].tags = tags_new ();
353 					plist->items[last_added].tags->time = time;
354 					plist->items[last_added].tags->filled |= TAGS_TIME;
355 				}
356 			}
357 		}
358 
359 		free (pls_file);
360 		if (pls_title)
361 			free (pls_title);
362 		if (pls_length)
363 			free (pls_length);
364 		added += 1;
365 	}
366 
367 err:
368 	free (line);
369 	fclose (file);
370 	return added;
371 }
372 
373 /* Load a playlist into plist. Return the number of items on the list. */
374 /* The playlist may have deleted items. */
plist_load(struct plist * plist,const char * fname,const char * cwd,const int load_serial)375 int plist_load (struct plist *plist, const char *fname, const char *cwd,
376 		const int load_serial)
377 {
378 	int num, read_tags;
379 	const char *ext;
380 
381 	read_tags = options_get_int ("ReadTags");
382 	ext = ext_pos (fname);
383 
384 	if (ext && !strcasecmp(ext, "pls"))
385 		num = plist_load_pls (plist, fname, cwd);
386 	else
387 		num = plist_load_m3u (plist, fname, cwd, load_serial);
388 
389 	if (read_tags)
390 		switch_titles_tags (plist);
391 	else
392 		switch_titles_file (plist);
393 
394 	return num;
395 }
396 
397 /* Save plist in m3u format. Strip paths by strip_path bytes.
398  * If save_serial is not 0, the playlist serial is saved in a
399  * comment. */
plist_save_m3u(struct plist * plist,const char * fname,const int strip_path,const int save_serial)400 static int plist_save_m3u (struct plist *plist, const char *fname,
401 		const int strip_path, const int save_serial)
402 {
403 	FILE *file = NULL;
404 	int i, ret, result = 0;
405 
406 	debug ("Saving playlist to '%s'", fname);
407 
408 	file = fopen (fname, "w");
409 	if (!file) {
410 		error ("Can't save playlist: %s", strerror (errno));
411 		return 0;
412 	}
413 
414 	/* Lock gets released by fclose(). */
415 	if (flock (fileno (file), LOCK_EX) == -1)
416 		logit ("Can't flock() the playlist file: %s", strerror (errno));
417 
418 	if (fprintf (file, "#EXTM3U\r\n") < 0) {
419 		error ("Error writing playlist: %s", strerror (errno));
420 		goto err;
421 	}
422 
423 	if (save_serial && fprintf (file, "#MOCSERIAL: %d\r\n",
424 	                                  plist_get_serial (plist)) < 0) {
425 		error ("Error writing playlist: %s", strerror (errno));
426 		goto err;
427 	}
428 
429 	for (i = 0; i < plist->num; i++) {
430 		if (!plist_deleted (plist, i)) {
431 
432 			/* EXTM3U */
433 			if (plist->items[i].tags)
434 				ret = fprintf (file, "#EXTINF:%d,%s\r\n",
435 						plist->items[i].tags->time,
436 						plist->items[i].title_tags ?
437 						plist->items[i].title_tags
438 						: plist->items[i].title_file);
439 			else
440 				ret = fprintf (file, "#EXTINF:%d,%s\r\n", 0,
441 						plist->items[i].title_file);
442 
443 			/* file */
444 			if (ret >= 0)
445 				ret = fprintf (file, "%s\r\n",
446 				                     plist->items[i].file + strip_path);
447 
448 			if (ret < 0) {
449 				error ("Error writing playlist: %s", strerror (errno));
450 				goto err;
451 			}
452 		}
453 	}
454 
455 	ret = fclose (file);
456 	file = NULL;
457 	if (ret)
458 		error ("Error writing playlist: %s", strerror (errno));
459 	else
460 		result = 1;
461 
462 err:
463 	if (file)
464 		fclose (file);
465 	return result;
466 }
467 
468 /* Save the playlist into the file. Return 0 on error. If cwd is NULL, use
469  * absolute paths. */
plist_save(struct plist * plist,const char * file,const char * cwd,const int save_serial)470 int plist_save (struct plist *plist, const char *file, const char *cwd,
471 		const int save_serial)
472 {
473 	char common_path[PATH_MAX+1];
474 
475 	/* FIXME: check if it possible to just add some directories to make
476 	 * relative path working. */
477 	return plist_save_m3u (plist, file, cwd && !strcmp(common_path, cwd) ?
478 			strlen(common_path) : 0, save_serial);
479 }
480