1
0
mirror of https://gitlab.xiph.org/xiph/icecast-server.git synced 2024-06-16 06:15:24 +00:00

Feature: Extract metadata and export them to stats.xml

All metadata is now extracted from Vorbis streams and ICY streams
and exported as new <metadata> tag into admin/stats if specific
mountpoint is requested.

To testers:
 - Please test with Ogg Vorbis streams.
 - Please test with Ogg Vorbis + Theora streams.
 - Please test chained Ogg streams.
 - Please test with ICY streams.

close #2106
This commit is contained in:
Philipp Schafft 2014-12-09 16:08:27 +00:00
parent 795aa278ad
commit 28572be2ec
16 changed files with 140 additions and 111 deletions

View File

@ -30,6 +30,8 @@
# include <sys/types.h>
#endif
#include <vorbis/codec.h>
#include "connection.h"
#include "refbuf.h"
@ -409,4 +411,28 @@ static int format_prepare_headers (source_t *source, client_t *client)
return 0;
}
void format_set_vorbiscomment(format_plugin_t *plugin, const char *tag, const char *value) {
if (vorbis_comment_query_count(&plugin->vc, tag) != 0) {
/* delete key */
/* as libvorbis hides away all the memory functions we need to copy
* the structure comment by comment. sorry about that...
*/
vorbis_comment vc;
int i; /* why does vorbis_comment use int, not size_t? */
size_t keylen = strlen(tag);
vorbis_comment_init(&vc);
/* copy tags */
for (i = 0; i < plugin->vc.comments; i++) {
if (strncasecmp(plugin->vc.user_comments[i], tag, keylen) == 0 && plugin->vc.user_comments[i][keylen] == '=')
continue;
vorbis_comment_add(&vc, plugin->vc.user_comments[i]);
}
/* move vendor */
vc.vendor = plugin->vc.vendor;
plugin->vc.vendor = NULL;
vorbis_comment_clear(&plugin->vc);
plugin->vc = vc;
}
vorbis_comment_add_tag(&plugin->vc, tag, value);
}

View File

@ -18,6 +18,8 @@
#ifndef __FORMAT_H__
#define __FORMAT_H__
#include <vorbis/codec.h>
#include "client.h"
#include "refbuf.h"
#include "common/httpp/httpp.h"
@ -53,6 +55,9 @@ typedef struct _format_plugin_tag
void (*free_plugin)(struct _format_plugin_tag *self);
void (*apply_settings)(client_t *client, struct _format_plugin_tag *format, struct _mount_proxy *mount);
/* meta data */
vorbis_comment vc;
/* for internal state management */
void *_state;
} format_plugin_t;
@ -66,8 +71,11 @@ int format_advance_queue (struct source_tag *source, client_t *client);
int format_check_http_buffer (struct source_tag *source, client_t *client);
int format_check_file_buffer (struct source_tag *source, client_t *client);
void format_send_general_headers(format_plugin_t *format,
struct source_tag *source, client_t *client);
void format_set_vorbiscomment(format_plugin_t *plugin, const char *tag, const char *value);
#endif /* __FORMAT_H__ */

View File

@ -98,6 +98,7 @@ int format_ebml_get_plugin(source_t *source)
plugin->contenttype = httpp_getvar(source->parser, "content-type");
plugin->_state = ebml_source_state;
vorbis_comment_init(&plugin->vc);
source->format = plugin;
ebml_source_state->ebml = ebml_create();
@ -112,8 +113,8 @@ static void ebml_free_plugin(format_plugin_t *plugin)
refbuf_release(ebml_source_state->header);
ebml_destroy(ebml_source_state->ebml);
free(ebml_source_state);
vorbis_comment_clear(&plugin->vc);
free(plugin);
}
static int send_ebml_header(client_t *client)

View File

@ -42,7 +42,7 @@ static void flac_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
/* Here, we just verify the page is ok and then add it to the queue */
static refbuf_t *process_flac_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page)
static refbuf_t *process_flac_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
refbuf_t * refbuf;

View File

@ -74,7 +74,7 @@ static void kate_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
/* kate pages are not rebuilt, so here we just for headers and then
* pass them straight through to the the queue
*/
static refbuf_t *process_kate_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page)
static refbuf_t *process_kate_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
kate_codec_t *kate = codec->specific;
ogg_packet packet;

View File

@ -41,7 +41,7 @@ static void midi_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
/* Here, we just verify the page is ok and then add it to the queue */
static refbuf_t *process_midi_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page)
static refbuf_t *process_midi_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
refbuf_t * refbuf;

View File

@ -118,6 +118,8 @@ int format_mp3_get_plugin(source_t *source)
state->interval = state->inline_metadata_interval;
}
}
vorbis_comment_init(&plugin->vc);
source->format = plugin;
thread_mutex_create(&state->url_lock);
@ -147,23 +149,17 @@ static void mp3_set_tag (format_plugin_t *plugin, const char *tag, const char *i
value = strdup (in_value);
}
if (strcmp (tag, "title") == 0 || strcmp (tag, "song") == 0)
{
free (source_mp3->url_title);
source_mp3->url_title = value;
if (strcmp(tag, "title") == 0 || strcmp(tag, "song") == 0) {
tag = MP3_METADATA_TITLE;
} else if (strcmp(tag, "artist") == 0) {
tag = MP3_METADATA_ARTIST;
} else if (strcmp(tag, "url") == 0) {
tag = MP3_METADATA_URL;
}
else if (strcmp (tag, "artist") == 0)
{
free (source_mp3->url_artist);
source_mp3->url_artist = value;
}
else if (strcmp (tag, "url") == 0)
{
free (source_mp3->url);
source_mp3->url = value;
}
else
free (value);
format_set_vorbiscomment(plugin, tag, value);
free (value);
thread_mutex_unlock (&source_mp3->url_lock);
}
@ -239,6 +235,9 @@ static void mp3_set_title(source_t *source)
{
const char streamtitle[] = "StreamTitle='";
const char streamurl[] = "StreamUrl='";
const char *url_artist = vorbis_comment_query(&source->format->vc, MP3_METADATA_ARTIST, 0);
const char *url_title = vorbis_comment_query(&source->format->vc, MP3_METADATA_TITLE, 0);
const char *url = vorbis_comment_query(&source->format->vc, MP3_METADATA_URL, 0);
size_t size;
unsigned char len_byte;
refbuf_t *p;
@ -249,11 +248,11 @@ static void mp3_set_title(source_t *source)
thread_mutex_lock (&source_mp3->url_lock);
/* work out message length */
if (source_mp3->url_artist)
len += strlen (source_mp3->url_artist);
if (source_mp3->url_title)
len += strlen (source_mp3->url_title);
if (source_mp3->url_artist && source_mp3->url_title)
if (url_artist)
len += strlen (url_artist);
if (url_title)
len += strlen (url_title);
if (url_artist && url_title)
len += 3;
if (source_mp3->inline_url)
{
@ -261,8 +260,8 @@ static void mp3_set_title(source_t *source)
if (end)
len += end - source_mp3->inline_url+2;
}
else if (source_mp3->url)
len += strlen (source_mp3->url) + strlen (streamurl) + 2;
else if (url)
len += strlen (url) + strlen (streamurl) + 2;
#define MAX_META_LEN 255*16
if (len > MAX_META_LEN)
{
@ -283,12 +282,12 @@ static void mp3_set_title(source_t *source)
int r;
memset (p->data, '\0', size);
if (source_mp3->url_artist && source_mp3->url_title)
if (url_artist && url_title)
r = snprintf (p->data, size, "%c%s%s - %s';", len_byte, streamtitle,
source_mp3->url_artist, source_mp3->url_title);
url_artist, url_title);
else
r = snprintf (p->data, size, "%c%s%s';", len_byte, streamtitle,
source_mp3->url_title);
url_title);
if (r > 0)
{
if (source_mp3->inline_url)
@ -299,8 +298,8 @@ static void mp3_set_title(source_t *source)
if ((ssize_t)(size-r) > urllen)
snprintf (p->data+r, size-r, "StreamUrl='%s';", source_mp3->inline_url+11);
}
else if (source_mp3->url)
snprintf (p->data+r, size-r, "StreamUrl='%s';", source_mp3->url);
else if (url)
snprintf (p->data+r, size-r, "StreamUrl='%s';", url);
}
ICECAST_LOG_DEBUG("shoutcast metadata block setup with %s", p->data+1);
filter_shoutcast_metadata (source, p->data, size);
@ -450,12 +449,11 @@ static void format_mp3_free_plugin(format_plugin_t *self)
mp3_state *state = self->_state;
thread_mutex_destroy(&state->url_lock);
free(state->url_artist);
free(state->url_title);
free(self->charset);
refbuf_release(state->metadata);
refbuf_release(state->read_data);
free(state);
vorbis_comment_clear(&self->vc);
free(self);
}

View File

@ -18,14 +18,15 @@
#ifndef __FORMAT_MP3_H__
#define __FORMAT_MP3_H__
#define MP3_METADATA_TITLE "X_ICY_TITLE"
#define MP3_METADATA_ARTIST "X_ICY_ARTIST"
#define MP3_METADATA_URL "X_ICY_URL"
typedef struct {
/* These are for inline metadata */
int inline_metadata_interval;
int offset;
int interval;
char *url_artist;
char *url_title;
char *url;
char *inline_url;
int update_metadata;

View File

@ -176,6 +176,7 @@ int format_ogg_get_plugin(source_t *source)
plugin->contenttype = httpp_getvar (source->parser, "content-type");
ogg_sync_init (&state->oy);
vorbis_comment_init(&plugin->vc);
plugin->_state = state;
source->format = plugin;
@ -192,12 +193,13 @@ static void format_ogg_free_plugin (format_plugin_t *plugin)
/* free memory associated with this plugin instance */
free_ogg_codecs (state);
free (state->artist);
free (state->title);
ogg_sync_clear (&state->oy);
free (state);
vorbis_comment_clear(&plugin->vc);
free (plugin);
}
@ -277,14 +279,14 @@ static int process_initial_page (format_plugin_t *plugin, ogg_page *page)
static void update_comments(source_t *source)
{
ogg_state_t *ogg_info = source->format->_state;
char *title = ogg_info->title;
char *artist = ogg_info->artist;
char *title = vorbis_comment_query(&source->format->vc, "TITLE", 0);
char *artist = vorbis_comment_query(&source->format->vc, "ARTIST", 0);
char *metadata = NULL;
unsigned int len = 1; /* space for the nul byte at least */
ogg_codec_t *codec;
char codec_names [100] = "";
if (ogg_info->artist)
if (artist)
{
if (title)
{
@ -367,7 +369,7 @@ static refbuf_t *complete_buffer (source_t *source, refbuf_t *refbuf)
/* process the incoming page. this requires searching through the
* currently known codecs that have been seen in the stream
*/
static refbuf_t *process_ogg_page(ogg_state_t *ogg_info, ogg_page *page)
static refbuf_t *process_ogg_page(ogg_state_t *ogg_info, ogg_page *page, format_plugin_t *plugin)
{
ogg_codec_t *codec = ogg_info->codecs;
refbuf_t *refbuf = NULL;
@ -377,7 +379,7 @@ static refbuf_t *process_ogg_page(ogg_state_t *ogg_info, ogg_page *page)
if (ogg_page_serialno (page) == codec->os.serialno)
{
if (codec->process_page)
refbuf = codec->process_page(ogg_info, codec, page);
refbuf = codec->process_page(ogg_info, codec, page, plugin);
break;
}
@ -410,7 +412,7 @@ static refbuf_t *ogg_get_buffer(source_t *source)
/* if a codec has just been given a page then process it */
if (codec && codec->process)
{
refbuf = codec->process (ogg_info, codec);
refbuf = codec->process (ogg_info, codec, source->format);
if (refbuf)
return complete_buffer (source, refbuf);
@ -426,7 +428,7 @@ static refbuf_t *ogg_get_buffer(source_t *source)
else
{
ogg_info->bos_completed = 1;
refbuf = process_ogg_page (ogg_info, &page);
refbuf = process_ogg_page (ogg_info, &page, source->format);
}
if (ogg_info->error)
{

View File

@ -30,8 +30,6 @@ typedef struct ogg_state_tag
int codec_count;
struct ogg_codec_tag *codecs;
char *artist;
char *title;
int log_metadata;
refbuf_t *file_headers;
refbuf_t *header_pages;
@ -55,9 +53,9 @@ typedef struct ogg_codec_tag
refbuf_t *possible_start;
refbuf_t *page;
refbuf_t *(*process)(ogg_state_t *ogg_info, struct ogg_codec_tag *codec);
refbuf_t *(*process)(ogg_state_t *ogg_info, struct ogg_codec_tag *codec, format_plugin_t *plugin);
refbuf_t *(*process_page)(ogg_state_t *ogg_info,
struct ogg_codec_tag *codec, ogg_page *page);
struct ogg_codec_tag *codec, ogg_page *page, format_plugin_t *plugin);
void (*codec_free)(ogg_state_t *ogg_info, struct ogg_codec_tag *codec);
} ogg_codec_t;

View File

@ -37,7 +37,7 @@ static void opus_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
static refbuf_t *process_opus_page (ogg_state_t *ogg_info,
ogg_codec_t *codec, ogg_page *page)
ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
refbuf_t *refbuf;

View File

@ -44,7 +44,7 @@ static void skeleton_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
/* skeleton pages are not rebuilt, so here we just for headers and then
* pass them straight through to the the queue
*/
static refbuf_t *process_skeleton_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page)
static refbuf_t *process_skeleton_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
ogg_packet packet;

View File

@ -38,7 +38,7 @@ static void speex_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
static refbuf_t *process_speex_page (ogg_state_t *ogg_info,
ogg_codec_t *codec, ogg_page *page)
ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
refbuf_t *refbuf;

View File

@ -63,7 +63,7 @@ static void theora_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
/* theora pages are not rebuilt, so here we just for headers and then
* pass them straight through to the the queue
*/
static refbuf_t *process_theora_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page)
static refbuf_t *process_theora_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
theora_codec_t *theora = codec->specific;
ogg_packet packet;

View File

@ -38,7 +38,6 @@
typedef struct vorbis_codec_tag
{
vorbis_info vi;
vorbis_comment vc;
int rebuild_comment;
int stream_notify;
@ -58,15 +57,15 @@ typedef struct vorbis_codec_tag
ogg_packet *header[3];
ogg_int64_t prev_page_samples;
int (*process_packet)(ogg_state_t *ogg_info, ogg_codec_t *codec);
int (*process_packet)(ogg_state_t *ogg_info, ogg_codec_t *codec, format_plugin_t *plugin);
refbuf_t *(*get_buffer_page)(ogg_state_t *ogg_info, ogg_codec_t *codec);
} vorbis_codec_t;
static int process_vorbis_headers (ogg_state_t *ogg_info, ogg_codec_t *codec);
static int process_vorbis_headers (ogg_state_t *ogg_info, ogg_codec_t *codec, format_plugin_t *plugin);
static refbuf_t *process_vorbis_page (ogg_state_t *ogg_info,
ogg_codec_t *codec, ogg_page *page);
static refbuf_t *process_vorbis (ogg_state_t *ogg_info, ogg_codec_t *codec);
ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin);
static refbuf_t *process_vorbis (ogg_state_t *ogg_info, ogg_codec_t *codec, format_plugin_t *plugin);
static void vorbis_set_tag (format_plugin_t *plugin, const char *tag, const char *value, const char *charset);
@ -89,7 +88,6 @@ static void vorbis_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec)
stats_event (ogg_info->mount, "audio_channels", NULL);
stats_event (ogg_info->mount, "audio_samplerate", NULL);
vorbis_info_clear (&vorbis->vi);
vorbis_comment_clear (&vorbis->vc);
ogg_stream_clear (&codec->os);
ogg_stream_clear (&vorbis->new_os);
free_ogg_packet (vorbis->header[0]);
@ -235,7 +233,7 @@ static void initiate_flush (vorbis_codec_t *source_vorbis)
* and add them into the new stream, flushing after so many samples. We
* also check if an new headers are requested after each processed page
*/
static int process_vorbis_audio(ogg_state_t *ogg_info, ogg_codec_t *codec)
static int process_vorbis_audio(ogg_state_t *ogg_info, ogg_codec_t *codec, format_plugin_t *plugin)
{
vorbis_codec_t *source_vorbis = codec->specific;
@ -311,7 +309,7 @@ static int process_vorbis_audio(ogg_state_t *ogg_info, ogg_codec_t *codec)
/* This handles the headers at the backend, here we insert the header packets
* we want for the queue.
*/
static int process_vorbis_headers (ogg_state_t *ogg_info, ogg_codec_t *codec)
static int process_vorbis_headers (ogg_state_t *ogg_info, ogg_codec_t *codec, format_plugin_t *plugin)
{
vorbis_codec_t *source_vorbis = codec->specific;
@ -323,22 +321,15 @@ static int process_vorbis_headers (ogg_state_t *ogg_info, ogg_codec_t *codec)
/* NOTE: we could build a separate comment packet each time */
if (source_vorbis->rebuild_comment)
{
vorbis_comment vc;
ogg_packet header;
ice_config_t *config;
vorbis_comment_init (&vc);
if (ogg_info->artist)
vorbis_comment_add_tag (&vc, "artist", ogg_info->artist);
if (ogg_info->title)
vorbis_comment_add_tag (&vc, "title", ogg_info->title);
config = config_get_config();
vorbis_comment_add_tag (&vc, "server", config->server_id);
format_set_vorbiscomment(plugin, "server", config->server_id);
config_release_config();
vorbis_commentheader_out (&vc, &header);
vorbis_commentheader_out (&plugin->vc, &header);
ogg_stream_packetin (&source_vorbis->new_os, &header);
vorbis_comment_clear (&vc);
ogg_packet_clear (&header);
}
else
@ -369,16 +360,18 @@ ogg_codec_t *initial_vorbis_page (format_plugin_t *plugin, ogg_page *page)
ogg_stream_pagein (&codec->os, page);
vorbis_info_init (&vorbis->vi);
vorbis_comment_init (&vorbis->vc);
ogg_stream_packetout (&codec->os, &packet);
vorbis_comment_clear(&plugin->vc);
vorbis_comment_init(&plugin->vc);
ICECAST_LOG_DEBUG("checking for vorbis codec");
if (vorbis_synthesis_headerin (&vorbis->vi, &vorbis->vc, &packet) < 0)
if (vorbis_synthesis_headerin (&vorbis->vi, &plugin->vc, &packet) < 0)
{
ogg_stream_clear (&codec->os);
vorbis_info_clear (&vorbis->vi);
vorbis_comment_clear (&vorbis->vc);
vorbis_comment_clear (&plugin->vc);
free (vorbis);
free (codec);
return NULL;
@ -440,30 +433,18 @@ static void vorbis_set_tag (format_plugin_t *plugin, const char *tag, const char
if (value == NULL)
value = strdup (in_value);
if (strcmp (tag, "artist") == 0)
{
free (ogg_info->artist);
ogg_info->artist = value;
}
else if (strcmp (tag, "title") == 0)
{
free (ogg_info->title);
ogg_info->title = value;
}
else if (strcmp (tag, "song") == 0)
{
free (ogg_info->title);
ogg_info->title = value;
}
else
free (value);
if (strcmp(tag, "song") == 0)
tag = "title";
format_set_vorbiscomment(plugin, tag, value);
free (value);
}
/* main backend routine when rebuilding streams. Here we loop until we either
* have a refbuf to add onto the queue, or we want more data to process.
*/
static refbuf_t *process_vorbis (ogg_state_t *ogg_info, ogg_codec_t *codec)
static refbuf_t *process_vorbis (ogg_state_t *ogg_info, ogg_codec_t *codec, format_plugin_t *plugin)
{
vorbis_codec_t *source_vorbis = codec->specific;
refbuf_t *refbuf;
@ -478,7 +459,7 @@ static refbuf_t *process_vorbis (ogg_state_t *ogg_info, ogg_codec_t *codec)
}
if (source_vorbis->process_packet &&
source_vorbis->process_packet (ogg_info, codec) > 0)
source_vorbis->process_packet (ogg_info, codec, plugin) > 0)
continue;
return NULL;
}
@ -489,7 +470,7 @@ static refbuf_t *process_vorbis (ogg_state_t *ogg_info, ogg_codec_t *codec)
* back for adding to the queue
*/
static refbuf_t *process_vorbis_passthru_page (ogg_state_t *ogg_info,
ogg_codec_t *codec, ogg_page *page)
ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
return make_refbuf_with_page (page);
}
@ -499,11 +480,10 @@ static refbuf_t *process_vorbis_passthru_page (ogg_state_t *ogg_info,
* add all pages from the stream before processing packets
*/
static refbuf_t *process_vorbis_page (ogg_state_t *ogg_info,
ogg_codec_t *codec, ogg_page *page)
ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin)
{
ogg_packet header;
vorbis_codec_t *source_vorbis = codec->specific;
char *comment;
if (ogg_stream_pagein (&codec->os, page) < 0)
{
@ -532,8 +512,7 @@ static refbuf_t *process_vorbis_page (ogg_state_t *ogg_info,
return NULL;
}
/* change comments here if need be */
if (vorbis_synthesis_headerin (&source_vorbis->vi, &source_vorbis->vc, &header) < 0)
if (vorbis_synthesis_headerin (&source_vorbis->vi, &plugin->vc, &header) < 0)
{
ogg_info->error = 1;
ICECAST_LOG_WARN("Problem parsing ogg vorbis header");
@ -560,19 +539,6 @@ static refbuf_t *process_vorbis_page (ogg_state_t *ogg_info,
codec->process_page = process_vorbis_passthru_page;
}
free (ogg_info->title);
comment = vorbis_comment_query (&source_vorbis->vc, "TITLE", 0);
if (comment)
ogg_info->title = strdup (comment);
else
ogg_info->title = NULL;
free (ogg_info->artist);
comment = vorbis_comment_query (&source_vorbis->vc, "ARTIST", 0);
if (comment)
ogg_info->artist = strdup (comment);
else
ogg_info->artist = NULL;
ogg_info->log_metadata = 1;
stats_event_args (ogg_info->mount, "audio_samplerate", "%ld", (long)source_vorbis->vi.rate);

View File

@ -19,6 +19,7 @@
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <libxml/xmlmemory.h>
#include <libxml/parser.h>
@ -985,6 +986,29 @@ void stats_transform_xslt(client_t *client, const char *uri)
free(xslpath);
}
static void __add_metadata(xmlNodePtr node, const char *tag) {
const char *value = strstr(tag, "=");
char *name = NULL;
size_t namelen = value - tag + 1;
size_t i;
if (!value)
return;
name = malloc(namelen);
if (!name)
return;
for (i = 0; i < (namelen - 1); i++)
name[i] = tolower(tag[i]);
name[namelen-1] = 0;
xmlNewTextChild(node, NULL, XMLSTR(name), XMLSTR(value+1));
free(name);
}
xmlDocPtr stats_get_xml(int show_hidden, const char *show_mount)
{
xmlDocPtr doc;
@ -998,8 +1022,13 @@ xmlDocPtr stats_get_xml(int show_hidden, const char *show_mount)
node = _dump_stats_to_doc (node, show_mount, show_hidden);
if (show_mount && node) {
xmlNodePtr metadata = xmlNewTextChild(node, NULL, XMLSTR("metadata"), NULL);
int i;
avl_tree_rlock(global.source_tree);
source = source_find_mount_raw(show_mount);
for (i = 0; i < source->format->vc.comments; i++)
__add_metadata(metadata, source->format->vc.user_comments[i]);
admin_add_listeners_to_mount(source, node);
avl_tree_unlock(global.source_tree);
}