1 /*
2  * Copyright (c) 2010, 2011 Ryan Flannery <ryan.flannery@gmail.com>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16 
17 #include "playlist.h"
18 
19 int history_size = DEFAULT_HISTORY_SIZE;
20 
21 void
playlist_increase_capacity(playlist * p)22 playlist_increase_capacity(playlist *p)
23 {
24    meta_info **new_files;
25    size_t      nbytes;
26 
27    p->capacity += PLAYLIST_CHUNK_SIZE;
28    nbytes = p->capacity * sizeof(meta_info*);
29    if ((new_files = realloc(p->files, nbytes)) == NULL)
30       err(1, "%s: failed to realloc(3) files", __FUNCTION__);
31 
32    p->files = new_files;
33 }
34 
35 /*
36  * Allocate a new playlist and return a pointer to it.  The resulting
37  * structure must be free(2)'d using playlist_free().
38  */
39 playlist *
playlist_new(void)40 playlist_new(void)
41 {
42    playlist *p;
43 
44    if ((p = malloc(sizeof(playlist))) == NULL)
45       err(1, "playlist_new: failed to allocate playlist");
46 
47    if ((p->files = calloc(PLAYLIST_CHUNK_SIZE, sizeof(meta_info*))) == NULL)
48       err(1, "playlist_new: failed to allocate files");
49 
50    p->capacity = PLAYLIST_CHUNK_SIZE;
51    p->filename = NULL;
52    p->name     = NULL;
53    p->nfiles   = 0;
54    p->history  = playlist_history_new();
55    p->hist_present = -1;
56    p->needs_saving = false;
57 
58    return p;
59 }
60 
61 /* Free all of the memory consumed by a given playlist. */
62 void
playlist_free(playlist * p)63 playlist_free(playlist *p)
64 {
65    if (p->filename != NULL) free(p->filename);
66    if (p->name != NULL) free(p->name);
67    if (p->files != NULL) free(p->files);
68    playlist_history_free(p);
69    free(p);
70 }
71 
72 /*
73  * Duplicate an existing playlist, returning a pointer to the newly allocated
74  * playlist.  The filename and name of the new playlist must be specified.
75  */
76 playlist *
playlist_dup(const playlist * original,const char * filename,const char * name)77 playlist_dup(const playlist *original, const char *filename,
78    const char *name)
79 {
80    playlist *newplist;
81    int i;
82 
83    /* create new playlist and copy simple members */
84    newplist           = playlist_new();
85    newplist->nfiles   = original->nfiles;
86    newplist->capacity = original->nfiles;
87 
88    if (name != NULL) {
89       if ((newplist->name = strdup(name)) == NULL)
90          err(1, "playlist_dup: strdup name failed");
91    }
92    if (filename != NULL) {
93       if ((newplist->filename = strdup(filename)) == NULL)
94          err(1, "playlist_dup: strdup filename failed");
95    }
96 
97    /* copy all of the files */
98    newplist->files = calloc(original->nfiles, sizeof(meta_info*));
99    if (newplist->files == NULL)
100       err(1, "playlist_dup: failed to allocate files");
101 
102    for (i = 0; i < original->nfiles; i++)
103       newplist->files[i] = original->files[i];
104 
105    return newplist;
106 }
107 
108 /*
109  * Add files to a playlist at the index specified by start.  Note that if
110  * start is the length of the files array the files are appended to the end.
111  */
112 void
playlist_files_add(playlist * p,meta_info ** f,int start,int size,bool record)113 playlist_files_add(playlist *p, meta_info **f, int start, int size, bool record)
114 {
115    playlist_changeset *changes;
116    int i;
117 
118    if (start < 0 || start > p->nfiles)
119       errx(1, "playlist_file_add: index %d out of range", start);
120 
121    while (p->capacity <= p->nfiles + size)
122       playlist_increase_capacity(p);
123 
124    /* push everything after start back size places */
125    for (i = p->nfiles + size; i > start; i--)
126       p->files[i] = p->files[i - size];
127 
128    /* add the files */
129    for (i = 0; i < size; i++)
130       p->files[start + i] = f[i];
131 
132    p->nfiles += size;
133 
134    /* update the history for this playlist */
135    if (record) {
136       changes = changeset_create(CHANGE_ADD, size, f, start);
137       playlist_history_push(p, changes);
138       p->needs_saving = true;
139    }
140 }
141 
142 /* Append a file to the end of a playlist */
143 void
playlist_files_append(playlist * p,meta_info ** f,int size,bool record)144 playlist_files_append(playlist *p, meta_info **f, int size, bool record)
145 {
146    return playlist_files_add(p, f, p->nfiles, size, record);
147 }
148 
149 /* Remove a file at a given index from a playlist. */
150 void
playlist_files_remove(playlist * p,int start,int size,bool record)151 playlist_files_remove(playlist *p, int start, int size, bool record)
152 {
153    playlist_changeset *changes;
154    int i;
155 
156    if (start < 0 || start >= p->nfiles)
157       errx(1, "playlist_remove_file: index %d out of range", start);
158 
159    if (record) {
160       changes = changeset_create(CHANGE_REMOVE, size, &(p->files[start]), start);
161       playlist_history_push(p, changes);
162       p->needs_saving = true;
163    }
164 
165    for (i = start; i < p->nfiles; i++)
166       p->files[i] = p->files[i + size];
167 
168    p->nfiles -= size;
169 
170 }
171 
172 /* Replaces the file at a given index in a playlist with a new file */
173 void
playlist_file_replace(playlist * p,int index,meta_info * newEntry)174 playlist_file_replace(playlist *p, int index, meta_info *newEntry)
175 {
176    if (index < 0 || index >= p->nfiles)
177       errx(1, "playlist_file_replace: index %d out of range", index);
178 
179    p->files[index] = newEntry;
180 }
181 
182 /* Used with bsearch to find playlist entry by filename. */
cmp_fn_mi(const void * ai,const void * bi)183 static int cmp_fn_mi(const void *ai, const void *bi)
184 {
185    const char  *a = (const char *) ai;
186    const meta_info **b2 = (const meta_info **) bi;
187    const meta_info *b = (const meta_info *) *b2;
188 
189    return strcmp(a, b->filename);
190 }
191 
192 /*
193  * Loads a playlist from the provided filename.  The files within the playlist
194  * are compared against the given meta-information-database to see if they
195  * exist there.  If they do, the corresponding entry in the playlist structure
196  * built is simply a pointer to the existing entry.  Otherwise, that file's
197  * meta information is set to NULL and the file is copied (allocated) in the
198  * playlist structure.
199  *
200  * A newly allocated playlist is returned.
201  */
202 playlist *
playlist_load(const char * filename,meta_info ** db,int ndb)203 playlist_load(const char *filename, meta_info **db, int ndb)
204 {
205    meta_info *mi, **mit;
206    FILE *fin;
207    char *period;
208    char  entry[PATH_MAX + 1];
209 
210    /* open file */
211    if ((fin = fopen(filename, "r")) == NULL)
212       err(1, "playlist_load: failed to open playlist '%s'", filename);
213 
214    /* create playlist and setup */
215    playlist *p = playlist_new();
216    p->filename = strdup(filename);
217    p->name     = strdup(basename(filename));
218    if (p->filename == NULL || p->name == NULL)
219       err(1, "playlist_load: failed to allocate info for playlist '%s'", filename);
220 
221    /* hack to remove '.playlist' from name */
222    period  = strrchr(p->name, '.');
223    *period = '\0';
224 
225    /* read each line from the file and copy into playlist object */
226    while (fgets(entry, PATH_MAX, fin) != NULL) {
227       /* sanitize */
228       entry[strcspn(entry, "\n")] = '\0';
229 
230       /* check if file exists in the meta info. db */
231       mit = bsearch(entry, db, ndb, sizeof(meta_info *), cmp_fn_mi);
232       mi = *mit;
233 
234       if (mi != NULL)   /* file DOES exist in DB */
235          playlist_files_append(p, &mi, 1, false);
236       else {            /* file does NOT exist in DB */
237          /* create empty meta-info object with just the file name */
238          mi = mi_new();
239          mi->filename = strdup(entry);
240          if (mi->filename == NULL)
241             err(1, "playlist_load: failed to strdup filename");
242 
243          /* add new record to the db and link it to the playlist */
244          playlist_files_append(p, &mi, 1, false);
245          warnx("playlist \"%s\", file \"%s\" is NOT in media database (added for now)",
246             p->name, entry);
247       }
248    }
249 
250    fclose(fin);
251    return p;
252 }
253 
254 /*
255  * Save a playlist to file.  The filename used is whatever is in the
256  * playlist.
257  */
258 void
playlist_save(const playlist * p)259 playlist_save(const playlist *p)
260 {
261    FILE *fout;
262    int   i;
263 
264    if ((fout = fopen(p->filename, "w")) == NULL)
265       err(1, "playlist_save: failed to open playlist \"%s\"", p->filename);
266 
267    /* write each song to file */
268    for (i = 0; i < p->nfiles; i++) {
269       if (fprintf(fout, "%s\n", p->files[i]->filename) == -1)
270          err(1, "playlist_save: failed to record playlist \"%s\"", p->filename);
271    }
272 
273    fclose(fout);
274 }
275 
276 /*
277  * Deletes playlist from disk and destroy's the object, free()'ing all
278  * memory.
279  */
280 void
playlist_delete(playlist * p)281 playlist_delete(playlist *p)
282 {
283    /* delete file if the playlist is stored in a file */
284    if (p->filename != NULL && unlink(p->filename) != 0)
285       err(1, "playlist_delete: failed to delete playlist \"%s\"", p->filename);
286 
287    /* destroy/free() all memory */
288    playlist_free(p);
289 }
290 
291 /*
292  * Filter a playlist.  After a query string is setup using the meta_info
293  * function "mi_query_set(..)" function, this function can be used to
294  * filter out all of the entried of a playlist that match/do-not-match
295  * that query string.
296  *
297  * The results are in the new playlist that is returned.
298  * If no query is set with mi_query_init(), NULL is returned.
299  *
300  * The 'm' parameter controls if records matching should be returned
301  * (m = true) or if records not matching should be returned (m=false)
302  */
303 playlist *
playlist_filter(const playlist * p,bool m)304 playlist_filter(const playlist *p, bool m)
305 {
306    playlist *results;
307    int       i;
308 
309    if (!mi_query_isset())
310       return NULL;
311 
312    results = playlist_new();
313    for (i = 0; i < p->nfiles; i++) {
314       if (mi_match(p->files[i])) {
315          if (m)  playlist_files_append(results, &(p->files[i]), 1, false);
316       } else {
317          if (!m) playlist_files_append(results, &(p->files[i]), 1, false);
318       }
319    }
320 
321    return results;
322 }
323 
324 /*
325  * Builds an array of all files in the given directory with a '.playlist'
326  * extension, returning the number of such files found.
327  *
328  * Parameters:
329  *    dirname        C-string of directory containing playlist files
330  *
331  *    filenames      Pointer to an array of C-strings that will be allocated
332  *                   and built in this function.  This array is where the
333  *                   filename of each playlist will be stored.
334  * Returns:
335  *    The number of files with a '.playlist' extension that were found.
336  *
337  * Notes:
338  *    All allocation of the filenames array is handled here.  It is the
339  *    responsibility of the caller to free()
340  *       1. each element of that array
341  *       2. the array itself.
342  */
343 int
retrieve_playlist_filenames(const char * dirname,char *** fnames)344 retrieve_playlist_filenames(const char *dirname, char ***fnames)
345 {
346    char   *glob_pattern;
347    int     fcount;
348    glob_t  files;
349    int     globbed;
350 
351    /* build the search pattern */
352    if (asprintf(&glob_pattern, "%s/*.playlist", dirname) == -1)
353       errx(1, "failed in building glob pattern");
354 
355    /* get the files */
356    globbed = glob(glob_pattern, 0, NULL, &files);
357    if (globbed != 0 && globbed != GLOB_NOMATCH && errno != 0)
358       err(1, "failed to glob playlists directory");
359 
360    /* allocate & copy each of the filenames found into the filenames array */
361    if ((*fnames = calloc(files.gl_pathc, sizeof(char*))) == NULL)
362       err(1, "failed to allocate playlist filenames array");
363 
364    for (fcount = 0; fcount < files.gl_pathc; fcount++) {
365       if (asprintf(&((*fnames)[fcount]), "%s", files.gl_pathv[fcount]) == -1)
366          errx(1, "failed to allocate filename for playlist");
367    }
368 
369    /* cleanup */
370    globfree(&files);
371    free(glob_pattern);
372 
373    return fcount;
374 }
375 
376 playlist_changeset*
changeset_create(short type,size_t size,meta_info ** files,int loc)377 changeset_create(short type, size_t size, meta_info **files, int loc)
378 {
379    size_t i;
380 
381    playlist_changeset *c;
382 
383    if ((c = malloc(sizeof(playlist_changeset))) == NULL)
384       err(1, "%s: malloc(3) failed", __FUNCTION__);
385 
386    if ((c->files = calloc(size, sizeof(meta_info*))) == NULL)
387       err(1, "%s: calloc(3) failed", __FUNCTION__);
388 
389    c->type = type;
390    c->size = size;
391    c->location = loc;
392 
393    for (i = 0; i < size; i++)
394       c->files[i] = files[i];
395 
396    return c;
397 }
398 
399 void
changeset_free(playlist_changeset * c)400 changeset_free(playlist_changeset *c)
401 {
402    free(c->files);
403    free(c);
404 }
405 
406 playlist_changeset**
playlist_history_new(void)407 playlist_history_new(void)
408 {
409    playlist_changeset **h;
410    int i;
411 
412    if ((h = calloc(history_size, sizeof(playlist_changeset*))) == NULL)
413       err(1, "%s: calloc(3) failed", __FUNCTION__);
414 
415    for (i = 0; i < history_size; i++)
416       h[i] = NULL;
417 
418    return h;
419 }
420 
421 void
playlist_history_free_future(playlist * p)422 playlist_history_free_future(playlist *p)
423 {
424    int i;
425 
426    for (i = p->hist_present + 1; i < history_size; i++) {
427       if (p->history[i] != NULL) {
428          changeset_free(p->history[i]);
429          p->history[i] = NULL;
430       }
431    }
432 }
433 
434 void
playlist_history_free(playlist * p)435 playlist_history_free(playlist *p)
436 {
437    p->hist_present = 0;
438    playlist_history_free_future(p);
439 }
440 
441 void
playlist_history_push(playlist * p,playlist_changeset * c)442 playlist_history_push(playlist *p, playlist_changeset *c)
443 {
444    int i;
445 
446    if (p->hist_present < history_size - 1)
447       playlist_history_free_future(p);
448    else {
449       for (i = 0; i < history_size - 1; i++)
450          p->history[i] = p->history[i + 1];
451 
452       p->hist_present--;
453    }
454 
455    p->hist_present++;
456    p->history[p->hist_present] = c;
457 }
458 
459 /* returns 0 if successfull, 1 if there was no history to undo */
460 int
playlist_undo(playlist * p)461 playlist_undo(playlist *p)
462 {
463    playlist_changeset *c;
464 
465    if (p->hist_present == -1)
466       return 1;
467 
468    c = p->history[p->hist_present];
469 
470    switch (c->type) {
471    case CHANGE_ADD:
472       playlist_files_remove(p, c->location, c->size, false);
473       break;
474    case CHANGE_REMOVE:
475       playlist_files_add(p, c->files, c->location, c->size, false);
476       break;
477    default:
478       errx(1, "%s: invalid change type", __FUNCTION__);
479    }
480 
481    p->hist_present--;
482    return 0;
483 }
484 
485 /* returns 0 if successfull, 1 if there was no history to re-do */
486 int
playlist_redo(playlist * p)487 playlist_redo(playlist *p)
488 {
489    playlist_changeset *c;
490 
491    if (p->hist_present == history_size - 1
492    ||  p->history[p->hist_present + 1] == NULL)
493       return 1;
494 
495    c = p->history[p->hist_present + 1];
496 
497    switch (c->type) {
498    case CHANGE_ADD:
499       playlist_files_add(p, c->files, c->location, c->size, false);
500       break;
501    case CHANGE_REMOVE:
502       playlist_files_remove(p, c->location, c->size, false);
503       break;
504    default:
505       errx(1, "%s: invalid change type", __FUNCTION__);
506    }
507 
508    p->hist_present++;
509    return 0;
510 }
511