/* * ezstream - source client for Icecast with external en-/decoder support * Copyright (C) 2003, 2004, 2005, 2006 Ed Zaleski * Copyright (C) 2007, 2009, 2015 Moritz Grimm * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "compat.h" #include "ezstream.h" #ifdef HAVE_SIGNAL_H # include #endif #include "cfg.h" #include "cmdline.h" #include "log.h" #include "metadata.h" #include "playlist.h" #include "stream.h" #include "util.h" #include "xalloc.h" #define STREAM_DONE 0 #define STREAM_CONT 1 #define STREAM_SKIP 2 #define STREAM_SERVERR 3 #define STREAM_UPDMDATA 4 playlist_t playlist; int playlistMode; unsigned int resource_errors; #ifdef HAVE_SIGNALS const int ezstream_signals[] = { SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2 }; volatile sig_atomic_t rereadPlaylist; volatile sig_atomic_t rereadPlaylist_notify; volatile sig_atomic_t skipTrack; volatile sig_atomic_t queryMetadata; volatile sig_atomic_t quit; #else int rereadPlaylist; int rereadPlaylist_notify; int skipTrack; int queryMetadata; int quit; #endif /* HAVE_SIGNALS */ typedef struct tag_ID3Tag { char tag[3]; char trackName[30]; char artistName[30]; char albumName[30]; char year[3]; char comment[30]; char genre; } ID3Tag; char * buildReencodeCommand(const char *, const char *, metadata_t); metadata_t getMetadata(const char *); FILE * openResource(stream_t, const char *, int *, metadata_t *, int *, long *); int reconnect(stream_t); const char * getTimeString(long); int sendStream(stream_t, FILE *, const char *, int, const char *, struct timespec *); int streamFile(stream_t, const char *); int streamPlaylist(stream_t); int ez_shutdown(int); #ifdef HAVE_SIGNALS void sig_handler(int); # ifndef SIG_IGN # define SIG_IGN (void (*)(int))1 # endif /* !SIG_IGN */ void sig_handler(int sig) { switch (sig) { case SIGTERM: case SIGINT: quit = 1; break; case SIGHUP: rereadPlaylist = 1; rereadPlaylist_notify = 1; break; case SIGUSR1: skipTrack = 1; break; case SIGUSR2: queryMetadata = 1; break; default: break; } } #endif /* HAVE_SIGNALS */ char * buildReencodeCommand(const char *extension, const char *fileName, metadata_t mdata) { cfg_decoder_t decoder; cfg_encoder_t encoder; char *dec_str, *enc_str; char *commandString; size_t commandStringLen; char *localTitle, *localArtist, *localMetaString, *localAlbum; decoder = cfg_decoder_find(extension); if (!decoder) { log_error("cannot decode: %s: unsupported file extension %s", fileName, extension); return (NULL); } encoder = cfg_encoder_get(cfg_get_stream_encoder()); if (!encoder) { log_error("cannot encode: %s: unknown encoder", cfg_get_stream_encoder()); return (NULL); } localTitle = UTF8toCHAR(metadata_get_title(mdata), ICONV_REPLACE); localArtist = UTF8toCHAR(metadata_get_artist(mdata), ICONV_REPLACE); localAlbum = UTF8toCHAR(metadata_get_album(mdata), ICONV_REPLACE); localMetaString = UTF8toCHAR(metadata_get_string(mdata), ICONV_REPLACE); dec_str = replaceString(cfg_decoder_get_program(decoder), PLACEHOLDER_TRACK, fileName); if (strstr(dec_str, PLACEHOLDER_ARTIST) != NULL) { char *tmpStr = replaceString(dec_str, PLACEHOLDER_ARTIST, localArtist); xfree(dec_str); dec_str = tmpStr; } if (strstr(dec_str, PLACEHOLDER_TITLE) != NULL) { char *tmpStr = replaceString(dec_str, PLACEHOLDER_TITLE, localTitle); xfree(dec_str); dec_str = tmpStr; } if (strstr(dec_str, PLACEHOLDER_ALBUM) != NULL) { char *tmpStr = replaceString(dec_str, PLACEHOLDER_ALBUM, localAlbum); xfree(dec_str); dec_str = tmpStr; } /* * if meta * if (prog && format) * metatoformat * else * if (!prog && title) * emptymeta * else * replacemeta */ if (strstr(dec_str, PLACEHOLDER_METADATA) != NULL) { if (cfg_get_metadata_program() && cfg_get_metadata_format_str()) { char *mdataString = metadata_format_string(mdata, cfg_get_metadata_format_str()); char *tmpStr = replaceString(dec_str, PLACEHOLDER_METADATA, mdataString); xfree(dec_str); xfree(mdataString); dec_str = tmpStr; } else { if (!cfg_get_metadata_program() && strstr(dec_str, PLACEHOLDER_TITLE) != NULL) { char *tmpStr = replaceString(dec_str, PLACEHOLDER_METADATA, ""); xfree(dec_str); dec_str = tmpStr; } else { char *tmpStr = replaceString(dec_str, PLACEHOLDER_METADATA, localMetaString); xfree(dec_str); dec_str = tmpStr; } } } if (!cfg_encoder_get_program(encoder)) return (dec_str); enc_str = replaceString(cfg_encoder_get_program(encoder), PLACEHOLDER_ARTIST, localArtist); if (strstr(enc_str, PLACEHOLDER_TITLE) != NULL) { char *tmpStr = replaceString(enc_str, PLACEHOLDER_TITLE, localTitle); xfree(enc_str); enc_str = tmpStr; } if (strstr(enc_str, PLACEHOLDER_ALBUM) != NULL) { char *tmpStr = replaceString(enc_str, PLACEHOLDER_ALBUM, localAlbum); xfree(enc_str); enc_str = tmpStr; } if (strstr(enc_str, PLACEHOLDER_METADATA) != NULL) { if (cfg_get_metadata_program() && cfg_get_metadata_format_str()) { char *mdataString = metadata_format_string(mdata, cfg_get_metadata_format_str()); char *tmpStr = replaceString(enc_str, PLACEHOLDER_METADATA, mdataString); xfree(enc_str); xfree(mdataString); enc_str = tmpStr; } else { if (!cfg_get_metadata_program() && strstr(enc_str, PLACEHOLDER_TITLE) != NULL) { char *tmpStr = replaceString(enc_str, PLACEHOLDER_METADATA, ""); xfree(enc_str); enc_str = tmpStr; } else { char *tmpStr = replaceString(enc_str, PLACEHOLDER_METADATA, localMetaString); xfree(enc_str); enc_str = tmpStr; } } } commandStringLen = strlen(dec_str) + strlen(" | ") + strlen(enc_str) + 1; commandString = xcalloc(commandStringLen, sizeof(char)); snprintf(commandString, commandStringLen, "%s | %s", dec_str, enc_str); xfree(localTitle); xfree(localArtist); xfree(localMetaString); xfree(dec_str); xfree(enc_str); return (commandString); } metadata_t getMetadata(const char *fileName) { metadata_t mdata; if (cfg_get_metadata_program()) { if (NULL == (mdata = metadata_program(fileName, cfg_get_metadata_normalize_strings()))) return (NULL); if (!metadata_program_update(mdata, METADATA_ALL)) { metadata_free(&mdata); return (NULL); } } else { if (NULL == (mdata = metadata_file(fileName, cfg_get_metadata_normalize_strings()))) return (NULL); if (!metadata_file_update(mdata)) { metadata_free(&mdata); return (NULL); } } return (mdata); } FILE * openResource(stream_t stream, const char *fileName, int *popenFlag, metadata_t *mdata_p, int *isStdin, long *songLen) { FILE *filep = NULL; char extension[25]; char *p = NULL; char *pCommandString = NULL; metadata_t mdata; if (mdata_p != NULL) *mdata_p = NULL; if (songLen != NULL) *songLen = 0; if (strcmp(fileName, "stdin") == 0) { if (cfg_get_metadata_program()) { if ((mdata = getMetadata(cfg_get_metadata_program())) == NULL) return (NULL); if (0 > stream_set_metadata(stream, mdata, NULL)) { metadata_free(&mdata); return (NULL); } if (mdata_p != NULL) *mdata_p = mdata; else metadata_free(&mdata); } if (isStdin != NULL) *isStdin = 1; filep = stdin; return (filep); } if (isStdin != NULL) *isStdin = 0; extension[0] = '\0'; p = strrchr(fileName, '.'); if (p != NULL) strlcpy(extension, p, sizeof(extension)); for (p = extension; *p != '\0'; p++) *p = tolower((int)*p); if (strlen(extension) == 0) { log_error("%s: cannot determine file type", fileName); return (filep); } if (cfg_get_metadata_program()) { if ((mdata = getMetadata(cfg_get_metadata_program())) == NULL) return (NULL); } else { if ((mdata = getMetadata(fileName)) == NULL) return (NULL); } if (songLen != NULL) *songLen = metadata_get_length(mdata); *popenFlag = 0; if (cfg_get_stream_encoder()) { int stderr_fd = -1; pCommandString = buildReencodeCommand(extension, fileName, mdata); if (mdata_p != NULL) *mdata_p = mdata; else metadata_free(&mdata); log_info("running command: %s", pCommandString); if (cfg_get_program_quiet_stderr()) { int fd; stderr_fd = dup(fileno(stderr)); if ((fd = open(_PATH_DEVNULL, O_RDWR, 0)) == -1) { log_alert("%s: %s", _PATH_DEVNULL, strerror(errno)); exit(1); } dup2(fd, fileno(stderr)); if (fd > 2) close(fd); } fflush(NULL); errno = 0; if ((filep = popen(pCommandString, "r")) == NULL) { /* popen() does not set errno reliably ... */ if (errno) log_error("execution error: %s: %s", pCommandString, strerror(errno)); else log_error("execution error: %s", pCommandString); } else { *popenFlag = 1; } xfree(pCommandString); if (cfg_get_program_quiet_stderr()) dup2(stderr_fd, fileno(stderr)); if (stderr_fd > 2) close(stderr_fd); return (filep); } if (mdata_p != NULL) *mdata_p = mdata; else metadata_free(&mdata); if ((filep = fopen(fileName, "rb")) == NULL) { log_error("%s: %s", fileName, strerror(errno)); return (NULL); } return (filep); } int reconnect(stream_t stream) { unsigned int i; i = 0; while (++i) { if (cfg_get_server_reconnect_attempts() > 0) log_notice("reconnect: %s: attempt #%u/%u ...", cfg_get_server_hostname(), i, cfg_get_server_reconnect_attempts()); else log_notice("reconnect: %s: attempt #%u ...", cfg_get_server_hostname(), i); stream_disconnect(stream); if (0 == stream_connect(stream)) { log_notice("reconnect: %s: success", cfg_get_server_hostname()); return (0); } if (cfg_get_server_reconnect_attempts() > 0 && i >= cfg_get_server_reconnect_attempts()) break; if (quit) return (-1); else sleep(5); }; log_warning("reconnect failed: giving up"); return (-1); } const char * getTimeString(long seconds) { static char str[20]; long secs, mins, hours; if (seconds < 0) return (NULL); secs = seconds; hours = secs / 3600; secs %= 3600; mins = secs / 60; secs %= 60; snprintf(str, sizeof(str), "%ldh%02ldm%02lds", hours, mins, secs); return ((const char *)str); } int sendStream(stream_t stream, FILE *filepstream, const char *fileName, int isStdin, const char *songLenStr, struct timespec *tv) { char buff[4096]; size_t bytes_read, total, oldTotal; int ret; double kbps = -1.0; struct timespec timeStamp, *startTime = tv; struct timespec callTime, currentTime; clock_gettime(CLOCK_MONOTONIC, &callTime); timeStamp.tv_sec = startTime->tv_sec; timeStamp.tv_nsec = startTime->tv_nsec; total = oldTotal = 0; ret = STREAM_DONE; while ((bytes_read = fread(buff, 1, sizeof(buff), filepstream)) > 0) { if (!stream_get_connected(stream)) { log_warning("%s: connection lost", cfg_get_server_hostname()); if (0 > reconnect(stream)) { ret = STREAM_SERVERR; break; } } stream_sync(stream); if (0 > stream_send(stream, buff, bytes_read)) { if (0 > reconnect(stream)) ret = STREAM_SERVERR; break; } if (quit) break; if (rereadPlaylist_notify) { rereadPlaylist_notify = 0; if (CFG_MEDIA_PLAYLIST == cfg_get_media_type()) log_notice("HUP signal received: playlist re-read scheduled"); } if (skipTrack) { skipTrack = 0; ret = STREAM_SKIP; break; } clock_gettime(CLOCK_MONOTONIC, ¤tTime); if (queryMetadata || (0 <= cfg_get_metadata_refresh_interval() && (currentTime.tv_sec - callTime.tv_sec >= cfg_get_metadata_refresh_interval()))) { queryMetadata = 0; if (cfg_get_metadata_program()) { ret = STREAM_UPDMDATA; break; } } total += bytes_read; if (cfg_get_program_rtstatus_output()) { double oldTime, newTime; if (!isStdin && playlistMode) { if (CFG_MEDIA_PROGRAM == cfg_get_media_type()) { char *tmp = xstrdup(cfg_get_media_filename()); printf(" [%s]", basename(tmp)); xfree(tmp); } else printf(" [%4lu/%-4lu]", playlist_get_position(playlist), playlist_get_num_items(playlist)); } oldTime = (double)timeStamp.tv_sec + (double)timeStamp.tv_nsec / 1000000000.0; newTime = (double)currentTime.tv_sec + (double)currentTime.tv_nsec / 1000000000.0; if (songLenStr == NULL) printf(" [ %s]", getTimeString(currentTime.tv_sec - startTime->tv_sec)); else printf(" [ %s/%s]", getTimeString(currentTime.tv_sec - startTime->tv_sec), songLenStr); if (newTime - oldTime >= 1.0) { kbps = (((double)(total - oldTotal) / (newTime - oldTime)) * 8.0) / 1000.0; timeStamp.tv_sec = currentTime.tv_sec; timeStamp.tv_nsec = currentTime.tv_nsec; oldTotal = total; } if (kbps < 0) printf(" "); else printf(" [%8.2f kbps]", kbps); printf(" \r"); fflush(stdout); } } if (ferror(filepstream)) { if (errno == EINTR) { clearerr(filepstream); ret = STREAM_CONT; } else if (errno == EBADF && isStdin) log_notice("no (more) data available on standard input"); else log_error("sendStream: %s: %s", fileName, strerror(errno)); } return (ret); } int streamFile(stream_t stream, const char *fileName) { FILE *filepstream = NULL; int popenFlag = 0; char *songLenStr = NULL; int isStdin = 0; int ret, retval = 0; long songLen; metadata_t mdata; struct timespec startTime; if ((filepstream = openResource(stream, fileName, &popenFlag, &mdata, &isStdin, &songLen)) == NULL) { if (++resource_errors > 100) { log_error("too many errors; giving up"); return (0); } /* Continue with next resource on failure: */ return (1); } resource_errors = 0; if (mdata != NULL) { char *tmp, *metaData; tmp = metadata_assemble_string(mdata); if ((metaData = UTF8toCHAR(tmp, ICONV_REPLACE)) == NULL) metaData = xstrdup("(unknown title)"); xfree(tmp); log_notice("streaming: %s (%s)", metaData, fileName); xfree(metaData); /* MP3 streams are special, so set the metadata explicitly: */ if (CFG_STREAM_MP3 == cfg_get_stream_format()) stream_set_metadata(stream, mdata, NULL); metadata_free(&mdata); } else if (isStdin) log_notice("streaming: standard input"); if (songLen > 0) songLenStr = xstrdup(getTimeString(songLen)); clock_gettime(CLOCK_MONOTONIC, &startTime); do { ret = sendStream(stream, filepstream, fileName, isStdin, songLenStr, &startTime); if (quit) break; if (ret != STREAM_DONE) { if ((skipTrack && rereadPlaylist) || (skipTrack && queryMetadata)) { skipTrack = 0; ret = STREAM_CONT; } if (queryMetadata && rereadPlaylist) { queryMetadata = 0; ret = STREAM_CONT; } if (ret == STREAM_SKIP || skipTrack) { skipTrack = 0; if (!isStdin) log_notice("USR1 signal received: skipping current track"); retval = 1; ret = STREAM_DONE; } if (ret == STREAM_UPDMDATA || queryMetadata) { queryMetadata = 0; if (cfg_get_metadata_no_updates()) continue; if (cfg_get_metadata_program()) { char *mdataStr = NULL; metadata_t prog_mdata; log_info("running metadata program: %s", cfg_get_metadata_program()); if ((prog_mdata = getMetadata(cfg_get_metadata_program())) == NULL) { retval = 0; ret = STREAM_DONE; continue; } if (0> stream_set_metadata(stream, prog_mdata, &mdataStr)) { retval = 0; ret = STREAM_DONE; continue; } metadata_free(&prog_mdata); log_info("new metadata: %s", mdataStr); xfree(mdataStr); } } if (ret == STREAM_SERVERR) { retval = 0; ret = STREAM_DONE; } } else retval = 1; } while (ret != STREAM_DONE); if (popenFlag) pclose(filepstream); else if (!isStdin) fclose(filepstream); if (songLenStr != NULL) xfree(songLenStr); return (retval); } int streamPlaylist(stream_t stream) { const char *song; char lastSong[PATH_MAX]; if (playlist == NULL) { switch (cfg_get_media_type()) { case CFG_MEDIA_PROGRAM: if ((playlist = playlist_program(cfg_get_media_filename())) == NULL) return (0); break; case CFG_MEDIA_STDIN: if ((playlist = playlist_read(NULL)) == NULL) return (0); break; default: if ((playlist = playlist_read(cfg_get_media_filename())) == NULL) return (0); if (playlist_get_num_items(playlist) == 0) log_warning("%s: playlist empty", cfg_get_media_filename()); break; } } else { /* * XXX: This preserves traditional behavior, however, * rereading the playlist after each walkthrough seems a * bit more logical. */ playlist_rewind(playlist); } if (CFG_MEDIA_PROGRAM != cfg_get_media_type() && cfg_get_media_shuffle()) playlist_shuffle(playlist); while ((song = playlist_get_next(playlist)) != NULL) { strlcpy(lastSong, song, sizeof(lastSong)); if (!streamFile(stream, song)) return (0); if (quit) break; if (rereadPlaylist) { rereadPlaylist = rereadPlaylist_notify = 0; if (CFG_MEDIA_PROGRAM == cfg_get_media_type()) continue; log_notice("rereading playlist"); if (!playlist_reread(&playlist)) return (0); if (cfg_get_media_shuffle()) playlist_shuffle(playlist); else { playlist_goto_entry(playlist, lastSong); playlist_skip_next(playlist); } continue; } } return (1); } int ez_shutdown(int exitval) { stream_exit(); playlist_exit(); cfg_encoder_exit(); cfg_decoder_exit(); log_exit(); cfg_exit(); return (exitval); } int main(int argc, char *argv[]) { int ret, cont; const char *errstr; stream_t stream; extern char *optarg; extern int optind; #ifdef HAVE_SIGNALS struct sigaction act; unsigned int i; #endif ret = 1; if (0 > cfg_init() || 0 > cmdline_parse(argc, argv, &ret) || 0 > log_init() || 0 > cfg_decoder_init() || 0 > cfg_encoder_init() || 0 > playlist_init() || 0 > cfg_file_reload() || 0 > stream_init()) return (ez_shutdown(ret)); if (0 > cfg_check(&errstr)) { log_error("%s: %s", cfg_get_program_config_file(), errstr); return (ez_shutdown(2)); } stream = stream_get(STREAM_DEFAULT); if (0 > stream_setup(stream)) return (ez_shutdown(1)); #ifdef HAVE_SIGNALS memset(&act, 0, sizeof(act)); act.sa_handler = sig_handler; # ifdef SA_RESTART act.sa_flags = SA_RESTART; # endif for (i = 0; i < sizeof(ezstream_signals) / sizeof(int); i++) { if (sigaction(ezstream_signals[i], &act, NULL) == -1) { log_syserr(ERROR, errno, "sigaction"); return (ez_shutdown(1)); } } /* * Ignore SIGPIPE, which has been seen to give a long-running ezstream * process trouble. EOF and/or EPIPE are also easier to handle. */ act.sa_handler = SIG_IGN; if (sigaction(SIGPIPE, &act, NULL) == -1) { log_syserr(ERROR, errno, "sigaction"); return (ez_shutdown(1)); } #endif /* HAVE_SIGNALS */ if (0 > stream_connect(stream)) { log_error("initial server connection failed"); return (ez_shutdown(1)); } log_notice("connected: %s://%s:%u%s", cfg_get_server_protocol_str(), cfg_get_server_hostname(), cfg_get_server_port(), cfg_get_stream_mountpoint()); if (CFG_MEDIA_PROGRAM == cfg_get_media_type() || CFG_MEDIA_PLAYLIST == cfg_get_media_type() || (CFG_MEDIA_AUTODETECT == cfg_get_media_type() && (strrcasecmp(cfg_get_media_filename(), ".m3u") == 0 || strrcasecmp(cfg_get_media_filename(), ".txt") == 0))) playlistMode = 1; else playlistMode = 0; do { if (playlistMode) { cont = streamPlaylist(stream); } else { cont = streamFile(stream, cfg_get_media_filename()); } if (quit) break; if (cfg_get_media_stream_once()) break; } while (cont); stream_disconnect(stream); if (quit) { if (cfg_get_program_quiet_stderr() && cfg_get_program_verbosity()) printf("\r"); log_notice("INT or TERM signal received"); } log_info("exiting"); playlist_free(&playlist); return (ez_shutdown(0)); }