output/recorder: dynamic file name
This commit is contained in:
parent
1caa41a623
commit
e8debd2e45
|
@ -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 \
|
||||
|
|
1
NEWS
1
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
|
||||
|
|
44
doc/user.xml
44
doc/user.xml
|
@ -3091,6 +3091,50 @@ buffer_size: 16384</programlisting>
|
|||
Write to this file.
|
||||
</entry>
|
||||
</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 "&"
|
||||
(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>
|
||||
<entry>
|
||||
<varname>encoder</varname>
|
||||
|
|
|
@ -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 <assert.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static constexpr Domain recorder_domain("recorder");
|
||||
|
||||
class RecorderOutput {
|
||||
friend struct AudioOutputWrapper<RecorderOutput>;
|
||||
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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, <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);
|
||||
}
|
|
@ -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
|
Loading…
Reference in New Issue