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