1 /*
2 Album Art plugin Cache Cleaner for DeaDBeeF
3 Copyright (C) 2014 Ian Nartowicz <deadbeef@nartowicz.co.uk>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17
18 2. Altered source versions must be plainly marked as such, and must not be
19 misrepresented as being the original software.
20
21 3. This notice may not be removed or altered from any source distribution.
22 */
23
24 #ifdef HAVE_CONFIG_H
25 #include "../../config.h"
26 #endif
27 #include <stdlib.h>
28 #include <string.h>
29 #include <time.h>
30 #include <libgen.h>
31 #include <dirent.h>
32 #include <unistd.h>
33 #include <pthread.h>
34 #include <sys/stat.h>
35 #include <limits.h>
36 #include "artwork_internal.h"
37 #include "../../deadbeef.h"
38
39 //#define trace(...) { fprintf(stderr, __VA_ARGS__); }
40 #define trace(...)
41
42 extern DB_functions_t *deadbeef;
43
44 static uintptr_t files_mutex;
45 static intptr_t tid;
46 static uintptr_t thread_mutex;
47 static uintptr_t thread_cond;
48 static int terminate;
49 static int32_t cache_expiry_seconds;
50
cache_lock(void)51 void cache_lock (void)
52 {
53 deadbeef->mutex_lock (files_mutex);
54 }
55
cache_unlock(void)56 void cache_unlock (void)
57 {
58 deadbeef->mutex_unlock (files_mutex);
59 }
60
make_cache_root_path(char * path,const size_t size)61 int make_cache_root_path (char *path, const size_t size)
62 {
63 const char *xdg_cache = getenv ("XDG_CACHE_HOME");
64 const char *cache_root = xdg_cache ? xdg_cache : getenv ("HOME");
65 if (snprintf (path, size, xdg_cache ? "%s/deadbeef/" : "%s/.cache/deadbeef/", cache_root) >= size) {
66 trace ("Cache root path truncated at %d bytes\n", (int)size);
67 return -1;
68 }
69 return 0;
70 }
71
72 static int
filter_scaled_dirs(const struct dirent * f)73 filter_scaled_dirs (const struct dirent *f)
74 {
75 return !strncasecmp (f->d_name, "covers-", 7);
76 }
77
remove_cache_item(const char * entry_path,const char * subdir_path,const char * subdir_name,const char * entry_name)78 void remove_cache_item (const char *entry_path, const char *subdir_path, const char *subdir_name, const char *entry_name)
79 {
80 /* Unlink the expired file, and the artist directory if it is empty */
81 cache_lock ();
82 unlink (entry_path);
83 rmdir (subdir_path);
84
85 /* Remove any scaled copies of this file, plus parent directories that are now empty */
86 char cache_root_path[PATH_MAX];
87 make_cache_root_path (cache_root_path, PATH_MAX);
88 struct dirent **scaled_dirs = NULL;
89 int scaled_dirs_count = scandir (cache_root_path, &scaled_dirs, filter_scaled_dirs, NULL);
90 if (scaled_dirs_count < 0) {
91 cache_unlock ();
92 return;
93 }
94 for (int i = 0; i < scaled_dirs_count; i++) {
95 char scaled_entry_path[PATH_MAX];
96 if (snprintf (scaled_entry_path, PATH_MAX, "%s%s/%s/%s", cache_root_path, scaled_dirs[i]->d_name, subdir_name, entry_name) < PATH_MAX) {
97 unlink (scaled_entry_path);
98 char *scaled_entry_dir = dirname (scaled_entry_path);
99 rmdir (scaled_entry_dir);
100 rmdir (dirname (scaled_entry_dir));
101 }
102 free (scaled_dirs[i]);
103 }
104 free (scaled_dirs);
105 cache_unlock ();
106 }
107
108 static int
path_ok(const size_t dir_length,const char * entry)109 path_ok (const size_t dir_length, const char *entry)
110 {
111 return strcmp (entry, ".") && strcmp (entry, "..") && dir_length + strlen (entry) + 1 < PATH_MAX;
112 }
113
114 static void
cache_cleaner_thread(void * none)115 cache_cleaner_thread (void *none)
116 {
117 /* Find where it all happens */
118 char covers_path[PATH_MAX];
119 if (make_cache_root_path (covers_path, PATH_MAX-10)) {
120 return;
121 }
122 strcat (covers_path, "covers");
123 const size_t covers_path_length = strlen (covers_path);
124
125 deadbeef->mutex_lock (thread_mutex);
126 while (!terminate) {
127 time_t oldest_mtime = time (NULL);
128
129 /* Loop through the artist directories */
130 DIR *covers_dir = opendir (covers_path);
131 struct dirent *covers_subdir;
132 while (!terminate && covers_dir && (covers_subdir = readdir (covers_dir))) {
133 const int32_t cache_secs = cache_expiry_seconds;
134 deadbeef->mutex_unlock (thread_mutex);
135 if (cache_secs > 0 && path_ok (covers_path_length, covers_subdir->d_name)) {
136 trace ("Analyse %s for expired files\n", covers_subdir->d_name);
137 const time_t cache_expiry = time (NULL) - cache_secs;
138
139 /* Loop through the image files in this artist directory */
140 char subdir_path[PATH_MAX];
141 sprintf (subdir_path, "%s/%s", covers_path, covers_subdir->d_name);
142 const size_t subdir_path_length = strlen (subdir_path);
143 DIR *subdir = opendir (subdir_path);
144 struct dirent *entry;
145 while (subdir && (entry = readdir (subdir))) {
146 if (path_ok (subdir_path_length, entry->d_name)) {
147 char entry_path[PATH_MAX];
148 sprintf (entry_path, "%s/%s", subdir_path, entry->d_name);
149
150 /* Test against the cache expiry time (cache invalidation resets are not handled here) */
151 struct stat stat_buf;
152 if (!stat (entry_path, &stat_buf)) {
153 if (stat_buf.st_mtime <= cache_expiry) {
154 trace ("%s expired from cache\n", entry_path);
155 remove_cache_item (entry_path, subdir_path, covers_subdir->d_name, entry->d_name);
156 }
157 else if (stat_buf.st_mtime < oldest_mtime) {
158 oldest_mtime = stat_buf.st_mtime;
159 }
160 }
161 }
162 }
163 if (subdir) {
164 closedir (subdir);
165 }
166 }
167 usleep (100000);
168 deadbeef->mutex_lock (thread_mutex);
169 }
170 if (covers_dir) {
171 closedir (covers_dir);
172 covers_dir = NULL;
173 }
174
175 /* Sleep until just after the oldest file expires */
176 if (cache_expiry_seconds > 0 && !terminate) {
177 struct timespec wake_time = {
178 .tv_sec = time (NULL) + max (60, oldest_mtime - time (NULL) + cache_expiry_seconds),
179 .tv_nsec = 999999
180 };
181 trace ("Cache cleaner sleeping for %d seconds\n", max (60, oldest_mtime - time (NULL) + cache_expiry_seconds));
182 pthread_cond_timedwait ( (pthread_cond_t *)thread_cond, (pthread_mutex_t *)thread_mutex, &wake_time);
183 }
184
185 /* Just go back to sleep if cache expiry is disabled */
186 while (cache_expiry_seconds <= 0 && !terminate) {
187 trace ("Cache cleaner sleeping forever\n");
188 pthread_cond_wait ( (pthread_cond_t *)thread_cond, (pthread_mutex_t *)thread_mutex);
189 }
190 }
191 deadbeef->mutex_unlock (thread_mutex);
192 }
193
cache_configchanged(void)194 void cache_configchanged (void)
195 {
196 const int32_t new_cache_expiry_seconds = deadbeef->conf_get_int ("artwork.cache.period", 48) * 60 * 60;
197 if (new_cache_expiry_seconds != cache_expiry_seconds) {
198 deadbeef->mutex_lock (thread_mutex);
199 cache_expiry_seconds = new_cache_expiry_seconds;
200 deadbeef->cond_signal (thread_cond);
201 deadbeef->mutex_unlock (thread_mutex);
202 }
203 }
204
stop_cache_cleaner(void)205 void stop_cache_cleaner (void)
206 {
207 if (tid) {
208 deadbeef->mutex_lock (thread_mutex);
209 terminate = 1;
210 deadbeef->cond_signal (thread_cond);
211 deadbeef->mutex_unlock (thread_mutex);
212 deadbeef->thread_join (tid);
213 tid = 0;
214 trace ("Cache cleaner thread stopped\n");
215 }
216
217 if (thread_mutex) {
218 deadbeef->mutex_free (thread_mutex);
219 thread_mutex = 0;
220 }
221
222 if (thread_cond) {
223 deadbeef->cond_free (thread_cond);
224 thread_cond = 0;
225 }
226
227 if (files_mutex) {
228 deadbeef->mutex_free (files_mutex);
229 files_mutex = 0;
230 }
231 }
232
start_cache_cleaner(void)233 int start_cache_cleaner (void)
234 {
235 terminate = 0;
236 cache_expiry_seconds = deadbeef->conf_get_int ("artwork.cache.period", 48) * 60 * 60;
237 files_mutex = deadbeef->mutex_create_nonrecursive ();
238 thread_mutex = deadbeef->mutex_create_nonrecursive ();
239 thread_cond = deadbeef->cond_create ();
240 if (files_mutex && thread_mutex && thread_cond) {
241 tid = deadbeef->thread_start_low_priority (cache_cleaner_thread, NULL);
242 trace ("Cache cleaner thread started\n");
243 }
244
245 if (!tid) {
246 stop_cache_cleaner ();
247 return -1;
248 }
249
250 return 0;
251 }
252