1 /*
2 M3U and PLS playlist plugin for DeaDBeeF Player
3 Copyright (C) 2009-2014 Alexey Yakovenko
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17
18 2. Altered source versions must be plainly marked as such, and must not be
19 misrepresented as being the original software.
20
21 3. This notice may not be removed or altered from any source distribution.
22 */
23
24 #ifdef HAVE_CONFIG_H
25 # include "../../config.h"
26 #endif
27 #include <string.h>
28 #include <stdlib.h>
29 #include <limits.h>
30 #include <math.h> // for ceil
31
32 #include "../../deadbeef.h"
33
34 //#define trace(...) { fprintf(stderr, __VA_ARGS__); }
35 #define trace(fmt,...)
36
37 #define min(x,y) ((x)<(y)?(x):(y))
38 #define max(x,y) ((x)>(y)?(x):(y))
39
40 static DB_functions_t *deadbeef;
41
42 static const uint8_t *
skipspaces(const uint8_t * p,const uint8_t * end)43 skipspaces (const uint8_t *p, const uint8_t *end) {
44 while (p < end && *p <= ' ') {
45 p++;
46 }
47 return p;
48 }
49
50 static DB_playItem_t *
load_m3u(ddb_playlist_t * plt,DB_playItem_t * after,const char * fname,int * pabort,int (* cb)(DB_playItem_t * it,void * data),void * user_data)51 load_m3u (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname, int *pabort, int (*cb)(DB_playItem_t *it, void *data), void *user_data) {
52 const char *slash = strrchr (fname, '/');
53 trace ("enter pl_insert_m3u\n");
54 // skip all empty lines and comments
55 DB_FILE *fp = deadbeef->fopen (fname);
56 if (!fp) {
57 trace ("failed to open file %s\n", fname);
58 return NULL;
59 }
60 int sz = deadbeef->fgetlength (fp);
61 trace ("loading m3u...\n");
62 uint8_t *membuffer = malloc (sz);
63 if (!membuffer) {
64 deadbeef->fclose (fp);
65 trace ("failed to allocate %d bytes to read the file %s\n", sz, fname);
66 return NULL;
67 }
68 uint8_t *buffer = membuffer;
69 deadbeef->fread (buffer, 1, sz, fp);
70 deadbeef->fclose (fp);
71
72 if (sz >= 3 && buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf) {
73 buffer += 3;
74 sz -= 3;
75 }
76 int line = 0;
77 int read_extm3u = 0;
78
79 const uint8_t *p = buffer;
80 const uint8_t *end = buffer+sz;
81 const uint8_t *e;
82 int length = -1;
83 char title[1000] = "";
84 char artist[1000] = "";
85 while (p < end) {
86 line++;
87 p = skipspaces (p, end);
88 if (p >= end) {
89 break;
90 }
91 if (*p == '#') {
92 if (line == 1) {
93 if (end - p >= 7 && !strncmp (p, "#EXTM3U", 7)) {
94 read_extm3u = 1;
95 }
96 }
97 else if (read_extm3u) {
98 if (end - p >= 8 && !strncmp (p, "#EXTINF:", 8)) {
99 length = -1;
100 memset (title, 0, sizeof (title));
101 memset (artist, 0, sizeof (artist));
102 p += 8;
103 e = p;
104 while (e < end && *e >= 0x20) {
105 e++;
106 }
107 int n = e-p;
108 uint8_t nm[n+1];
109 memcpy (nm, p, n);
110 nm[n] = 0;
111 length = atoi (nm);
112 char *c = nm;
113 while (*c && *c != ',') {
114 c++;
115 }
116 if (*c == ',') {
117 c++;
118 const char *dash = NULL;
119 const char *newdash = strstr (c, " - ");
120
121 while (newdash) {
122 dash = newdash;
123 newdash = strstr (newdash+3, " - ");
124 }
125
126 if (dash) {
127 strncpy (title, dash+3, sizeof (title)-1);
128 title[sizeof(title)-1] = 0;
129 int l = dash - c;
130 strncpy (artist, c, min(l, sizeof (artist)));
131 artist[sizeof(artist)-1] = 0;
132 }
133 else {
134 strncpy (title, c, sizeof (title)-1);
135 title[sizeof(title)-1] = 0;
136 }
137 trace ("title: %s, artist: %s\n", title, artist);
138 }
139 }
140 }
141 while (p < end && *p >= 0x20) {
142 p++;
143 }
144 if (p >= end) {
145 break;
146 }
147 continue;
148 }
149 e = p;
150 while (e < end && *e >= 0x20) {
151 e++;
152 }
153 int n = e-p;
154 uint8_t nm[n+1];
155 memcpy (nm, p, n);
156 nm[n] = 0;
157
158 if (title[0]) {
159 const char *cs = deadbeef->junk_detect_charset (title);
160 if (cs) {
161 char tmp[1000];
162 if (deadbeef->junk_iconv (title, strlen (title), tmp, sizeof (tmp), cs, "utf-8") >= 0) {
163 strcpy (title, tmp);
164 }
165 }
166 }
167 if (artist[0]) {
168 const char *cs = deadbeef->junk_detect_charset (artist);
169 if (cs) {
170 char tmp[1000];
171 if (deadbeef->junk_iconv (artist, strlen (artist), tmp, sizeof (tmp), cs, "utf-8") >= 0) {
172 strcpy (artist, tmp);
173 }
174 }
175 }
176
177 DB_playItem_t *it = NULL;
178 int is_fullpath = 0;
179 if (nm[0] == '/') {
180 is_fullpath = 1;
181 }
182 else {
183 uint8_t *p = strstr (nm, "://");
184 if (p) {
185 p--;
186 while (p >= nm) {
187 if (*p < 'a' && *p > 'z') {
188 break;
189 }
190 p--;
191 }
192 if (p < nm) {
193 is_fullpath = 1;
194 }
195 }
196 }
197 if (is_fullpath) { // full path
198 trace ("pl_insert_m3u: adding file %s\n", nm);
199 it = deadbeef->plt_insert_file2 (0, plt, after, nm, pabort, cb, user_data);
200 if (it) {
201 if (length >= 0 && deadbeef->pl_get_item_duration (it) < 0) {
202 deadbeef->plt_set_item_duration (plt, it, length);
203 }
204 if (title[0]) {
205 deadbeef->pl_add_meta (it, "title", title);
206 }
207 if (artist[0]) {
208 deadbeef->pl_add_meta (it, "artist", artist);
209 }
210 }
211 // reset title/artist, to avoid them from being reused in the next track
212 memset (title, 0, sizeof (title));
213 memset (artist, 0, sizeof (artist));
214 }
215 else {
216 int l = strlen (nm);
217 char fullpath[slash - fname + l + 2];
218 memcpy (fullpath, fname, slash - fname + 1);
219 strcpy (fullpath + (slash - fname + 1), nm);
220 trace ("pl_insert_m3u: adding file %s\n", fullpath);
221 it = deadbeef->plt_insert_file2 (0, plt, after, fullpath, pabort, cb, user_data);
222 }
223 if (it) {
224 after = it;
225 }
226 if (pabort && *pabort) {
227 free (membuffer);
228 return after;
229 }
230 p = e;
231 if (p >= end) {
232 break;
233 }
234 }
235 trace ("leave pl_insert_m3u\n");
236 free (membuffer);
237 return after;
238 }
239
240 static DB_playItem_t *
pls_insert_file(ddb_playlist_t * plt,DB_playItem_t * after,const char * fname,const char * uri,int * pabort,int (* cb)(DB_playItem_t * it,void * data),void * user_data,const char * title,const char * length)241 pls_insert_file (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname, const char *uri, int *pabort, int (*cb)(DB_playItem_t *it, void *data), void *user_data, const char *title, const char *length) {
242 trace ("pls_insert_file uri: %s\n", uri);
243 trace ("pls_insert_file fname: %s\n", fname);
244 DB_playItem_t *it = NULL;
245 const char *slash = NULL;
246
247 if (strrchr (uri, '/')) {
248 trace ("pls: inserting from uri: %s\n", uri);
249 it = deadbeef->plt_insert_file2 (0, plt, after, uri, pabort, cb, user_data);
250 }
251
252 if (!it) {
253 slash = strrchr (fname, '/');
254 }
255 if (slash) {
256 int l = strlen (uri);
257 char fullpath[slash - fname + l + 2];
258 memcpy (fullpath, fname, slash - fname + 1);
259 strcpy (fullpath + (slash - fname + 1), uri);
260 trace ("pls: inserting from calculated relative path: %s\n", fullpath);
261 it = deadbeef->plt_insert_file2 (0, plt, after, fullpath, pabort, cb, user_data);
262 }
263 if (it) {
264 if (length[0]) {
265 deadbeef->plt_set_item_duration (plt, it, atoi (length));
266 }
267 if (title[0]) {
268 deadbeef->pl_add_meta (it, "title", title);
269 }
270 }
271 return it;
272 }
273
274 static DB_playItem_t *
load_pls(ddb_playlist_t * plt,DB_playItem_t * after,const char * fname,int * pabort,int (* cb)(DB_playItem_t * it,void * data),void * user_data)275 load_pls (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname, int *pabort, int (*cb)(DB_playItem_t *it, void *data), void *user_data) {
276 trace ("load_pls %s\n", fname);
277 const char *slash = strrchr (fname, '/');
278 DB_FILE *fp = deadbeef->fopen (fname);
279 if (!fp) {
280 trace ("failed to open file %s\n", fname);
281 return NULL;
282 }
283 int sz = deadbeef->fgetlength (fp);
284 deadbeef->rewind (fp);
285 uint8_t *buffer = malloc (sz);
286 if (!buffer) {
287 deadbeef->fclose (fp);
288 trace ("failed to allocate %d bytes to read the file %s\n", sz, fname);
289 return NULL;
290 }
291 deadbeef->fread (buffer, 1, sz, fp);
292 deadbeef->fclose (fp);
293 // 1st line must be "[playlist]"
294 const uint8_t *p = buffer;
295 const uint8_t *end = buffer+sz;
296 if (strncasecmp (p, "[playlist]", 10)) {
297 trace ("file %s doesn't begin with [playlist]\n", fname);
298 free (buffer);
299 return NULL;
300 }
301 p += 10;
302 p = skipspaces (p, end);
303 if (p >= end) {
304 trace ("file %s finished before numberofentries had been read\n", fname);
305 free (buffer);
306 return NULL;
307 }
308 // fetch all tracks
309 char uri[1024] = "";
310 char title[1024] = "";
311 char length[20] = "";
312 int lastidx = -1;
313 while (p < end) {
314 p = skipspaces (p, end);
315 if (p >= end) {
316 break;
317 }
318 if (end-p < 6) {
319 break;
320 }
321 const uint8_t *e;
322 int n;
323 if (!strncasecmp (p, "file", 4)) {
324 int idx = atoi (p + 4);
325 if (uri[0] && idx != lastidx && lastidx != -1) {
326 DB_playItem_t *it = pls_insert_file (plt, after, fname, uri, pabort, cb, user_data, title, length);
327 if (it) {
328 after = it;
329 }
330 if (pabort && *pabort) {
331 free (buffer);
332 return after;
333 }
334 uri[0] = 0;
335 title[0] = 0;
336 length[0] = 0;
337 }
338 lastidx = idx;
339 p += 4;
340 while (p < end && *p != '=') {
341 p++;
342 }
343 p++;
344 while (p < end && *p <= 0x20) {
345 p++;
346 }
347 if (p >= end) {
348 break;
349 }
350 e = p;
351 while (e < end && *e >= 0x20) {
352 e++;
353 }
354 n = e-p;
355 n = min (n, sizeof (uri)-1);
356 memcpy (uri, p, n);
357 uri[n] = 0;
358 trace ("uri: %s\n", uri);
359 trace ("uri%d=%s\n", idx, uri);
360 p = ++e;
361 }
362 else if (!strncasecmp (p, "title", 5)) {
363 int idx = atoi (p + 5);
364 if (uri[0] && idx != lastidx && lastidx != -1) {
365 trace ("title%d\n", idx);
366 DB_playItem_t *it = pls_insert_file (plt, after, fname, uri, pabort, cb, user_data, title, length);
367 if (it) {
368 after = it;
369 }
370 if (pabort && *pabort) {
371 free (buffer);
372 return after;
373 }
374 uri[0] = 0;
375 title[0] = 0;
376 length[0] = 0;
377 }
378 lastidx = idx;
379 p += 5;
380 while (p < end && *p != '=') {
381 p++;
382 }
383 p++;
384 while (p < end && *p <= 0x20) {
385 p++;
386 }
387 if (p >= end) {
388 break;
389 }
390 e = p;
391 while (e < end && *e >= 0x20) {
392 e++;
393 }
394 n = e-p;
395 n = min (n, sizeof (title)-1);
396 memcpy (title, p, n);
397 title[n] = 0;
398 trace ("title%d=%s\n", idx, title);
399 p = ++e;
400 }
401 else if (!strncasecmp (p, "length", 6)) {
402 int idx = atoi (p + 6);
403 if (uri[0] && idx != lastidx && lastidx != -1) {
404 trace ("length%d\n", idx);
405 DB_playItem_t *it = pls_insert_file (plt, after, fname, uri, pabort, cb, user_data, title, length);
406 if (it) {
407 after = it;
408 }
409 if (pabort && *pabort) {
410 free (buffer);
411 return after;
412 }
413 uri[0] = 0;
414 title[0] = 0;
415 length[0] = 0;
416 }
417 lastidx = idx;
418 p += 6;
419 // skip =
420 while (p < end && *p != '=') {
421 p++;
422 }
423 p++;
424 if (p >= end) {
425 break;
426 }
427 e = p;
428 while (e < end && *e >= 0x20) {
429 e++;
430 }
431 n = e-p;
432 n = min (n, sizeof (length)-1);
433 memcpy (length, p, n);
434 trace ("length%d=%s\n", idx, length);
435 }
436 else {
437 trace ("pls: skipping unrecognized entry in pls file: %s\n", p);
438 e = p;
439 while (e < end && *e >= 0x20) {
440 e++;
441 }
442 }
443 while (e < end && *e < 0x20) {
444 e++;
445 }
446 p = e;
447 }
448 if (uri[0]) {
449 DB_playItem_t *it = pls_insert_file (plt, after, fname, uri, pabort, cb, user_data, title, length);
450 if (it) {
451 after = it;
452 }
453 }
454 free (buffer);
455 return after;
456 }
457
458 static DB_playItem_t *
m3uplug_load(ddb_playlist_t * plt,DB_playItem_t * after,const char * fname,int * pabort,int (* cb)(DB_playItem_t * it,void * data),void * user_data)459 m3uplug_load (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname, int *pabort, int (*cb)(DB_playItem_t *it, void *data), void *user_data) {
460 char resolved_fname[PATH_MAX];
461 char *res = realpath (fname, resolved_fname);
462 if (res) {
463 fname = resolved_fname;
464 }
465
466 const char *ext = strrchr (fname, '.');
467 if (ext) {
468 ext++;
469 }
470
471 DB_playItem_t *ret = NULL;
472
473 int tried_pls = 0;
474
475 if (ext && !strcasecmp (ext, "pls")) {
476 tried_pls = 1;
477 ret = load_pls (plt, after, fname, pabort, cb, user_data);
478 }
479
480 if (!ret) {
481 ret = load_m3u (plt, after, fname, pabort, cb, user_data);
482 }
483
484 if (!ret && !tried_pls) {
485 ret = load_pls (plt, after, fname, pabort, cb, user_data);
486 }
487
488 return ret;
489 }
490
491 int
m3uplug_save_m3u(const char * fname,DB_playItem_t * first,DB_playItem_t * last)492 m3uplug_save_m3u (const char *fname, DB_playItem_t *first, DB_playItem_t *last) {
493 FILE *fp = fopen (fname, "w+t");
494 if (!fp) {
495 return -1;
496 }
497 DB_playItem_t *it = first;
498 deadbeef->pl_item_ref (it);
499 fprintf (fp, "#EXTM3U\n");
500 while (it) {
501 // skip subtracks, pls and m3u formats don't support that
502 uint32_t flags = deadbeef->pl_get_item_flags (it);
503 if (flags & DDB_IS_SUBTRACK) {
504 DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
505 deadbeef->pl_item_unref (it);
506 it = next;
507 continue;
508 }
509 int dur = (int)floor(deadbeef->pl_get_item_duration (it));
510 char s[1000];
511 int has_artist = deadbeef->pl_meta_exists (it, "artist");
512 int has_title = deadbeef->pl_meta_exists (it, "title");
513 if (has_artist && has_title) {
514 deadbeef->pl_format_title (it, -1, s, sizeof (s), -1, "%a - %t");
515 fprintf (fp, "#EXTINF:%d,%s\n", dur, s);
516 }
517 else if (has_title) {
518 deadbeef->pl_format_title (it, -1, s, sizeof (s), -1, "%t");
519 fprintf (fp, "#EXTINF:%d,%s\n", dur, s);
520 }
521 deadbeef->pl_lock ();
522 {
523 const char *fname = deadbeef->pl_find_meta (it, ":URI");
524 fprintf (fp, "%s\n", fname);
525 }
526 deadbeef->pl_unlock ();
527
528 if (it == last) {
529 break;
530 }
531 DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
532 deadbeef->pl_item_unref (it);
533 it = next;
534 }
535 fclose (fp);
536 return 0;
537 }
538
539 int
m3uplug_save_pls(const char * fname,DB_playItem_t * first,DB_playItem_t * last)540 m3uplug_save_pls (const char *fname, DB_playItem_t *first, DB_playItem_t *last) {
541 FILE *fp = fopen (fname, "w+t");
542 if (!fp) {
543 return -1;
544 }
545
546 int n = 0;
547 DB_playItem_t *it = first;
548 deadbeef->pl_item_ref (it);
549 while (it) {
550 // skip subtracks, pls and m3u formats don't support that
551 uint32_t flags = deadbeef->pl_get_item_flags (it);
552 if (flags & DDB_IS_SUBTRACK) {
553 DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
554 deadbeef->pl_item_unref (it);
555 it = next;
556 continue;
557 }
558 n++;
559 if (it == last) {
560 break;
561 }
562 DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
563 deadbeef->pl_item_unref (it);
564 it = next;
565 }
566
567 fprintf (fp, "[playlist]\n");
568 fprintf (fp, "NumberOfEntries=%d\n", n);
569
570 it = first;
571 deadbeef->pl_item_ref (it);
572 int i = 1;
573 while (it) {
574 // skip subtracks, pls and m3u formats don't support that
575 uint32_t flags = deadbeef->pl_get_item_flags (it);
576 if (flags & DDB_IS_SUBTRACK) {
577 DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
578 deadbeef->pl_item_unref (it);
579 it = next;
580 continue;
581 }
582 deadbeef->pl_lock ();
583 {
584 const char *fname = deadbeef->pl_find_meta (it, ":URI");
585 fprintf (fp, "File%d=%s\n", i, fname);
586 }
587 deadbeef->pl_unlock ();
588
589 if (it == last) {
590 break;
591 }
592 DB_playItem_t *next = deadbeef->pl_get_next (it, PL_MAIN);
593 deadbeef->pl_item_unref (it);
594 it = next;
595 i++;
596 }
597 fclose (fp);
598 return 0;
599 }
600
601 int
m3uplug_save(ddb_playlist_t * plt,const char * fname,DB_playItem_t * first,DB_playItem_t * last)602 m3uplug_save (ddb_playlist_t *plt, const char *fname, DB_playItem_t *first, DB_playItem_t *last) {
603 const char *e = strrchr (fname, '.');
604 if (!e) {
605 return -1;
606 }
607 if (!strcasecmp (e, ".m3u") || !strcasecmp (e, ".m3u8")) {
608 return m3uplug_save_m3u (fname, first, last);
609 }
610 else if (!strcasecmp (e, ".pls")) {
611 return m3uplug_save_pls (fname, first, last);
612 }
613 return -1;
614 }
615
616 static const char * exts[] = { "m3u", "m3u8", "pls", NULL };
617 DB_playlist_t plugin = {
618 .plugin.api_vmajor = 1,
619 .plugin.api_vminor = 0,
620 .plugin.version_major = 1,
621 .plugin.version_minor = 0,
622 .plugin.type = DB_PLUGIN_PLAYLIST,
623 .plugin.id = "m3u",
624 .plugin.name = "M3U and PLS support",
625 .plugin.descr = "Importing and exporting M3U and PLS formats\nRecognizes .pls, .m3u and .m3u8 file types\n\nNOTE: only utf8 file names are currently supported",
626 .plugin.copyright =
627 "M3U and PLS playlist plugin for DeaDBeeF Player\n"
628 "Copyright (C) 2009-2014 Alexey Yakovenko\n"
629 "\n"
630 "This software is provided 'as-is', without any express or implied\n"
631 "warranty. In no event will the authors be held liable for any damages\n"
632 "arising from the use of this software.\n"
633 "\n"
634 "Permission is granted to anyone to use this software for any purpose,\n"
635 "including commercial applications, and to alter it and redistribute it\n"
636 "freely, subject to the following restrictions:\n"
637 "\n"
638 "1. The origin of this software must not be misrepresented; you must not\n"
639 " claim that you wrote the original software. If you use this software\n"
640 " in a product, an acknowledgment in the product documentation would be\n"
641 " appreciated but is not required.\n"
642 "\n"
643 "2. Altered source versions must be plainly marked as such, and must not be\n"
644 " misrepresented as being the original software.\n"
645 "\n"
646 "3. This notice may not be removed or altered from any source distribution.\n"
647 ,
648 .plugin.website = "http://deadbeef.sf.net",
649 .load = m3uplug_load,
650 .save = m3uplug_save,
651 .extensions = exts,
652 };
653
654 DB_plugin_t *
m3u_load(DB_functions_t * api)655 m3u_load (DB_functions_t *api) {
656 deadbeef = api;
657 return &plugin.plugin;
658 }
659