#include #include #include #include #include #include #include #include #include "../../deadbeef.h" #include "artwork.h" #include "lastfm.h" #include "albumartorg.h" #define min(x,y) ((x)<(y)?(x):(y)) //#define trace(...) { fprintf(stderr, __VA_ARGS__); } #define trace(...) #define DEFAULT_COVER_PATH (PREFIX "/share/deadbeef/pixmaps/blank_cd.jpg") static DB_artwork_plugin_t plugin; DB_functions_t *deadbeef; typedef struct cover_query_s { char *fname; char *artist; char *album; artwork_callback callback; struct cover_query_s *next; } cover_query_t; cover_query_t *queue; cover_query_t *queue_tail; uintptr_t mutex; int terminate; intptr_t tid; void make_cache_dir_path (char *path, int size, const char *album, const char *artist) { int sz = snprintf (path, size, "%s/artcache/", deadbeef->get_config_dir ()); size -= sz; path += sz; sz = snprintf (path, size, "%s", artist); for (char *p = path; *p; p++) { if (*p == '/') { *p = '_'; } } } void make_cache_path (char *path, int size, const char *album, const char *artist) { int sz = snprintf (path, size, "%s/artcache/", deadbeef->get_config_dir ()); size -= sz; path += sz; sz = snprintf (path, size, "%s", artist); for (char *p = path; *p; p++) { if (*p == '/') { *p = '_'; } } size -= sz; path += sz; sz = snprintf (path, size, "/%s.jpg", album); for (char *p = path+1; *p; p++) { if (*p == '/') { *p = '_'; } } } void queue_add (const char *fname, const char *artist, const char *album, artwork_callback callback) { if (!artist) { artist = ""; } if (!album) { album = ""; } deadbeef->mutex_lock (mutex); for (cover_query_t *q = queue; q; q = q->next) { if (!strcasecmp (artist, q->artist) || !strcasecmp (album, q->album)) { deadbeef->mutex_unlock (mutex); return; // already in queue } } cover_query_t *q = malloc (sizeof (cover_query_t)); memset (q, 0, sizeof (cover_query_t)); q->fname = strdup (fname); q->artist = strdup (artist); q->album = strdup (album); q->callback = callback; if (queue_tail) { queue_tail->next = q; queue_tail = q; } else { queue = queue_tail = q; } deadbeef->mutex_unlock (mutex); } void queue_pop (void) { deadbeef->mutex_lock (mutex); cover_query_t *next = queue ? queue->next : NULL; free (queue->fname); free (queue->artist); free (queue->album); free (queue); queue = next; if (!queue) { queue_tail = NULL; } deadbeef->mutex_unlock (mutex); } int fetch_to_stream (const char *url, FILE *stream) { CURL *curl = curl_easy_init(); curl_easy_setopt (curl, CURLOPT_URL, url); curl_easy_setopt (curl, CURLOPT_WRITEDATA, stream); curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, 1); CURLcode ret = curl_easy_perform (curl); curl_easy_cleanup (curl); return ret; } int fetch_to_file (const char *url, const char *filename) { /** * Downloading files directly to its locations can cause * cachehits of semi-downloaded files. That's why I use * temporary files */ char temp [1024]; int ret; snprintf (temp, sizeof (temp), "%s.part", filename); FILE *stream = fopen (temp, "wb"); if (!stream) { trace (stderr, "Could not open %s for writing\n", temp); return 0; } ret = fetch_to_stream (url, stream); if (ret != 0) { trace ("Failed to fetch %s\n", url); } fclose (stream); if (0 == ret) { ret = (0 == rename (temp, filename)); if (!ret) trace ("Could not move %s to %s: %d\n", temp, filename, errno); } return ret; } char* fetch (const char *url) { char *data; size_t size; FILE *stream = open_memstream (&data, &size); fetch_to_stream (url, stream); fclose (stream); return data; } static int check_dir (const char *dir, mode_t mode) { char *tmp = strdup (dir); char *slash = tmp; struct stat stat_buf; do { slash = strstr (slash+1, "/"); if (slash) *slash = 0; if (-1 == stat (tmp, &stat_buf)) { trace ("creating dir %s\n", tmp); if (0 != mkdir (tmp, mode)) { trace ("Failed to create %s (%d)\n", tmp, errno); free (tmp); return 0; } } if (slash) *slash = '/'; } while (slash); free (tmp); return 1; } #define BUFFER_SIZE 4096 static int copy_file (const char *in, const char *out) { trace ("copying %s to %s\n", in, out); char *buf = malloc (BUFFER_SIZE); if (!buf) { trace ("artwork: failed to alloc %d bytes\n", BUFFER_SIZE); return -1; } FILE *fin = fopen (in, "rb"); if (!fin) { trace ("artwork: failed to open file %s for reading\n", in); return -1; } FILE *fout = fopen (out, "w+b"); if (!fout) { fclose (fin); trace ("artwork: failed to open file %s for writing\n", out); return -1; } fseek (fin, 0, SEEK_END); size_t sz = ftell (fin); rewind (fin); while (sz > 0) { int rs = min (sz, BUFFER_SIZE); if (fread (buf, rs, 1, fin) != 1) { trace ("artwork: failed to read file %s\n", in); break; } if (fwrite (buf, rs, 1, fout) != 1) { trace ("artwork: failed to write file %s\n", out); break; } sz -= rs; } free (buf); fclose (fin); fclose (fout); if (sz > 0) { unlink (out); } return 0; } static int filter_jpg (const struct dirent *f) { const char *ext = strrchr (f->d_name, '.'); if (!ext) return 0; if (0 == strcasecmp (ext, ".jpg") || 0 == strcasecmp (ext, ".jpeg")) return 1; return 0; } static void fetcher_thread (void *none) { while (!terminate) { if (!queue) { usleep (100000); continue; } cover_query_t *param = queue; char path [1024]; struct dirent **files; int files_count; make_cache_dir_path (path, sizeof (path), param->album, param->artist); trace ("cache folder: %s\n", path); if (!check_dir (path, 0755)) { queue_pop (); trace ("failed to create folder for %s %s\n", param->album, param->artist); continue; } trace ("fetching cover for %s %s\n", param->album, param->artist); /* Searching in track directory */ strncpy (path, param->fname, sizeof (path)); char *slash = strrchr (path, '/'); if (slash) { *slash = 0; // assuming at least one slash exist } trace ("scanning directory: %s\n", path); files_count = scandir (path, &files, filter_jpg, alphasort); if (files_count > 0) { trace ("found cover for %s - %s in local folder\n", param->artist, param->album); if (check_dir (path, 0755)) { strcat (path, "/"); strcat (path, files[0]->d_name); char cache_path[1024]; make_cache_path (cache_path, sizeof (cache_path), param->album, param->artist); copy_file (path, cache_path); int i; for (i = 0; i < files_count; i++) { free (files [i]); } if (param->callback) { param->callback (param->artist, param->album); } queue_pop (); continue; } } make_cache_path (path, sizeof (path), param->album, param->artist); if (!fetch_from_lastfm (param->artist, param->album, path)) { if (!fetch_from_albumart_org (param->artist, param->album, path)) { trace ("art not found for %s %s\n", param->album, param->artist); queue_pop (); copy_file (DEFAULT_COVER_PATH, path); continue; } } trace ("downloaded art for %s %s\n", param->album, param->artist); if (param->callback) { param->callback (param->artist, param->album); } queue_pop (); } tid = 0; } char* get_album_art (DB_playItem_t *track, artwork_callback callback) { char path [1024]; const char *album = deadbeef->pl_find_meta (track, "album"); const char *artist = deadbeef->pl_find_meta (track, "artist"); if (!album) { album = ""; } if (!artist) { artist = ""; } trace ("looking for %s - %s\n", artist, album); /* Searching in cache */ if (!*artist || !*album) { //give up return strdup (DEFAULT_COVER_PATH); } make_cache_path (path, sizeof (path), album, artist); struct stat stat_buf; if (0 == stat (path, &stat_buf)) { trace ("found %s in cache\n", path); return strdup (path); } queue_add (track->fname, artist, album, callback); return strdup (DEFAULT_COVER_PATH); } DB_plugin_t * artwork_load (DB_functions_t *api) { deadbeef = api; return DB_PLUGIN (&plugin); } static int artwork_plugin_start (void) { terminate = 0; mutex = deadbeef->mutex_create (); tid = deadbeef->thread_start (fetcher_thread, NULL); } static int artwork_plugin_stop (void) { if (tid) { terminate = 1; deadbeef->thread_join (tid); } while (queue) { queue_pop (); } if (mutex) { deadbeef->mutex_free (mutex); mutex = 0; } } // define plugin interface static DB_artwork_plugin_t plugin = { .plugin.plugin.api_vmajor = DB_API_VERSION_MAJOR, .plugin.plugin.api_vminor = DB_API_VERSION_MINOR, .plugin.plugin.type = DB_PLUGIN_MISC, .plugin.plugin.id = "cover_loader", .plugin.plugin.name = "Album Artwork", .plugin.plugin.descr = "Loads album artwork either from local directories or from internet", .plugin.plugin.author = "Viktor Semykin", .plugin.plugin.email = "thesame.ml@gmail.com", .plugin.plugin.website = "http://deadbeef.sf.net", .plugin.plugin.start = artwork_plugin_start, .plugin.plugin.stop = artwork_plugin_stop, .get_album_art = get_album_art, };