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