2 * Metadata hub and access methods.
3 * Basically, if you need to store metadata
4 * (e.g. for use with the dbus interfaces),
5 * then you need a metadata hub,
6 * where everything is stored
7 * This file is part of Shairport Sync.
8 * Copyright (c) Mike Brady 2017--2022
11 * Permission is hereby granted, free of charge, to any person
12 * obtaining a copy of this software and associated documentation
13 * files (the "Software"), to deal in the Software without
14 * restriction, including without limitation the rights to use,
15 * copy, modify, merge, publish, distribute, sublicense, and/or
16 * sell copies of the Software, and to permit persons to whom the
17 * Software is furnished to do so, subject to the following conditions:
19 * The above copyright notice and this permission notice shall be
20 * included in all copies or substantial portions of the Software.
22 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 * OTHER DEALINGS IN THE SOFTWARE.
40 #include <sys/types.h>
47 #include "metadata_hub.h"
50 #include <mbedtls/md5.h>
51 #include <mbedtls/version.h>
54 #ifdef CONFIG_POLARSSL
55 #include <polarssl/md5.h>
59 #include <openssl/evp.h>
62 struct metadata_bundle metadata_store
;
64 int metadata_hub_initialised
= 0;
66 pthread_rwlock_t metadata_hub_re_lock
= PTHREAD_RWLOCK_INITIALIZER
;
68 int string_update(char **str
, int *flag
, char *s
) {
70 return string_update_with_size(str
, flag
, s
, strlen(s
));
72 return string_update_with_size(str
, flag
, NULL
, 0);
75 void metadata_hub_init(void) {
76 // debug(1, "Metadata bundle initialisation.");
77 memset(&metadata_store
, 0, sizeof(metadata_store
));
78 metadata_hub_initialised
= 1;
81 void metadata_hub_stop(void) {}
83 void add_metadata_watcher(metadata_watcher fn
, void *userdata
) {
85 for (i
= 0; i
< number_of_watchers
; i
++) {
86 if (metadata_store
.watchers
[i
] == NULL
) {
87 metadata_store
.watchers
[i
] = fn
;
88 metadata_store
.watchers_data
[i
] = userdata
;
89 // debug(1, "Added a metadata watcher into slot %d", i);
95 void run_metadata_watchers(void) {
97 for (i
= 0; i
< number_of_watchers
; i
++) {
98 if (metadata_store
.watchers
[i
]) {
99 metadata_store
.watchers
[i
](&metadata_store
, metadata_store
.watchers_data
[i
]);
102 // turn off changed flags
103 metadata_store
.cover_art_pathname_changed
= 0;
104 metadata_store
.client_ip_changed
= 0;
105 metadata_store
.client_name_changed
= 0;
106 metadata_store
.server_ip_changed
= 0;
107 metadata_store
.progress_string_changed
= 0;
108 metadata_store
.item_id_changed
= 0;
109 metadata_store
.item_composite_id_changed
= 0;
110 metadata_store
.artist_name_changed
= 0;
111 metadata_store
.album_artist_name_changed
= 0;
112 metadata_store
.album_name_changed
= 0;
113 metadata_store
.song_data_kind_changed
= 0;
114 metadata_store
.track_name_changed
= 0;
115 metadata_store
.genre_changed
= 0;
116 metadata_store
.comment_changed
= 0;
117 metadata_store
.composer_changed
= 0;
118 metadata_store
.file_kind_changed
= 0;
119 metadata_store
.song_description_changed
= 0;
120 metadata_store
.song_album_artist_changed
= 0;
121 metadata_store
.sort_artist_changed
= 0;
122 metadata_store
.sort_album_changed
= 0;
123 metadata_store
.sort_composer_changed
= 0;
124 metadata_store
.songtime_in_milliseconds_changed
= 0;
127 void metadata_hub_unlock_hub_mutex_cleanup(__attribute__((unused
)) void *arg
) {
128 debug(1, "metadata_hub_unlock_hub_mutex_cleanup called.");
129 metadata_hub_modify_epilog(0);
132 char *last_metadata_hub_modify_prolog_file
= NULL
;
133 int last_metadata_hub_modify_prolog_line
;
134 int metadata_hub_re_lock_access_is_delayed
;
136 void _metadata_hub_modify_prolog(const char *filename
, const int linenumber
) {
137 // always run this before changing an entry or a sequence of entries in the metadata_hub
138 // debug(1, "locking metadata hub for writing");
139 if (pthread_rwlock_trywrlock(&metadata_hub_re_lock
) != 0) {
140 if (last_metadata_hub_modify_prolog_file
)
141 debug(2, "Metadata_hub write lock at \"%s:%d\" is already taken at \"%s:%d\" -- must wait.",
142 filename
, linenumber
, last_metadata_hub_modify_prolog_file
,
143 last_metadata_hub_modify_prolog_line
);
145 debug(2, "Metadata_hub write lock is already taken by unknown -- must wait.");
146 metadata_hub_re_lock_access_is_delayed
= 0;
147 pthread_rwlock_wrlock(&metadata_hub_re_lock
);
148 debug(2, "Okay -- acquired the metadata_hub write lock at \"%s:%d\".", filename
, linenumber
);
150 if (last_metadata_hub_modify_prolog_file
) {
151 free(last_metadata_hub_modify_prolog_file
);
153 last_metadata_hub_modify_prolog_file
= strdup(filename
);
154 last_metadata_hub_modify_prolog_line
= linenumber
;
155 // debug(3, "Metadata_hub write lock acquired.");
157 metadata_hub_re_lock_access_is_delayed
= 0;
160 void _metadata_hub_modify_epilog(int modified
, const char *filename
, const int linenumber
) {
161 metadata_store
.dacp_server_has_been_active
=
162 metadata_store
.dacp_server_active
; // set the scanner_has_been_active now.
164 run_metadata_watchers();
166 if (metadata_hub_re_lock_access_is_delayed
) {
167 if (last_metadata_hub_modify_prolog_file
) {
168 debug(1, "Metadata_hub write lock taken at \"%s:%d\" is freed at \"%s:%d\".",
169 last_metadata_hub_modify_prolog_file
, last_metadata_hub_modify_prolog_line
, filename
,
171 free(last_metadata_hub_modify_prolog_file
);
172 last_metadata_hub_modify_prolog_file
= NULL
;
174 debug(1, "Metadata_hub write lock taken at an unknown place is freed at \"%s:%d\".", filename
,
178 pthread_rwlock_unlock(&metadata_hub_re_lock
);
179 // debug(3, "Metadata_hub write lock unlocked.");
183 void _metadata_hub_read_prolog(const char *filename, const int linenumber) {
184 // always run this before reading an entry or a sequence of entries in the metadata_hub
185 // debug(1, "locking metadata hub for reading");
186 if (pthread_rwlock_tryrdlock(&metadata_hub_re_lock) != 0) {
187 debug(1, "Metadata_hub read lock is already taken -- must wait.");
188 pthread_rwlock_rdlock(&metadata_hub_re_lock);
189 debug(1, "Okay -- acquired the metadata_hub read lock.");
193 void _metadata_hub_read_epilog(const char *filename, const int linenumber) {
194 // always run this after reading an entry or a sequence of entries in the metadata_hub
195 // debug(1, "unlocking metadata hub for reading");
196 pthread_rwlock_unlock(&metadata_hub_re_lock);
199 char *metadata_write_image_file(const char *buf
, int len
) {
201 // warning -- this removes all files from the directory apart from this one, if it exists
202 // it will return a path to the image file allocated with malloc.
203 // free it if you don't need it.
205 char *path
= NULL
; // this will be what is returned
206 if (strcmp(config
.cover_art_cache_dir
, "") != 0) { // an empty string means do not write the file
209 // uint8_t ap_md5[16];
211 #ifdef CONFIG_OPENSSL
213 unsigned int img_md5_len
= EVP_MD_size(EVP_md5());
215 ctx
= EVP_MD_CTX_new();
216 EVP_DigestInit_ex(ctx
, EVP_md5(), NULL
);
217 EVP_DigestUpdate(ctx
, buf
, len
);
218 EVP_DigestFinal_ex(ctx
, img_md5
, &img_md5_len
);
219 EVP_MD_CTX_free(ctx
);
222 #ifdef CONFIG_MBEDTLS
223 #if MBEDTLS_VERSION_MINOR >= 7
224 mbedtls_md5_context tctx
;
225 mbedtls_md5_starts_ret(&tctx
);
226 mbedtls_md5_update_ret(&tctx
, (const unsigned char *)buf
, len
);
227 mbedtls_md5_finish_ret(&tctx
, img_md5
);
229 mbedtls_md5_context tctx
;
230 mbedtls_md5_starts(&tctx
);
231 mbedtls_md5_update(&tctx
, (const unsigned char *)buf
, len
);
232 mbedtls_md5_finish(&tctx
, img_md5
);
236 #ifdef CONFIG_POLARSSL
239 md5_update(&tctx
, (const unsigned char *)buf
, len
);
240 md5_finish(&tctx
, img_md5
);
243 char img_md5_str
[33];
244 memset(img_md5_str
, 0, sizeof(img_md5_str
));
249 for (i
= 0; i
< 16; i
++)
250 snprintf(&img_md5_str
[i
* 2], 3, "%02x", (uint8_t)img_md5
[i
]);
251 // see if the file is a jpeg or a png
252 if (strncmp(buf
, "\xFF\xD8\xFF", 3) == 0)
254 else if (strncmp(buf
, "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", 8) == 0)
257 debug(1, "Unidentified image type of cover art -- jpg extension used.");
260 mode_t oldumask
= umask(000);
261 int result
= mkpath(config
.cover_art_cache_dir
, 0777);
263 if ((result
== 0) || (result
== -EEXIST
)) {
264 // see if the file exists by opening it.
265 // if it exists, we're done
266 char *prefix
= "cover-";
268 size_t pl
= strlen(config
.cover_art_cache_dir
) + 1 + strlen(prefix
) + strlen(img_md5_str
) +
271 path
= malloc(pl
+ 1);
272 snprintf(path
, pl
+ 1, "%s/%s%s.%s", config
.cover_art_cache_dir
, prefix
, img_md5_str
, ext
);
273 int cover_fd
= open(path
, O_WRONLY
| O_CREAT
| O_EXCL
, S_IRWXU
| S_IRGRP
| S_IROTH
);
275 // write the contents
276 if (write(cover_fd
, buf
, len
) < len
) {
277 warn("Writing cover art file \"%s\" failed!", path
);
283 // now delete all other files, if requested
284 if (config
.retain_coverart
== 0) {
287 d
= opendir(config
.cover_art_cache_dir
);
289 int fnl
= strlen(prefix
) + strlen(img_md5_str
) + 1 + strlen(ext
) + 1;
291 char *full_filename
= malloc(fnl
);
292 if (full_filename
== NULL
)
293 die("Can't allocate memory at metadata_write_image_file.");
294 memset(full_filename
, 0, fnl
);
295 snprintf(full_filename
, fnl
, "%s%s.%s", prefix
, img_md5_str
, ext
);
296 int dir_fd
= open(config
.cover_art_cache_dir
, O_DIRECTORY
);
298 while ((dir
= readdir(d
)) != NULL
) {
299 if (dir
->d_type
== DT_REG
) {
300 if (strcmp(full_filename
, dir
->d_name
) != 0) {
301 if (unlinkat(dir_fd
, dir
->d_name
, 0) != 0) {
302 debug(1, "Error %d deleting cover art file \"%s\".", errno
, dir
->d_name
);
307 if (close(dir_fd
) < 0)
308 debug(1, "Error %d closing directory \"%s\"", errno
, config
.cover_art_cache_dir
);
310 debug(1, "Can't open the directory \"%s\" for deletion -- error %d.",
311 config
.cover_art_cache_dir
, errno
);
318 // if (errno == EEXIST)
319 // debug(1, "Cover art file \"%s\" already exists!", path);
321 if (errno
!= EEXIST
) {
322 warn("Could not open file \"%s\" for writing cover art", path
);
328 debug(1, "Couldn't access or create the cover art cache directory \"%s\".",
329 config
.cover_art_cache_dir
);
335 int metadata_packet_item_changed
= 0; // set if any parsed part of a metadata stream changes
337 void metadata_hub_process_metadata(uint32_t type
, uint32_t code
, char *data
, uint32_t length
) {
338 // metadata coming in from the audio source or from Shairport Sync itself passes through here
339 // this has more information about tags, which might be relevant:
340 // https://code.google.com/p/ytrack/wiki/DMAP
342 // Some metadata items are contained in one metadata packet.
343 // The start of the metadata packet is signalled by an 'ssnc' 'mdst' item and
344 // the end of it by an 'ssnc 'mden' item.
345 // We don't set "changed" for them individually; instead we set it when the 'mden' token
346 // comes in if the metadata_packet_item_changed item is set by parsed items
347 // within the packet.
350 metadata_hub_modify_prolog();
351 pthread_cleanup_push(metadata_hub_unlock_hub_mutex_cleanup
, NULL
);
354 if (type
== 'core') {
357 // get the one-byte number as an unsigned number
358 int song_data_kind
= data
[0]; // one byte
359 song_data_kind
= song_data_kind
& 0xFF; // unsigned
360 debug(2, "MH Song Data Kind seen: \"%d\" of length %u.", song_data_kind
, length
);
361 if ((song_data_kind
!= metadata_store
.song_data_kind
) ||
362 (metadata_store
.song_data_kind_is_valid
== 0)) {
363 metadata_store
.song_data_kind
= song_data_kind
;
364 metadata_store
.song_data_kind_changed
= 1;
365 metadata_store
.song_data_kind_is_valid
= 1;
366 debug(2, "MH Song Data Kind set to: \"%d\"", metadata_store
.song_data_kind
);
367 metadata_packet_item_changed
= 1;
371 // get the 64-bit number as a uint64_t by reading two uint32_t s and combining them
372 uint64_t vl
= ntohl(*(uint32_t *)data
); // get the high order 32 bits
373 vl
= vl
<< 32; // shift them into the correct location
374 uint64_t ul
= ntohl(*(uint32_t *)(data
+ sizeof(uint32_t))); // and the low order 32 bits
376 debug(2, "MH Item ID seen: \"%" PRIx64
"\" of length %u.", vl
, length
);
377 if ((vl
!= metadata_store
.item_id
) || (metadata_store
.item_id_is_valid
== 0)) {
378 metadata_store
.item_id
= vl
;
379 metadata_store
.item_id_changed
= 1;
380 metadata_store
.item_id_is_valid
= 1;
381 debug(2, "MH Item ID set to: \"%" PRIx64
"\"", metadata_store
.item_id
);
382 metadata_packet_item_changed
= 1;
386 uint32_t ui
= ntohl(*(uint32_t *)data
);
387 debug(2, "MH Song Time seen: \"%u\" of length %u.", ui
, length
);
388 if ((ui
!= metadata_store
.songtime_in_milliseconds
) ||
389 (metadata_store
.songtime_in_milliseconds_is_valid
== 0)) {
390 metadata_store
.songtime_in_milliseconds
= ui
;
391 metadata_store
.songtime_in_milliseconds_changed
= 1;
392 metadata_store
.songtime_in_milliseconds_is_valid
= 1;
393 debug(2, "MH Song Time set to: \"%u\"", metadata_store
.songtime_in_milliseconds
);
394 metadata_packet_item_changed
= 1;
398 cs
= strndup(data
, length
);
399 if (string_update(&metadata_store
.album_name
, &metadata_store
.album_name_changed
, cs
)) {
400 debug(2, "MH Album name set to: \"%s\"", metadata_store
.album_name
);
401 metadata_packet_item_changed
= 1;
406 cs
= strndup(data
, length
);
407 if (string_update(&metadata_store
.artist_name
, &metadata_store
.artist_name_changed
, cs
)) {
408 debug(2, "MH Artist name set to: \"%s\"", metadata_store
.artist_name
);
409 metadata_packet_item_changed
= 1;
414 cs
= strndup(data
, length
);
415 if (string_update(&metadata_store
.album_artist_name
,
416 &metadata_store
.album_artist_name_changed
, cs
)) {
417 debug(2, "MH Album Artist name set to: \"%s\"", metadata_store
.album_artist_name
);
418 metadata_packet_item_changed
= 1;
423 cs
= strndup(data
, length
);
424 if (string_update(&metadata_store
.comment
, &metadata_store
.comment_changed
, cs
)) {
425 debug(2, "MH Comment set to: \"%s\"", metadata_store
.comment
);
426 metadata_packet_item_changed
= 1;
431 cs
= strndup(data
, length
);
432 if (string_update(&metadata_store
.genre
, &metadata_store
.genre_changed
, cs
)) {
433 debug(2, "MH Genre set to: \"%s\"", metadata_store
.genre
);
434 metadata_packet_item_changed
= 1;
439 cs
= strndup(data
, length
);
440 if (string_update(&metadata_store
.track_name
, &metadata_store
.track_name_changed
, cs
)) {
441 debug(2, "MH Track Name set to: \"%s\"", metadata_store
.track_name
);
442 metadata_packet_item_changed
= 1;
447 cs
= strndup(data
, length
);
448 if (string_update(&metadata_store
.composer
, &metadata_store
.composer_changed
, cs
)) {
449 debug(2, "MH Composer set to: \"%s\"", metadata_store
.composer
);
450 metadata_packet_item_changed
= 1;
455 cs
= strndup(data
, length
);
456 if (string_update(&metadata_store
.song_description
, &metadata_store
.song_description_changed
,
458 debug(2, "MH Song Description set to: \"%s\"", metadata_store
.song_description
);
463 cs
= strndup(data
, length
);
464 if (string_update(&metadata_store
.song_album_artist
,
465 &metadata_store
.song_album_artist_changed
, cs
)) {
466 debug(2, "MH Song Album Artist set to: \"%s\"", metadata_store
.song_album_artist
);
467 metadata_packet_item_changed
= 1;
472 cs
= strndup(data
, length
);
473 if (string_update(&metadata_store
.sort_name
, &metadata_store
.sort_name_changed
, cs
)) {
474 debug(2, "MH Sort Name set to: \"%s\"", metadata_store
.sort_name
);
475 metadata_packet_item_changed
= 1;
480 cs
= strndup(data
, length
);
481 if (string_update(&metadata_store
.sort_artist
, &metadata_store
.sort_artist_changed
, cs
)) {
482 debug(2, "MH Sort Artist set to: \"%s\"", metadata_store
.sort_artist
);
483 metadata_packet_item_changed
= 1;
488 cs
= strndup(data
, length
);
489 if (string_update(&metadata_store
.sort_album
, &metadata_store
.sort_album_changed
, cs
)) {
490 debug(2, "MH Sort Album set to: \"%s\"", metadata_store
.sort_album
);
491 metadata_packet_item_changed
= 1;
496 cs
= strndup(data
, length
);
497 if (string_update(&metadata_store
.sort_composer
, &metadata_store
.sort_composer_changed
, cs
)) {
498 debug(2, "MH Sort Composer set to: \"%s\"", metadata_store
.sort_composer
);
499 metadata_packet_item_changed
= 1;
506 *(uint32_t *)typestring = htonl(type);
509 *(uint32_t *)codestring = htonl(code);
513 payload = strndup(data, length);
516 debug(1, "MH \"%s\" \"%s\" (%d bytes): \"%s\".", typestring, codestring, length,
524 } else if (type
== 'ssnc') {
526 // ignore the following
531 debug(2, "MH Metadata stream processing start.");
532 metadata_packet_item_changed
= 0;
535 if (metadata_packet_item_changed
!= 0)
536 debug(2, "MH Metadata stream processing end with changes.");
538 debug(2, "MH Metadata stream processing end without changes.");
539 changed
= metadata_packet_item_changed
;
542 debug(2, "MH Picture received, length %u bytes.", length
);
546 (strcmp(config
.cover_art_cache_dir
, "") != 0)) { // if it's okay to write the file
547 // make this uncancellable
549 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE
, &oldState
); // make this un-cancellable
550 char *pathname
= metadata_write_image_file(data
, length
);
551 snprintf(uri
, sizeof(uri
), "file://%s", pathname
);
553 pthread_setcancelstate(oldState
, NULL
);
558 if (string_update(&metadata_store
.cover_art_pathname
,
559 &metadata_store
.cover_art_pathname_changed
,
560 uri
)) // if the picture's file path is different from the stored one...
564 // pthread_cleanup_pop(0); // don't remove the lock -- it'll have been done
567 cs
= strndup(data
, length
);
568 if (string_update(&metadata_store
.client_ip
, &metadata_store
.client_ip_changed
, cs
)) {
570 debug(2, "MH Client IP set to: \"%s\"", metadata_store
.client_ip
);
575 cs
= strndup(data
, length
);
576 if (string_update(&metadata_store
.client_name
, &metadata_store
.client_name_changed
, cs
)) {
578 debug(2, "MH Client Name set to: \"%s\"", metadata_store
.client_name
);
583 cs
= strndup(data
, length
);
584 if (string_update(&metadata_store
.progress_string
, &metadata_store
.progress_string_changed
,
587 debug(2, "MH Progress String set to: \"%s\"", metadata_store
.progress_string
);
592 cs
= strndup(data
, length
);
593 if (string_update(&metadata_store
.frame_position_string
,
594 &metadata_store
.frame_position_string_changed
, cs
)) {
596 debug(2, "MH Frame Position String set to: \"%s\"", metadata_store
.frame_position_string
);
601 cs
= strndup(data
, length
);
602 if (string_update(&metadata_store
.first_frame_position_string
,
603 &metadata_store
.first_frame_position_string_changed
, cs
)) {
605 debug(2, "MH First Frame Position String set to: \"%s\"",
606 metadata_store
.first_frame_position_string
);
611 cs
= strndup(data
, length
);
612 if (string_update(&metadata_store
.stream_type
, &metadata_store
.stream_type_changed
, cs
)) {
614 debug(2, "MH Stream Type set to: \"%s\"", metadata_store
.stream_type
);
619 cs
= strndup(data
, length
);
620 if (string_update(&metadata_store
.server_ip
, &metadata_store
.server_ip_changed
, cs
)) {
622 debug(2, "MH Server IP set to: \"%s\"", metadata_store
.server_ip
);
627 changed
= (metadata_store
.active_state
!= AM_ACTIVE
);
628 metadata_store
.active_state
= AM_ACTIVE
;
631 changed
= (metadata_store
.active_state
!= AM_INACTIVE
);
632 metadata_store
.active_state
= AM_INACTIVE
;
636 changed
= ((metadata_store
.player_state
!= PS_PLAYING
) ||
637 (metadata_store
.player_thread_active
== 0));
638 metadata_store
.player_state
= PS_PLAYING
;
639 metadata_store
.player_thread_active
= 1;
642 changed
= ((metadata_store
.player_state
!= PS_STOPPED
) ||
643 (metadata_store
.player_thread_active
== 1));
644 metadata_store
.player_state
= PS_STOPPED
;
645 metadata_store
.player_thread_active
= 0;
648 changed
= (metadata_store
.player_state
!= PS_PAUSED
);
649 metadata_store
.player_state
= PS_PAUSED
;
652 // not using this anymore.
653 case 'pffr': // this is sent when the first frame has been received
655 changed = (metadata_store.player_state != PS_PLAYING);
656 metadata_store.player_state = PS_PLAYING;
660 // Note: it's assumed that the config.airplay volume has already been correctly set.
661 // int32_t actual_volume;
662 // int gv = dacp_get_volume(&actual_volume);
663 // metadata_hub_modify_prolog();
664 // if ((gv == 200) && (metadata_store.speaker_volume != actual_volume)) {
665 // metadata_store.speaker_volume = actual_volume;
668 if (metadata_store
.airplay_volume
!= config
.airplay_volume
) {
669 metadata_store
.airplay_volume
= config
.airplay_volume
;
676 uint32_t tm
= htonl(type
);
677 memcpy(typestring
, &tm
, sizeof(uint32_t));
680 uint32_t cm
= htonl(code
);
681 memcpy(codestring
, &cm
, sizeof(uint32_t));
685 payload
= strndup(data
, length
);
688 // debug(1, "MH \"%s\" \"%s\" (%d bytes): \"%s\".", typestring, codestring, length, payload);
694 pthread_cleanup_pop(0); // don't remove the lock
695 metadata_hub_modify_epilog(changed
);