1
0
mirror of https://gitlab.xiph.org/xiph/ezstream.git synced 2024-12-04 14:46:31 -05:00

Add new <metadata_progname> 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
This commit is contained in:
moritz 2007-03-09 02:30:29 +00:00
parent 163b7ca187
commit 304908bff4
7 changed files with 324 additions and 42 deletions

6
NEWS
View File

@ -19,6 +19,12 @@ Changes in 0.4.0, (SVN trunk):
* various: * various:
- [ADD] Allow ezstream to use TagLib for reading metadata from media - [ADD] Allow ezstream to use TagLib for reading metadata from media
files. TagLib (libtag_c) is now an optional dependency. files. TagLib (libtag_c) is now an optional dependency.
- [ADD] New <metadata_progname> 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_progname> (metadata
is always read at song changes.)

View File

@ -8,7 +8,7 @@
.Os .Os
.Sh NAME .Sh NAME
.Nm ezstream .Nm ezstream
.Nd source client for Icecast with external en-/decoder support .Nd source client for Icecast with external de-/encoder support
.Sh SYNOPSIS .Sh SYNOPSIS
.Nm .Nm
.Bk -words .Bk -words
@ -59,8 +59,7 @@ and bitrate \(em is displayed.
.Ss Runtime control .Ss Runtime control
On POSIX systems, On POSIX systems,
.Nm .Nm
offers limited runtime control via signals when it is not streaming data from offers limited runtime control via signals.
standard input.
By sending a signal to the ezstream process, e.g. with the By sending a signal to the ezstream process, e.g. with the
.Xr kill 1 .Xr kill 1
utility, a certain action will be triggered. 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 .It Cd SIGUSR1
Skips the currently playing track and moves on to the next in playlist mode, or Skips the currently playing track and moves on to the next in playlist mode, or
restarts the current track when streaming a single file. 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 \&<metadata_progname/\&>
.Pq see below.
This is the only meaningful signal when streaming from standard input.
.El .El
.Pp .Pp
.Sh CONFIGURATION FILE SYNTAX .Sh CONFIGURATION FILE SYNTAX
@ -103,7 +107,7 @@ In the configuration file, they need to be used as
.Em start tag + content + end tag , .Em start tag + content + end tag ,
like in the introductory example shown above. like in the introductory example shown above.
.Ss Root element .Ss Root element
.Bl -ohang .Bl -tag -width -Ds
.It Sy \&<ezstream\ /\&> .It Sy \&<ezstream\ /\&>
.Pq Mandatory. .Pq Mandatory.
The configuration file's root element. The configuration file's root element.
@ -112,7 +116,7 @@ It contains all other configuration elements.
.Ss Global configuration elements .Ss Global configuration elements
Each of the global configuration elements have the \&<ezstream/\&> element as Each of the global configuration elements have the \&<ezstream/\&> element as
their parent. their parent.
.Bl -ohang .Bl -tag -width -Ds
.It Sy \&<url\ /\&> .It Sy \&<url\ /\&>
.Pq Mandatory. .Pq Mandatory.
Specifies the location and mountpoint of the Icecast server, to which the Specifies the location and mountpoint of the Icecast server, to which the
@ -170,10 +174,6 @@ Set to
.Pq one .Pq one
to indicate that the file in \&<filename/\&> is actually an executable program to indicate that the file in \&<filename/\&> is actually an executable program
or script. 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 If set to
.Sy 0 .Sy 0
.Pq zero , .Pq zero ,
@ -181,6 +181,10 @@ If set to
keyword keyword
.Pa stdin .Pa stdin
.Pq the default . .Pq the default .
.Pp
See the
.Sy SCRIPTING
section for details on how the playlist program must behave.
.It Sy \&<shuffle\ /\&> .It Sy \&<shuffle\ /\&>
.Pq Optional. .Pq Optional.
Set to Set to
@ -193,6 +197,24 @@ Files are played sequentially if set to
or when the \&<shuffle/\&> element is absent. or when the \&<shuffle/\&> element is absent.
This option will be ignored if \&<playlist_program/\&> is set to 1 This option will be ignored if \&<playlist_program/\&> is set to 1
.Pq one. .Pq one.
.It Sy \&<metadata_progname\ /\&>
.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 \&<metadata_progname/\&> 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 \&<stream_once\ /\&> .It Sy \&<stream_once\ /\&>
Set to Set to
.Sy 1 .Sy 1
@ -268,7 +290,7 @@ should be done.
.Ss Reencoding settings .Ss Reencoding settings
Each of the reencoding configuration elements have the \&<reencode/\&> Each of the reencoding configuration elements have the \&<reencode/\&>
element as their parent. element as their parent.
.Bl -ohang .Bl -tag -width -Ds
.It Sy \&<enable\ /\&> .It Sy \&<enable\ /\&>
Set to Set to
.Sy 1 .Sy 1
@ -287,7 +309,7 @@ Each format is described by a separate \&<encdec/\&> element.
.Ss Decoder/Encoder settings .Ss Decoder/Encoder settings
Each of the decoder/encoder configuration elements have the \&<encdec/\&> Each of the decoder/encoder configuration elements have the \&<encdec/\&>
element as their parent. element as their parent.
.Bl -ohang .Bl -tag -width -Ds
.It Sy \&<format\ /\&> .It Sy \&<format\ /\&>
This element is used by This element is used by
.Nm .Nm
@ -353,6 +375,54 @@ utility:
.Pp .Pp
.Dl \&<encode\&>oggenc -r -q 1.5 -t \&"@M@\&" -\&</encode\&> .Dl \&<encode\&>oggenc -r -q 1.5 -t \&"@M@\&" -\&</encode\&>
.El .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 \&<stream_once/\&> 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 .Sh FILES
.Bl -tag -width "!!EXAMPLES_DIR!!" -compact .Bl -tag -width "!!EXAMPLES_DIR!!" -compact
.It Pa !!EXAMPLES_DIR!! .It Pa !!EXAMPLES_DIR!!

View File

@ -98,6 +98,8 @@ freeConfig(EZCONFIG *cfg)
xfree(cfg->format); xfree(cfg->format);
if (cfg->fileName != NULL) if (cfg->fileName != NULL)
xfree(cfg->fileName); xfree(cfg->fileName);
if (cfg->metadataProgram != NULL)
xfree(cfg->metadataProgram);
if (cfg->serverName != NULL) if (cfg->serverName != NULL)
xfree(cfg->serverName); xfree(cfg->serverName);
if (cfg->serverURL != NULL) if (cfg->serverURL != NULL)
@ -228,6 +230,25 @@ parseConfig(const char *fileName)
xmlFree(ls_xmlContentPtr); xmlFree(ls_xmlContentPtr);
} }
} }
if (!xmlStrcmp(cur->name, BAD_CAST "metadata_progname")) {
if (ezConfig.metadataProgram != NULL) {
printf("%s[%ld]: Error: Cannot have multiple <metadata_progname> 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 <metadata_progname> 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 (!xmlStrcmp(cur->name, BAD_CAST "playlist_program")) {
if (program_set) { if (program_set) {
printf("%s[%ld]: Error: Cannot have multiple <playlist_program> elements\n", printf("%s[%ld]: Error: Cannot have multiple <playlist_program> elements\n",

View File

@ -44,6 +44,7 @@ typedef struct tag_EZCONFIG {
char *password; char *password;
char *format; char *format;
char *fileName; char *fileName;
char *metadataProgram;
char *serverName; char *serverName;
char *serverURL; char *serverURL;
char *serverGenre; char *serverGenre;

View File

@ -69,6 +69,7 @@
#define STREAM_CONT 1 #define STREAM_CONT 1
#define STREAM_SKIP 2 #define STREAM_SKIP 2
#define STREAM_SERVERR 3 #define STREAM_SERVERR 3
#define STREAM_UPDMDATA 4
#ifdef HAVE___PROGNAME #ifdef HAVE___PROGNAME
extern char *__progname; extern char *__progname;
@ -78,6 +79,7 @@ char *__progname;
int qFlag; int qFlag;
int vFlag; int vFlag;
int metadataFromProgram;
EZCONFIG *pezConfig = NULL; EZCONFIG *pezConfig = NULL;
static const char *blankString = ""; static const char *blankString = "";
@ -85,15 +87,17 @@ playlist_t *playlist = NULL;
int playlistMode = 0; int playlistMode = 0;
#ifdef HAVE_SIGNALS #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 = 0;
volatile sig_atomic_t rereadPlaylist_notify = 0; volatile sig_atomic_t rereadPlaylist_notify = 0;
volatile sig_atomic_t skipTrack = 0; volatile sig_atomic_t skipTrack = 0;
volatile sig_atomic_t queryMetadata = 0;
#else #else
int rereadPlaylist = 0; int rereadPlaylist = 0;
int rereadPlaylist_notify = 0; int rereadPlaylist_notify = 0;
int skipTrack = 0; int skipTrack = 0;
int queryMetadata = 0;
#endif /* HAVE_SIGNALS */ #endif /* HAVE_SIGNALS */
typedef struct tag_ID3Tag { typedef struct tag_ID3Tag {
@ -133,6 +137,9 @@ sig_handler(int sig)
case SIGUSR1: case SIGUSR1:
skipTrack = 1; skipTrack = 1;
break; break;
case SIGUSR2:
queryMetadata = 1;
break;
default: default:
break; break;
} }
@ -291,16 +298,26 @@ processMetadata(shout_t *shout, const char *fileName)
shout_metadata_t *shout_mdata = NULL; shout_metadata_t *shout_mdata = NULL;
metadata_t *mdata = NULL; metadata_t *mdata = NULL;
if ((mdata = metadata_file(fileName)) == NULL) { if (metadataFromProgram) {
if ((mdata = metadata_program(fileName)) == NULL)
return (NULL);
if (!metadata_program_update(mdata, METADATA_STRING)) {
metadata_free(&mdata);
songInfo = xstrdup(blankString); songInfo = xstrdup(blankString);
return (songInfo); return (songInfo);
} }
} else {
if ((mdata = metadata_file(fileName)) == NULL)
return (NULL);
if (!metadata_file_update(mdata)) { if (!metadata_file_update(mdata)) {
metadata_free(&mdata); metadata_free(&mdata);
songInfo = xstrdup(blankString); songInfo = xstrdup(blankString);
return (songInfo); return (songInfo);
} }
}
songInfo = xstrdup(metadata_get_string(mdata)); songInfo = xstrdup(metadata_get_string(mdata));
metadata_free(&mdata); metadata_free(&mdata);
@ -327,6 +344,10 @@ openResource(shout_t *shout, const char *fileName, int *popenFlag,
char *pCommandString = NULL; char *pCommandString = NULL;
if (strcmp(fileName, "stdin") == 0) { if (strcmp(fileName, "stdin") == 0) {
if (metadataFromProgram &&
processMetadata(shout, pezConfig->metadataProgram) == NULL)
return (filep);
if (vFlag) if (vFlag)
printf("%s: Reading from standard input\n", printf("%s: Reading from standard input\n",
__progname); __progname);
@ -354,7 +375,12 @@ openResource(shout_t *shout, const char *fileName, int *popenFlag,
return (filep); return (filep);
} }
if (metadataFromProgram)
pMetadata = processMetadata(shout, pezConfig->metadataProgram);
else
pMetadata = processMetadata(shout, fileName); pMetadata = processMetadata(shout, fileName);
if (pMetadata == NULL)
return (filep);
if (metaCopy != NULL) if (metaCopy != NULL)
*metaCopy = xstrdup(pMetadata); *metaCopy = xstrdup(pMetadata);
@ -487,6 +513,19 @@ sendStream(shout_t *shout, FILE *filepstream, const char *fileName,
break; 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) { if (rereadPlaylist_notify) {
rereadPlaylist_notify = 0; rereadPlaylist_notify = 0;
if (!pezConfig->fileNameIsProgram) if (!pezConfig->fileNameIsProgram)
@ -498,16 +537,10 @@ sendStream(shout_t *shout, FILE *filepstream, const char *fileName,
ret = STREAM_SKIP; ret = STREAM_SKIP;
break; break;
} }
if (queryMetadata) {
shout_sync(shout); queryMetadata = 0;
if (metadataFromProgram) {
if (shout_send(shout, buff, read) != SHOUTERR_SUCCESS) { ret = STREAM_UPDMDATA;
printf("%s: shout_send(): %s\n", __progname,
shout_get_error(shout));
if (reconnectServer(shout, 1))
break;
else {
ret = STREAM_SERVERR;
break; break;
} }
} }
@ -609,9 +642,14 @@ streamFile(shout_t *shout, const char *fileName)
ret = sendStream(shout, filepstream, fileName, isStdin, NULL); ret = sendStream(shout, filepstream, fileName, isStdin, NULL);
#endif #endif
if (ret != STREAM_DONE) { if (ret != STREAM_DONE) {
if (skipTrack && rereadPlaylist) { if ((skipTrack && rereadPlaylist) ||
(skipTrack && queryMetadata)) {
skipTrack = 0; skipTrack = 0;
ret = 1; ret = STREAM_CONT;
}
if (queryMetadata && rereadPlaylist) {
queryMetadata = 0;
ret = STREAM_CONT;
} }
if (ret == STREAM_SKIP || skipTrack) { if (ret == STREAM_SKIP || skipTrack) {
skipTrack = 0; skipTrack = 0;
@ -621,13 +659,30 @@ streamFile(shout_t *shout, const char *fileName)
retval = 1; retval = 1;
ret = STREAM_DONE; 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) { if (ret == STREAM_SERVERR) {
retval = 0; retval = 0;
ret = STREAM_DONE; ret = STREAM_DONE;
} }
} else } else
retval = 1; retval = 1;
} while (ret); } while (ret != STREAM_DONE);
if (popenFlag) if (popenFlag)
pclose(filepstream); pclose(filepstream);
@ -683,9 +738,6 @@ streamPlaylist(shout_t *shout, const char *fileName)
} }
} }
if (pezConfig->streamOnce)
return (0);
else
return (1); return (1);
} }
@ -976,6 +1028,11 @@ main(int argc, char *argv[])
return (1); return (1);
} }
if (pezConfig->metadataProgram != NULL)
metadataFromProgram = 1;
else
metadataFromProgram = 0;
#ifdef HAVE_SIGNALS #ifdef HAVE_SIGNALS
memset(&act, 0, sizeof(act)); memset(&act, 0, sizeof(act));
act.sa_handler = sig_handler; act.sa_handler = sig_handler;

View File

@ -352,6 +352,11 @@ metadata_t *
metadata_program(const char *program) metadata_program(const char *program)
{ {
metadata_t *md; metadata_t *md;
#ifdef HAVE_STAT
struct stat st;
#else
FILE *filep;
#endif
if (program == NULL || strlen(program) == 0) { if (program == NULL || strlen(program) == 0) {
printf("%s: metadata_program(): Internal error: Bad arguments\n", printf("%s: metadata_program(): Internal error: Bad arguments\n",
@ -361,6 +366,27 @@ metadata_program(const char *program)
md = metadata_create(program); md = metadata_create(program);
md->program = 1; 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); return (md);
} }
@ -418,8 +444,110 @@ metadata_file_update(metadata_t *md)
int int
metadata_program_update(metadata_t *md, enum metadata_request md_req) metadata_program_update(metadata_t *md, enum metadata_request md_req)
{ {
/* XXX not implemented */ 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); 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 * const char *

View File

@ -45,8 +45,7 @@ metadata_t * metadata_file(const char * /* filename */);
* - Accept no command line parameter and return a complete metadata string * - Accept no command line parameter and return a complete metadata string
* (for metadata_get_string()). The program *should* always return * (for metadata_get_string()). The program *should* always return
* something in this case (e.g. something based on the filename in case no * something in this case (e.g. something based on the filename in case no
* metadata is available.) This string will default to "[unknown]" * metadata is available.)
* otherwise.
* - Accept the command line parameter "artist" and return only the artist * - Accept the command line parameter "artist" and return only the artist
* metadata, or an empty string if no artist information is available. * metadata, or an empty string if no artist information is available.
* - Accept the command line parameter "title" and return only the song title * - Accept the command line parameter "title" and return only the song title