/* * Metadata hub and access methods. * Basically, if you need to store metadata * (e.g. for use with the dbus interfaces), * then you need a metadata hub, * where everything is stored * This file is part of Shairport Sync. * Copyright (c) Mike Brady 2017--2020 * All rights reserved. * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include "config.h" #include "common.h" #include "dacp.h" #include "metadata_hub.h" #ifdef CONFIG_MBEDTLS #include #include #endif #ifdef CONFIG_POLARSSL #include #endif #ifdef CONFIG_OPENSSL #include #endif struct metadata_bundle metadata_store; int metadata_hub_initialised = 0; pthread_rwlock_t metadata_hub_re_lock = PTHREAD_RWLOCK_INITIALIZER; int string_update(char **str, int *flag, char *s) { if (s) return string_update_with_size(str, flag, s, strlen(s)); else return string_update_with_size(str, flag, NULL, 0); } void metadata_hub_init(void) { // debug(1, "Metadata bundle initialisation."); memset(&metadata_store, 0, sizeof(metadata_store)); metadata_hub_initialised = 1; } void metadata_hub_stop(void) {} void add_metadata_watcher(metadata_watcher fn, void *userdata) { int i; for (i = 0; i < number_of_watchers; i++) { if (metadata_store.watchers[i] == NULL) { metadata_store.watchers[i] = fn; metadata_store.watchers_data[i] = userdata; // debug(1, "Added a metadata watcher into slot %d", i); break; } } } void run_metadata_watchers(void) { int i; for (i = 0; i < number_of_watchers; i++) { if (metadata_store.watchers[i]) { metadata_store.watchers[i](&metadata_store, metadata_store.watchers_data[i]); } } // turn off changed flags metadata_store.cover_art_pathname_changed = 0; metadata_store.client_ip_changed = 0; metadata_store.server_ip_changed = 0; metadata_store.progress_string_changed = 0; metadata_store.item_id_changed = 0; metadata_store.item_composite_id_changed = 0; metadata_store.artist_name_changed = 0; metadata_store.album_artist_name_changed = 0; metadata_store.album_name_changed = 0; metadata_store.track_name_changed = 0; metadata_store.genre_changed = 0; metadata_store.comment_changed = 0; metadata_store.composer_changed = 0; metadata_store.file_kind_changed = 0; metadata_store.song_description_changed = 0; metadata_store.song_album_artist_changed = 0; metadata_store.sort_artist_changed = 0; metadata_store.sort_album_changed = 0; metadata_store.sort_composer_changed = 0; metadata_store.songtime_in_milliseconds_changed = 0; } void metadata_hub_unlock_hub_mutex_cleanup(__attribute__((unused)) void *arg) { debug(1, "metadata_hub_unlock_hub_mutex_cleanup called."); metadata_hub_modify_epilog(0); } char *last_metadata_hub_modify_prolog_file = NULL; int last_metadata_hub_modify_prolog_line; int metadata_hub_re_lock_access_is_delayed; void _metadata_hub_modify_prolog(const char *filename, const int linenumber) { // always run this before changing an entry or a sequence of entries in the metadata_hub // debug(1, "locking metadata hub for writing"); if (pthread_rwlock_trywrlock(&metadata_hub_re_lock) != 0) { if (last_metadata_hub_modify_prolog_file) debug(2, "Metadata_hub write lock at \"%s:%d\" is already taken at \"%s:%d\" -- must wait.", filename, linenumber, last_metadata_hub_modify_prolog_file, last_metadata_hub_modify_prolog_line); else debug(2, "Metadata_hub write lock is already taken by unknown -- must wait."); metadata_hub_re_lock_access_is_delayed = 0; pthread_rwlock_wrlock(&metadata_hub_re_lock); debug(2, "Okay -- acquired the metadata_hub write lock at \"%s:%d\".", filename, linenumber); } else { if (last_metadata_hub_modify_prolog_file) { free(last_metadata_hub_modify_prolog_file); } last_metadata_hub_modify_prolog_file = strdup(filename); last_metadata_hub_modify_prolog_line = linenumber; debug(3, "Metadata_hub write lock acquired."); } metadata_hub_re_lock_access_is_delayed = 0; } void _metadata_hub_modify_epilog(int modified, const char *filename, const int linenumber) { metadata_store.dacp_server_has_been_active = metadata_store.dacp_server_active; // set the scanner_has_been_active now. if (modified) { run_metadata_watchers(); } if (metadata_hub_re_lock_access_is_delayed) { if (last_metadata_hub_modify_prolog_file) { debug(1, "Metadata_hub write lock taken at \"%s:%d\" is freed at \"%s:%d\".", last_metadata_hub_modify_prolog_file, last_metadata_hub_modify_prolog_line, filename, linenumber); free(last_metadata_hub_modify_prolog_file); last_metadata_hub_modify_prolog_file = NULL; } else { debug(1, "Metadata_hub write lock taken at an unknown place is freed at \"%s:%d\".", filename, linenumber); } } pthread_rwlock_unlock(&metadata_hub_re_lock); debug(3, "Metadata_hub write lock unlocked."); } /* void _metadata_hub_read_prolog(const char *filename, const int linenumber) { // always run this before reading an entry or a sequence of entries in the metadata_hub // debug(1, "locking metadata hub for reading"); if (pthread_rwlock_tryrdlock(&metadata_hub_re_lock) != 0) { debug(1, "Metadata_hub read lock is already taken -- must wait."); pthread_rwlock_rdlock(&metadata_hub_re_lock); debug(1, "Okay -- acquired the metadata_hub read lock."); } } void _metadata_hub_read_epilog(const char *filename, const int linenumber) { // always run this after reading an entry or a sequence of entries in the metadata_hub // debug(1, "unlocking metadata hub for reading"); pthread_rwlock_unlock(&metadata_hub_re_lock); } */ char *metadata_write_image_file(const char *buf, int len) { // warning -- this removes all files from the directory apart from this one, if it exists // it will return a path to the image file allocated with malloc. // free it if you don't need it. char *path = NULL; // this will be what is returned if (strcmp(config.cover_art_cache_dir, "") != 0) { // an empty string means do not write the file uint8_t img_md5[16]; // uint8_t ap_md5[16]; #ifdef CONFIG_OPENSSL MD5_CTX ctx; MD5_Init(&ctx); MD5_Update(&ctx, buf, len); MD5_Final(img_md5, &ctx); #endif #ifdef CONFIG_MBEDTLS #if MBEDTLS_VERSION_MINOR >= 7 mbedtls_md5_context tctx; mbedtls_md5_starts_ret(&tctx); mbedtls_md5_update_ret(&tctx, (const unsigned char *)buf, len); mbedtls_md5_finish_ret(&tctx, img_md5); #else mbedtls_md5_context tctx; mbedtls_md5_starts(&tctx); mbedtls_md5_update(&tctx, (const unsigned char *)buf, len); mbedtls_md5_finish(&tctx, img_md5); #endif #endif #ifdef CONFIG_POLARSSL md5_context tctx; md5_starts(&tctx); md5_update(&tctx, (const unsigned char *)buf, len); md5_finish(&tctx, img_md5); #endif char img_md5_str[33]; memset(img_md5_str, 0, sizeof(img_md5_str)); char *ext; char png[] = "png"; char jpg[] = "jpg"; int i; for (i = 0; i < 16; i++) snprintf(&img_md5_str[i * 2], 3, "%02x", (uint8_t)img_md5[i]); // see if the file is a jpeg or a png if (strncmp(buf, "\xFF\xD8\xFF", 3) == 0) ext = jpg; else if (strncmp(buf, "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", 8) == 0) ext = png; else { debug(1, "Unidentified image type of cover art -- jpg extension used."); ext = jpg; } mode_t oldumask = umask(000); int result = mkpath(config.cover_art_cache_dir, 0777); umask(oldumask); if ((result == 0) || (result == -EEXIST)) { // see if the file exists by opening it. // if it exists, we're done char *prefix = "cover-"; size_t pl = strlen(config.cover_art_cache_dir) + 1 + strlen(prefix) + strlen(img_md5_str) + 1 + strlen(ext); path = malloc(pl + 1); snprintf(path, pl + 1, "%s/%s%s.%s", config.cover_art_cache_dir, prefix, img_md5_str, ext); int cover_fd = open(path, O_WRONLY | O_CREAT | O_EXCL, S_IRWXU | S_IRGRP | S_IROTH); if (cover_fd > 0) { // write the contents if (write(cover_fd, buf, len) < len) { warn("Writing cover art file \"%s\" failed!", path); free(path); path = NULL; } close(cover_fd); // now delete all other files, if requested if (config.retain_coverart == 0) { DIR *d; struct dirent *dir; d = opendir(config.cover_art_cache_dir); if (d) { int fnl = strlen(prefix) + strlen(img_md5_str) + 1 + strlen(ext) + 1; char *full_filename = malloc(fnl); if (full_filename == NULL) die("Can't allocate memory at metadata_write_image_file."); memset(full_filename, 0, fnl); snprintf(full_filename, fnl, "%s%s.%s", prefix, img_md5_str, ext); int dir_fd = open(config.cover_art_cache_dir, O_DIRECTORY); if (dir_fd > 0) { while ((dir = readdir(d)) != NULL) { if (dir->d_type == DT_REG) { if (strcmp(full_filename, dir->d_name) != 0) { if (unlinkat(dir_fd, dir->d_name, 0) != 0) { debug(1, "Error %d deleting cover art file \"%s\".", errno, dir->d_name); } } } } } else { debug(1, "Can't open the directory for deletion."); } free(full_filename); closedir(d); } } } else { // if (errno == EEXIST) // debug(1, "Cover art file \"%s\" already exists!", path); // else { if (errno != EEXIST) { warn("Could not open file \"%s\" for writing cover art", path); free(path); path = NULL; } } } else { debug(1, "Couldn't access or create the cover art cache directory \"%s\".", config.cover_art_cache_dir); } } return path; } void metadata_hub_process_metadata(uint32_t type, uint32_t code, char *data, uint32_t length) { // metadata coming in from the audio source or from Shairport Sync itself passes through here // this has more information about tags, which might be relevant: // https://code.google.com/p/ytrack/wiki/DMAP // all the following items of metadata are contained in one metadata packet // they are preceded by an 'ssnc' 'mdst' item and followed by an 'ssnc 'mden' item. // we don't set "changed" for them individually; instead we set it when the 'mden' token // comes in if the metadata_packet_item_changed is set. int changed = 0; int metadata_packet_item_changed = 0; metadata_hub_modify_prolog(); pthread_cleanup_push(metadata_hub_unlock_hub_mutex_cleanup, NULL); char *cs; if (type == 'core') { switch (code) { case 'mper': { // get the 64-bit number as a uint64_t by reading two uint32_t s and combining them uint64_t vl = ntohl(*(uint32_t *)data); // get the high order 32 bits vl = vl << 32; // shift them into the correct location uint64_t ul = ntohl(*(uint32_t *)(data + sizeof(uint32_t))); // and the low order 32 bits vl = vl + ul; debug(2, "MH Item ID seen: \"%" PRIx64 "\" of length %u.", vl, length); if (vl != metadata_store.item_id) { metadata_store.item_id = vl; metadata_store.item_id_changed = 1; metadata_store.item_id_received = 1; debug(2, "MH Item ID set to: \"%" PRIx64 "\"", metadata_store.item_id); metadata_packet_item_changed = 1; } } break; case 'astm': { uint32_t ui = ntohl(*(uint32_t *)data); debug(2, "MH Song Time seen: \"%u\" of length %u.", ui, length); if (ui != metadata_store.songtime_in_milliseconds) { metadata_store.songtime_in_milliseconds = ui; metadata_store.songtime_in_milliseconds_changed = 1; debug(2, "MH Song Time set to: \"%u\"", metadata_store.songtime_in_milliseconds); metadata_packet_item_changed = 1; } } break; case 'asal': cs = strndup(data, length); if (string_update(&metadata_store.album_name, &metadata_store.album_name_changed, cs)) { debug(2, "MH Album name set to: \"%s\"", metadata_store.album_name); metadata_packet_item_changed = 1; } free(cs); break; case 'asar': cs = strndup(data, length); if (string_update(&metadata_store.artist_name, &metadata_store.artist_name_changed, cs)) { debug(2, "MH Artist name set to: \"%s\"", metadata_store.artist_name); metadata_packet_item_changed = 1; } free(cs); break; case 'assl': cs = strndup(data, length); if (string_update(&metadata_store.album_artist_name, &metadata_store.album_artist_name_changed, cs)) { debug(2, "MH Album Artist name set to: \"%s\"", metadata_store.album_artist_name); metadata_packet_item_changed = 1; } free(cs); break; case 'ascm': cs = strndup(data, length); if (string_update(&metadata_store.comment, &metadata_store.comment_changed, cs)) { debug(2, "MH Comment set to: \"%s\"", metadata_store.comment); metadata_packet_item_changed = 1; } free(cs); break; case 'asgn': cs = strndup(data, length); if (string_update(&metadata_store.genre, &metadata_store.genre_changed, cs)) { debug(2, "MH Genre set to: \"%s\"", metadata_store.genre); metadata_packet_item_changed = 1; } free(cs); break; case 'minm': cs = strndup(data, length); if (string_update(&metadata_store.track_name, &metadata_store.track_name_changed, cs)) { debug(2, "MH Track Name set to: \"%s\"", metadata_store.track_name); metadata_packet_item_changed = 1; } free(cs); break; case 'ascp': cs = strndup(data, length); if (string_update(&metadata_store.composer, &metadata_store.composer_changed, cs)) { debug(2, "MH Composer set to: \"%s\"", metadata_store.composer); metadata_packet_item_changed = 1; } free(cs); break; case 'asdt': cs = strndup(data, length); if (string_update(&metadata_store.song_description, &metadata_store.song_description_changed, cs)) { debug(2, "MH Song Description set to: \"%s\"", metadata_store.song_description); } free(cs); break; case 'asaa': cs = strndup(data, length); if (string_update(&metadata_store.song_album_artist, &metadata_store.song_album_artist_changed, cs)) { debug(2, "MH Song Album Artist set to: \"%s\"", metadata_store.song_album_artist); metadata_packet_item_changed = 1; } free(cs); break; case 'assn': cs = strndup(data, length); if (string_update(&metadata_store.sort_name, &metadata_store.sort_name_changed, cs)) { debug(2, "MH Sort Name set to: \"%s\"", metadata_store.sort_name); metadata_packet_item_changed = 1; } free(cs); break; case 'assa': cs = strndup(data, length); if (string_update(&metadata_store.sort_artist, &metadata_store.sort_artist_changed, cs)) { debug(2, "MH Sort Artist set to: \"%s\"", metadata_store.sort_artist); metadata_packet_item_changed = 1; } free(cs); break; case 'assu': cs = strndup(data, length); if (string_update(&metadata_store.sort_album, &metadata_store.sort_album_changed, cs)) { debug(2, "MH Sort Album set to: \"%s\"", metadata_store.sort_album); metadata_packet_item_changed = 1; } free(cs); break; case 'assc': cs = strndup(data, length); if (string_update(&metadata_store.sort_composer, &metadata_store.sort_composer_changed, cs)) { debug(2, "MH Sort Composer set to: \"%s\"", metadata_store.sort_composer); metadata_packet_item_changed = 1; } free(cs); default: /* { char typestring[5]; *(uint32_t *)typestring = htonl(type); typestring[4] = 0; char codestring[5]; *(uint32_t *)codestring = htonl(code); codestring[4] = 0; char *payload; if (length < 2048) payload = strndup(data, length); else payload = NULL; debug(1, "MH \"%s\" \"%s\" (%d bytes): \"%s\".", typestring, codestring, length, payload); if (payload) free(payload); } */ break; } } else if (type == 'ssnc') { switch (code) { // ignore the following case 'pcst': case 'pcen': break; case 'mdst': debug(2, "MH Metadata stream processing start."); metadata_packet_item_changed = 0; break; case 'mden': debug(2, "MH Metadata stream processing end."); changed = metadata_packet_item_changed; break; case 'PICT': debug(2, "MH Picture received, length %u bytes.", length); char uri[2048]; if ((length > 16) && (strcmp(config.cover_art_cache_dir,"")!=0)) { // if it's okay to write the file // make this uncancellable int oldState; pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldState); // make this un-cancellable char *pathname = metadata_write_image_file(data, length); snprintf(uri, sizeof(uri), "file://%s", pathname); free(pathname); pthread_setcancelstate(oldState, NULL); } else { uri[0] = '\0'; } if (string_update(&metadata_store.cover_art_pathname, &metadata_store.cover_art_pathname_changed, uri)) // if the picture's file path is different from the stored one... changed = 1; else changed = 0; // pthread_cleanup_pop(0); // don't remove the lock -- it'll have been done break; case 'clip': cs = strndup(data, length); if (string_update(&metadata_store.client_ip, &metadata_store.client_ip_changed, cs)) { changed = 1; debug(2, "MH Client IP set to: \"%s\"", metadata_store.client_ip); } free(cs); break; case 'prgr': cs = strndup(data, length); if (string_update(&metadata_store.progress_string, &metadata_store.progress_string_changed, cs)) { changed = 1; debug(2, "MH Progress String set to: \"%s\"", metadata_store.progress_string); } free(cs); break; case 'svip': cs = strndup(data, length); if (string_update(&metadata_store.server_ip, &metadata_store.server_ip_changed, cs)) { changed = 1; debug(2, "MH Server IP set to: \"%s\"", metadata_store.server_ip); } free(cs); break; case 'abeg': changed = (metadata_store.active_state != AM_ACTIVE); metadata_store.active_state = AM_ACTIVE; break; case 'aend': changed = (metadata_store.active_state != AM_INACTIVE); metadata_store.active_state = AM_INACTIVE; break; case 'pbeg': changed = ((metadata_store.player_state != PS_PLAYING) || (metadata_store.player_thread_active == 0)); metadata_store.player_state = PS_PLAYING; metadata_store.player_thread_active = 1; break; case 'pend': changed = ((metadata_store.player_state != PS_STOPPED) || (metadata_store.player_thread_active == 1)); metadata_store.player_state = PS_STOPPED; metadata_store.player_thread_active = 0; break; case 'pfls': changed = (metadata_store.player_state != PS_PAUSED); metadata_store.player_state = PS_PAUSED; break; case 'pffr': // this is sent when the first frame has been received case 'prsm': changed = (metadata_store.player_state != PS_PLAYING); metadata_store.player_state = PS_PLAYING; break; case 'pvol': { // Note: it's assumed that the config.airplay volume has already been correctly set. // int32_t actual_volume; // int gv = dacp_get_volume(&actual_volume); // metadata_hub_modify_prolog(); // if ((gv == 200) && (metadata_store.speaker_volume != actual_volume)) { // metadata_store.speaker_volume = actual_volume; // changed = 1; //} if (metadata_store.airplay_volume != config.airplay_volume) { metadata_store.airplay_volume = config.airplay_volume; changed = 1; } } break; default: { char typestring[5]; uint32_t tm = htonl(type); memcpy(typestring, &tm, sizeof(uint32_t)); typestring[4] = 0; char codestring[5]; uint32_t cm = htonl(code); memcpy(codestring, &cm, sizeof(uint32_t)); codestring[4] = 0; char *payload; if (length < 2048) payload = strndup(data, length); else payload = NULL; // debug(1, "MH \"%s\" \"%s\" (%d bytes): \"%s\".", typestring, codestring, length, payload); if (payload) free(payload); } } } pthread_cleanup_pop(0); // don't remove the lock metadata_hub_modify_epilog(changed); }