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