diff --git a/Makefile.am b/Makefile.am
index 7c0e6dfa7..bb6a0c53f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -399,6 +399,7 @@ libutil_a_SOURCES = \
src/util/OptionParser.cxx src/util/OptionParser.hxx \
src/util/OptionDef.hxx \
src/util/ByteReverse.cxx src/util/ByteReverse.hxx \
+ src/util/format.c src/util/format.h \
src/util/bit_reverse.c src/util/bit_reverse.h
# Multi-threading library
@@ -797,6 +798,7 @@ libtag_a_SOURCES =\
src/tag/TagPool.cxx src/tag/TagPool.hxx \
src/tag/TagTable.cxx src/tag/TagTable.hxx \
src/tag/Set.cxx src/tag/Set.hxx \
+ src/tag/Format.cxx src/tag/Format.hxx \
src/tag/VorbisComment.cxx src/tag/VorbisComment.hxx \
src/tag/ReplayGain.cxx src/tag/ReplayGain.hxx \
src/tag/MixRamp.cxx src/tag/MixRamp.hxx \
diff --git a/NEWS b/NEWS
index d70b04690..b0905abf3 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,7 @@ ver 0.20 (not yet released)
- jack: reduce CPU usage
- pulse: set channel map to WAVE-EX
- recorder: record tags
+ - recorder: allow dynamic file names
* mixer
- null: new plugin
* reset song priority on playback
diff --git a/doc/user.xml b/doc/user.xml
index e5b89e27d..74c169c11 100644
--- a/doc/user.xml
+++ b/doc/user.xml
@@ -3091,6 +3091,50 @@ buffer_size: 16384
Write to this file.
+
+
+
+ format_path
+ P
+
+
+
+ An alternative to path which
+ provides a format string referring to tag values.
+ Every time a new song starts or a new tag gets
+ received from a radio station, a new file is
+ opened. If the format does not render a file
+ name, nothing is recorded.
+
+
+
+ A tag name enclosed in percent signs ('%') is
+ replaced with the tag value. Example:
+ ~/.mpd/recorder/%artist% -
+ %title%.ogg
+
+
+
+ Square brackets can be used to group a substring.
+ If none of the tags referred in the group can be
+ found, the whole group is omitted. Example:
+ [~/.mpd/recorder/[%artist% -
+ ]%title%.ogg] (this omits the dash
+ when no artist tag exists; if title also doesn't
+ exist, no file is written)
+
+
+
+ The operators "|" (logical "or") and "&"
+ (logical "and") can be used to select portions of
+ the format string depending on the existing tag
+ values. Example:
+ ~/.mpd/recorder/[%title|%name%].ogg
+ (use the "name" tag if no title exists)
+
+
+
+
encoder
diff --git a/src/output/plugins/RecorderOutputPlugin.cxx b/src/output/plugins/RecorderOutputPlugin.cxx
index 973e60b47..b0488080a 100644
--- a/src/output/plugins/RecorderOutputPlugin.cxx
+++ b/src/output/plugins/RecorderOutputPlugin.cxx
@@ -21,17 +21,23 @@
#include "RecorderOutputPlugin.hxx"
#include "../OutputAPI.hxx"
#include "../Wrapper.hxx"
+#include "tag/Format.hxx"
#include "encoder/ToOutputStream.hxx"
#include "encoder/EncoderInterface.hxx"
#include "encoder/EncoderPlugin.hxx"
#include "encoder/EncoderList.hxx"
#include "config/ConfigError.hxx"
+#include "config/ConfigPath.hxx"
#include "Log.hxx"
#include "fs/AllocatedPath.hxx"
#include "fs/io/FileOutputStream.hxx"
#include "util/Error.hxx"
+#include "util/Domain.hxx"
#include
+#include
+
+static constexpr Domain recorder_domain("recorder");
class RecorderOutput {
friend struct AudioOutputWrapper;
@@ -48,6 +54,18 @@ class RecorderOutput {
*/
AllocatedPath path;
+ /**
+ * A string that will be used with FormatTag() to build the
+ * destination path.
+ */
+ std::string format_path;
+
+ /**
+ * The #AudioFormat that is currently active. This is used
+ * for switching to another file.
+ */
+ AudioFormat effective_audio_format;
+
/**
* The destination file.
*/
@@ -84,10 +102,18 @@ class RecorderOutput {
size_t Play(const void *chunk, size_t size, Error &error);
private:
+ gcc_pure
+ bool HasDynamicPath() const {
+ return !format_path.empty();
+ }
+
/**
* Finish the encoder and commit the file.
*/
bool Commit(Error &error);
+
+ void FinishFormat();
+ bool ReopenFormat(AllocatedPath &&new_path, Error &error);
};
inline bool
@@ -105,9 +131,20 @@ RecorderOutput::Configure(const config_param ¶m, Error &error)
}
path = param.GetBlockPath("path", error);
- if (path.IsNull()) {
- if (!error.IsDefined())
- error.Set(config_domain, "'path' not configured");
+ if (error.IsDefined())
+ return false;
+
+ const char *fmt = param.GetBlockValue("format_path", nullptr);
+ if (fmt != nullptr)
+ format_path = fmt;
+
+ if (path.IsNull() && fmt == nullptr) {
+ error.Set(config_domain, "'path' not configured");
+ return false;
+ }
+
+ if (!path.IsNull() && fmt != nullptr) {
+ error.Set(config_domain, "Cannot have both 'path' and 'format_path'");
return false;
}
@@ -152,9 +189,19 @@ RecorderOutput::Open(AudioFormat &audio_format, Error &error)
{
/* create the output file */
- file = FileOutputStream::Create(path, error);
- if (file == nullptr)
- return false;
+ if (!HasDynamicPath()) {
+ assert(!path.IsNull());
+
+ file = FileOutputStream::Create(path, error);
+ if (file == nullptr)
+ return false;
+ } else {
+ /* don't open the file just yet; wait until we have
+ a tag that we can use to build the path */
+ assert(path.IsNull());
+
+ file = nullptr;
+ }
/* open the encoder */
@@ -163,10 +210,19 @@ RecorderOutput::Open(AudioFormat &audio_format, Error &error)
return false;
}
- if (!EncoderToFile(error)) {
+ if (!HasDynamicPath()) {
+ if (!EncoderToFile(error)) {
+ encoder->Close();
+ delete file;
+ return false;
+ }
+ } else {
+ /* remember the AudioFormat for ReopenFormat() */
+ effective_audio_format = audio_format;
+
+ /* close the encoder for now; it will be opened as
+ soon as we have received a tag */
encoder->Close();
- delete file;
- return false;
}
return true;
@@ -175,6 +231,8 @@ RecorderOutput::Open(AudioFormat &audio_format, Error &error)
inline bool
RecorderOutput::Commit(Error &error)
{
+ assert(!path.IsNull());
+
/* flush the encoder and write the rest to the file */
bool success = encoder_end(encoder, error) &&
@@ -195,14 +253,108 @@ RecorderOutput::Commit(Error &error)
inline void
RecorderOutput::Close()
{
+ if (file == nullptr) {
+ /* not currently encoding to a file; nothing needs to
+ be done now */
+ assert(HasDynamicPath());
+ assert(path.IsNull());
+ return;
+ }
+
Error error;
if (!Commit(error))
LogError(error);
+
+ if (HasDynamicPath()) {
+ assert(!path.IsNull());
+ path.SetNull();
+ }
+}
+
+void
+RecorderOutput::FinishFormat()
+{
+ assert(HasDynamicPath());
+
+ if (file == nullptr)
+ return;
+
+ Error error;
+ if (!Commit(error))
+ LogError(error);
+
+ file = nullptr;
+ path.SetNull();
+}
+
+inline bool
+RecorderOutput::ReopenFormat(AllocatedPath &&new_path, Error &error)
+{
+ assert(HasDynamicPath());
+ assert(path.IsNull());
+ assert(file == nullptr);
+
+ FileOutputStream *new_file =
+ FileOutputStream::Create(new_path, error);
+ if (new_file == nullptr)
+ return false;
+
+ AudioFormat new_audio_format = effective_audio_format;
+ if (!encoder->Open(new_audio_format, error)) {
+ delete new_file;
+ return false;
+ }
+
+ /* reopening the encoder must always result in the same
+ AudioFormat as before */
+ assert(new_audio_format == effective_audio_format);
+
+ if (!EncoderToOutputStream(*new_file, *encoder, error)) {
+ encoder->Close();
+ delete new_file;
+ return false;
+ }
+
+ path = std::move(new_path);
+ file = new_file;
+
+ FormatDebug(recorder_domain, "Recording to \"%s\"", path.c_str());
+
+ return true;
}
inline void
RecorderOutput::SendTag(const Tag &tag)
{
+ if (HasDynamicPath()) {
+ char *p = FormatTag(tag, format_path.c_str());
+ if (p == nullptr || *p == 0) {
+ /* no path could be composed with this tag:
+ don't write a file */
+ free(p);
+ FinishFormat();
+ return;
+ }
+
+ Error error;
+ AllocatedPath new_path = ParsePath(p, error);
+ free(p);
+ if (new_path.IsNull()) {
+ LogError(error);
+ FinishFormat();
+ return;
+ }
+
+ if (new_path != path) {
+ FinishFormat();
+
+ if (!ReopenFormat(std::move(new_path), error)) {
+ LogError(error);
+ return;
+ }
+ }
+ }
+
Error error;
if (!encoder_pre_tag(encoder, error) ||
!EncoderToFile(error) ||
@@ -213,6 +365,14 @@ RecorderOutput::SendTag(const Tag &tag)
inline size_t
RecorderOutput::Play(const void *chunk, size_t size, Error &error)
{
+ if (file == nullptr) {
+ /* not currently encoding to a file; discard incoming
+ data */
+ assert(HasDynamicPath());
+ assert(path.IsNull());
+ return size;
+ }
+
return encoder_write(encoder, chunk, size, error) &&
EncoderToFile(error)
? size : 0;
diff --git a/src/tag/Format.cxx b/src/tag/Format.cxx
new file mode 100644
index 000000000..3fdcc7db6
--- /dev/null
+++ b/src/tag/Format.cxx
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2003-2015 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "Format.hxx"
+#include "Tag.hxx"
+#include "util/format.h"
+#include "util/StringUtil.hxx"
+
+#include
+
+#include
+
+struct FormatTagContext {
+ const Tag &tag;
+
+ char buffer[256];
+
+ explicit FormatTagContext(const Tag &_tag):tag(_tag) {}
+};
+
+/**
+ * Is this a character unsafe to use in a path name segment?
+ */
+static constexpr bool
+IsUnsafeChar(char ch)
+{
+ return
+ /* disallow characters illegal in file names on
+ Windows (Linux allows almost anything) */
+ ch == '\\' || ch == '/' || ch == ':' || ch == '*' ||
+ ch == '?' || ch == '<' || ch == '>' || ch == '|' ||
+ /* allow space, but disallow all other whitespace */
+ (unsigned char)ch < 0x20;
+}
+
+gcc_pure
+static bool
+HasUnsafeChar(const char *s)
+{
+ for (; *s; ++s)
+ if (IsUnsafeChar(*s))
+ return true;
+
+ return false;
+}
+
+static const char *
+SanitizeString(const char *s, char *buffer, size_t buffer_size)
+{
+ /* skip leading dots to avoid generating "../" sequences */
+ while (*s == '.')
+ ++s;
+
+ if (!HasUnsafeChar(s))
+ return s;
+
+ char *end = CopyString(buffer, s, buffer_size);
+ std::replace_if(buffer, end, IsUnsafeChar, ' ');
+ return buffer;
+}
+
+gcc_pure gcc_nonnull_all
+static const char *
+TagGetter(const void *object, const char *name)
+{
+ const auto &_ctx = *(const FormatTagContext *)object;
+ auto &ctx = const_cast(_ctx);
+ const Tag &tag = ctx.tag;
+
+ TagType tag_type = tag_name_parse_i(name);
+ if (tag_type == TAG_NUM_OF_ITEM_TYPES)
+ /* unknown tag name */
+ return nullptr;
+
+ const char *value = tag.GetValue(tag_type);
+ if (value == nullptr)
+ /* known tag name, but not present in this object */
+ value = "";
+
+ // TODO: handle multiple tag values
+ return SanitizeString(value, ctx.buffer, sizeof(ctx.buffer));
+}
+
+char *
+FormatTag(const Tag &tag, const char *format)
+{
+ FormatTagContext ctx(tag);
+ return format_object(format, &ctx, TagGetter);
+}
diff --git a/src/tag/Format.hxx b/src/tag/Format.hxx
new file mode 100644
index 000000000..a08e687d0
--- /dev/null
+++ b/src/tag/Format.hxx
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2003-2015 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_TAG_FORMAT_HXX
+#define MPD_TAG_FORMAT_HXX
+
+#include "check.h"
+#include "Compiler.h"
+
+struct Tag;
+
+gcc_malloc gcc_nonnull_all
+char *
+FormatTag(const Tag &tag, const char *format);
+
+#endif
diff --git a/src/util/format.c b/src/util/format.c
new file mode 100644
index 000000000..d360df3dc
--- /dev/null
+++ b/src/util/format.c
@@ -0,0 +1,252 @@
+/*
+ * music player command (mpc)
+ * Copyright (C) 2003-2015 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "format.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ * Reallocate the given string and append the source string.
+ */
+gcc_malloc
+static char *
+string_append(char *dest, const char *src, size_t len)
+{
+ size_t destlen = dest != NULL
+ ? strlen(dest)
+ : 0;
+
+ dest = realloc(dest, destlen + len + 1);
+ memcpy(dest + destlen, src, len);
+ dest[destlen + len] = '\0';
+
+ return dest;
+}
+
+/**
+ * Skip the format string until the current group is closed by either
+ * '&', '|' or ']' (supports nesting).
+ */
+gcc_pure
+static const char *
+skip_format(const char *p)
+{
+ unsigned stack = 0;
+
+ while (*p != '\0') {
+ if (*p == '[')
+ stack++;
+ else if (*p == '#' && p[1] != '\0')
+ /* skip escaped stuff */
+ ++p;
+ else if (stack > 0) {
+ if (*p == ']')
+ --stack;
+ } else if (*p == '&' || *p == '|' || *p == ']')
+ break;
+
+ ++p;
+ }
+
+ return p;
+}
+
+static char *
+format_object2(const char *format, const char **last, const void *object,
+ const char *(*getter)(const void *object, const char *name))
+{
+ char *ret = NULL;
+ const char *p;
+ bool found = false;
+
+ for (p = format; *p != '\0';) {
+ switch (p[0]) {
+ case '|':
+ ++p;
+ if (!found) {
+ /* nothing found yet: try the next
+ section */
+ free(ret);
+ ret = NULL;
+ } else
+ /* already found a value: skip the
+ next section */
+ p = skip_format(p);
+ break;
+
+ case '&':
+ ++p;
+ if (!found)
+ /* nothing found yet, so skip this
+ section */
+ p = skip_format(p);
+ else
+ /* we found something yet, but it will
+ only be used if the next section
+ also found something, so reset the
+ flag */
+ found = false;
+ break;
+
+ case '[': {
+ char *t = format_object2(p + 1, &p, object, getter);
+ if (t != NULL) {
+ ret = string_append(ret, t, strlen(t));
+ free(t);
+ found = true;
+ }
+ }
+ break;
+
+ case ']':
+ if (last != NULL)
+ *last = p + 1;
+ if (!found) {
+ free(ret);
+ ret = NULL;
+ }
+ return ret;
+
+ case '\\': {
+ /* take care of escape sequences */
+ char ltemp;
+ switch (p[1]) {
+ case 'a':
+ ltemp = '\a';
+ break;
+
+ case 'b':
+ ltemp = '\b';
+ break;
+
+ case 't':
+ ltemp = '\t';
+ break;
+
+ case 'n':
+ ltemp = '\n';
+ break;
+
+ case 'v':
+ ltemp = '\v';
+ break;
+
+ case 'f':
+ ltemp = '\f';
+ break;
+
+ case 'r':
+ ltemp = '\r';
+ break;
+
+ case '[':
+ case ']':
+ ltemp = p[1];
+ break;
+
+ default:
+ /* unknown escape: copy the
+ backslash */
+ ltemp = p[0];
+ --p;
+ break;
+ }
+
+ ret = string_append(ret, <emp, 1);
+ p += 2;
+ }
+ break;
+
+ case '%': {
+ /* find the extent of this format specifier
+ (stop at \0, ' ', or esc) */
+ const char *end = p + 1;
+ while (*end >= 'a' && *end <= 'z')
+ ++end;
+
+ const size_t length = end - p + 1;
+
+ if (*end != '%') {
+ ret = string_append(ret, p, length - 1);
+ p = end;
+ continue;
+ }
+
+ char name[32];
+ if (length > (int)sizeof(name)) {
+ ret = string_append(ret, p, length);
+ p = end + 1;
+ continue;
+ }
+
+ memcpy(name, p + 1, length - 2);
+ name[length - 2] = 0;
+
+ const char *value = getter(object, name);
+ size_t value_length;
+ if (value != NULL) {
+ if (*value != 0)
+ found = true;
+ value_length = strlen(value);
+ } else {
+ /* unknown variable: copy verbatim
+ from format string */
+ value = p;
+ value_length = length;
+ }
+
+ ret = string_append(ret, value, value_length);
+
+ /* advance past the specifier */
+ p = end + 1;
+ }
+ break;
+
+ case '#':
+ /* let the escape character escape itself */
+ if (p[1] != '\0') {
+ ret = string_append(ret, p + 1, 1);
+ p += 2;
+ break;
+ }
+
+ /* fall through */
+
+ default:
+ /* pass-through non-escaped portions of the format string */
+ ret = string_append(ret, p, 1);
+ ++p;
+ }
+ }
+
+ if (last != NULL)
+ *last = p;
+ return ret;
+}
+
+char *
+format_object(const char *format, const void *object,
+ const char *(*getter)(const void *object, const char *name))
+{
+ return format_object2(format, NULL, object, getter);
+}
diff --git a/src/util/format.h b/src/util/format.h
new file mode 100644
index 000000000..fa3624b51
--- /dev/null
+++ b/src/util/format.h
@@ -0,0 +1,51 @@
+/*
+ * music player command (mpc)
+ * Copyright (C) 2003-2015 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPC_FORMAT_H
+#define MPC_FORMAT_H
+
+#include "Compiler.h"
+
+struct mpd_song;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Pretty-print an object into a string using the given format
+ * specification.
+ *
+ * @param format the format string
+ * @param object the object
+ * @param getter a getter function that extracts a value from the object
+ * @return the resulting string to be freed by free(); NULL if
+ * no format string group produced any output
+ */
+gcc_malloc
+char *
+format_object(const char *format, const void *object,
+ const char *(*getter)(const void *object, const char *name));
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif