1 /*
2 playlist: playlist logic
3
4 copyright 1995-2008 by the mpg123 project - free software under the terms of the LGPL 2.1
5 see COPYING and AUTHORS files in distribution or http://mpg123.org
6 initially written by Michael Hipp, outsourced/reorganized by Thomas Orgis
7
8 If we officially support Windows again, we should have this reworked to really cope with Windows paths, too.
9 */
10
11 /* Need random(). */
12 #define _DEFAULT_SOURCE
13 #define _BSD_SOURCE
14
15 #include "mpg123app.h"
16 #include "sysutil.h"
17 #include "getlopt.h" /* for loptind */
18 #include "term.h" /* for term_restore */
19 #include "playlist.h"
20 #include "httpget.h"
21 #include <time.h> /* For srand(). */
22 #include "debug.h"
23
24 #ifdef HAVE_RANDOM
25 #define RAND random
26 #define SRAND srandom
27 #else
28 #define RAND rand
29 #define SRAND srand
30 #endif
31
32 /* increase linebuf in blocks of ... bytes */
33 #define LINEBUF_STEP 100
34
35 enum playlist_type { UNKNOWN = 0, M3U, PLS, NO_LIST };
36
37 typedef struct listitem
38 {
39 char* url; /* the filename */
40 char freeit; /* if it was allocated and should be free()d here */
41 size_t playcount; /* has been played as ...th track in overall counting */
42 } listitem;
43
44 typedef struct playlist_struct
45 {
46 FILE* file; /* the current playlist stream */
47 size_t entry; /* entry in the playlist file */
48 size_t playcount; /* overall track counter for playback */
49 long loop; /* repeat a track n times */
50 size_t size;
51 size_t fill;
52 size_t pos; /* (next) position, internal use */
53 size_t num; /* current track number */
54 size_t alloc_step;
55 struct listitem* list;
56 mpg123_string linebuf;
57 mpg123_string dir;
58 enum playlist_type type;
59 #if defined (WANT_WIN32_SOCKETS)
60 int sockd; /* Is Win32 socket descriptor working? */
61 #endif
62 } playlist_struct;
63
64 /* one global instance... add a pointer to this to every function definition and you have OO-style... */
65 static playlist_struct pl;
66
67 static int add_next_file (int argc, char *argv[]);
68 static void shuffle_playlist(void);
69 static void init_playlist(void);
70 static int add_copy_to_playlist(char* new_entry);
71 static int add_to_playlist(char* new_entry, char freeit);
72
73 /* used to be init_input */
prepare_playlist(int argc,char ** argv)74 void prepare_playlist(int argc, char** argv)
75 {
76 /*
77 fetch all playlist entries ... I don't consider playlists to be an endless stream.
78 If you want to intentionally hang mpg123 on some other prog that may take infinite time to produce the full list (perhaps load tracks on demand), then just use the remote control and let that program print "load filename" instead of "filename".
79 We may even provide a simple wrapper script that emulates the old playlist reading behaviour (for files and stdin, http playlists are actually a strong point on reading the list in _before_ starting playback since http connections don't last forever).
80 */
81 init_playlist();
82 while (add_next_file(argc, argv)) {}
83 if(param.verbose > 1)
84 {
85 fprintf(stderr, "\nplaylist in normal order:\n");
86 print_playlist(stderr, 0);
87 fprintf(stderr, "\n");
88 }
89 if(param.shuffle == 1) shuffle_playlist();
90 /* Don't need these anymore, we have copies! */
91 mpg123_free_string(&pl.linebuf);
92 mpg123_free_string(&pl.dir);
93 }
94
95 /* Return a random number >= 0 and < n */
rando(size_t n)96 static size_t rando(size_t n)
97 {
98 long ran;
99 long limit = RAND_MAX - (RAND_MAX % (long)n);
100 if(n<2) return 0; /* Better settle that here than in an endless loop... */
101 do{ ran = RAND(); }while( ran >= limit );
102 return (size_t)(ran%n);
103 }
104
get_next_file(void)105 char *get_next_file(void)
106 {
107 struct listitem *newitem = NULL;
108
109 /* Zero looping is nothing, as is nothing at all. */
110 if(pl.fill == 0 || param.loop == 0)
111 return NULL;
112
113 ++pl.playcount;
114
115 /* normal order, just pick next thing */
116 if(param.shuffle < 2)
117 {
118 do
119 {
120 if(pl.pos < pl.fill)
121 {
122 newitem = &pl.list[pl.pos];
123 pl.num = pl.pos+1;
124 }
125 else newitem = NULL;
126 /* if we have rounds left, decrease loop, else reinit loop because it's a new track */
127 if(pl.loop > 0) --pl.loop; /* loop for current track... */
128 if(pl.loop == 0)
129 {
130 pl.loop = param.loop;
131 ++pl.pos;
132 }
133 } while(pl.loop == 0 && newitem != NULL);
134 }
135 else
136 {
137 /* Handle looping first, but only if there is a random track selection
138 presently active (see playlist_jump() for interaction). */
139 if(!(pl.num && ((pl.loop > 0 && --pl.loop) || pl.loop < 0)))
140 {
141 /* Randomly select the next track. */
142 do /* limiting randomness: don't repeat too early */
143 {
144 pl.pos = rando(pl.fill);
145 } while( pl.list[pl.pos].playcount
146 && (pl.playcount - pl.list[pl.pos].playcount) <= pl.fill/2 );
147 pl.loop = param.loop;
148 }
149
150 newitem = &pl.list[pl.pos];
151 pl.num = pl.pos+1;
152 }
153
154 /* "-" is STDOUT, "" is dumb, NULL is nothing */
155 if(newitem != NULL)
156 {
157 /* Remember the playback position of the track. */
158 newitem->playcount = pl.playcount;
159 return newitem->url;
160 }
161 else return NULL;
162 }
163
playlist_pos(size_t * total,long * loop)164 size_t playlist_pos(size_t *total, long *loop)
165 {
166 if(total)
167 *total = pl.fill;
168 if(loop)
169 *loop = pl.loop;
170 return pl.num;
171 }
172
playlist_jump(ssize_t incr)173 void playlist_jump(ssize_t incr)
174 {
175 size_t off = incr < 0 ? -incr : incr;
176
177 pl.loop = 0; /* This loop is done, always. */
178 /* Straight or shuffled lists can be jumped around in. */
179 if(pl.fill && param.shuffle < 2)
180 {
181 debug3("jump %"SIZE_P" (%ld) + %"SSIZE_P, pl.pos, pl.loop, incr);
182 if(pl.pos)
183 --pl.pos;
184 /* Now we're at the _current_ position. */
185 if(incr < 0)
186 pl.pos -= off > pl.pos ? pl.pos : off;
187 else
188 {
189 if(off >= pl.fill - pl.pos)
190 pl.pos = pl.fill; /* Any value >= pl.fill would actually be OK. */
191 else
192 pl.pos += off;
193 }
194 debug2("jumped %"SIZE_P" (%ld)", pl.pos, pl.loop);
195 }
196 }
197
198 /* Directory jumping based on comparing the directory part of the playlist
199 URLs. */
cmp_dir(const char * patha,const char * pathb)200 static int cmp_dir(const char* patha, const char* pathb)
201 {
202 size_t dirlen[2];
203 dirlen[0] = dir_length(patha);
204 dirlen[1] = dir_length(pathb);
205 return (dirlen[0] < dirlen[1])
206 ? -1
207 : ( dirlen[0] > dirlen[1]
208 ? 1
209 : memcmp(patha, pathb, dirlen[0])
210 );
211 }
212
playlist_next_dir(void)213 void playlist_next_dir(void)
214 {
215 if(pl.fill && param.shuffle < 2)
216 {
217 size_t npos = pl.pos ? pl.pos-1 : 0;
218 do ++npos;
219 while(npos < pl.fill && !cmp_dir(pl.list[npos-1].url, pl.list[npos].url));
220 pl.pos = npos;
221 }
222 pl.loop = 0;
223 }
224
playlist_prev_dir(void)225 void playlist_prev_dir(void)
226 {
227 if(pl.fill && param.shuffle < 2)
228 {
229 size_t npos = pl.pos ? pl.pos-1 : 0;
230 /* 1. Find end of previous directory. */
231 if(npos && npos < pl.fill)
232 do --npos;
233 while(npos && !cmp_dir(pl.list[npos+1].url, pl.list[npos].url));
234 /* npos == the last track of previous directory */
235 /* 2. Find the first track of this directory */
236 if(npos < pl.fill)
237 while(npos && !cmp_dir(pl.list[npos-1].url, pl.list[npos].url))
238 --npos;
239 pl.pos = npos;
240 }
241 pl.loop = 0;
242 }
243
244 /* It doesn't really matter on program exit, but anyway...
245 Make sure you don't free() an item of argv! */
free_playlist(void)246 void free_playlist(void)
247 {
248 if(pl.list != NULL)
249 {
250 debug("going to free() the playlist");
251 while(pl.fill)
252 {
253 --pl.fill;
254 debug1("free()ing entry %lu", (unsigned long)pl.fill);
255 if(pl.list[pl.fill].freeit) free(pl.list[pl.fill].url);
256 }
257 free(pl.list);
258 pl.list = NULL;
259 pl.size = 0;
260 debug("free()d the playlist");
261 }
262 mpg123_free_string(&pl.linebuf);
263 mpg123_free_string(&pl.dir);
264 }
265
266 /* the constructor... */
init_playlist(void)267 static void init_playlist(void)
268 {
269 SRAND(time(NULL));
270 pl.file = NULL;
271 pl.entry = 0;
272 pl.playcount = 0;
273 pl.size = 0;
274 pl.fill = 0;
275 pl.pos = 0;
276 pl.num = 0;
277 if(APPFLAG(MPG123APP_CONTINUE) && param.listentry > 0)
278 pl.pos = param.listentry - 1;
279
280 pl.list = NULL;
281 pl.alloc_step = 10;
282 mpg123_init_string(&pl.dir);
283 mpg123_init_string(&pl.linebuf);
284 pl.type = UNKNOWN;
285 pl.loop = param.loop;
286 #ifdef WANT_WIN32_SOCKETS
287 pl.sockd = -1;
288 #endif
289 }
290
291 /*
292 slightly modified find_next_file from mpg123.c
293 now doesn't return the next entry but adds it to playlist struct
294 returns 1 if it found something, 0 on end
295 */
add_next_file(int argc,char * argv[])296 static int add_next_file (int argc, char *argv[])
297 {
298 int firstline = 0;
299
300 /* hack for url that has been detected as track, not playlist */
301 if(pl.type == NO_LIST) return 0;
302
303 /* Get playlist dirname to append it to the files in playlist */
304 if (param.listname)
305 {
306 char* slashpos;
307 /* Oh, right... that doesn't look good for Windows... */
308 if ((slashpos=strrchr(param.listname, '/')))
309 {
310 /* up to and including /, with space for \0 */
311 if(mpg123_resize_string(&pl.dir, 2 + slashpos - param.listname))
312 {
313 memcpy(pl.dir.p, param.listname, pl.dir.size-1);
314 pl.dir.p[pl.dir.size-1] = 0;
315 }
316 else
317 {
318 error("cannot allocate memory for list directory!");
319 pl.dir.size = 0;
320 }
321 }
322 }
323
324 if (param.listname || pl.file)
325 {
326 size_t line_offset = 0;
327 #ifndef WANT_WIN32_SOCKETS
328 if (!pl.file)
329 #else
330 if (!pl.file && pl.sockd == -1)
331 #endif
332 {
333 /* empty or "-" */
334 if (!*param.listname || !strcmp(param.listname, "-"))
335 {
336 pl.file = stdin;
337 param.listname = NULL;
338 pl.entry = 0;
339 }
340 else if (!strncmp(param.listname, "http://", 7))
341 {
342 int fd;
343 struct httpdata htd;
344 httpdata_init(&htd);
345 #ifndef WANT_WIN32_SOCKETS
346 fd = http_open(param.listname, &htd);
347 #else
348 fd = win32_net_http_open(param.listname, &htd);
349 #endif
350 debug1("htd.content_type.p: %p", (void*) htd.content_type.p);
351 if(!APPFLAG(MPG123APP_IGNORE_MIME) && htd.content_type.p != NULL)
352 {
353 int mimi;
354 debug1("htd.content_type.p value: %s", htd.content_type.p);
355 mimi = debunk_mime(htd.content_type.p);
356
357 if(mimi & IS_M3U) pl.type = M3U;
358 else if(mimi & IS_PLS) pl.type = PLS;
359 else
360 {
361 #ifndef WANT_WIN32_SOCKETS
362 if(fd >= 0) close(fd);
363 #else
364 if(fd != SOCKET_ERROR) win32_net_close(fd);
365 #endif
366 fd = -1;
367
368 if(mimi & IS_FILE)
369 {
370 pl.type = NO_LIST;
371 if(param.listentry < 0)
372 {
373 printf("#note you gave me a file url, no playlist, so...\n#entry 1\n%s\n", param.listname);
374 return 0;
375 }
376 else
377 {
378 fprintf(stderr, "Note: MIME type indicates that this is no playlist but an mpeg audio file... reopening as such.\n");
379 add_to_playlist(param.listname, 0);
380 return 1;
381 }
382 }
383 error1("Unknown playlist MIME type %s; maybe "PACKAGE_NAME" can support it in future if you report this to the maintainer.", htd.content_type.p);
384 }
385 httpdata_free(&htd);
386 }
387 if(fd < 0)
388 {
389 param.listname = NULL;
390 pl.file = NULL;
391 #ifdef WANT_WIN32_SOCKETS
392 pl.sockd = -1;
393 #endif
394 error("Invalid playlist from http_open()!\n");
395 }
396 else
397 {
398 pl.entry = 0;
399 #ifndef WANT_WIN32_SOCKETS
400 pl.file = compat_fdopen(fd,"r");
401 #else
402 pl.sockd = fd;
403 #endif
404 }
405 }
406 else if (!(pl.file = fopen(param.listname, "rb")))
407 {
408 perror (param.listname);
409 return 0;
410 }
411 else
412 {
413 debug("opened ordinary list file");
414 pl.entry = 0;
415 }
416 if (param.verbose && pl.file) fprintf (stderr, "Using playlist from %s ...\n", param.listname ? param.listname : "standard input");
417 firstline = 1; /* just opened */
418 }
419 /* reading the file line by line */
420 #ifndef WANT_WIN32_SOCKETS
421 while (pl.file)
422 #else
423 while (pl.file || (pl.sockd) != -1)
424 #endif
425 {
426 /*
427 now read a string of arbitrary size...
428 read a chunk, see if lineend, realloc, read another chunk
429
430 fgets reads at most size-1 bytes and always appends the \0
431 */
432 size_t have = 0;
433 do
434 {
435 /* have is the length of the string read, without the closing \0 */
436 if(pl.linebuf.size <= have+1)
437 {
438 if(!mpg123_resize_string(&pl.linebuf, pl.linebuf.size+LINEBUF_STEP))
439 {
440 error("cannot increase line buffer");
441 break;
442 }
443 }
444 /* I rely on fgets writing the \0 at the end! */
445 #ifndef WANT_WIN32_SOCKETS
446 if(fgets(pl.linebuf.p+have, pl.linebuf.size-have, pl.file))
447 #else
448 if( (pl.file ? (fgets(pl.linebuf.p+have, pl.linebuf.size-have, pl.file)) : (win32_net_fgets(pl.linebuf.p+have, pl.linebuf.size-have, pl.sockd))))
449 #endif
450 {
451 have += strlen(pl.linebuf.p+have);
452 debug2("have read %lu characters into linebuf: [%s]", (unsigned long)have, pl.linebuf.p);
453 }
454 else
455 {
456 debug("fgets failed to deliver something... file ended?");
457 break;
458 }
459 } while(have && pl.linebuf.p[have-1] != '\r' && pl.linebuf.p[have-1] != '\n');
460 if(have)
461 {
462 pl.linebuf.p[strcspn(pl.linebuf.p, "\t\n\r")] = '\0';
463 /* a bit of fuzzyness */
464 if(firstline)
465 {
466 if(pl.type == UNKNOWN)
467 {
468 if(!strcmp("[playlist]", pl.linebuf.p))
469 {
470 fprintf(stderr, "Note: detected Shoutcast/Winamp PLS playlist\n");
471 pl.type = PLS;
472 continue;
473 }
474 else if
475 (
476 (!strncasecmp("#M3U", pl.linebuf.p ,4))
477 ||
478 (!strncasecmp("#EXTM3U", pl.linebuf.p ,7))
479 ||
480 (param.listname != NULL && (strrchr(param.listname, '.')) != NULL && !strcasecmp(".m3u", strrchr(param.listname, '.')))
481 )
482 {
483 if(param.verbose) fprintf(stderr, "Note: detected M3U playlist type\n");
484 pl.type = M3U;
485 }
486 else
487 {
488 if(param.verbose) fprintf(stderr, "Note: guessed M3U playlist type\n");
489 pl.type = M3U;
490 }
491 }
492 else
493 {
494 if(param.verbose)
495 {
496 fprintf(stderr, "Note: Interpreting as ");
497 switch(pl.type)
498 {
499 case M3U: fprintf(stderr, "M3U"); break;
500 case PLS: fprintf(stderr, "PLS (Winamp/Shoutcast)"); break;
501 default: fprintf(stderr, "???");
502 }
503 fprintf(stderr, " playlist\n");
504 }
505 }
506 firstline = 0;
507 }
508 #if !defined(WIN32)
509 {
510 size_t i;
511 /* convert \ to / (from MS-like directory format) */
512 for (i=0;pl.linebuf.p[i]!='\0';i++)
513 {
514 if (pl.linebuf.p[i] == '\\') pl.linebuf.p[i] = '/';
515 }
516 }
517 #endif
518 if (pl.linebuf.p[0]=='\0') continue; /* skip empty lines... */
519 if (((pl.type == M3U) && (pl.linebuf.p[0]=='#')))
520 {
521 /* a comment line in m3u file */
522 if(param.listentry < 0) printf("%s\n", pl.linebuf.p);
523 continue;
524 }
525
526 /* real filename may start at an offset */
527 line_offset = 0;
528 /* extract path out of PLS */
529 if(pl.type == PLS)
530 {
531 if(!strncasecmp("File", pl.linebuf.p, 4))
532 {
533 /* too lazy to really check for file number... would have to change logic to support unordered file entries anyway */
534 char* in_line;
535 if((in_line = strchr(pl.linebuf.p+4, '=')) != NULL)
536 {
537 /* FileN=? */
538 if(in_line[1] != 0)
539 {
540 ++in_line;
541 line_offset = (size_t) (in_line-pl.linebuf.p);
542 }
543 else
544 {
545 fprintf(stderr, "Warning: Invalid PLS line (empty filename) - corrupt playlist file?\n");
546 continue;
547 }
548 }
549 else
550 {
551 fprintf(stderr, "Warning: Invalid PLS line (no '=' after 'File') - corrupt playlist file?\n");
552 continue;
553 }
554 }
555 else
556 {
557 if(param.listentry < 0) printf("#metainfo %s\n", pl.linebuf.p);
558 continue;
559 }
560 }
561
562 /* make paths absolute */
563 /* Windows knows absolute paths with c: in front... should handle this if really supporting win32 again */
564 if
565 (
566 (pl.dir.p != NULL)
567 && (pl.linebuf.p[line_offset]!='/')
568 && (pl.linebuf.p[line_offset]!='\\')
569 && strncmp(pl.linebuf.p+line_offset, "http://", 7)
570 )
571 {
572 size_t need;
573 need = pl.dir.size + strlen(pl.linebuf.p+line_offset);
574 if(pl.linebuf.size < need)
575 {
576 if(!mpg123_resize_string(&pl.linebuf, need))
577 {
578 error("unable to enlarge linebuf for appending path! skipping");
579 continue;
580 }
581 }
582 /* move to have the space at beginning */
583 memmove(pl.linebuf.p+pl.dir.size-1, pl.linebuf.p+line_offset, strlen(pl.linebuf.p+line_offset)+1);
584 /* prepend path */
585 memcpy(pl.linebuf.p, pl.dir.p, pl.dir.size-1);
586 line_offset = 0;
587 }
588 ++pl.entry;
589 if(param.listentry < 0) printf("#entry %lu\n%s\n", (unsigned long)pl.entry,pl.linebuf.p+line_offset);
590 else if((param.listentry == 0) || (param.listentry == pl.entry) || APPFLAG(MPG123APP_CONTINUE))
591 {
592 add_copy_to_playlist(pl.linebuf.p+line_offset);
593 return 1;
594 }
595 }
596 else
597 {
598 if (param.listname)
599 if(pl.file) fclose (pl.file);
600 param.listname = NULL;
601 pl.file = NULL;
602 #ifdef WANT_WIN32_SOCKETS
603 if( pl.sockd != -1)
604 {
605 win32_net_close(pl.sockd);
606 pl.sockd = -1;
607 }
608 #endif
609 }
610 }
611 }
612 if(loptind < argc)
613 {
614 add_to_playlist(argv[loptind++], 0);
615 return 1;
616 }
617 return 0;
618 }
619
shuffle_playlist(void)620 static void shuffle_playlist(void)
621 {
622 size_t loop;
623 size_t rannum;
624 if(pl.fill >= 2)
625 {
626 /* Refer to bug 1777621 for discussion on that.
627 It's Durstenfeld... */
628 for (loop = 0; loop < pl.fill; loop++)
629 {
630 struct listitem tmp;
631 rannum = loop + rando(pl.fill-loop);
632 /*
633 Small test on your binary operation skills (^ is XOR):
634 a = b^(a^b)
635 b = (a^b)^(b^(a^b))
636 And, understood? ;-)
637
638 pl.list[loop] ^= pl.list[rannum];
639 pl.list[rannum] ^= pl.list[loop];
640 pl.list[loop] ^= pl.list[rannum];
641
642 But since this is not allowed with pointers and any speed gain questionable (well, saving _some_ memory...), doing it the lame way:
643 */
644 tmp = pl.list[rannum];
645 pl.list[rannum] = pl.list[loop];
646 pl.list[loop] = tmp;
647 }
648 }
649
650 if(param.verbose > 1)
651 {
652 /* print them */
653 fprintf(stderr, "\nshuffled playlist:\n");
654 print_playlist(stderr, 0);
655 fprintf(stderr, "\n");
656 }
657 }
658
print_playlist(FILE * out,int showpos)659 void print_playlist(FILE* out, int showpos)
660 {
661 size_t loop;
662 for (loop = 0; loop < pl.fill; loop++)
663 {
664 char *pre = "";
665 if(showpos)
666 pre = (loop+1==pl.num) ? "> " : " ";
667
668 fprintf(out, "%s%s\n", pre, pl.list[loop].url);
669 }
670 }
671
672
add_copy_to_playlist(char * new_entry)673 static int add_copy_to_playlist(char* new_entry)
674 {
675 char* cop;
676 if((cop = (char*) malloc(strlen(new_entry)+1)) != NULL)
677 {
678 strcpy(cop, new_entry);
679 return add_to_playlist(cop, 1);
680 }
681 else return 0;
682 }
683
684 /* add new entry to playlist - no string copy, just the pointer! */
add_to_playlist(char * new_entry,char freeit)685 static int add_to_playlist(char* new_entry, char freeit)
686 {
687 if(pl.fill == pl.size)
688 {
689 struct listitem* tmp = NULL;
690 /* enlarge the list */
691 tmp = (struct listitem*) safe_realloc(pl.list, (pl.size + pl.alloc_step) * sizeof(struct listitem));
692 if(!tmp)
693 {
694 error("unable to allocate more memory for playlist");
695 perror("");
696 return 0;
697 }
698 else
699 {
700 pl.list = tmp;
701 pl.size += pl.alloc_step;
702 }
703 }
704 /* paranoid */
705 if(pl.fill < pl.size)
706 {
707 pl.list[pl.fill].freeit = freeit;
708 pl.list[pl.fill].url = new_entry;
709 pl.list[pl.fill].playcount = 0;
710 ++pl.fill;
711 }
712 else
713 {
714 error("playlist memory still too small?!");
715 return 0;
716 }
717 return 1;
718 }
719
720