Commit 26c82350 authored by Josh Coalson's avatar Josh Coalson

add new options: --tag-from-file for flac and --set-tag-from-file for metaflac

parent 6c088862
......@@ -83,7 +83,8 @@
<li>
flac:
<ul>
<li>Add support for encoding from non-compressed AIFF-C (<a href="https://sourceforge.net/tracker/?func=detail&amp;atid=113478&amp;aid=1090933&amp;group_id=13478">SF #1090933</a>).</li>
<li>Added a new option <a href="documentation.html#flac_options_tag_from_file"><span class="argument">--tag-from-file</span></a> for setting a tag from file (e.g. for importing a cuesheet as a tag).</li>
<li>Added support for encoding from non-compressed AIFF-C (<a href="https://sourceforge.net/tracker/?func=detail&amp;atid=113478&amp;aid=1090933&amp;group_id=13478">SF #1090933</a>).</li>
<li>Importing of non-CDDA-compliant cuesheets now only issues a warning, not an error (see <a href="http://www.hydrogenaudio.org/forums/index.php?showtopic=31282">here</a>).</li>
<li>Fixed a bug in cuesheet parsing where it would return an error if the last line of the cuesheet did not end with a newline.</li>
<li>Fixed a bug that caused a crash when <span class="argument">-a</span> and <span class="argument">-t</span> were used together (<a href="https://sourceforge.net/tracker/index.php?func=detail&amp;aid=1229481&amp;group_id=13478&amp;atid=113478">SF #1229481</a>).</li>
......@@ -95,6 +96,7 @@
<li>
metaflac:
<ul>
<li>Added a new option <a href="documentation.html#metaflac_shorthand_set_tag_from_file"><span class="argument">--set-tag-from-file</span></a> for setting a tag from file (e.g. for importing a cuesheet as a tag).</li>
<li>Added shorthand operation <a href="documentation.html#metaflac_shorthand_remove_replay_gain"><span class="argument">--remove-replay-gain</span></a> for removing ReplayGain tags.</li>
<li>Importing of non-CDDA-compliant cuesheets now issues a warning.</li>
</ul>
......
......@@ -583,6 +583,15 @@
Add a FLAC tag. The comment must adhere to the Vorbis comment spec (which FLAC tags implement), i.e. the FIELD must contain only legal characters, terminated by an 'equals' sign. Make sure to quote the comment if necessary. This option may appear more than once to add several comments. NOTE: all tags will be added to all encoded files.
</td>
</tr>
<tr>
<td nowrap="nowrap" align="right" valign="top" bgcolor="#F4F4CC">
<a name="flac_options_tag_from_file" />
<span class="argument">--tag-from-file=FIELD=FILENAME</span>
</td>
<td>
Like <a href="#flac_options_tag"><span class="argument">--tag</span></a>, except FILENAME is a file whose contents will be read verbatim to set the tag value. The contents will be converted to UTF-8 from the local charset. This can be used to store a cuesheet in a tag (e.g. <span class="argument">--tag-from-file="CUESHEET=image.cue"</span>). Do not try to store binary data in tag fields! Use APPLICATION blocks for that.
</td>
</tr>
<tr>
<td nowrap="nowrap" align="right" valign="top" bgcolor="#F4F4CC">
<a name="flac_options_blocksize" />
......@@ -1133,13 +1142,22 @@
Add a tag. The <span class="argument">FIELD</span> must comply with the Vorbis comment spec, of the form <span class="argument">NAME=VALUE</span>. If there is currently no tag block, one will be created.
</td>
</tr>
<tr>
<td nowrap="nowrap" align="right" valign="top" bgcolor="#F4F4CC">
<a name="metaflac_shorthand_set_tag_from_file" />
<span class="argument">--set-tag-from-file=FIELD</span>
</td>
<td>
Like <a href="#metaflac_shorthand_set_tag"><span class="argument">--set-tag</span></a>, except the VALUE is a filename whose contents will be read verbatim to set the tag value. Unless <a href="#metaflac_options_no_utf8_convert"><span class="argument">--no-utf8-convert</span></a> is specified, the contents will be converted to UTF-8 from the local charset. This can be used to store a cuesheet in a tag (e.g. <span class="argument">--set-tag-from-file="CUESHEET=image.cue"</span>). Do not try to store binary data in tag fields! Use APPLICATION blocks for that.
</td>
</tr>
<tr>
<td nowrap="nowrap" align="right" valign="top" bgcolor="#F4F4CC">
<a name="metaflac_shorthand_import_tags_from" />
<span class="argument">--import-tags-from=FILE</span>
</td>
<td>
Import tags from a file. Use '-' for stdin. Each line should be of the form <span class="argument">NAME=VALUE</span>. Multi-line comments are currently not supported. Specify <span class="argument">--remove-all-tags</span> and/or <span class="argument">--no-utf8-convert</span> before <span class="argument">--import-tags-from</span> if necessary.
Import tags from a file. Use '-' for stdin. Each line should be of the form <span class="argument">NAME=VALUE</span>. Multi-line comments are currently not supported. Specify <span class="argument">--remove-all-tags</span> and/or <a href="#metaflac_options_no_utf8_convert"><span class="argument">--no-utf8-convert</span></a> before <span class="argument">--import-tags-from</span> if necessary.
</td>
</tr>
<tr>
......@@ -1148,7 +1166,7 @@
<span class="argument">--export-tags-to=FILE</span>
</td>
<td>
Export tags to a file. Use '-' for stdin. Each line will be of the form <span class="argument">NAME=VALUE</span>. Specify <span class="argument">--no-utf8-convert</span> if necessary.
Export tags to a file. Use '-' for stdin. Each line will be of the form <span class="argument">NAME=VALUE</span>. Specify <a href="#metaflac_options_no_utf8_convert"><span class="argument">--no-utf8-convert</span></a> if necessary.
</td>
</tr>
<tr>
......
......@@ -465,6 +465,21 @@
</listitem>
</varlistentry>
<varlistentry>
<term><option>--tag-from-file</option>=<replaceable>FIELD=FILENAME</replaceable></term>
<listitem>
<para>Like --tag, except FILENAME is a file whose
contents will be read verbatim to set the tag
value. The contents will be converted to UTF-8
from the local charset. This can be used to
store a cuesheet in a tag (e.g.
--tag-from-file="CUESHEET=image.cue"). Do not
try to store binary data in tag fields! Use
APPLICATION blocks for that.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-b</option> <replaceable>#</replaceable>, <option>--blocksize</option>=<replaceable>#</replaceable></term>
......
......@@ -243,6 +243,21 @@ manpage.1: manpage.sgml
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--set-tag-from-file=field</option></term>
<listitem>
<para>
Like --set-tag, except the VALUE is a filename whose
contents will be read verbatim to set the tag value.
Unless --no-utf8-convert is specified, the contents will be
converted to UTF-8 from the local charset. This can be used
to store a cuesheet in a tag (e.g.
--set-tag-from-file="CUESHEET=image.cue"). Do not try to
store binary data in tag fields! Use APPLICATION blocks for
that.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--import-tags-from=file</option></term>
<listitem>
......
......@@ -119,6 +119,7 @@ static struct share__option long_options_[] = {
{ "cuesheet" , share__required_argument, 0, 0 },
{ "no-cued-seekpoints" , share__no_argument, 0, 0 },
{ "tag" , share__required_argument, 0, 'T' },
{ "tag-from-file" , share__required_argument, 0, 0 },
{ "compression-level-0" , share__no_argument, 0, '0' },
{ "compression-level-1" , share__no_argument, 0, '1' },
{ "compression-level-2" , share__no_argument, 0, '2' },
......@@ -656,6 +657,7 @@ int parse_options(int argc, char *argv[])
int parse_option(int short_option, const char *long_option, const char *option_argument)
{
const char *violation;
char *p;
if(short_option == 0) {
......@@ -715,6 +717,11 @@ int parse_option(int short_option, const char *long_option, const char *option_a
FLAC__ASSERT(0 != option_argument);
option_values.cuesheet_filename = option_argument;
}
else if(0 == strcmp(long_option, "tag-from-file")) {
FLAC__ASSERT(0 != option_argument);
if(!flac__vorbiscomment_add(option_values.vorbis_comment, option_argument, /*value_from_file=*/true, &violation))
return usage_error("ERROR: (--tag-from-file) %s\n", violation);
}
else if(0 == strcmp(long_option, "no-cued-seekpoints")) {
option_values.cued_seekpoints = false;
}
......@@ -858,7 +865,6 @@ int parse_option(int short_option, const char *long_option, const char *option_a
}
}
else {
const char *violation;
switch(short_option) {
case 'h':
option_values.show_help = true;
......@@ -898,7 +904,7 @@ int parse_option(int short_option, const char *long_option, const char *option_a
break;
case 'T':
FLAC__ASSERT(0 != option_argument);
if(!flac__vorbiscomment_add(option_values.vorbis_comment, option_argument, &violation))
if(!flac__vorbiscomment_add(option_values.vorbis_comment, option_argument, /*value_from_file=*/false, &violation))
return usage_error("ERROR: (-T/--tag) %s\n", violation);
break;
case '0':
......@@ -1208,6 +1214,7 @@ void show_help()
printf(" --replay-gain Calculate ReplayGain & store in FLAC tags\n");
printf(" --cuesheet=FILENAME Import cuesheet and store in CUESHEET block\n");
printf(" -T, --tag=FIELD=VALUE Add a FLAC tag; may appear multiple times\n");
printf(" --tag-from-file=FIELD=FILENAME Like --tag but gets value from file\n");
printf(" -S, --seekpoint={#|X|#x|#s} Add seek point(s)\n");
printf(" -P, --padding=# Write a PADDING block of length #\n");
printf(" -0, --compression-level-0, --fast Synonymous with -l 0 -b 1152 -r 2,2\n");
......@@ -1392,6 +1399,14 @@ void show_explain()
printf(" comment if necessary. This option may appear\n");
printf(" more than once to add several comments. NOTE:\n");
printf(" all tags will be added to all encoded files.\n");
printf(" --tag-from-file=FIELD=FILENAME Like --tag, except FILENAME is a file\n");
printf(" whose contents will be read verbatim to set the\n");
printf(" tag value. The contents will be converted to\n");
printf(" UTF-8 from the local charset. This can be used\n");
printf(" to store a cuesheet in a tag (e.g.\n");
printf(" --tag-from-file=\"CUESHEET=image.cue\"). Do not\n");
printf(" try to store binary data in tag fields! Use\n");
printf(" APPLICATION blocks for that.\n");
printf(" -S, --seekpoint={#|X|#x|#s} Include a point or points in a SEEKTABLE\n");
printf(" # : a specific sample number for a seek point\n");
printf(" X : a placeholder point (always goes at the end of the SEEKTABLE)\n");
......
......@@ -23,6 +23,7 @@
#include "vorbiscomment.h"
#include "FLAC/assert.h"
#include "FLAC/metadata.h"
#include "share/grabbag.h" /* for grabbag__file_get_filesize() */
#include "share/utf8.h"
#include <ctype.h>
#include <stdio.h>
......@@ -32,7 +33,7 @@
/*
* This struct and the following 4 static functions are copied from
* ../metaflac/main.c. Maybe someday there will be a convenience
* ../metaflac/. Maybe someday there will be a convenience
* library for Vorbis comment parsing.
*/
typedef struct {
......@@ -41,6 +42,7 @@ typedef struct {
/* according to the vorbis spec, field values can contain \0 so simple C strings are not enough here */
unsigned field_value_length;
char *field_value;
FLAC__bool field_value_from_file; /* true if field_value holds a filename for the value, false for plain value */
} Argument_VcField;
static void die(const char *message)
......@@ -101,34 +103,102 @@ static FLAC__bool set_vc_field(FLAC__StreamMetadata *block, const Argument_VcFie
{
FLAC__StreamMetadata_VorbisComment_Entry entry;
char *converted;
FLAC__bool needs_free = false;
FLAC__ASSERT(0 != block);
FLAC__ASSERT(block->type == FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__ASSERT(0 != field);
FLAC__ASSERT(0 != needs_write);
if(raw) {
entry.entry = (FLAC__byte *)field->field;
}
else if(utf8_encode(field->field, &converted) >= 0) {
entry.entry = (FLAC__byte *)converted;
needs_free = true;
}
else {
*violation = "couldn't convert comment to UTF-8";
return false;
}
if(field->field_value_from_file) {
/* read the file into 'data' */
FILE *f = 0;
char *data = 0;
const off_t size = grabbag__file_get_filesize(field->field_value);
if(size < 0) {
*violation = "can't open file for tag value";
return false;
}
if(size >= 0x100000) { /* magic arbitrary limit, actual format limit is near 16MB */
*violation = "file for tag value is too large";
return false;
}
if(0 == (data = malloc(size+1)))
die("out of memory allocating tag value");
data[size] = '\0';
if(0 == (f = fopen(field->field_value, "rb")) || fread(data, 1, size, f) != (size_t)size) {
free(data);
if(f)
fclose(f);
*violation = "error while reading file for tag value";
return false;
}
fclose(f);
if(strlen(data) != (size_t)size) {
free(data);
*violation = "file for tag value has embedded NULs";
return false;
}
entry.length = strlen((const char *)entry.entry);
/* move 'data' into 'converted', converting to UTF-8 if necessary */
if(raw) {
converted = data;
}
else if(utf8_encode(data, &converted) >= 0) {
free(data);
}
else {
free(data);
*violation = "error converting file contents to UTF-8 for tag value";
return false;
}
if(!FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/true)) {
if(needs_free)
/* create and entry and append it */
if(!FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, field->field_name, converted)) {
free(converted);
*violation = "memory allocation failure";
return false;
*violation = "file for tag value is not valid UTF-8";
return false;
}
free(converted);
if(!FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/false)) {
*violation = "memory allocation failure";
return false;
}
*needs_write = true;
return true;
}
else {
FLAC__bool needs_free = false;
if(raw) {
entry.entry = (FLAC__byte *)field->field;
}
else if(utf8_encode(field->field, &converted) >= 0) {
entry.entry = (FLAC__byte *)converted;
needs_free = true;
}
else {
*violation = "error converting comment to UTF-8";
return false;
}
entry.length = strlen((const char *)entry.entry);
if(!FLAC__format_vorbiscomment_entry_is_legal(entry.entry, entry.length)) {
if(needs_free)
free(converted);
/*
* our previous parsing has already established that the field
* name is OK, so it must be the field value
*/
*violation = "tag value for is not valid UTF-8";
return false;
}
if(!FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/true)) {
if(needs_free)
free(converted);
*violation = "memory allocation failure";
return false;
}
*needs_write = true;
if(needs_free)
free(converted);
......@@ -150,7 +220,7 @@ static void free_field(Argument_VcField *obj)
free(obj->field_value);
}
FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comment, const char **violation)
FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comment, FLAC__bool value_from_file, const char **violation)
{
Argument_VcField parsed;
FLAC__bool dummy;
......@@ -161,6 +231,7 @@ FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comm
memset(&parsed, 0, sizeof(parsed));
parsed.field_value_from_file = value_from_file;
if(!parse_vorbis_comment_field(comment, &(parsed.field), &(parsed.field_name), &(parsed.field_value), &(parsed.field_value_length), violation)) {
free_field(&parsed);
return false;
......
......@@ -21,6 +21,6 @@
#include "FLAC/metadata.h"
FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comment, const char **violation);
FLAC__bool flac__vorbiscomment_add(FLAC__StreamMetadata *block, const char *comment, FLAC__bool value_from_file, const char **violation);
#endif
......@@ -19,6 +19,7 @@
#include "options.h"
#include "utils.h"
#include "FLAC/assert.h"
#include "share/grabbag.h" /* for grabbag__file_get_filesize() */
#include "share/utf8.h"
#include <stdlib.h>
#include <string.h>
......@@ -171,34 +172,102 @@ FLAC__bool set_vc_field(const char *filename, FLAC__StreamMetadata *block, const
{
FLAC__StreamMetadata_VorbisComment_Entry entry;
char *converted;
FLAC__bool needs_free = false;
FLAC__ASSERT(0 != block);
FLAC__ASSERT(block->type == FLAC__METADATA_TYPE_VORBIS_COMMENT);
FLAC__ASSERT(0 != field);
FLAC__ASSERT(0 != needs_write);
if(raw) {
entry.entry = (FLAC__byte *)field->field;
}
else if(utf8_encode(field->field, &converted) >= 0) {
entry.entry = (FLAC__byte *)converted;
needs_free = true;
}
else {
fprintf(stderr, "%s: ERROR: couldn't convert comment to UTF-8\n", filename);
return false;
}
if(field->field_value_from_file) {
/* read the file into 'data' */
FILE *f = 0;
char *data = 0;
const off_t size = grabbag__file_get_filesize(field->field_value);
if(size < 0) {
fprintf(stderr, "%s: ERROR: can't open file '%s' for '%s' tag value\n", filename, field->field_value, field->field_name);
return false;
}
if(size >= 0x100000) { /* magic arbitrary limit, actual format limit is near 16MB */
fprintf(stderr, "%s: ERROR: file '%s' for '%s' tag value is too large\n", filename, field->field_value, field->field_name);
return false;
}
if(0 == (data = malloc(size+1)))
die("out of memory allocating tag value");
data[size] = '\0';
if(0 == (f = fopen(field->field_value, "rb")) || fread(data, 1, size, f) != (size_t)size) {
free(data);
if(f)
fclose(f);
fprintf(stderr, "%s: ERROR: while reading file '%s' for '%s' tag value\n", filename, field->field_value, field->field_name);
return false;
}
fclose(f);
if(strlen(data) != (size_t)size) {
free(data);
fprintf(stderr, "%s: ERROR: file '%s' for '%s' tag value has embedded NULs\n", filename, field->field_value, field->field_name);
return false;
}
entry.length = strlen((const char *)entry.entry);
/* move 'data' into 'converted', converting to UTF-8 if necessary */
if(raw) {
converted = data;
}
else if(utf8_encode(data, &converted) >= 0) {
free(data);
}
else {
free(data);
fprintf(stderr, "%s: ERROR: converting file '%s' contents to UTF-8 for tag value\n", filename, field->field_value);
return false;
}
if(!FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/true)) {
if(needs_free)
/* create and entry and append it */
if(!FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, field->field_name, converted)) {
free(converted);
fprintf(stderr, "%s: ERROR: memory allocation failure\n", filename);
return false;
fprintf(stderr, "%s: ERROR: file '%s' for '%s' tag value is not valid UTF-8\n", filename, field->field_value, field->field_name);
return false;
}
free(converted);
if(!FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/false)) {
fprintf(stderr, "%s: ERROR: memory allocation failure\n", filename);
return false;
}
*needs_write = true;
return true;
}
else {
FLAC__bool needs_free = false;
if(raw) {
entry.entry = (FLAC__byte *)field->field;
}
else if(utf8_encode(field->field, &converted) >= 0) {
entry.entry = (FLAC__byte *)converted;
needs_free = true;
}
else {
fprintf(stderr, "%s: ERROR: converting comment '%s' to UTF-8\n", filename, field->field);
return false;
}
entry.length = strlen((const char *)entry.entry);
if(!FLAC__format_vorbiscomment_entry_is_legal(entry.entry, entry.length)) {
if(needs_free)
free(converted);
/*
* our previous parsing has already established that the field
* name is OK, so it must be the field value
*/
fprintf(stderr, "%s: ERROR: tag value for '%s' is not valid UTF-8\n", filename, field->field_name);
return false;
}
if(!FLAC__metadata_object_vorbiscomment_append_comment(block, entry, /*copy=*/true)) {
if(needs_free)
free(converted);
fprintf(stderr, "%s: ERROR: memory allocation failure\n", filename);
return false;
}
*needs_write = true;
if(needs_free)
free(converted);
......@@ -240,6 +309,7 @@ FLAC__bool import_vc_from(const char *filename, FLAC__StreamMetadata *block, con
Argument_VcField field;
*p = '\0';
memset(&field, 0, sizeof(Argument_VcField));
field.field_value_from_file = false;
if(!parse_vorbis_comment_field(line, &field.field, &field.field_name, &field.field_value, &field.field_value_length, &violation)) {
FLAC__ASSERT(0 != violation);
fprintf(stderr, "%s: ERROR: malformed vorbis comment field \"%s\",\n %s\n", vc_filename->value, line, violation);
......
......@@ -63,6 +63,7 @@ struct share__option long_options_[] = {
{ "remove-tag", 1, 0, 0 },
{ "remove-first-tag", 1, 0, 0 },
{ "set-tag", 1, 0, 0 },
{ "set-tag-from-file", 1, 0, 0 },
{ "import-tags-from", 1, 0, 0 },
{ "export-tags-to", 1, 0, 0 },
{ "show-vc-vendor", 0, 0, 0 }, /* deprecated */
......@@ -503,6 +504,18 @@ FLAC__bool parse_option(int option_index, const char *option_argument, CommandLi
fprintf(stderr, "WARNING: --%s is deprecated, the new name is --set-tag\n", opt);
op = append_shorthand_operation(options, OP__SET_VC_FIELD);
FLAC__ASSERT(0 != option_argument);
op->argument.vc_field.field_value_from_file = false;
if(!parse_vorbis_comment_field(option_argument, &(op->argument.vc_field.field), &(op->argument.vc_field.field_name), &(op->argument.vc_field.field_value), &(op->argument.vc_field.field_value_length), &violation)) {
FLAC__ASSERT(0 != violation);
fprintf(stderr, "ERROR (--%s): malformed vorbis comment field \"%s\",\n %s\n", opt, option_argument, violation);
ok = false;
}
}
else if(0 == strcmp(opt, "set-tag-from-file")) {
const char *violation;
op = append_shorthand_operation(options, OP__SET_VC_FIELD);
FLAC__ASSERT(0 != option_argument);
op->argument.vc_field.field_value_from_file = true;
if(!parse_vorbis_comment_field(option_argument, &(op->argument.vc_field.field), &(op->argument.vc_field.field_name), &(op->argument.vc_field.field_value), &(op->argument.vc_field.field_value_length), &violation)) {
FLAC__ASSERT(0 != violation);
fprintf(stderr, "ERROR (--%s): malformed vorbis comment field \"%s\",\n %s\n", opt, option_argument, violation);
......
......@@ -101,6 +101,7 @@ typedef struct {
/* according to the vorbis spec, field values can contain \0 so simple C strings are not enough here */
unsigned field_value_length;
char *field_value;
FLAC__bool field_value_from_file; /* true if field_value holds a filename for the value, false for plain value */
} Argument_VcField;
typedef struct {
......
......@@ -122,6 +122,14 @@ int long_usage(const char *message, ...)
fprintf(out, "--set-tag=FIELD Add a tag. The FIELD must comply with the Vorbis comment\n");
fprintf(out, " spec, of the form \"NAME=VALUE\". If there is currently\n");
fprintf(out, " no tag block, one will be created.\n");
fprintf(out, "--set-tag-from-file=FIELD Like --set-tag, except the VALUE is a filename\n");
fprintf(out, " whose contents will be read verbatim to set the tag value.\n");
fprintf(out, " Unless --no-utf8-convert is specified, the contents will\n");
fprintf(out, " be converted to UTF-8 from the local charset. This can\n");
fprintf(out, " be used to store a cuesheet in a tag (e.g.\n");
fprintf(out, " --set-tag-from-file=\"CUESHEET=image.cue\"). Do not try to\n");
fprintf(out, " store binary data in tag fields! Use APPLICATION blocks\n");
fprintf(out, " for that.\n");
fprintf(out, "--import-tags-from=FILE Import tags from a file. Use '-' for stdin. Each line\n");
fprintf(out, " should be of the form NAME=VALUE. Multi-line comments\n");
fprintf(out, " are currently not supported. Specify --remove-all-tags\n");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment