diff options
Diffstat (limited to '')
-rw-r--r-- | src/update.c | 444 |
1 files changed, 444 insertions, 0 deletions
diff --git a/src/update.c b/src/update.c new file mode 100644 index 000000000..ca3640b73 --- /dev/null +++ b/src/update.c @@ -0,0 +1,444 @@ +/* the Music Player Daemon (MPD) + * Copyright (C) 2003-2007 by Warren Dukes (warren.dukes@gmail.com) + * Copyright (C) 2008 Max Kellermann <max@duempel.org> + * This project's homepage is: http://www.musicpd.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "update.h" +#include "database.h" +#include "log.h" +#include "ls.h" +#include "path.h" +#include "playlist.h" +#include "utils.h" +#include "main_notify.h" +#include "condition.h" +#include "update.h" + +static enum update_progress { + UPDATE_PROGRESS_IDLE = 0, + UPDATE_PROGRESS_RUNNING = 1, + UPDATE_PROGRESS_DONE = 2 +} progress; + +static int modified; + +/* make this dynamic?, or maybe this is big enough... */ +static char *update_paths[32]; +static size_t update_paths_nr; + +static pthread_t update_thr; + +static const unsigned update_task_id_max = 1 << 15; + +static unsigned update_task_id; + +static struct mpd_song *delete; + +static struct condition delete_cond; + +unsigned isUpdatingDB(void) +{ + return (progress != UPDATE_PROGRESS_IDLE) ? update_task_id : 0; +} + +static void directory_set_stat(struct directory *dir, const struct stat *st) +{ + dir->inode = st->st_ino; + dir->device = st->st_dev; + dir->stat = 1; +} + +static void delete_song(struct directory *dir, struct mpd_song *del) +{ + /* first, prevent traversers in main task from getting this */ + songvec_delete(&dir->songs, del); + + /* now take it out of the playlist (in the main_task) */ + cond_enter(&delete_cond); + assert(!delete); + delete = del; + wakeup_main_task(); + do { cond_wait(&delete_cond); } while (delete); + cond_leave(&delete_cond); + + /* finally, all possible references gone, free it */ + song_free(del); +} + +static int delete_each_song(struct mpd_song *song, mpd_unused void *data) +{ + struct directory *dir = data; + assert(song->parent == dir); + delete_song(dir, song); + return 0; +} + +/** + * Recursively remove all sub directories and songs from a directory, + * leaving an empty directory. + */ +static void clear_directory(struct directory *dir) +{ + int i; + + for (i = dir->children.nr; --i >= 0;) + clear_directory(dir->children.base[i]); + dirvec_clear(&dir->children); + + songvec_for_each(&dir->songs, delete_each_song, dir); +} + +/** + * Recursively free a directory and all its contents. + */ +static void delete_directory(struct directory *dir) +{ + assert(dir->parent != NULL); + + clear_directory(dir); + dirvec_delete(&dir->parent->children, dir); + directory_free(dir); +} + +struct delete_data { + char *tmp; + struct directory *dir; +}; + +/* passed to songvec_for_each */ +static int delete_song_if_removed(struct mpd_song *song, void *_data) +{ + struct delete_data *data = _data; + + data->tmp = song_get_url(song, data->tmp); + assert(data->tmp); + + if (!isFile(data->tmp, NULL)) { + delete_song(data->dir, song); + modified = 1; + } + return 0; +} + +static void delete_path(const char *path) +{ + struct directory *dir = db_get_directory(path); + struct mpd_song *song = db_get_song(path); + + if (dir) { + delete_directory(dir); + modified = 1; + } + if (song) { + delete_song(song->parent, song); + modified = 1; + } +} + +static void +removeDeletedFromDirectory(char *path_max_tmp, struct directory *dir) +{ + int i; + struct dirvec *dv = &dir->children; + struct delete_data data; + + for (i = dv->nr; --i >= 0; ) { + if (isDir(dv->base[i]->path)) + continue; + LOG("removing directory: %s\n", dv->base[i]->path); + dirvec_delete(dv, dv->base[i]); + modified = 1; + } + + data.dir = dir; + data.tmp = path_max_tmp; + songvec_for_each(&dir->songs, delete_song_if_removed, &data); +} + +static const char *opendir_path(char *path_max_tmp, const char *dirname) +{ + if (*dirname != '\0') + return rmp2amp_r(path_max_tmp, + utf8_to_fs_charset(path_max_tmp, dirname)); + return musicDir; +} + +static int statDirectory(struct directory *dir) +{ + struct stat st; + + if (myStat(directory_get_path(dir), &st) < 0) + return -1; + + directory_set_stat(dir, &st); + + return 0; +} + +static int +inodeFoundInParent(struct directory *parent, ino_t inode, dev_t device) +{ + while (parent) { + if (!parent->stat && statDirectory(parent) < 0) + return -1; + if (parent->inode == inode && parent->device == device) { + DEBUG("recursive directory found\n"); + return 1; + } + parent = parent->parent; + } + + return 0; +} + +static int updateDirectory(struct directory *dir, const struct stat *st); + +static void +updateInDirectory(struct directory *dir, + const char *name, const struct stat *st) +{ + if (S_ISREG(st->st_mode) && hasMusicSuffix(name, 0)) { + const char *shortname = mpd_basename(name); + struct mpd_song *song; + + if (!(song = songvec_find(&dir->songs, shortname))) { + if (!(song = song_file_load(shortname, dir))) + return; + songvec_add(&dir->songs, song); + modified = 1; + LOG("added %s\n", name); + } else if (st->st_mtime != song->mtime) { + LOG("updating %s\n", name); + if (!song_file_update(song)) + delete_song(dir, song); + modified = 1; + } + } else if (S_ISDIR(st->st_mode)) { + struct directory *subdir; + + if (inodeFoundInParent(dir, st->st_ino, st->st_dev)) + return; + + if (!(subdir = directory_get_child(dir, name))) + subdir = directory_new_child(dir, name); + + assert(dir == subdir->parent); + + if (!updateDirectory(subdir, st)) + delete_directory(subdir); + } else { + DEBUG("update: %s is not a directory or music\n", name); + } +} + +/* we don't look at hidden files nor files with newlines in them */ +static int skip_path(const char *path) +{ + return (path[0] == '.' || strchr(path, '\n')) ? 1 : 0; +} + +static int updateDirectory(struct directory *dir, const struct stat *st) +{ + DIR *fs_dir; + const char *dirname = directory_get_path(dir); + struct dirent *ent; + char path_max_tmp[MPD_PATH_MAX]; + + assert(S_ISDIR(st->st_mode)); + + directory_set_stat(dir, st); + + if (!(fs_dir = opendir(opendir_path(path_max_tmp, dirname)))) + return 0; + + removeDeletedFromDirectory(path_max_tmp, dir); + + while ((ent = readdir(fs_dir))) { + char *utf8; + struct stat st2; + + if (skip_path(ent->d_name)) + continue; + + utf8 = fs_charset_to_utf8(path_max_tmp, ent->d_name); + if (!utf8) + continue; + + if (!isRootDirectory(dir->path)) + utf8 = pfx_dir(path_max_tmp, utf8, strlen(utf8), + dirname, strlen(dirname)); + + if (myStat(path_max_tmp, &st2) == 0) + updateInDirectory(dir, path_max_tmp, &st2); + else + delete_path(path_max_tmp); + } + + closedir(fs_dir); + + return 1; +} + +static struct directory * +directory_make_child_checked(struct directory *parent, const char *path) +{ + struct directory *dir; + struct stat st; + struct mpd_song *conflicting; + + if ((dir = directory_get_child(parent, path))) + return dir; + + if (myStat(path, &st) < 0 || + inodeFoundInParent(parent, st.st_ino, st.st_dev)) + return NULL; + + /* if we're adding directory paths, make sure to delete filenames + with potentially the same name */ + if ((conflicting = songvec_find(&parent->songs, mpd_basename(path)))) + delete_song(parent, conflicting); + + dir = directory_new_child(parent, path); + directory_set_stat(dir, &st); + return dir; +} + +static struct directory * +addParentPathToDB(const char *utf8path) +{ + struct directory *dir = db_get_root(); + char *duplicated = xstrdup(utf8path); + char *slash = duplicated; + + while ((slash = strchr(slash, '/'))) { + *slash = 0; + + dir = directory_make_child_checked(dir, duplicated); + if (!dir || !slash) + break; + + *slash++ = '/'; + } + + free(duplicated); + return dir; +} + +static void updatePath(const char *utf8path) +{ + struct stat st; + + if (myStat(utf8path, &st) < 0) + delete_path(utf8path); + else + updateInDirectory(addParentPathToDB(utf8path), utf8path, &st); +} + +static void * update_task(void *_path) +{ + if (_path != NULL && !isRootDirectory(_path)) { + updatePath((char *)_path); + free(_path); + } else { + struct directory *dir = db_get_root(); + struct stat st; + + if (myStat(directory_get_path(dir), &st) == 0) + updateDirectory(dir, &st); + } + + if (modified) + db_save(); + progress = UPDATE_PROGRESS_DONE; + wakeup_main_task(); + return NULL; +} + +static void spawn_update_task(char *path) +{ + pthread_attr_t attr; + + assert(pthread_equal(pthread_self(), main_task)); + + progress = UPDATE_PROGRESS_RUNNING; + modified = 0; + pthread_attr_init(&attr); + if (pthread_create(&update_thr, &attr, update_task, path)) + FATAL("Failed to spawn update task: %s\n", strerror(errno)); + if (++update_task_id > update_task_id_max) + update_task_id = 1; + DEBUG("spawned thread for update job id %i\n", update_task_id); +} + +unsigned directory_update_init(char *path) +{ + assert(pthread_equal(pthread_self(), main_task)); + + if (progress != UPDATE_PROGRESS_IDLE) { + unsigned next_task_id; + + if (!path) + return 0; + if (update_paths_nr == ARRAY_SIZE(update_paths)) { + free(path); + return 0; + } + + assert(update_paths_nr < ARRAY_SIZE(update_paths)); + update_paths[update_paths_nr++] = path; + next_task_id = update_task_id + update_paths_nr; + + return next_task_id > update_task_id_max ? 1 : next_task_id; + } + spawn_update_task(path); + return update_task_id; +} + +void reap_update_task(void) +{ + assert(pthread_equal(pthread_self(), main_task)); + + if (progress == UPDATE_PROGRESS_IDLE) + return; + + cond_enter(&delete_cond); + if (delete) { + char tmp[MPD_PATH_MAX]; + LOG("removing: %s\n", song_get_url(delete, tmp)); + deleteASongFromPlaylist(delete); + delete = NULL; + cond_signal(&delete_cond); + } + cond_leave(&delete_cond); + + if (progress != UPDATE_PROGRESS_DONE) + return; + if (pthread_join(update_thr, NULL)) + FATAL("error joining update thread: %s\n", strerror(errno)); + + if (modified) + playlistVersionChange(); + + if (update_paths_nr) { + char *path = update_paths[0]; + memmove(&update_paths[0], &update_paths[1], + --update_paths_nr * sizeof(char *)); + spawn_update_task(path); + } else { + progress = UPDATE_PROGRESS_IDLE; + } +} |