From e8debd2e454f9a7399104b5c77e44d5dce6aa203 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Sat, 10 Jan 2015 08:58:31 +0100 Subject: output/recorder: dynamic file name --- src/output/plugins/RecorderOutputPlugin.cxx | 178 +++++++++++++++++++- src/tag/Format.cxx | 106 ++++++++++++ src/tag/Format.hxx | 32 ++++ src/util/format.c | 252 ++++++++++++++++++++++++++++ src/util/format.h | 51 ++++++ 5 files changed, 610 insertions(+), 9 deletions(-) create mode 100644 src/tag/Format.cxx create mode 100644 src/tag/Format.hxx create mode 100644 src/util/format.c create mode 100644 src/util/format.h (limited to 'src') 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 -- cgit v1.2.3