output/recorder: dynamic file name

This commit is contained in:
Max Kellermann 2015-01-10 08:58:31 +01:00
parent 1caa41a623
commit e8debd2e45
8 changed files with 657 additions and 9 deletions

View File

@ -399,6 +399,7 @@ libutil_a_SOURCES = \
src/util/OptionParser.cxx src/util/OptionParser.hxx \ src/util/OptionParser.cxx src/util/OptionParser.hxx \
src/util/OptionDef.hxx \ src/util/OptionDef.hxx \
src/util/ByteReverse.cxx src/util/ByteReverse.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 src/util/bit_reverse.c src/util/bit_reverse.h
# Multi-threading library # Multi-threading library
@ -797,6 +798,7 @@ libtag_a_SOURCES =\
src/tag/TagPool.cxx src/tag/TagPool.hxx \ src/tag/TagPool.cxx src/tag/TagPool.hxx \
src/tag/TagTable.cxx src/tag/TagTable.hxx \ src/tag/TagTable.cxx src/tag/TagTable.hxx \
src/tag/Set.cxx src/tag/Set.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/VorbisComment.cxx src/tag/VorbisComment.hxx \
src/tag/ReplayGain.cxx src/tag/ReplayGain.hxx \ src/tag/ReplayGain.cxx src/tag/ReplayGain.hxx \
src/tag/MixRamp.cxx src/tag/MixRamp.hxx \ src/tag/MixRamp.cxx src/tag/MixRamp.hxx \

1
NEWS
View File

@ -15,6 +15,7 @@ ver 0.20 (not yet released)
- jack: reduce CPU usage - jack: reduce CPU usage
- pulse: set channel map to WAVE-EX - pulse: set channel map to WAVE-EX
- recorder: record tags - recorder: record tags
- recorder: allow dynamic file names
* mixer * mixer
- null: new plugin - null: new plugin
* reset song priority on playback * reset song priority on playback

View File

@ -3091,6 +3091,50 @@ buffer_size: 16384</programlisting>
Write to this file. Write to this file.
</entry> </entry>
</row> </row>
<row>
<entry>
<varname>format_path</varname>
<parameter>P</parameter>
</entry>
<entry>
<para>
An alternative to <varname>path</varname> 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.
</para>
<para>
A tag name enclosed in percent signs ('%') is
replaced with the tag value. Example:
<parameter>~/.mpd/recorder/%artist% -
%title%.ogg</parameter>
</para>
<para>
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:
<parameter>[~/.mpd/recorder/[%artist% -
]%title%.ogg]</parameter> (this omits the dash
when no artist tag exists; if title also doesn't
exist, no file is written)
</para>
<para>
The operators "|" (logical "or") and "&amp;"
(logical "and") can be used to select portions of
the format string depending on the existing tag
values. Example:
<parameter>~/.mpd/recorder/[%title|%name%].ogg</parameter>
(use the "name" tag if no title exists)
</para>
</entry>
</row>
<row> <row>
<entry> <entry>
<varname>encoder</varname> <varname>encoder</varname>

View File

@ -21,17 +21,23 @@
#include "RecorderOutputPlugin.hxx" #include "RecorderOutputPlugin.hxx"
#include "../OutputAPI.hxx" #include "../OutputAPI.hxx"
#include "../Wrapper.hxx" #include "../Wrapper.hxx"
#include "tag/Format.hxx"
#include "encoder/ToOutputStream.hxx" #include "encoder/ToOutputStream.hxx"
#include "encoder/EncoderInterface.hxx" #include "encoder/EncoderInterface.hxx"
#include "encoder/EncoderPlugin.hxx" #include "encoder/EncoderPlugin.hxx"
#include "encoder/EncoderList.hxx" #include "encoder/EncoderList.hxx"
#include "config/ConfigError.hxx" #include "config/ConfigError.hxx"
#include "config/ConfigPath.hxx"
#include "Log.hxx" #include "Log.hxx"
#include "fs/AllocatedPath.hxx" #include "fs/AllocatedPath.hxx"
#include "fs/io/FileOutputStream.hxx" #include "fs/io/FileOutputStream.hxx"
#include "util/Error.hxx" #include "util/Error.hxx"
#include "util/Domain.hxx"
#include <assert.h> #include <assert.h>
#include <stdlib.h>
static constexpr Domain recorder_domain("recorder");
class RecorderOutput { class RecorderOutput {
friend struct AudioOutputWrapper<RecorderOutput>; friend struct AudioOutputWrapper<RecorderOutput>;
@ -48,6 +54,18 @@ class RecorderOutput {
*/ */
AllocatedPath path; 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. * The destination file.
*/ */
@ -84,10 +102,18 @@ class RecorderOutput {
size_t Play(const void *chunk, size_t size, Error &error); size_t Play(const void *chunk, size_t size, Error &error);
private: private:
gcc_pure
bool HasDynamicPath() const {
return !format_path.empty();
}
/** /**
* Finish the encoder and commit the file. * Finish the encoder and commit the file.
*/ */
bool Commit(Error &error); bool Commit(Error &error);
void FinishFormat();
bool ReopenFormat(AllocatedPath &&new_path, Error &error);
}; };
inline bool inline bool
@ -105,12 +131,23 @@ RecorderOutput::Configure(const config_param &param, Error &error)
} }
path = param.GetBlockPath("path", error); path = param.GetBlockPath("path", error);
if (path.IsNull()) { if (error.IsDefined())
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"); error.Set(config_domain, "'path' not configured");
return false; return false;
} }
if (!path.IsNull() && fmt != nullptr) {
error.Set(config_domain, "Cannot have both 'path' and 'format_path'");
return false;
}
/* initialize encoder */ /* initialize encoder */
encoder = encoder_init(*encoder_plugin, param, error); encoder = encoder_init(*encoder_plugin, param, error);
@ -152,9 +189,19 @@ RecorderOutput::Open(AudioFormat &audio_format, Error &error)
{ {
/* create the output file */ /* create the output file */
if (!HasDynamicPath()) {
assert(!path.IsNull());
file = FileOutputStream::Create(path, error); file = FileOutputStream::Create(path, error);
if (file == nullptr) if (file == nullptr)
return false; 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 */ /* open the encoder */
@ -163,11 +210,20 @@ RecorderOutput::Open(AudioFormat &audio_format, Error &error)
return false; return false;
} }
if (!HasDynamicPath()) {
if (!EncoderToFile(error)) { if (!EncoderToFile(error)) {
encoder->Close(); encoder->Close();
delete file; delete file;
return false; 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();
}
return true; return true;
} }
@ -175,6 +231,8 @@ RecorderOutput::Open(AudioFormat &audio_format, Error &error)
inline bool inline bool
RecorderOutput::Commit(Error &error) RecorderOutput::Commit(Error &error)
{ {
assert(!path.IsNull());
/* flush the encoder and write the rest to the file */ /* flush the encoder and write the rest to the file */
bool success = encoder_end(encoder, error) && bool success = encoder_end(encoder, error) &&
@ -195,14 +253,108 @@ RecorderOutput::Commit(Error &error)
inline void inline void
RecorderOutput::Close() 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; Error error;
if (!Commit(error)) if (!Commit(error))
LogError(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 inline void
RecorderOutput::SendTag(const Tag &tag) 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; Error error;
if (!encoder_pre_tag(encoder, error) || if (!encoder_pre_tag(encoder, error) ||
!EncoderToFile(error) || !EncoderToFile(error) ||
@ -213,6 +365,14 @@ RecorderOutput::SendTag(const Tag &tag)
inline size_t inline size_t
RecorderOutput::Play(const void *chunk, size_t size, Error &error) 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) && return encoder_write(encoder, chunk, size, error) &&
EncoderToFile(error) EncoderToFile(error)
? size : 0; ? size : 0;

106
src/tag/Format.cxx Normal file
View File

@ -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 <algorithm>
#include <string.h>
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<FormatTagContext &>(_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);
}

32
src/tag/Format.hxx Normal file
View File

@ -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

252
src/util/format.c Normal file
View File

@ -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 <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/**
* 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, &ltemp, 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);
}

51
src/util/format.h Normal file
View File

@ -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