output/recorder: dynamic file name
This commit is contained in:
		| @@ -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,9 +131,20 @@ 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; | ||||||
| 			error.Set(config_domain, "'path' not configured"); |  | ||||||
|  | 	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; | 		return false; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -152,9 +189,19 @@ RecorderOutput::Open(AudioFormat &audio_format, Error &error) | |||||||
| { | { | ||||||
| 	/* create the output file */ | 	/* create the output file */ | ||||||
|  |  | ||||||
| 	file = FileOutputStream::Create(path, error); | 	if (!HasDynamicPath()) { | ||||||
| 	if (file == nullptr) | 		assert(!path.IsNull()); | ||||||
| 		return false; |  | ||||||
|  | 		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 */ | 	/* open the encoder */ | ||||||
|  |  | ||||||
| @@ -163,10 +210,19 @@ RecorderOutput::Open(AudioFormat &audio_format, Error &error) | |||||||
| 		return false; | 		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(); | 		encoder->Close(); | ||||||
| 		delete file; |  | ||||||
| 		return false; |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	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 | ||||||
		Reference in New Issue
	
	Block a user
	 Max Kellermann
					Max Kellermann