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