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/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
1
NEWS
@ -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
|
||||||
|
44
doc/user.xml
44
doc/user.xml
@ -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 "&"
|
||||||
|
(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>
|
||||||
|
@ -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 ¶m, 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
106
src/tag/Format.cxx
Normal 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
32
src/tag/Format.hxx
Normal 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
252
src/util/format.c
Normal 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, <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);
|
||||||
|
}
|
51
src/util/format.h
Normal file
51
src/util/format.h
Normal 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
|
Loading…
Reference in New Issue
Block a user