diff --git a/README b/README index 4d16d0f8..996ff114 100644 --- a/README +++ b/README @@ -257,6 +257,12 @@ be offered until they've been scanned. Changes to the library are reflected in real time after the initial scan. The directories are monitored for changes and rescanned on the fly. +If you place a file with the filename ending .force-rescan in your library, +you can trigger a full rescan of your library. This will clear all music and +playlists from forked-daapd's database and initiate a fresh bulk scan. Pairing +and speaker information will be kept. Only use this for troubleshooting, it is +not necessary during normal operation. + Symlinks are supported and dereferenced. This does interact in tricky ways with the above monitoring and rescanning, so you've been warned. Changes to symlinks themselves won't be taken into account, or not the way you'd expect. diff --git a/forked-daapd.conf b/forked-daapd.conf index a6035bcb..7be42a22 100644 --- a/forked-daapd.conf +++ b/forked-daapd.conf @@ -44,6 +44,14 @@ library { # a single name which will be used for all music in the compilation dir compilation_artist = "Various artists" + # There are 5 default playlists: "Library", "Music", "Movies", "TV Shows" + # and "Podcasts". Here you can change the names of these playlists. +# name_library = "Library" +# name_music = "Music" +# name_movies = "Movies" +# name_tvshows = "TV Shows" +# name_podcasts = "Podcasts" + # Artwork file names (without file type extension) # forked-daapd will look for jpg and png files with these base names # artwork_basenames = { "artwork", "cover", "Folder" } diff --git a/src/conffile.c b/src/conffile.c index dedb7d28..ebff3f6e 100644 --- a/src/conffile.c +++ b/src/conffile.c @@ -63,6 +63,11 @@ static cfg_opt_t sec_library[] = CFG_STR_LIST("podcasts", NULL, CFGF_NONE), CFG_STR_LIST("compilations", NULL, CFGF_NONE), CFG_STR("compilation_artist", NULL, CFGF_NONE), + CFG_STR("name_library", "Library", CFGF_NONE), + CFG_STR("name_music", "Music", CFGF_NONE), + CFG_STR("name_movies", "Movies", CFGF_NONE), + CFG_STR("name_tvshows", "TV Shows", CFGF_NONE), + CFG_STR("name_podcasts", "Podcasts", CFGF_NONE), CFG_STR_LIST("artwork_basenames", "{artwork,cover,Folder}", CFGF_NONE), CFG_STR_LIST("filetypes_ignore", "{.db,.ini}", CFGF_NONE), CFG_BOOL("itunes_overrides", cfg_false, CFGF_NONE), diff --git a/src/db.c b/src/db.c index 99c414d0..13ecc2b3 100644 --- a/src/db.c +++ b/src/db.c @@ -588,6 +588,55 @@ db_analyze(void) } } +/* Set names of default playlists according to config */ +static void +db_set_cfg_names(void) +{ +#define Q_TMPL "UPDATE playlists SET title = '%q' WHERE type = 1 AND special_id = %d;" + char *cfg_item[5] = { "name_library", "name_music", "name_movies", "name_tvshows", "name_podcasts" }; + char special_id[5] = { 0, 6, 4, 5, 1 }; + cfg_t *lib; + char *query; + char *title; + char *errmsg; + int ret; + int i; + + lib = cfg_getsec(cfg, "library"); + + for (i = 0; i < (sizeof(cfg_item) / sizeof(cfg_item[0])); i++) + { + title = cfg_getstr(lib, cfg_item[i]); + if (!title) + { + DPRINTF(E_LOG, L_DB, "Internal error, unknown config item '%s'\n", cfg_item[i]); + + continue; + } + + query = sqlite3_mprintf(Q_TMPL, title, special_id[i]); + if (!query) + { + DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); + + return; + } + + ret = db_exec(query, &errmsg); + if (ret != SQLITE_OK) + { + DPRINTF(E_LOG, L_DB, "Error setting playlist title, query %s, error: %s\n", query, errmsg); + + sqlite3_free(errmsg); + } + else + DPRINTF(E_DBG, L_DB, "Playlist title for config item '%s' set with query '%s'\n", cfg_item[i], query); + + sqlite3_free(query); + } +#undef Q_TMPL +} + void db_hook_post_scan(void) { @@ -651,6 +700,36 @@ db_purge_cruft(time_t ref) } +void +db_purge_all(void) +{ + char *queries[4] = + { + "DELETE FROM inotify;", + "DELETE FROM playlistitems;", + "DELETE FROM playlists WHERE type <> 1;", + "DELETE FROM files;" + }; + char *errmsg; + int i; + int ret; + + for (i = 0; i < (sizeof(queries) / sizeof(queries[0])); i++) + { + DPRINTF(E_DBG, L_DB, "Running purge query '%s'\n", queries[i]); + + ret = db_exec(queries[i], &errmsg); + if (ret != SQLITE_OK) + { + DPRINTF(E_LOG, L_DB, "Purge query %d error: %s\n", i, errmsg); + + sqlite3_free(errmsg); + } + else + DPRINTF(E_DBG, L_DB, "Purged %d rows\n", sqlite3_changes(hdl)); + } +} + static int db_get_count(char *query) { @@ -1566,12 +1645,13 @@ db_files_get_count(void) } int -db_files_get_count_bypathpattern(char *path) +db_files_get_count_bymatch(char *path) { +#define Q_TMPL "SELECT COUNT(*) FROM files f WHERE f.path LIKE '%%%q';" char *query; int count; - query = sqlite3_mprintf("SELECT COUNT(*) FROM files f WHERE f.path LIKE '%%%q';", path); + query = sqlite3_mprintf(Q_TMPL, path); if (!query) { DPRINTF(E_LOG, L_DB, "Out of memory making count query string.\n"); @@ -1583,6 +1663,7 @@ db_files_get_count_bypathpattern(char *path) sqlite3_free(query); return count; +#undef Q_TMPL } void @@ -1659,6 +1740,34 @@ db_file_ping(int id) #undef Q_TMPL } +void +db_file_ping_bymatch(char *path) +{ +#define Q_TMPL "UPDATE files SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q/%%';" + char *query; + char *errmsg; + int ret; + + query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), path); + if (!query) + { + DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); + + return; + } + + DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); + + ret = db_exec(query, &errmsg); + if (ret != SQLITE_OK) + DPRINTF(E_LOG, L_DB, "Error pinging files matching %s: %s\n", path, errmsg); + + sqlite3_free(errmsg); + sqlite3_free(query); + +#undef Q_TMPL +} + char * db_file_path_byid(int id) { @@ -1785,7 +1894,7 @@ db_file_id_bypath(char *path) } int -db_file_id_bypathpattern(char *path) +db_file_id_bymatch(char *path) { #define Q_TMPL "SELECT f.id FROM files f WHERE f.path LIKE '%%%q';" char *query; @@ -2441,6 +2550,34 @@ db_pl_ping(int id) #undef Q_TMPL } +void +db_pl_ping_bymatch(char *path) +{ +#define Q_TMPL "UPDATE playlists SET db_timestamp = %" PRIi64 " WHERE path LIKE '%q/%%';" + char *query; + char *errmsg; + int ret; + + query = sqlite3_mprintf(Q_TMPL, (int64_t)time(NULL), path); + if (!query) + { + DPRINTF(E_LOG, L_DB, "Out of memory for query string\n"); + + return; + } + + DPRINTF(E_DBG, L_DB, "Running query '%s'\n", query); + + ret = db_exec(query, &errmsg); + if (ret != SQLITE_OK) + DPRINTF(E_LOG, L_DB, "Error pinging playlists matching %s: %s\n", path, errmsg); + + sqlite3_free(errmsg); + sqlite3_free(query); + +#undef Q_TMPL +} + static int db_pl_id_bypath(char *path, int *id) { @@ -4959,6 +5096,8 @@ db_init(void) db_analyze(); + db_set_cfg_names(); + files = db_files_get_count(); pls = db_pl_get_count(); diff --git a/src/db.h b/src/db.h index e99f8338..67fc6214 100644 --- a/src/db.h +++ b/src/db.h @@ -295,6 +295,9 @@ db_hook_post_scan(void); void db_purge_cruft(time_t ref); +void +db_purge_all(void); + /* Queries */ int db_query_start(struct query_params *qp); @@ -322,7 +325,7 @@ int db_files_get_count(void); int -db_files_get_count_bypathpattern(char *path); +db_files_get_count_bymatch(char *path); void db_files_update_songalbumid(void); @@ -333,6 +336,9 @@ db_file_inc_playcount(int id); void db_file_ping(int id); +void +db_file_ping_bymatch(char *path); + char * db_file_path_byid(int id); @@ -340,7 +346,7 @@ int db_file_id_bypath(char *path); int -db_file_id_bypathpattern(char *path); +db_file_id_bymatch(char *path); int db_file_id_byfilebase(char *filename, char *base); @@ -382,6 +388,9 @@ db_pl_get_count(void); void db_pl_ping(int id); +void +db_pl_ping_bymatch(char *path); + struct playlist_info * db_pl_fetch_bypath(char *path); diff --git a/src/filescanner.c b/src/filescanner.c index 1ef84938..d3316f28 100644 --- a/src/filescanner.c +++ b/src/filescanner.c @@ -90,6 +90,9 @@ static pthread_t tid_scan; static struct deferred_pl *playlists; static struct stacked_dir *dirstack; +/* Forward */ +static void +bulk_scan(void); static int push_dir(struct stacked_dir **s, char *path) @@ -588,6 +591,19 @@ process_file(char *file, time_t mtime, off_t size, int type, int flags) return; } + else if (strcmp(ext, ".force-rescan") == 0) + { + if (flags & F_SCAN_BULK) + return; + else + { + DPRINTF(E_LOG, L_SCAN, "Forcing full rescan, found force-rescan file: %s\n", file); + db_purge_all(); + bulk_scan(); + + return; + } + } } /* Not any kind of special file, so let's see if it's a media file */ @@ -859,6 +875,13 @@ bulk_scan(void) { DPRINTF(E_LOG, L_SCAN, "Skipping library directory %s, could not dereference: %s\n", path, strerror(errno)); + /* Assume dir is mistakenly not mounted, so just disable everything and update timestamps */ + db_file_disable_bymatch(path, "", 0); + db_pl_disable_bymatch(path, "", 0); + + db_file_ping_bymatch(path); + db_pl_ping_bymatch(path); + continue; } diff --git a/src/filescanner_itunes.c b/src/filescanner_itunes.c index 46a727fb..fad7d85e 100644 --- a/src/filescanner_itunes.c +++ b/src/filescanner_itunes.c @@ -301,13 +301,13 @@ find_track_file(char *location) entry = location; DPRINTF(E_SPAM, L_SCAN, "iTunes XML playlist entry is now %s\n", entry); - ret = db_files_get_count_bypathpattern(entry); + ret = db_files_get_count_bymatch(entry); } while (ptr && (ret > 1)); if (ret > 0) { - mfi_id = db_file_id_bypathpattern(entry); + mfi_id = db_file_id_bymatch(entry); DPRINTF(E_DBG, L_SCAN, "Found iTunes XML playlist entry match, id is %d, entry is %s\n", mfi_id, entry); free(location); diff --git a/src/filescanner_m3u.c b/src/filescanner_m3u.c index e18bf329..0ab05888 100644 --- a/src/filescanner_m3u.c +++ b/src/filescanner_m3u.c @@ -242,13 +242,13 @@ scan_m3u_playlist(char *file, time_t mtime) entry = buf; DPRINTF(E_SPAM, L_SCAN, "Playlist entry is now %s\n", entry); - ret = db_files_get_count_bypathpattern(entry); + ret = db_files_get_count_bymatch(entry); } while (ptr && (ret > 1)); if (ret > 0) { - mfi_id = db_file_id_bypathpattern(entry); + mfi_id = db_file_id_bymatch(entry); DPRINTF(E_DBG, L_SCAN, "Found playlist entry match, id is %d, entry is %s\n", mfi_id, entry); filename = db_file_path_byid(mfi_id);