diff --git a/NEWS b/NEWS index ba8c2d9..ca2cb91 100644 --- a/NEWS +++ b/NEWS @@ -25,6 +25,10 @@ Changes in 0.4.0, (SVN trunk): - [ADD] New runtime control via the SIGUSR2 signal, which triggers reading of fresh metadata information from (metadata is always read at song changes.) + - [ADD] New configuration option, to customize metadata + strings when used with the new feature. + - [ADD] New '@a@' and '@t@' placeholders for separate artist and title + metadata in de-/encoder commands. diff --git a/doc/ezstream.1.in b/doc/ezstream.1.in index 2d96d04..cde6986 100644 --- a/doc/ezstream.1.in +++ b/doc/ezstream.1.in @@ -215,6 +215,17 @@ configuration file. See the .Sy SCRIPTING section for details on how the metadata program must behave. +.It Sy \& +.Pq Optional. +Set the format of the string that should be used for the +.Sq @M@ +placeholder when setting metadata with an external program or script via +\&. +.Pp +See the +.Sy METADATA +section for details on how metadata is handled by +.Nm . .It Sy \& Set to .Sy 1 @@ -339,16 +350,17 @@ Set the command to decode the specified media file format to raw data and send it to standard output. During runtime, the placeholder .Sq Li @T@ -is replaced with the fully qualified name of the media file, as specified in -the \& element or a playlist file. +is replaced with the name of the media file, as it is specified in the +\& element or contained in a playlist file. It should always be enclosed in quotes, to prevent problems with filenames that contain whitespaces. .Pp -The metadata placeholder, -.Sq @M@ , -is also available in the \& element. -That way it can be used for combined de-/encoder programs that produce readily -streamable data. +Metadata placeholders can be used in the \& element as well, for +combined de-/encoder programs that produce streamable data. +See the +.Sy METADATA +section for details on how metadata is handled by +.Nm . .Pp For example, to decode Ogg Vorbis files using the .Cm oggdec @@ -358,15 +370,13 @@ utility: .It Sy \& Set the command to encode raw data, received from standard input, to the specified stream format. -During runtime, the placeholder -.Sq Li @M@ -is replaced with the metadata -.Po -e.g. -.Dq Artist - Title -.Pc -for the current track. -It also should be enclosed in quotes at all times. +.Pp +Metadata placeholders can be used in the \& element. +For details about using metadata in +.Nm , +see below in the +.Sy METADATA +section. .Pp For example, to encode an Ogg Vorbis stream using the quality setting 1.5 with the @@ -423,11 +433,89 @@ When called with the command line parameter the program should return only the title information of the metadata. .Pq Optional. .El +.Sh METADATA +The main tool for handling metadata with +.Nm +is placeholders in decoder and encoder commands that are replaced with real +content during runtime. +The tricky part about is that one placeholders has to be handled differently +depending on where the metadata comes from. +This section will explain each possible scenario. +.Ss Metadata Placeholders +.Bl -tag -width -Ds +.It Sy @T@ +Replaced with the media file name. +Required in \& and is available in \&. +.It Sy @M@ +Replaced with a metadata string. +See below for a detailed explanation. +Available in \& and \&. +.It Sy @a@ +Replaced with the artist information. +Available in \&, \& and \&. +.It Sy @t@ +Replaced with the title information. +Available in \&, \& and \&. +.It Sy @s@ +Replaced with the string returned by \& when called +without any command line parameters. +Available only in \&. +.El +.Ss The @M@ Placeholder +While all other placeholders are simply replaced with whatever data they are +associated with, +.Sq @M@ +is context-sensitive. +The logic used by +.Nm +is the following: +.Bd -literal -offset indent +If ('@M@ is present') + If ('\&' AND '\&') + Replace with format string result. + Else + If (NOT '\&' AND '@t@ is present') + Replace with empty string. + else + Replace with generated metadata string. + Endif + Endif +Endif +.Ed +.Pp +The generated metadata string for +.Sq @M@ +is of the format +.Dq Artist - Title , +if both artist and title information is available. +If one of the two is missing, the available one is displayed without a leading +or trailing dash, e.g. just +.Dq Artist . +If neither artist nor title are available, the name of the media file, without +its file extension, is used. +.Ss Metadata Caveats +It is possible to generate strange results with odd combinations of +placeholders, external metadata programs and updates during runtime via +.Sy SIGUSR2 . +If things start to become just confusing, simplify. +.Pp +Metadata updates during runtime are done with a relatively broken feature of +libshout. +Additional metadata information that is already present in the stream sent via +.Nm +is usually destroyed and replaced with the new data. +It is not possible to properly discern between artist and title information, +which means that anything set with the +.Sy SIGUSR2 +feature will continue to end up entirely in the +.Qq Title +field of a stream. .Sh FILES .Bl -tag -width "!!EXAMPLES_DIR!!" -compact .It Pa !!EXAMPLES_DIR!! Directory containing example configuration files for various uses of -.Nm . +.Nm , +as well as example playlist and metadata scripts. .El .Sh AUTHORS .Nm diff --git a/src/configfile.c b/src/configfile.c index 01c5498..5cd94e4 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -39,6 +39,7 @@ static const char *blankString = ""; void freeConfig(EZCONFIG *); unsigned int checkDecoderLine(const char *, const char *, long); unsigned int checkEncoderLine(const char *, const char *, long); +unsigned int checkFormatLine(const char *, const char *, long); EZCONFIG * getEZConfig(void) @@ -198,6 +199,27 @@ parseConfig(const char *fileName) xmlFree(ls_xmlContentPtr); } } + if (!xmlStrcmp(cur->name, BAD_CAST "metadata_format")) { + if (ezConfig.metadataFormat != NULL) { + printf("%s[%ld]: Error: Cannot have multiple elements\n", + fileName, xmlGetLineNo(cur)); + config_error++; + continue; + } + if (cur->xmlChildrenNode != NULL) { + unsigned int ret; + + ls_xmlContentPtr = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); + ezConfig.metadataFormat = xstrdup(ls_xmlContentPtr); + xmlFree(ls_xmlContentPtr); + if ((ret = checkFormatLine(ezConfig.metadataFormat, + fileName, xmlGetLineNo(cur))) + > 0) { + config_error += ret; + continue; + } + } + } if (!xmlStrcmp(cur->name, BAD_CAST "playlist_program")) { if (program_set) { printf("%s[%ld]: Error: Cannot have multiple elements\n", @@ -564,6 +586,8 @@ freeConfig(EZCONFIG *cfg) xfree(cfg->fileName); if (cfg->metadataProgram != NULL) xfree(cfg->metadataProgram); + if (cfg->metadataFormat != NULL) + xfree(cfg->metadataFormat); if (cfg->serverName != NULL) xfree(cfg->serverName); if (cfg->serverURL != NULL) @@ -604,15 +628,22 @@ checkDecoderLine(const char *str, const char *file, long line) { unsigned int errors; char *p; + int have_track = 0; errors = 0; + if ((p = strstr(str, STRING_PLACEHOLDER)) != NULL) { + printf("%s[%ld]: Error: `%s' placeholder not allowed in decoder command\n", + file, line, STRING_PLACEHOLDER); + errors++; + } if ((p = strstr(str, TRACK_PLACEHOLDER)) != NULL) { p += strlen(TRACK_PLACEHOLDER); if ((p = strstr(p, TRACK_PLACEHOLDER)) != NULL) { printf("%s[%ld]: Error: Multiple `%s' placeholders in decoder command\n", file, line, TRACK_PLACEHOLDER); errors++; - } + } else + have_track = 1; } if ((p = strstr(str, METADATA_PLACEHOLDER)) != NULL) { p += strlen(METADATA_PLACEHOLDER); @@ -639,6 +670,12 @@ checkDecoderLine(const char *str, const char *file, long line) } } + if (!have_track) { + printf("%s[%ld]: Error: The decoder command requires the '%s' track placeholder\n", + file, line, TRACK_PLACEHOLDER); + errors++; + } + return (errors); } @@ -654,6 +691,11 @@ checkEncoderLine(const char *str, const char *file, long line) file, line, TRACK_PLACEHOLDER); errors++; } + if ((p = strstr(str, STRING_PLACEHOLDER)) != NULL) { + printf("%s[%ld]: Error: `%s' placeholder not allowed in encoder command\n", + file, line, STRING_PLACEHOLDER); + errors++; + } if ((p = strstr(str, METADATA_PLACEHOLDER)) != NULL) { p += strlen(METADATA_PLACEHOLDER); if ((p = strstr(p, METADATA_PLACEHOLDER)) != NULL) { @@ -681,3 +723,51 @@ checkEncoderLine(const char *str, const char *file, long line) return (errors); } + +unsigned int +checkFormatLine(const char *str, const char *file, long line) +{ + unsigned int errors; + char *p; + + errors = 0; + if ((p = strstr(str, METADATA_PLACEHOLDER)) != NULL) { + printf("%s[%ld]: Error: `%s' placeholder not allowed in \n", + file, line, METADATA_PLACEHOLDER); + errors++; + } + if ((p = strstr(str, TRACK_PLACEHOLDER)) != NULL) { + p += strlen(TRACK_PLACEHOLDER); + if ((p = strstr(p, TRACK_PLACEHOLDER)) != NULL) { + printf("%s[%ld]: Error: Multiple `%s' placeholders in \n", + file, line, TRACK_PLACEHOLDER); + errors++; + } + } + if ((p = strstr(str, STRING_PLACEHOLDER)) != NULL) { + p += strlen(STRING_PLACEHOLDER); + if ((p = strstr(p, STRING_PLACEHOLDER)) != NULL) { + printf("%s[%ld]: Error: Multiple `%s' placeholders in \n", + file, line, STRING_PLACEHOLDER); + errors++; + } + } + if ((p = strstr(str, ARTIST_PLACEHOLDER)) != NULL) { + p += strlen(ARTIST_PLACEHOLDER); + if ((p = strstr(p, ARTIST_PLACEHOLDER)) != NULL) { + printf("%s[%ld]: Error: Multiple `%s' placeholders in \n", + file, line, ARTIST_PLACEHOLDER); + errors++; + } + } + if ((p = strstr(str, TITLE_PLACEHOLDER)) != NULL) { + p += strlen(TITLE_PLACEHOLDER); + if ((p = strstr(p, TITLE_PLACEHOLDER)) != NULL) { + printf("%s[%ld]: Error: Multiple `%s' placeholders in \n", + file, line, TITLE_PLACEHOLDER); + errors++; + } + } + + return (errors); +} diff --git a/src/configfile.h b/src/configfile.h index 8ae3f8a..fcd1f6b 100644 --- a/src/configfile.h +++ b/src/configfile.h @@ -33,6 +33,7 @@ #define METADATA_PLACEHOLDER "@M@" #define ARTIST_PLACEHOLDER "@a@" #define TITLE_PLACEHOLDER "@t@" +#define STRING_PLACEHOLDER "@s@" typedef struct tag_FORMAT_ENCDEC { char *format; @@ -47,6 +48,7 @@ typedef struct tag_EZCONFIG { char *format; char *fileName; char *metadataProgram; + char *metadataFormat; char *serverName; char *serverURL; char *serverGenre; diff --git a/src/ezstream.c b/src/ezstream.c index d8b7b18..2f3c169 100644 --- a/src/ezstream.c +++ b/src/ezstream.c @@ -112,6 +112,7 @@ typedef struct tag_ID3Tag { int urlParse(const char *, char **, int *, char **); void replaceString(const char *, char *, size_t, const char *, const char *); char * buildCommandString(const char *, const char *, metadata_t *); +char * getMetadataString(const char *, metadata_t *); metadata_t * getMetadata(const char *); int setMetadata(shout_t *, metadata_t *, char **); FILE * openResource(shout_t *, const char *, int *, char **, int *); @@ -247,14 +248,61 @@ buildCommandString(const char *extension, const char *fileName, newDecoder = xcalloc(1, newDecoderLen); replaceString(decoder, newDecoder, newDecoderLen, TRACK_PLACEHOLDER, fileName); - if (strstr(decoder, METADATA_PLACEHOLDER) != NULL) { - size_t tmpLen = strlen(newDecoder) + strlen(metadata_get_string(mdata)) + 1; + if (strstr(decoder, ARTIST_PLACEHOLDER) != NULL) { + size_t tmpLen = strlen(newDecoder) + strlen(metadata_get_artist(mdata)) + 1; char *tmpStr = xcalloc(1, tmpLen); - replaceString(newDecoder, tmpStr, tmpLen, METADATA_PLACEHOLDER, - metadata_get_string(mdata)); + replaceString(newDecoder, tmpStr, tmpLen, ARTIST_PLACEHOLDER, + metadata_get_artist(mdata)); xfree(newDecoder); newDecoder = tmpStr; } + if (strstr(decoder, TITLE_PLACEHOLDER) != NULL) { + size_t tmpLen = strlen(newDecoder) + strlen(metadata_get_title(mdata)) + 1; + char *tmpStr = xcalloc(1, tmpLen); + replaceString(newDecoder, tmpStr, tmpLen, TITLE_PLACEHOLDER, + metadata_get_title(mdata)); + xfree(newDecoder); + newDecoder = tmpStr; + } + /* + * if meta + * if (prog && format) + * metatoformat + * else + * if (!prog && title) + * emptymeta + * else + * replacemeta + */ + if (strstr(decoder, METADATA_PLACEHOLDER) != NULL) { + if (metadataFromProgram && pezConfig->metadataFormat != NULL) { + char *mdataString = getMetadataString(pezConfig->metadataFormat, mdata); + size_t tmpLen = strlen(newDecoder) + strlen(mdataString) + 1; + char *tmpStr = xcalloc(1, tmpLen); + replaceString(newDecoder, tmpStr, tmpLen, + METADATA_PLACEHOLDER, mdataString); + xfree(newDecoder); + xfree(mdataString); + newDecoder = tmpStr; + } else { + if (!metadataFromProgram && strstr(decoder, TITLE_PLACEHOLDER) != NULL) { + size_t tmpLen = strlen(newDecoder) + 1; + char *tmpStr = xcalloc(1, tmpLen); + replaceString(newDecoder, tmpStr, tmpLen, + METADATA_PLACEHOLDER, ""); + xfree(newDecoder); + newDecoder = tmpStr; + } else { + size_t tmpLen = strlen(newDecoder) + strlen(metadata_get_string(mdata)) + 1; + char *tmpStr = xcalloc(1, tmpLen); + replaceString(newDecoder, tmpStr, tmpLen, + METADATA_PLACEHOLDER, + metadata_get_string(mdata)); + xfree(newDecoder); + newDecoder = tmpStr; + } + } + } encoder = xstrdup(getFormatEncoder(pezConfig->format)); if (strlen(encoder) == 0) { @@ -272,10 +320,47 @@ buildCommandString(const char *extension, const char *fileName, return (commandString); } - newEncoderLen = strlen(encoder) + strlen(metadata_get_string(mdata)) + 1; + newEncoderLen = strlen(encoder) + strlen(metadata_get_artist(mdata)) + 1; newEncoder = xcalloc(1, newEncoderLen); - replaceString(encoder, newEncoder, newEncoderLen, METADATA_PLACEHOLDER, - metadata_get_string(mdata)); + replaceString(encoder, newEncoder, newEncoderLen, ARTIST_PLACEHOLDER, + metadata_get_artist(mdata)); + if (strstr(encoder, TITLE_PLACEHOLDER) != NULL) { + size_t tmpLen = strlen(newEncoder) + strlen(metadata_get_title(mdata)) + 1; + char *tmpStr = xcalloc(1, tmpLen); + replaceString(newEncoder, tmpStr, tmpLen, TITLE_PLACEHOLDER, + metadata_get_title(mdata)); + xfree(newEncoder); + newEncoder = tmpStr; + } + if (strstr(encoder, METADATA_PLACEHOLDER) != NULL) { + if (metadataFromProgram && pezConfig->metadataFormat != NULL) { + char *mdataString = getMetadataString(pezConfig->metadataFormat, mdata); + size_t tmpLen = strlen(newEncoder) + strlen(mdataString) + 1; + char *tmpStr = xcalloc(1, tmpLen); + replaceString(newEncoder, tmpStr, tmpLen, + METADATA_PLACEHOLDER, mdataString); + xfree(newEncoder); + xfree(mdataString); + newEncoder = tmpStr; + } else { + if (!metadataFromProgram && strstr(encoder, TITLE_PLACEHOLDER) != NULL) { + size_t tmpLen = strlen(newEncoder) + 1; + char *tmpStr = xcalloc(1, tmpLen); + replaceString(newEncoder, tmpStr, tmpLen, + METADATA_PLACEHOLDER, ""); + xfree(newEncoder); + newEncoder = tmpStr; + } else { + size_t tmpLen = strlen(newEncoder) + strlen(metadata_get_string(mdata)) + 1; + char *tmpStr = xcalloc(1, tmpLen); + replaceString(newEncoder, tmpStr, tmpLen, + METADATA_PLACEHOLDER, + metadata_get_string(mdata)); + xfree(newEncoder); + newEncoder = tmpStr; + } + } + } commandStringLen = strlen(newDecoder) + strlen(" | ") + strlen(newEncoder) + 1; @@ -291,6 +376,59 @@ buildCommandString(const char *extension, const char *fileName, return (commandString); } +char * +getMetadataString(const char *format, metadata_t *mdata) +{ + char *tmp, *str; + size_t siz; + + if (mdata == NULL) { + printf("%s: getMetadataString(): Internal error: NULL metadata_t\n", + __progname); + abort(); + } + + if (format == NULL) + return (NULL); + + str = xstrdup(format); + + if (strstr(format, ARTIST_PLACEHOLDER) != NULL) { + siz = strlen(str) + strlen(metadata_get_artist(mdata)) + 1; + tmp = xcalloc(1, siz); + replaceString(str, tmp, siz, ARTIST_PLACEHOLDER, + metadata_get_artist(mdata)); + xfree(str); + str = tmp; + } + if (strstr(format, TITLE_PLACEHOLDER) != NULL) { + siz = strlen(str) + strlen(metadata_get_title(mdata)) + 1; + tmp = xcalloc(1, siz); + replaceString(str, tmp, siz, TITLE_PLACEHOLDER, + metadata_get_title(mdata)); + xfree(str); + str = tmp; + } + if (strstr(format, STRING_PLACEHOLDER) != NULL) { + siz = strlen(str) + strlen(metadata_get_string(mdata)) + 1; + tmp = xcalloc(1, siz); + replaceString(str, tmp, siz, STRING_PLACEHOLDER, + metadata_get_string(mdata)); + xfree(str); + str = tmp; + } + if (strstr(format, TRACK_PLACEHOLDER) != NULL) { + siz = strlen(str) + strlen(metadata_get_filename(mdata)) + 1; + tmp = xcalloc(1, siz); + replaceString(str, tmp, siz, TRACK_PLACEHOLDER, + metadata_get_filename(mdata)); + xfree(str); + str = tmp; + } + + return (str); +} + metadata_t * getMetadata(const char *fileName) { @@ -339,10 +477,13 @@ setMetadata(shout_t *shout, metadata_t *mdata, char **mdata_copy) exit(1); } - if (metadata_get_artist(mdata) == NULL && metadata_get_title(mdata) == NULL) - songInfo = xstrdup(metadata_get_string(mdata)); - else - songInfo = metadata_assemble_string(mdata); + if ((songInfo = getMetadataString(pezConfig->metadataFormat, mdata)) == NULL) { + if (strlen(metadata_get_artist(mdata)) == 0 && + strlen(metadata_get_title(mdata)) == 0) + songInfo = xstrdup(metadata_get_string(mdata)); + else + songInfo = metadata_assemble_string(mdata); + } if (shout_metadata_add(shout_mdata, "song", songInfo) != SHOUTERR_SUCCESS) { /* Assume SHOUTERR_MALLOC */ diff --git a/src/metadata.c b/src/metadata.c index d5959fa..3a3e07d 100644 --- a/src/metadata.c +++ b/src/metadata.c @@ -43,8 +43,10 @@ #include "strfctns.h" #include "util.h" -extern char *__progname; -extern int vFlag; +extern char *__progname; +extern int vFlag; + +static const char *blankString = ""; struct metadata { char *filename; @@ -563,7 +565,10 @@ metadata_get_artist(metadata_t *md) abort(); } - return (md->artist); + if (md->artist == NULL) + return (blankString); + else + return (md->artist); } const char * @@ -575,7 +580,26 @@ metadata_get_title(metadata_t *md) abort(); } - return (md->title); + if (md->title == NULL) + return (blankString); + else + return (md->title); +} + +const char * +metadata_get_filename(metadata_t *md) +{ + if (md == NULL) { + printf("%s: metadata_get_filename(): Internal error: Bad arguments\n", + __progname); + abort(); + } + + if (md->filename == NULL) + /* Should never happen: */ + return (blankString); + else + return (md->filename); } char * diff --git a/src/metadata.h b/src/metadata.h index 692fb0e..a80ab97 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -75,23 +75,27 @@ int metadata_program_update(metadata_t *, enum metadata_request); /* * Returns a pointer to a metadata string ``artist - title'', or just * ``artist'' or ``title'' if one of the two is not available. If neither - * are present, it usually returns the filename without the extension. - * This function never returns NULL. + * are present, it returns the filename without the extension. An empty string + * is returned for metadata_program() handles that didn't supply any generic + * information. */ const char * metadata_get_string(metadata_t *); /* - * Returns a pointer to the artist string, or NULL if the file did not - * contain any artist information. + * Returns a pointer to the artist string, which may be empty. */ const char * metadata_get_artist(metadata_t *); /* - * Returns a pointer to the title string, or NULL if the file did not - * contain any title information. + * Returns a pointer to the title string, which may be empty. */ const char * metadata_get_title(metadata_t *); +/* + * Returns a pointer to the filename used in the metadata handle. + */ +const char * metadata_get_filename(metadata_t *); + /* * Allocates and returns a meaningful string based on a metadata handle's * content. The result is what metadata_get_string() defaults to if an external