diff --git a/opustags.1 b/opustags.1 index 1d4f032..79d7004 100644 --- a/opustags.1 +++ b/opustags.1 @@ -106,6 +106,16 @@ Edit tags interactively by spawning the program specified by the EDITOR environment variable. The allowed format is the same as \fB--set-all\fP. If TERM and VISUAL are set, VISUAL takes precedence over EDITOR. .TP +.B \-\-output-cover \fIFILE\fP +Save the cover art of the input file at the specified location. +If the input file does not contain any cover art, this option has no effect. +To allow overwriting the target location, specify \fB--overwrite\fP. +In the case of multiple pictures embedded in the Opus tags, only the first one is saved. +Note that the since the image format is not fixed, you should consider an extension-less file name +and rely on the magic number to deduce the type. opustags does not add or check the target file’s +extension. +You can specify \fB-\fP for standard output, in which case the regular output will be suppressed. +.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 0eaa828..a5601a4 100644 --- a/src/cli.cc +++ b/src/cli.cc @@ -36,6 +36,7 @@ Options: -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 --raw disable encoding conversion See the man page for extensive documentation. @@ -52,6 +53,7 @@ static struct option getopt_options[] = { {"delete-all", no_argument, 0, 'D'}, {"set-all", no_argument, 0, 'S'}, {"edit", no_argument, 0, 'e'}, + {"output-cover", required_argument, 0, 'c'}, {"raw", no_argument, 0, 'r'}, {NULL, 0, 0, 0} }; @@ -107,6 +109,11 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) case 'e': opt.edit_interactively = true; break; + case 'c': + if (opt.cover_out) + throw status {st::bad_arguments, "Cannot specify --output-cover more than once."}; + opt.cover_out = optarg; + break; case 'r': opt.raw = true; break; @@ -160,6 +167,12 @@ ot::options ot::parse_options(int argc, char** argv, FILE* comments_input) if (opt.edit_interactively && (opt.delete_all || !opt.to_add.empty() || !opt.to_delete.empty())) throw status {st::bad_arguments, "Cannot mix --edit with -adDsS."}; + if (opt.cover_out == "-" && opt.path_out == "-") + throw status {st::bad_arguments, "Cannot specify standard output for both --output and --output-cover."}; + + if (opt.cover_out && opt.paths_in.size() > 1) + throw status {st::bad_arguments, "Cannot use --output-cover with multiple input files."}; + if (set_all) { // Read comments from stdin and prepend them to opt.to_add. std::list comments = read_comments(comments_input, opt.raw); @@ -385,6 +398,35 @@ static void edit_tags_interactively(ot::opus_tags& tags, const std::optional cover = extract_cover(tags); + if (!cover) { + fputs("warning: no cover found.\n", stderr); + return; + } + + ot::file output; + if (opt.cover_out == "-") { + output = stdout; + } else { + struct stat output_info; + if (stat(opt.cover_out->c_str(), &output_info) == 0) { + if (S_ISREG(output_info.st_mode) && !opt.overwrite) + throw ot::status {ot::st::error, "'" + opt.cover_out.value() + "' already exists. Use -y to overwrite."}; + } else if (errno != ENOENT) { + throw ot::status {ot::st::error, "Could not identify '" + opt.cover_out.value() + "': " + strerror(errno)}; + } + + output = fopen(opt.cover_out->c_str(), "w"); + if (output == nullptr) + throw ot::status {ot::st::standard_error, "Could not open '" + opt.cover_out.value() + "' for writing: " + strerror(errno)}; + } + + if (fwrite(cover->picture_data.data(), 1, cover->picture_data.size(), output.get()) < cover->picture_data.size()) + throw ot::status {ot::st::standard_error, "fwrite error: "s + strerror(errno)}; +} + /** * Main loop of opustags. Read the packets from the reader, and forwards them to the writer. * Transform the OpusTags packet on the fly. @@ -420,6 +462,8 @@ static void process(ot::ogg_reader& reader, ot::ogg_writer* writer, const ot::op } else if (reader.absolute_page_no == 1) { // Comment header ot::opus_tags tags; reader.process_header_packet([&tags](ogg_packet& p) { tags = ot::parse_tags(p); }); + if (opt.cover_out) + output_cover(tags, opt); edit_tags(tags, opt); if (writer) { if (opt.edit_interactively) { @@ -430,7 +474,8 @@ 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 { - ot::print_comments(tags.comments, stdout, opt.raw); + if (opt.cover_out != "-") + ot::print_comments(tags.comments, stdout, opt.raw); break; } } else if (writer) { diff --git a/src/opustags.h b/src/opustags.h index f18d567..d422b2d 100644 --- a/src/opustags.h +++ b/src/opustags.h @@ -504,6 +504,14 @@ struct options { * Options: --add, --set, --set-all */ std::list to_add; + /** + * If set, the input file’s cover art is exported to the specified file. - for stdout. Does + * not overwrite the file if it already exists unless -y is specified. Does nothing if the + * input file does not contain a cover art. + * + * Option: --output-cover + */ + std::optional cover_out; /** * 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 2791fe3..51e0493 100644 --- a/t/cli.cc +++ b/t/cli.cc @@ -180,6 +180,12 @@ void check_bad_arguments() error_case({"opustags", "--edit", "x", "-i", "-d", "X"}, "Cannot mix --edit with -adDsS.", "mixing -e and -d"); error_case({"opustags", "--edit", "x", "-i", "-D"}, "Cannot mix --edit with -adDsS.", "mixing -e and -D"); error_case({"opustags", "--edit", "x", "-i", "-S"}, "Cannot mix --edit with -adDsS.", "mixing -e and -S"); + error_case({"opustags", "--output-cover", "x", "--output-cover", "y"}, + "Cannot specify --output-cover more than once.", "multiple --output-cover"); + error_case({"opustags", "x", "-o", "-", "--output-cover", "-"}, + "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", "-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 8ae295a..0d60c6e 100755 --- a/t/opustags.t +++ b/t/opustags.t @@ -72,6 +72,7 @@ Options: -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 --raw disable encoding conversion See the man page for extensive documentation.