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 "medialib.h"
18 
19 /* The global media library struct */
20 medialib mdb;
21 
22 /*
23  * Load the global media library from disk. The location of the database file
24  * and the directory containing all of the playlists must be specified.
25  */
26 void
medialib_load(const char * db_file,const char * playlist_dir)27 medialib_load(const char *db_file, const char *playlist_dir)
28 {
29    playlist  *p;
30    char     **pfiles;
31    int        npfiles;
32    int        i;
33 
34    /* copy file/directory names */
35    mdb.db_file      = strdup(db_file);
36    mdb.playlist_dir = strdup(playlist_dir);
37    if (mdb.db_file == NULL || mdb.playlist_dir == NULL)
38       err(1, "failed to strdup db file and playlist dir in medialib_init");
39 
40    /* setup pseudo-playlists */
41    mdb.library = playlist_new();
42    mdb.library->filename = NULL;
43    mdb.library->name = strdup("--LIBRARY--");
44 
45    mdb.filter_results = playlist_new();
46    mdb.filter_results->filename = NULL;
47    mdb.filter_results->name = strdup("--FILTER--");
48 
49    if (mdb.library->name == NULL || mdb.filter_results->name == NULL)
50       err(1, "failed to strdup psuedo-names in medialib_load");
51 
52    /* load the actual database */
53    medialib_db_load(db_file);
54 
55    /* setup initial record keeping for playlists */
56    mdb.nplaylists = 0;
57    mdb.playlists_capacity = 2;
58    mdb.playlists = calloc(2, sizeof(playlist*));
59    if (mdb.playlists == NULL)
60       err(1, "medialib_load: failed to allocate initial playlists");
61 
62    /* add library/filter pseudo-playlists */
63    medialib_playlist_add(mdb.library);
64    medialib_playlist_add(mdb.filter_results);
65 
66    /* load the rest */
67    npfiles = retrieve_playlist_filenames(mdb.playlist_dir, &pfiles);
68    for (i = 0; i < npfiles; i++) {
69       p = playlist_load(pfiles[i], mdb.library->files, mdb.library->nfiles);
70       medialib_playlist_add(p);
71       free(pfiles[i]);
72    }
73 
74    /* set all playlists as saved initially */
75    for (i = 0; i < mdb.nplaylists; i++)
76       mdb.playlists[i]->needs_saving = false;
77 
78    free(pfiles);
79 }
80 
81 /* free() all memory associated with global media library */
82 void
medialib_destroy()83 medialib_destroy()
84 {
85    int i;
86 
87    /* free the database */
88    for (i = 0; i < mdb.library->nfiles; i++)
89       mi_free(mdb.library->files[i]);
90 
91    /* free all the playlists */
92    for (i = 0; i < mdb.nplaylists; i++)
93       playlist_free(mdb.playlists[i]);
94 
95    /* free all other allocated mdb members */
96    free(mdb.playlists);
97    free(mdb.db_file);
98    free(mdb.playlist_dir);
99 
100    /* reset counters */
101    mdb.nplaylists = 0;
102    mdb.playlists_capacity = 0;
103 }
104 
105 /* add a new playlist to the media library */
106 void
medialib_playlist_add(playlist * p)107 medialib_playlist_add(playlist *p)
108 {
109    playlist **new_playlists;
110    int        size;
111 
112    /* check to see if we need to resize the array */
113    if (mdb.nplaylists == mdb.playlists_capacity) {
114       mdb.playlists_capacity += MEDIALIB_PLAYLISTS_CHUNK_SIZE;
115       size = mdb.playlists_capacity * sizeof(playlist*);
116       if ((new_playlists = realloc(mdb.playlists, size)) == NULL)
117          err(1, "medialib_playlist_add: realloc failed");
118 
119       mdb.playlists = new_playlists;
120    }
121 
122    mdb.playlists[mdb.nplaylists++] = p;
123 }
124 
125 /*
126  * remove a playlist from the media library, and disk, given the playlist's
127  * index in the playlists array.
128  */
129 void
medialib_playlist_remove(int pindex)130 medialib_playlist_remove(int pindex)
131 {
132    int i;
133 
134    if (pindex < 0 || pindex >= mdb.nplaylists)
135       errx(1, "medialib_playlist_remove: index %d out of range", pindex);
136 
137    playlist_delete(mdb.playlists[pindex]);
138 
139    /* reorder */
140    for (i = pindex + 1; i < mdb.nplaylists; i++)
141       mdb.playlists[i - 1] = mdb.playlists[i];
142 
143    mdb.nplaylists--;
144 }
145 
146 /*
147  * create the vitunes directory, database file (initially empty, and
148  * playlists directory.
149  */
150 void
medialib_setup_files(const char * vitunes_dir,const char * db_file,const char * playlist_dir)151 medialib_setup_files(const char *vitunes_dir, const char *db_file,
152    const char *playlist_dir)
153 {
154    struct stat sb;
155    FILE *f;
156    int   version[3] = {DB_VERSION_MAJOR, DB_VERSION_MINOR, DB_VERSION_OTHER};
157 
158    /* create vitunes directory */
159    if (mkdir(vitunes_dir, S_IRWXU) == -1) {
160       if (errno == EEXIST)
161          warnx("vitunes directory '%s' already exists (OK)", vitunes_dir);
162       else
163          err(1, "unable to create vitunes directory '%s'", vitunes_dir);
164    } else
165       warnx("vitunes directory '%s' created", vitunes_dir);
166 
167    /* create playlists directory */
168    if (mkdir(playlist_dir, S_IRWXU) == -1) {
169       if (errno == EEXIST)
170          warnx("playlists directory '%s' already exists (OK)", playlist_dir);
171       else
172          err(1, "unable to create playlists directory '%s'", playlist_dir);
173    } else
174       warnx("playlists directory '%s' created", playlist_dir);
175 
176    /* create database file */
177    if (stat(db_file, &sb) < 0) {
178       if (errno == ENOENT) {
179 
180          /* open for writing */
181          if ((f = fopen(db_file, "w")) == NULL)
182             err(1, "failed to create database file '%s'", db_file);
183 
184          /* save header & version */
185          fwrite("vitunes", strlen("vitunes"), 1, f);
186          fwrite(version, sizeof(version), 1, f);
187 
188          warnx("empty database at '%s' created", db_file);
189          fclose(f);
190       } else
191          err(1, "database file '%s' exists, but cannot access it", db_file);
192    } else
193       warnx("database file '%s' already exists (OK)", db_file);
194 }
195 
196 /* used to sort media db by filenames. */
mi_cmp_fn(const void * ai,const void * bi)197 static int mi_cmp_fn(const void *ai, const void *bi)
198 {
199    const meta_info **a2 = (const meta_info **) ai;
200    const meta_info **b2 = (const meta_info **) bi;
201    const meta_info *a = (const meta_info *) *a2;
202    const meta_info *b = (const meta_info *) *b2;
203 
204    return strcmp(a->filename, b->filename);
205 }
206 
207 /* load the library database into the global media library */
208 void
medialib_db_load(const char * db_file)209 medialib_db_load(const char *db_file)
210 {
211    meta_info *mi;
212    FILE      *fin;
213    char       header[255] = { 0 };
214    int        version[3];
215 
216    if ((fin = fopen(db_file, "r")) == NULL)
217       err(1, "medialib_db_load: failed to open database file '%s'", db_file);
218 
219    /* read and check header & version */
220    fread(header, strlen("vitunes"), 1, fin);
221    if (strncmp(header, "vitunes", strlen("vitunes")) != 0)
222       errx(1, "medialib_db_load: db file '%s' NOT a vitunes database", db_file);
223 
224    fread(version, sizeof(version), 1, fin);
225    if (version[0] != DB_VERSION_MAJOR || version[1] != DB_VERSION_MINOR
226    ||  version[2] != DB_VERSION_OTHER) {
227       printf("Loading vitunes database: old database version detected.\n");
228       printf("\tExisting database at '%s' is of version %d.%d.%d\n",
229          db_file, version[0], version[1], version[2]);
230       printf("\tThis version of vitunes only works with version %d.%d.%d\n",
231          DB_VERSION_MAJOR, DB_VERSION_MINOR, DB_VERSION_OTHER);
232       printf("Remove the existing database and rebuild by doing:\n");
233       printf("\t$ rm %s\n", db_file);
234       printf("\t$ vitunes -e init\n");
235       printf("\t$ vitunes -e add /path/to/music ...\n");
236       fflush(stdout);
237       exit(1);
238    }
239 
240    /* read rest of records */
241    while (!feof(fin)) {
242       mi = mi_new();
243       mi_fread(mi, fin);
244       if (feof(fin))
245          mi_free(mi);
246       else if (ferror(fin))
247          err(1, "medialib_db_load: error loading database file '%s'", db_file);
248       else
249          playlist_files_append(mdb.library, &mi, 1, false);
250    }
251 
252    fclose(fin);
253 
254    /* sort library by filenames */
255    qsort(mdb.library->files, mdb.library->nfiles, sizeof(meta_info*), mi_cmp_fn);
256 }
257 
258 /* save the library database from the global media library to disk */
259 void
medialib_db_save(const char * db_file)260 medialib_db_save(const char *db_file)
261 {
262    FILE *fout;
263    int   version[3] = {DB_VERSION_MAJOR, DB_VERSION_MINOR, DB_VERSION_OTHER};
264    int   i;
265 
266    if ((fout = fopen(db_file, "w")) == NULL)
267       err(1, "medialib_db_save: failed to open database file '%s'", db_file);
268 
269    /* save header & version */
270    fwrite("vitunes", strlen("vitunes"), 1, fout);
271    fwrite(version, sizeof(version), 1, fout);
272 
273    /* save records */
274    for (i = 0; i < mdb.library->nfiles; i++) {
275       mi_fwrite(mdb.library->files[i], fout);
276       if (ferror(fout))
277          err(1, "medialib_db_save: error saving database");
278    }
279 
280    fclose(fout);
281 }
282 
283 /* flush the library to stdout in a csv format */
284 void
medialib_db_flush(FILE * fout,const char * timefmt)285 medialib_db_flush(FILE *fout, const char *timefmt)
286 {
287    meta_info *mi;
288    struct tm *ltime;
289    char stime[255];
290    int f, i;
291 
292    /* header row */
293    fprintf(fout, "filename, ");
294    for (i = 0; i < MI_NUM_CINFO; i++)
295       fprintf(fout, "\"%s\", ", MI_CINFO_NAMES[i]);
296 
297    fprintf(fout, "length-seconds, is_url, \"last-updated\"\n");
298    fflush(fout);
299 
300    /* start output of db */
301    for (f = 0; f < mdb.library->nfiles; f++) {
302 
303       /* get record */
304       mi = mdb.library->files[f];
305 
306       /* output record */
307       fprintf(fout, "%s, ", mi->filename);
308       for (i = 0; i < MI_NUM_CINFO; i++)
309          fprintf(fout, "\"%s\", ", mi->cinfo[i]);
310 
311       /* convert last-updated time to string */
312       ltime = localtime(&(mi->last_updated));
313       strftime(stime, sizeof(stime), timefmt, ltime);
314 
315       fprintf(fout, "%i, %s, \"%s\"\n",
316          mi->length, (mi->is_url ? "true" : "false"), stime);
317 
318       fflush(fout);
319    }
320 }
321 
322 /*
323  * AFTER loading the global media library using medialib_load(), this function
324  * is used to re-scan all files that exist in the database and re-check their
325  * meta_info.  Any files that no longer exist are removed, and any meta
326  * information that has changed is updated.
327  *
328  * The database is then re-saved to disk.
329  */
330 void
medialib_db_update(bool show_skipped)331 medialib_db_update(bool show_skipped)
332 {
333    meta_info *mi;
334    struct stat sb;
335    char  *filename;
336    int    i;
337 
338    /* stat counters */
339    int    count_removed_file_gone = 0;
340    int    count_removed_meta_gone = 0;
341    int    count_skipped_not_updated = 0;
342    int    count_updated = 0;
343    int    count_errors = 0;
344    int    count_urls = 0;
345 
346    for (i = 0; i < mdb.library->nfiles; i++) {
347 
348       filename = mdb.library->files[i]->filename;
349 
350       /* skip url's */
351       if (mdb.library->files[i]->is_url) {
352          printf("s %s\n", filename);
353          count_urls++;
354          continue;
355       }
356 
357       if (stat(filename, &sb) == -1) {
358 
359          /* file was removed -or- stat() failed */
360 
361          if (errno == ENOENT) {
362             /* file was removed, remove from library */
363             playlist_files_remove(mdb.library, i, 1, false);
364             i--;  /* since removed a file, we want to decrement i */
365             printf("x %s\n", filename);
366             count_removed_file_gone++;
367          } else {
368             /* stat() failed for some reason - unknown error */
369             printf("? %s\n", filename);
370             count_errors++;
371          }
372 
373       } else {
374 
375          /*
376           * file still exists... check if it has been modified since we
377           * last extracted meta-info from it (otherwise we ignore)
378           */
379 
380          if (sb.st_mtime > mdb.library->files[i]->last_updated) {
381 
382             mi = mi_extract(filename);
383             if (mi == NULL) {
384                /* file now has no meta-info, remove from library */
385                playlist_files_remove(mdb.library, i, 1, false);
386                i--;  /* since removed a file, we want to decrement i */
387                printf("- %s\n", filename);
388                count_removed_meta_gone++;
389             } else {
390                /* file's meta-info has changed, update it */
391                mi_sanitize(mi);
392                playlist_file_replace(mdb.library, i, mi);
393                printf("u %s\n", filename);
394                count_updated++;
395             }
396          } else {
397             count_skipped_not_updated++;
398             if (show_skipped)
399                printf(". %s\n", filename);
400          }
401 
402       }
403    }
404 
405    /* save to file */
406    medialib_db_save(mdb.db_file);
407 
408    /* output some of our stats */
409    printf("--------------------------------------------------\n");
410    printf("Results of updating database...\n");
411    printf("(s) %9d url's skipped\n", count_urls);
412    printf("(u) %9d files updated\n", count_updated);
413    printf("(x) %9d files removed (file no longer exists)\n",
414       count_removed_file_gone);
415    printf("(-) %9d files removed (meta-info gone)\n",
416       count_removed_meta_gone);
417    printf("(.) %9d files skipped (file unchanged since last checked)\n",
418       count_skipped_not_updated);
419    printf("(?) %9d files with errors (couldn't stat, but kept)\n",
420       count_errors);
421 }
422 
423 /*
424  * AFTER loading the global media library using medialib_load(), this function
425  * will scan the list of directories specified in the parameter and
426  */
427 void
medialib_db_scan_dirs(char * dirlist[])428 medialib_db_scan_dirs(char *dirlist[])
429 {
430    FTS        *fts;
431    FTSENT     *ftsent;
432    meta_info  *mi;
433    char        fullname[PATH_MAX];
434    int         i, idx;
435 
436    /* stat counters */
437    int         count_removed_lost_info = 0;
438    int         count_updated = 0;
439    int         count_skipped_no_info = 0;
440    int         count_skipped_dir = 0;
441    int         count_skipped_error = 0;
442    int         count_skipped_not_updated = 0;
443    int         count_added = 0;
444 
445 
446 
447    fts = fts_open(dirlist, FTS_LOGICAL | FTS_NOCHDIR, NULL);
448    if (fts == NULL)
449       err(1, "medialib_db_scan_dirs: fts_open failed");
450 
451    while ((ftsent = fts_read(fts)) != NULL) {
452 
453       switch (ftsent->fts_info) {   /* file type */
454          case FTS_D:    /* TYPE: directory (going in) */
455             printf("Checking Directory: %s\n", ftsent->fts_path);
456             break;
457 
458          case FTS_DP:   /* TYPE: directory (coming out) */
459             break;
460 
461          case FTS_DNR:  /* TYPE: unreadable directory */
462             printf("Directory '%s' Unreadable\n", ftsent->fts_accpath);
463             count_skipped_dir++;
464             break;
465 
466          case FTS_NS:   /* TYPE: file/dir that couldn't be stat(2) */
467          case FTS_ERR:  /* TYPE: other error */
468             printf("? %s\n", ftsent->fts_path);
469             count_skipped_error++;
470             break;
471 
472          case FTS_F:    /* TYPE: regular file */
473 
474             /* get the full name for the file */
475             if (realpath(ftsent->fts_accpath, fullname) == NULL) {
476                err(1, "medialib_db_scan_dirs: realpath failed for '%s'",
477                   ftsent->fts_accpath);
478             }
479 
480             /* check if the file already exists in the db */
481             idx = -1;
482             for (i = 0; i < mdb.library->nfiles; i++) {
483                if (strcmp(fullname, mdb.library->files[i]->filename) == 0)
484                   idx = i;
485             }
486 
487             if (idx != -1) {
488                /* file already exists in library database - update */
489 
490                if (ftsent->fts_statp->st_mtime >
491                    mdb.library->files[idx]->last_updated) {
492 
493                   /* file has been modified since we last extracted info */
494 
495                   mi = mi_extract(ftsent->fts_accpath);
496 
497                   if (mi == NULL) {
498                      /* file now has no meta-info, remove from library */
499                      playlist_files_remove(mdb.library, idx, 1, false);
500                      printf("- %s\n", ftsent->fts_accpath);
501                      count_removed_lost_info++;
502                   } else {
503                      /* file's meta-info has changed, update it */
504                      mi_sanitize(mi);
505                      playlist_file_replace(mdb.library, idx, mi);
506                      printf("u %s\n", ftsent->fts_accpath);
507                      count_updated++;
508                   }
509                } else {
510                   printf(". %s\n", ftsent->fts_accpath);
511                   count_skipped_not_updated++;
512                }
513 
514             } else {
515 
516                /* file does NOT exists in library database - add it */
517 
518                mi = mi_extract(ftsent->fts_accpath);
519 
520                if (mi == NULL) {
521                   /* file has no info */
522                   printf("s %s\n", ftsent->fts_accpath);
523                   count_skipped_no_info++;
524                } else {
525                   /* file does have info, add it to library */
526                   mi_sanitize(mi);
527                   playlist_files_append(mdb.library, &mi, 1, false);
528                   printf("+ %s\n", ftsent->fts_accpath);
529                   count_added++;
530                }
531             }
532       }
533    }
534 
535    if (fts_close(fts) == -1)
536       err(1, "medialib_db_scan_dirs: failed to close file heirarchy");
537 
538    /* save to file */
539    medialib_db_save(mdb.db_file);
540 
541    /* output some of our stats */
542    printf("--------------------------------------------------\n");
543    printf("Results of scanning directories...\n");
544    printf("(+) %9d files added\n", count_added);
545    printf("(u) %9d files updated\n", count_updated);
546    printf("(-) %9d files removed (was in DB, but no longer has meta-info)\n",
547       count_removed_lost_info);
548    printf("(.) %9d files skipped (in DB, file unchanged since last checked)\n",
549       count_skipped_not_updated);
550    printf("(s) %9d files skipped (no info)\n", count_skipped_no_info);
551    printf("(?) %9d files skipped (other error)\n", count_skipped_error);
552    printf("    %9d directories skipped (couldn't read)\n", count_skipped_dir);
553 }
554