From 304908bff43ec96e91bce2dd1e7366aed131c025 Mon Sep 17 00:00:00 2001 From: moritz Date: Fri, 9 Mar 2007 02:30:29 +0000 Subject: [PATCH] Add new configuration option, which specifies an external program/script to get metadata from. Also include SIGUSR2 handling that triggers metadata updates from the external program mid-stream. git-svn-id: https://svn.xiph.org/trunk/ezstream@12693 0101bb08-14d6-0310-b084-bc0e0c8e3800 --- NEWS | 6 +++ doc/ezstream.1.in | 92 ++++++++++++++++++++++++++++---- src/configfile.c | 21 ++++++++ src/configfile.h | 1 + src/ezstream.c | 111 ++++++++++++++++++++++++++++---------- src/metadata.c | 132 +++++++++++++++++++++++++++++++++++++++++++++- src/metadata.h | 3 +- 7 files changed, 324 insertions(+), 42 deletions(-) diff --git a/NEWS b/NEWS index 76490e2..ba8c2d9 100644 --- a/NEWS +++ b/NEWS @@ -19,6 +19,12 @@ Changes in 0.4.0, (SVN trunk): * various: - [ADD] Allow ezstream to use TagLib for reading metadata from media files. TagLib (libtag_c) is now an optional dependency. + - [ADD] New configuration option, which causes + metadata to be read from the output of an external program or + script. + - [ADD] New runtime control via the SIGUSR2 signal, which triggers reading + of fresh metadata information from (metadata + is always read at song changes.) diff --git a/doc/ezstream.1.in b/doc/ezstream.1.in index 200d5ef..2d96d04 100644 --- a/doc/ezstream.1.in +++ b/doc/ezstream.1.in @@ -8,7 +8,7 @@ .Os .Sh NAME .Nm ezstream -.Nd source client for Icecast with external en-/decoder support +.Nd source client for Icecast with external de-/encoder support .Sh SYNOPSIS .Nm .Bk -words @@ -59,8 +59,7 @@ and bitrate \(em is displayed. .Ss Runtime control On POSIX systems, .Nm -offers limited runtime control via signals when it is not streaming data from -standard input. +offers limited runtime control via signals. By sending a signal to the ezstream process, e.g. with the .Xr kill 1 utility, a certain action will be triggered. @@ -74,6 +73,11 @@ following it, or restarts from the beginning of the list otherwise. .It Cd SIGUSR1 Skips the currently playing track and moves on to the next in playlist mode, or restarts the current track when streaming a single file. +.It Cd SIGUSR2 +Triggers rereading of metadata for the stream by running the program or script +specified in \& +.Pq see below. +This is the only meaningful signal when streaming from standard input. .El .Pp .Sh CONFIGURATION FILE SYNTAX @@ -103,7 +107,7 @@ In the configuration file, they need to be used as .Em start tag + content + end tag , like in the introductory example shown above. .Ss Root element -.Bl -ohang +.Bl -tag -width -Ds .It Sy \& .Pq Mandatory. The configuration file's root element. @@ -112,7 +116,7 @@ It contains all other configuration elements. .Ss Global configuration elements Each of the global configuration elements have the \& element as their parent. -.Bl -ohang +.Bl -tag -width -Ds .It Sy \& .Pq Mandatory. Specifies the location and mountpoint of the Icecast server, to which the @@ -170,10 +174,6 @@ Set to .Pq one to indicate that the file in \& is actually an executable program or script. -This program is supposed to print -.Pq to standard output -one line with the name of a file that should be streamed next and then exit. -.Pp If set to .Sy 0 .Pq zero , @@ -181,6 +181,10 @@ If set to keyword .Pa stdin .Pq the default . +.Pp +See the +.Sy SCRIPTING +section for details on how the playlist program must behave. .It Sy \& .Pq Optional. Set to @@ -193,6 +197,24 @@ Files are played sequentially if set to or when the \& element is absent. This option will be ignored if \& is set to 1 .Pq one. +.It Sy \& +.Pq Optional. +Set the path and name of an executable program or script that should be used by +.Nm +to set the metadata of the stream. +The program is automatically queried when a new track is streamed, or whenever +the +.Sy SIGUSR2 +signal is received. +.Pp +If the \& element is present in the configuration, no +attempts will be made to read metadata from files that are being streamed. +If this behavior is not desired, it should be removed or commented out in the +configuration file. +.Pp +See the +.Sy SCRIPTING +section for details on how the metadata program must behave. .It Sy \& Set to .Sy 1 @@ -268,7 +290,7 @@ should be done. .Ss Reencoding settings Each of the reencoding configuration elements have the \& element as their parent. -.Bl -ohang +.Bl -tag -width -Ds .It Sy \& Set to .Sy 1 @@ -287,7 +309,7 @@ Each format is described by a separate \& element. .Ss Decoder/Encoder settings Each of the decoder/encoder configuration elements have the \& element as their parent. -.Bl -ohang +.Bl -tag -width -Ds .It Sy \& This element is used by .Nm @@ -353,6 +375,54 @@ utility: .Pp .Dl \&oggenc -r -q 1.5 -t \&"@M@\&" -\& .El +.Sh SCRIPTING +The +.Nm +utility provides hooks for externally controlled playlist and metadata +management. +This is done by running external programs or scripts that need to behave in +ways explained here. +.Ss Common Rules +.Bl -dash -compact +.It +The program must be an executable file. +.It +The program must write one line to standard output and exit. +.It +The program must not require arbitary command line options to function. +A wrapper script must be used if there is no other way. +.El +.Ss Playlist Programs +.Bl -dash -compact +.It +The program must return only filenames, with one filename per execution. +.It +The program should not return an empty line unless +.Nm +is supposed to know that the end of the playlist has been reached. +This is significant when the \& option is enabled. +.El +.Ss Metadata Programs +.Bl -dash -compact +.It +The program must not return anything (just a newline character is okay) if it +is called by +.Nm +with a command line parameter that the program does not support. +.It +When called without command line parameters, the program should return a +complete string that should be used for metadata. +.It +When called with the command line parameter +.Qq artist , +the program should return only the artist information of the metadata. +.Pq Optional. +.It +When called with the command line parameter +.Qq title , +the program should return only the title information of the metadata. +.Pq Optional. +.El .Sh FILES .Bl -tag -width "!!EXAMPLES_DIR!!" -compact .It Pa !!EXAMPLES_DIR!! diff --git a/src/configfile.c b/src/configfile.c index 03beb3e..d9429eb 100644 --- a/src/configfile.c +++ b/src/configfile.c @@ -98,6 +98,8 @@ freeConfig(EZCONFIG *cfg) xfree(cfg->format); if (cfg->fileName != NULL) xfree(cfg->fileName); + if (cfg->metadataProgram != NULL) + xfree(cfg->metadataProgram); if (cfg->serverName != NULL) xfree(cfg->serverName); if (cfg->serverURL != NULL) @@ -228,6 +230,25 @@ parseConfig(const char *fileName) xmlFree(ls_xmlContentPtr); } } + if (!xmlStrcmp(cur->name, BAD_CAST "metadata_progname")) { + if (ezConfig.metadataProgram != NULL) { + printf("%s[%ld]: Error: Cannot have multiple elements\n", + fileName, xmlGetLineNo(cur)); + config_error++; + continue; + } + if (cur->xmlChildrenNode != NULL) { + ls_xmlContentPtr = (char *)xmlNodeListGetString(doc, cur->xmlChildrenNode, 1); + if (strlen(ls_xmlContentPtr) > PATH_MAX - 1) { + printf("%s[%ld]: Error: Path or filename in is too long\n", + fileName, xmlGetLineNo(cur)); + config_error++; + continue; + } + ezConfig.metadataProgram = xstrdup(ls_xmlContentPtr); + xmlFree(ls_xmlContentPtr); + } + } if (!xmlStrcmp(cur->name, BAD_CAST "playlist_program")) { if (program_set) { printf("%s[%ld]: Error: Cannot have multiple elements\n", diff --git a/src/configfile.h b/src/configfile.h index 7b90976..cdbdc20 100644 --- a/src/configfile.h +++ b/src/configfile.h @@ -44,6 +44,7 @@ typedef struct tag_EZCONFIG { char *password; char *format; char *fileName; + char *metadataProgram; char *serverName; char *serverURL; char *serverGenre; diff --git a/src/ezstream.c b/src/ezstream.c index 5d2495f..0a1a15b 100644 --- a/src/ezstream.c +++ b/src/ezstream.c @@ -69,6 +69,7 @@ #define STREAM_CONT 1 #define STREAM_SKIP 2 #define STREAM_SERVERR 3 +#define STREAM_UPDMDATA 4 #ifdef HAVE___PROGNAME extern char *__progname; @@ -78,6 +79,7 @@ char *__progname; int qFlag; int vFlag; +int metadataFromProgram; EZCONFIG *pezConfig = NULL; static const char *blankString = ""; @@ -85,15 +87,17 @@ playlist_t *playlist = NULL; int playlistMode = 0; #ifdef HAVE_SIGNALS -const int ezstream_signals[] = { SIGHUP, SIGUSR1 }; +const int ezstream_signals[] = { SIGHUP, SIGUSR1, SIGUSR2 }; volatile sig_atomic_t rereadPlaylist = 0; volatile sig_atomic_t rereadPlaylist_notify = 0; volatile sig_atomic_t skipTrack = 0; +volatile sig_atomic_t queryMetadata = 0; #else int rereadPlaylist = 0; int rereadPlaylist_notify = 0; int skipTrack = 0; +int queryMetadata = 0; #endif /* HAVE_SIGNALS */ typedef struct tag_ID3Tag { @@ -133,6 +137,9 @@ sig_handler(int sig) case SIGUSR1: skipTrack = 1; break; + case SIGUSR2: + queryMetadata = 1; + break; default: break; } @@ -291,16 +298,26 @@ processMetadata(shout_t *shout, const char *fileName) shout_metadata_t *shout_mdata = NULL; metadata_t *mdata = NULL; - if ((mdata = metadata_file(fileName)) == NULL) { - songInfo = xstrdup(blankString); - return (songInfo); + if (metadataFromProgram) { + if ((mdata = metadata_program(fileName)) == NULL) + return (NULL); + + if (!metadata_program_update(mdata, METADATA_STRING)) { + metadata_free(&mdata); + songInfo = xstrdup(blankString); + return (songInfo); + } + } else { + if ((mdata = metadata_file(fileName)) == NULL) + return (NULL); + + if (!metadata_file_update(mdata)) { + metadata_free(&mdata); + songInfo = xstrdup(blankString); + return (songInfo); + } } - if (!metadata_file_update(mdata)) { - metadata_free(&mdata); - songInfo = xstrdup(blankString); - return (songInfo); - } songInfo = xstrdup(metadata_get_string(mdata)); metadata_free(&mdata); @@ -327,6 +344,10 @@ openResource(shout_t *shout, const char *fileName, int *popenFlag, char *pCommandString = NULL; if (strcmp(fileName, "stdin") == 0) { + if (metadataFromProgram && + processMetadata(shout, pezConfig->metadataProgram) == NULL) + return (filep); + if (vFlag) printf("%s: Reading from standard input\n", __progname); @@ -354,7 +375,12 @@ openResource(shout_t *shout, const char *fileName, int *popenFlag, return (filep); } - pMetadata = processMetadata(shout, fileName); + if (metadataFromProgram) + pMetadata = processMetadata(shout, pezConfig->metadataProgram); + else + pMetadata = processMetadata(shout, fileName); + if (pMetadata == NULL) + return (filep); if (metaCopy != NULL) *metaCopy = xstrdup(pMetadata); @@ -487,6 +513,19 @@ sendStream(shout_t *shout, FILE *filepstream, const char *fileName, break; } + shout_sync(shout); + + if (shout_send(shout, buff, read) != SHOUTERR_SUCCESS) { + printf("%s: shout_send(): %s\n", __progname, + shout_get_error(shout)); + if (reconnectServer(shout, 1)) + break; + else { + ret = STREAM_SERVERR; + break; + } + } + if (rereadPlaylist_notify) { rereadPlaylist_notify = 0; if (!pezConfig->fileNameIsProgram) @@ -498,16 +537,10 @@ sendStream(shout_t *shout, FILE *filepstream, const char *fileName, ret = STREAM_SKIP; break; } - - shout_sync(shout); - - if (shout_send(shout, buff, read) != SHOUTERR_SUCCESS) { - printf("%s: shout_send(): %s\n", __progname, - shout_get_error(shout)); - if (reconnectServer(shout, 1)) - break; - else { - ret = STREAM_SERVERR; + if (queryMetadata) { + queryMetadata = 0; + if (metadataFromProgram) { + ret = STREAM_UPDMDATA; break; } } @@ -609,9 +642,14 @@ streamFile(shout_t *shout, const char *fileName) ret = sendStream(shout, filepstream, fileName, isStdin, NULL); #endif if (ret != STREAM_DONE) { - if (skipTrack && rereadPlaylist) { + if ((skipTrack && rereadPlaylist) || + (skipTrack && queryMetadata)) { skipTrack = 0; - ret = 1; + ret = STREAM_CONT; + } + if (queryMetadata && rereadPlaylist) { + queryMetadata = 0; + ret = STREAM_CONT; } if (ret == STREAM_SKIP || skipTrack) { skipTrack = 0; @@ -621,13 +659,30 @@ streamFile(shout_t *shout, const char *fileName) retval = 1; ret = STREAM_DONE; } + if (ret == STREAM_UPDMDATA || queryMetadata) { + queryMetadata = 0; + if (metadataFromProgram) { + char *mdataStr; + + if (vFlag > 1) + printf("%s: Querying '%s' for fresh metadata\n", + __progname, pezConfig->metadataProgram); + if ((mdataStr = processMetadata(shout, pezConfig->metadataProgram)) == NULL) { + retval = 0; + ret = STREAM_DONE; + } + printf("%s: New metadata: ``%s''\n", + __progname, mdataStr); + xfree(mdataStr); + } + } if (ret == STREAM_SERVERR) { retval = 0; ret = STREAM_DONE; } } else retval = 1; - } while (ret); + } while (ret != STREAM_DONE); if (popenFlag) pclose(filepstream); @@ -683,10 +738,7 @@ streamPlaylist(shout_t *shout, const char *fileName) } } - if (pezConfig->streamOnce) - return (0); - else - return (1); + return (1); } /* Borrowed from OpenNTPd-portable's compat-openbsd/bsd-misc.c */ @@ -976,6 +1028,11 @@ main(int argc, char *argv[]) return (1); } + if (pezConfig->metadataProgram != NULL) + metadataFromProgram = 1; + else + metadataFromProgram = 0; + #ifdef HAVE_SIGNALS memset(&act, 0, sizeof(act)); act.sa_handler = sig_handler; diff --git a/src/metadata.c b/src/metadata.c index 195cbfb..2512b80 100644 --- a/src/metadata.c +++ b/src/metadata.c @@ -352,6 +352,11 @@ metadata_t * metadata_program(const char *program) { metadata_t *md; +#ifdef HAVE_STAT + struct stat st; +#else + FILE *filep; +#endif if (program == NULL || strlen(program) == 0) { printf("%s: metadata_program(): Internal error: Bad arguments\n", @@ -361,6 +366,27 @@ metadata_program(const char *program) md = metadata_create(program); md->program = 1; + md->string = xstrdup(""); + +#ifdef HAVE_STAT + if (stat(program, &st) == -1) { + printf("%s: %s: %s\n", __progname, program, strerror(errno)); + metadata_free(&md); + return (NULL); + } + if (!(st.st_mode & (S_IEXEC | S_IXGRP | S_IXOTH))) { + printf("%s: %s: Not an executable program\n", __progname, program); + metadata_free(&md); + return (NULL); + } +#else + if ((filep = fopen(program, "r")) == NULL) { + printf("%s: %s: %s\n", __progname, program, strerror(errno)); + metadata_free(&md); + return (NULL); + } + fclose(filep); +#endif /* HAVE_STAT */ return (md); } @@ -418,8 +444,110 @@ metadata_file_update(metadata_t *md) int metadata_program_update(metadata_t *md, enum metadata_request md_req) { - /* XXX not implemented */ - return (0); + FILE *filep; + char buf[METADATA_MAX + 1]; + char command[PATH_MAX + strlen(" artist") + 1]; + + if (md == NULL) { + printf("%s: metadata_program_update(): Internal error: NULL argument\n", + __progname); + abort(); + } + + if (!md->program) { + printf("%s: metadata_program_update(): Internal error: Received file handle\n", + __progname); + abort(); + } + + switch (md_req) { + case METADATA_ALL: + metadata_clean_md(md); + if (!metadata_program_update(md, METADATA_STRING) || + !metadata_program_update(md, METADATA_ARTIST) || + !metadata_program_update(md, METADATA_TITLE)) + return (0); + break; + case METADATA_STRING: + strlcpy(command, md->filename, sizeof(command)); + if (md->string != NULL) + xfree(md->string); + break; + case METADATA_ARTIST: + snprintf(command, sizeof(command), "%s artist", md->filename); + if (md->artist != NULL) + xfree(md->artist); + break; + case METADATA_TITLE: + snprintf(command, sizeof(command), "%s title", md->filename); + if (md->title != NULL) + xfree(md->title); + break; + default: + printf("%s: metadata_program_update(): Internal error: Unknown md_req\n", + __progname); + abort(); + } + + fflush(NULL); + errno = 0; + if ((filep = popen(command, "r")) == NULL) { + printf("%s: playlist_run_program(): Error while executing '%s'", + __progname, command); + /* popen() does not set errno reliably ... */ + if (errno) + printf(": %s\n", strerror(errno)); + else + printf("\n"); + return (0); + } + + if (fgets(buf, sizeof(buf), filep) == NULL) { + if (ferror(filep)) + printf("%s: Error while reading output from program '%s': %s\n", + __progname, md->filename, strerror(errno)); + pclose(filep); + printf("%s: FATAL: External program '%s' not (or no longer) usable.\n", + __progname, md->filename); + exit(1); + } + + pclose(filep); + + if (strlen(buf) == sizeof(buf) - 1) + printf("%s: Warning: Metadata string received via '%s' is too long and has been truncated\n", + __progname, command); + + if (buf[0] != '\0' && buf[strlen(buf) - 1] == '\n') + buf[strlen(buf) - 1] = '\0'; + if (buf[0] != '\0' && buf[strlen(buf) - 1] == '\r') + buf[strlen(buf) - 1] = '\0'; + + switch (md_req) { + case METADATA_STRING: + if (strlen(buf) == 0) { + printf("%s: Warning: Empty metadata string received from '%s'\n", + __progname, md->filename); + md->string = xstrdup(""); + } else + md->string = xstrdup(buf); + break; + case METADATA_ARTIST: + if (strlen(buf) > 0) + md->artist = xstrdup(buf); + break; + case METADATA_TITLE: + if (strlen(buf) > 0) + md->title = xstrdup(buf); + break; + case METADATA_ALL: + default: + printf("%s: metadata_program_update(): Internal error: METADATA_ALL in code unreachable by METADATA_ALL\n", + __progname); + abort(); + } + + return (1); } const char * diff --git a/src/metadata.h b/src/metadata.h index 9037f22..97a177a 100644 --- a/src/metadata.h +++ b/src/metadata.h @@ -45,8 +45,7 @@ metadata_t * metadata_file(const char * /* filename */); * - Accept no command line parameter and return a complete metadata string * (for metadata_get_string()). The program *should* always return * something in this case (e.g. something based on the filename in case no - * metadata is available.) This string will default to "[unknown]" - * otherwise. + * metadata is available.) * - Accept the command line parameter "artist" and return only the artist * metadata, or an empty string if no artist information is available. * - Accept the command line parameter "title" and return only the song title