diff --git a/README.md b/README.md index 9a098ce..972330e 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Documentation -e, --edit edit tags interactively in VISUAL/EDITOR --output-cover FILE extract and save the cover art, if any --set-cover FILE sets the cover art + --vendor print the vendor string --raw disable encoding conversion See the man page, `opustags.1`, for extensive documentation. diff --git a/opustags.1 b/opustags.1 index 624aca6..77a9af5 100644 --- a/opustags.1 +++ b/opustags.1 @@ -121,6 +121,10 @@ Specify \fB-\fP to read the picture from standard input. In theory, an Opus file may contain multiple pictures with different roles, though in practice only the front cover really matters. opustags can currently only handle one front cover and nothing else. .TP +.B \-\-vendor +Print the vendor string from the OpusTags packet and do nothing else. Standard tags operations are +not supported when specifying this flag. +.TP .B \-\-raw OpusTags metadata should always be encoded in UTF-8, as per RFC 7845. However, some files may be corrupted or possibly even contain intentional binary data. In that case, --raw lets you edit that diff --git a/src/cli.cc b/src/cli.cc index ae890c4..bd5935f 100644 --- a/src/cli.cc +++ b/src/cli.cc @@ -38,6 +38,7 @@ Options: -e, --edit edit tags interactively in VISUAL/EDITOR --output-cover FILE extract and save the cover art, if any --set-cover FILE sets the cover art + --vendor print the vendor string --raw disable encoding conversion See the man page for extensive documentation. @@ -56,6 +57,7 @@ static struct option getopt_options[] = { {"edit", no_argument, 0, 'e'}, {"output-cover", required_argument, 0, 'c'}, {"set-cover", required_argument, 0, 'C'}, + {"vendor", no_argument, 0, 'v'}, {"raw", no_argument, 0, 'r'}, {NULL, 0, 0, 0} }; @@ -123,6 +125,9 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) throw status {st::bad_arguments, "Cannot specify --set-cover more than once."}; set_cover = optarg; break; + case 'v': + opt.print_vendor = true; + break; case 'r': opt.raw = true; break; @@ -172,6 +177,8 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) } } + bool read_only = !opt.in_place && !opt.path_out.has_value(); + if (opt.in_place && opt.path_out) throw status {st::bad_arguments, "Cannot combine --in-place and --output."}; @@ -184,7 +191,7 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) if (opt.edit_interactively && (stdin_as_input || opt.path_out == "-" || opt.cover_out == "-")) throw status {st::bad_arguments, "Cannot edit interactively when standard input or standard output are already used."}; - if (opt.edit_interactively && !opt.path_out.has_value() && !opt.in_place) + if (opt.edit_interactively && read_only) throw status {st::bad_arguments, "Cannot edit interactively when no output is specified."}; if (opt.edit_interactively && (opt.delete_all || !opt.to_add.empty() || !opt.to_delete.empty())) @@ -196,6 +203,9 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) if (opt.cover_out && opt.paths_in.size() > 1) throw status {st::bad_arguments, "Cannot use --output-cover with multiple input files."}; + if (opt.print_vendor && !read_only) + throw status {st::bad_arguments, "--vendor is only supported in read-only mode."}; + if (set_cover) { byte_string picture_data = ot::slurp_binary_file(set_cover->c_str()); opt.to_delete.push_back(u8"METADATA_BLOCK_PICTURE"s); @@ -231,6 +241,26 @@ static std::u8string format_value(const std::u8string& source) return formatted; } +/** + * Convert the comment from UTF-8 to the system encoding if relevant, and print it with a trailing + * line feed. + */ +static void puts_utf8(std::u8string_view str, FILE* output, bool raw) +{ + if (raw) { + fwrite(str.data(), 1, str.size(), output); + } else { + try { + std::string local = ot::decode_utf8(str); + fwrite(local.data(), 1, local.size(), output); + } catch (ot::status& rc) { + rc.message += " See --raw."; + throw; + } + } + putc('\n', output); +} + /** * Print comments in a human readable format that can also be read back in by #read_comment. * @@ -249,21 +279,8 @@ void ot::print_comments(const std::list& comments, FILE* output, } } } - std::u8string utf8_comment = format_value(source_comment); - // Convert the comment from UTF-8 to the system encoding if relevant. - if (raw) { - fwrite(utf8_comment.data(), 1, utf8_comment.size(), output); - } else { - try { - std::string local = decode_utf8(utf8_comment); - fwrite(local.data(), 1, local.size(), output); - } catch (ot::status& rc) { - rc.message += " See --raw."; - throw; - } - } - putc('\n', output); + puts_utf8(utf8_comment, output, raw); } if (has_control) fputs("warning: Some tags contain control characters.\n", stderr); @@ -498,8 +515,12 @@ static void process(ot::ogg_reader& reader, ot::ogg_writer* writer, const ot::op writer->write_header_packet(serialno, pageno, packet); pageno_offset = writer->next_page_no - 1 - reader.absolute_page_no; } else { - if (opt.cover_out != "-") - ot::print_comments(tags.comments, stdout, opt.raw); + if (opt.cover_out != "-") { + if (opt.print_vendor) + puts_utf8(tags.vendor, stdout, opt.raw); + else + ot::print_comments(tags.comments, stdout, opt.raw); + } break; } } else if (writer) { diff --git a/src/opus.cc b/src/opus.cc index 6a6ce68..85dbe53 100644 --- a/src/opus.cc +++ b/src/opus.cc @@ -3,7 +3,7 @@ * \ingroup opus * * The way Opus is encapsulated into an Ogg stream, and the content of the packets we're dealing - * with here is defined by [RFC 7584](https://tools.ietf.org/html/rfc7845.html). + * with here is defined by [RFC 7845](https://tools.ietf.org/html/rfc7845.html). * * Section 3 "Packet Organization" is critical for us: * diff --git a/src/opustags.h b/src/opustags.h index 47775cf..7d6ab04 100644 --- a/src/opustags.h +++ b/src/opustags.h @@ -515,6 +515,13 @@ struct options { * Option: --output-cover */ std::optional cover_out; + /** + * Print the vendor string at the beginning of the OpusTags packet instead of printing the + * tags. Only applicable in read-only mode. + * + * Option: --vendor + */ + bool print_vendor = false; /** * Disable encoding conversions. OpusTags are specified to always be encoded as UTF-8, but * if for some reason a specific file contains binary tags that someone would like to diff --git a/t/cli.cc b/t/cli.cc index d8e1577..0f556e7 100644 --- a/t/cli.cc +++ b/t/cli.cc @@ -185,6 +185,8 @@ void check_bad_arguments() "Cannot specify standard output for both --output and --output-cover.", "-o and --output-cover conflict"); error_case({"opustags", "-i", "x", "y", "--output-cover", "z"}, "Cannot use --output-cover with multiple input files.", "--output-cover with multiple input"); + error_case({"opustags", "-i", "--vendor", "x"}, + "--vendor is only supported in read-only mode.", "--vendor when editing"); error_case({"opustags", "-d", "\xFF", "x"}, "Could not encode argument into UTF-8:", "-d with binary data"); diff --git a/t/opustags.t b/t/opustags.t index 47957ab..ddd6e0d 100755 --- a/t/opustags.t +++ b/t/opustags.t @@ -4,7 +4,8 @@ use strict; use warnings; use utf8; -use Test::More tests => 59; +use Test::More tests => 60; +use Test::Deep qw(cmp_deeply re); use Digest::MD5; use File::Basename; @@ -53,34 +54,9 @@ $help->[0] =~ /^([^\n]*+)/; my $version = $1; like($version, qr/^opustags version (\d+\.\d+\.\d+)/, 'get the version string'); -my $expected_help = <<"EOF"; -$version - -Usage: opustags --help - opustags [OPTIONS] FILE - opustags OPTIONS -i FILE... - opustags OPTIONS FILE -o FILE - -Options: - -h, --help print this help - -o, --output FILE specify the output file - -i, --in-place overwrite the input files - -y, --overwrite overwrite the output file if it already exists - -a, --add FIELD=VALUE add a comment - -d, --delete FIELD[=VALUE] delete previously existing comments - -D, --delete-all delete all the previously existing comments - -s, --set FIELD=VALUE replace a comment - -S, --set-all import comments from standard input - -e, --edit edit tags interactively in VISUAL/EDITOR - --output-cover FILE extract and save the cover art, if any - --set-cover FILE sets the cover art - --raw disable encoding conversion - -See the man page for extensive documentation. -EOF - -is_deeply(opustags('--help'), [$expected_help, '', 0], '--help displays the help message'); -is_deeply(opustags('-h'), [$expected_help, '', 0], '-h displays the help message too'); +my $expected_help = qr{opustags version .*\n\nUsage: opustags --help\n}; +cmp_deeply(opustags('--help'), [re($expected_help), '', 0], '--help displays the help message'); +cmp_deeply(opustags('-h'), [re($expected_help), '', 0], '-h displays the help message too'); is_deeply(opustags('--derp'), ['', <<"EOF", 512], 'unrecognized option shows an error'); error: Unrecognized option '--derp'. @@ -342,3 +318,8 @@ unlink('out.png'); is_deeply(opustags(qw(-D --set-cover - gobble.opus), { in => "GIF8 x" }), [<<'END_OUT', '', 0], 'read the cover from stdin'); METADATA_BLOCK_PICTURE=AAAAAwAAAAlpbWFnZS9naWYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZHSUY4IHg= END_OUT + +#################################################################################################### +# Vendor string + +is_deeply(opustags(qw(--vendor gobble.opus)), ["Lavf58.12.100\n", '', 0], 'print the vendor string');