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