mirror of
https://github.com/fmang/opustags.git
synced 2025-07-08 02:24:30 +02:00
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
e2e7e2a5a0 | |||
70500a6aac | |||
49bb94841e | |||
dcb128f179 |
@ -1,6 +1,12 @@
|
||||
opustags changelog
|
||||
==================
|
||||
|
||||
1.9.0 - 2023-06-07
|
||||
------------------
|
||||
|
||||
- Introduce --vendor and --set-vendor.
|
||||
- Close the input file before finalizing the output, in order to fix --in-place on SMB drives.
|
||||
|
||||
1.8.0 - 2023-03-07
|
||||
------------------
|
||||
|
||||
|
@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.11)
|
||||
|
||||
project(
|
||||
opustags
|
||||
VERSION 1.8.0
|
||||
VERSION 1.9.0
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
|
@ -64,6 +64,8 @@ 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
|
||||
--set-vendor VALUE set the vendor string
|
||||
--raw disable encoding conversion
|
||||
|
||||
See the man page, `opustags.1`, for extensive documentation.
|
||||
|
@ -121,6 +121,14 @@ 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 \-\-set-vendor \fIVALUE\fP
|
||||
Replace the vendor string by the specified value. This action can be performed alongside tag
|
||||
edition.
|
||||
.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
|
||||
|
74
src/cli.cc
74
src/cli.cc
@ -38,6 +38,8 @@ 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
|
||||
--set-vendor VALUE set the vendor string
|
||||
--raw disable encoding conversion
|
||||
|
||||
See the man page for extensive documentation.
|
||||
@ -56,6 +58,8 @@ 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'},
|
||||
{"set-vendor", required_argument, 0, 'V'},
|
||||
{"raw", no_argument, 0, 'r'},
|
||||
{NULL, 0, 0, 0}
|
||||
};
|
||||
@ -69,6 +73,7 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
|
||||
std::list<std::string> local_to_delete; // opt.to_delete before UTF-8 conversion.
|
||||
bool set_all = false;
|
||||
std::optional<std::string> set_cover;
|
||||
std::optional<std::string> set_vendor;
|
||||
opt = {};
|
||||
if (argc == 1)
|
||||
throw status {st::bad_arguments, "No arguments specified. Use -h for help."};
|
||||
@ -123,6 +128,14 @@ 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 'V':
|
||||
if (set_vendor)
|
||||
throw status {st::bad_arguments, "Cannot specify --set-vendor more than once."};
|
||||
set_vendor = optarg;
|
||||
break;
|
||||
case 'r':
|
||||
opt.raw = true;
|
||||
break;
|
||||
@ -161,17 +174,23 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input)
|
||||
std::back_inserter(opt.to_add), cast_to_utf8);
|
||||
std::transform(local_to_delete.begin(), local_to_delete.end(),
|
||||
std::back_inserter(opt.to_delete), cast_to_utf8);
|
||||
if (set_vendor)
|
||||
opt.set_vendor = cast_to_utf8(*set_vendor);
|
||||
} else {
|
||||
try {
|
||||
std::transform(local_to_add.begin(), local_to_add.end(),
|
||||
std::back_inserter(opt.to_add), encode_utf8);
|
||||
std::transform(local_to_delete.begin(), local_to_delete.end(),
|
||||
std::back_inserter(opt.to_delete), encode_utf8);
|
||||
if (set_vendor)
|
||||
opt.set_vendor = encode_utf8(*set_vendor);
|
||||
} catch (const ot::status& rc) {
|
||||
throw status {st::bad_arguments, "Could not encode argument into UTF-8: " + rc.message};
|
||||
}
|
||||
}
|
||||
|
||||
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 +203,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 +215,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 +253,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 +291,8 @@ void ot::print_comments(const std::list<std::u8string>& 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);
|
||||
@ -348,6 +377,9 @@ void ot::delete_comments(std::list<std::u8string>& comments, const std::u8string
|
||||
/** Apply the modifications requested by the user to the opustags packet. */
|
||||
static void edit_tags(ot::opus_tags& tags, const ot::options& opt)
|
||||
{
|
||||
if (opt.set_vendor)
|
||||
tags.vendor = *opt.set_vendor;
|
||||
|
||||
if (opt.delete_all) {
|
||||
tags.comments.clear();
|
||||
} else for (const std::u8string& name : opt.to_delete) {
|
||||
@ -498,8 +530,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) {
|
||||
@ -577,6 +613,10 @@ static void run_single(const ot::options& opt, const std::string& path_in, const
|
||||
ot::ogg_writer writer(output);
|
||||
writer.path = path_out;
|
||||
process(reader, &writer, opt);
|
||||
|
||||
// Close the input file and finalize the output. When --in-place is specified, some file
|
||||
// systems like SMB require that the input is closed first.
|
||||
input.reset();
|
||||
temporary_output.commit();
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
*
|
||||
|
@ -515,6 +515,19 @@ struct options {
|
||||
* Option: --output-cover
|
||||
*/
|
||||
std::optional<std::string> 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;
|
||||
/**
|
||||
* Replace the vendor string by the one specified by the user.
|
||||
*
|
||||
* Option: --set-vendor
|
||||
*/
|
||||
std::optional<std::u8string> set_vendor;
|
||||
/**
|
||||
* 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
|
||||
|
2
t/cli.cc
2
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");
|
||||
|
43
t/opustags.t
43
t/opustags.t
@ -4,7 +4,8 @@ use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Test::More tests => 59;
|
||||
use Test::More tests => 62;
|
||||
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,12 @@ 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');
|
||||
|
||||
is_deeply(opustags(qw(--set-vendor opustags gobble.opus -o out.opus)), ['', '', 0], 'set the vendor string');
|
||||
is_deeply(opustags(qw(--vendor out.opus)), ["opustags\n", '', 0], 'the vendor string was updated');
|
||||
unlink('out.opus');
|
||||
|
Reference in New Issue
Block a user