mirror of
				https://github.com/fmang/opustags.git
				synced 2025-10-31 00:48:10 +01:00 
			
		
		
		
	Introduce the --edit option
This commit is contained in:
		| @@ -98,6 +98,10 @@ Sets the tags from scratch. | ||||
| All the original tags are deleted and new ones are read from standard input. | ||||
| Each line must specify a \fIFIELD=VALUE\fP pair and be separated with line feeds. | ||||
| Blank lines and lines starting with \fI#\fP are ignored. | ||||
| .TP | ||||
| .B \-e, \-\-edit | ||||
| Edit tags interactively by spawning the program specified by the EDITOR | ||||
| environment variable. The allowed format is the same as \fB--set-all\fP. | ||||
| .SH EXAMPLES | ||||
| .PP | ||||
| List all the tags in file foo.opus: | ||||
| @@ -116,6 +120,10 @@ Remove the previously existing ARTIST tags and add the two X and Y ARTIST tags, | ||||
| tags without writing them to the Opus file: | ||||
| .PP | ||||
| 	opustags in.opus --add ARTIST=X --add ARTIST=Y --delete ARTIST | ||||
| .PP | ||||
| Edit tags interactively in Vim: | ||||
| .PP | ||||
| 	EDITOR=vim opustags --in-place --edit file.opus | ||||
| .SH CAVEATS | ||||
| .PP | ||||
| \fBopustags\fP currently has the following limitations: | ||||
|   | ||||
							
								
								
									
										68
									
								
								src/cli.cc
									
									
									
									
									
								
							
							
						
						
									
										68
									
								
								src/cli.cc
									
									
									
									
									
								
							| @@ -36,6 +36,7 @@ Options: | ||||
|   -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 EDITOR | ||||
|  | ||||
| See the man page for extensive documentation. | ||||
| )raw"; | ||||
| @@ -50,6 +51,7 @@ static struct option getopt_options[] = { | ||||
| 	{"set", required_argument, 0, 's'}, | ||||
| 	{"delete-all", no_argument, 0, 'D'}, | ||||
| 	{"set-all", no_argument, 0, 'S'}, | ||||
| 	{"edit", no_argument, 0, 'e'}, | ||||
| 	{NULL, 0, 0, 0} | ||||
| }; | ||||
|  | ||||
| @@ -65,7 +67,7 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm | ||||
| 		return {st::bad_arguments, "No arguments specified. Use -h for help."}; | ||||
| 	int c; | ||||
| 	optind = 0; | ||||
| 	while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DS", getopt_options, NULL)) != -1) { | ||||
| 	while ((c = getopt_long(argc, argv, ":ho:iyd:a:s:DSe", getopt_options, NULL)) != -1) { | ||||
| 		switch (c) { | ||||
| 		case 'h': | ||||
| 			opt.print_help = true; | ||||
| @@ -106,6 +108,9 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm | ||||
| 		case 'D': | ||||
| 			opt.delete_all = true; | ||||
| 			break; | ||||
| 		case 'e': | ||||
| 			opt.edit_interactively = true; | ||||
| 			break; | ||||
| 		case ':': | ||||
| 			return {st::bad_arguments, | ||||
| 			        "Missing value for option '"s + argv[optind - 1] + "'."}; | ||||
| @@ -130,12 +135,18 @@ ot::status ot::parse_options(int argc, char** argv, ot::options& opt, FILE* comm | ||||
| 	if (opt.in_place && stdin_as_input) | ||||
| 		return {st::bad_arguments, "Cannot modify standard input in place."}; | ||||
|  | ||||
| 	if (!opt.in_place && opt.paths_in.size() != 1) | ||||
| 	if ((!opt.in_place || opt.edit_interactively) && opt.paths_in.size() != 1) | ||||
| 		return {st::bad_arguments, "Exactly one input file must be specified."}; | ||||
|  | ||||
| 	if (set_all && stdin_as_input) | ||||
| 		return {st::bad_arguments, "Cannot use standard input as input file when --set-all is specified."}; | ||||
|  | ||||
| 	if (opt.edit_interactively && (set_all || stdin_as_input || opt.path_out == "-")) | ||||
| 		return {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) | ||||
| 		return {st::bad_arguments, "Cannot edit interactively when no output is specified."}; | ||||
|  | ||||
| 	if (set_all) { | ||||
| 		// Read comments from stdin and prepend them to opt.to_add. | ||||
| 		std::list<std::string> comments; | ||||
| @@ -260,6 +271,55 @@ static ot::status edit_tags(ot::opus_tags& tags, const ot::options& opt) | ||||
| 	return ot::st::ok; | ||||
| } | ||||
|  | ||||
| /** Spawn EDITOR to edit the given tags. */ | ||||
| static ot::status edit_tags_interactively(ot::opus_tags& tags, const std::optional<std::string>& base_path) | ||||
| { | ||||
| 	const char* editor = getenv("EDITOR"); | ||||
| 	if (editor == nullptr || *editor == '\0') | ||||
| 		return {ot::st::error, | ||||
| 		        "No editor specified in environment variable EDITOR."}; | ||||
|  | ||||
| 	std::string tags_path = base_path.value_or("tags") + ".XXXXXX.opustags"; | ||||
| 	int fd = mkstemps(const_cast<char*>(tags_path.data()), 9); | ||||
| 	FILE* tags_file; | ||||
| 	if (fd == -1 || (tags_file = fdopen(fd, "w")) == nullptr) | ||||
| 		return {ot::st::standard_error, | ||||
| 		        "Could not open '" + tags_path + "': " + strerror(errno)}; | ||||
|  | ||||
| 	ot::print_comments(tags.comments, tags_file); | ||||
| 	fputs("\n" | ||||
| 	      "# Edit these tags to your liking and close your editor to apply them.\n" | ||||
| 	      "# If you delete all the tags however, tag edition will be cancelled.\n", | ||||
| 	      tags_file); | ||||
| 	if (fclose(tags_file) != 0) | ||||
| 		return {ot::st::standard_error, "fclose error: "s + strerror(errno)}; | ||||
|  | ||||
| 	ot::status rc = ot::execute_process(editor, tags_path); | ||||
| 	if (rc != ot::st::ok) | ||||
| 		return rc; | ||||
|  | ||||
| 	tags_file = fopen(tags_path.c_str(), "r"); | ||||
| 	if (tags_file == nullptr) | ||||
| 		return {ot::st::standard_error, "Error opening " + tags_path + ": " + strerror(errno)}; | ||||
| 	if ((rc = ot::read_comments(tags_file, tags.comments)) != ot::st::ok) { | ||||
| 		fprintf(stderr, "warning: Leaving %s on the disk.\n", tags_path.c_str()); | ||||
| 		return rc; | ||||
| 	} | ||||
| 	fclose(tags_file); | ||||
|  | ||||
| 	if (tags.comments.size() == 0) { | ||||
| 		remove(tags_path.c_str()); // it’s empty anyway | ||||
| 		return {ot::st::error, "Tag edition was cancelled because all the tags were deleted."}; | ||||
| 	} | ||||
|  | ||||
| 	// Remove the temporary tags file only on success, because unlike the | ||||
| 	// partial Ogg file that is irrecoverable, the edited tags file | ||||
| 	// contains user data, so let’s leave users a chance to recover it. | ||||
| 	remove(tags_path.c_str()); | ||||
|  | ||||
| 	return ot::st::ok; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Main loop of opustags. Read the packets from the reader, and forwards them to the writer. | ||||
|  * Transform the OpusTags packet on the fly. | ||||
| @@ -306,6 +366,9 @@ static ot::status process(ot::ogg_reader& reader, ot::ogg_writer* writer, const | ||||
| 			if ((rc = edit_tags(tags, opt)) != ot::st::ok) | ||||
| 				return rc; | ||||
| 			if (writer) { | ||||
| 				if (opt.edit_interactively && | ||||
| 				    (rc = edit_tags_interactively(tags, writer->path)) != ot::st::ok) | ||||
| 					return rc; | ||||
| 				auto packet = ot::render_tags(tags); | ||||
| 				rc = writer->write_header_packet(serialno, pageno, packet); | ||||
| 				if (rc != ot::st::ok) | ||||
| @@ -392,6 +455,7 @@ static ot::status run_single(const ot::options& opt, const std::string& path_in, | ||||
| 		return rc; | ||||
|  | ||||
| 	ot::ogg_writer writer(output); | ||||
| 	writer.path = path_out; | ||||
| 	rc = process(reader, &writer, opt); | ||||
| 	if (rc == ot::st::ok) | ||||
| 		rc = temporary_output.commit(); | ||||
|   | ||||
| @@ -289,6 +289,10 @@ struct ogg_writer { | ||||
| 	 * represented as a block of data and a length. | ||||
| 	 */ | ||||
| 	FILE* file; | ||||
| 	/** | ||||
| 	 * Path to the output file. | ||||
| 	 */ | ||||
| 	std::optional<std::string> path; | ||||
| }; | ||||
|  | ||||
| /** | ||||
| @@ -408,6 +412,15 @@ struct options { | ||||
| 	 * Options: --in-place | ||||
| 	 */ | ||||
| 	bool in_place = false; | ||||
| 	/** | ||||
| 	 * Spawn EDITOR to edit tags interactively. | ||||
| 	 * | ||||
| 	 * stdin and stdout must be left free for the editor, so paths_in and | ||||
| 	 * path_out can’t take `-`, and --set-all is not supported. | ||||
| 	 * | ||||
| 	 * Option: --edit | ||||
| 	 */ | ||||
| 	bool edit_interactively = false; | ||||
| 	/** | ||||
| 	 * List of comments to delete. Each string is a selector according to the definition of | ||||
| 	 * #delete_comments. | ||||
|   | ||||
| @@ -14,6 +14,8 @@ add_executable(oggdump EXCLUDE_FROM_ALL oggdump.cc) | ||||
| target_link_libraries(oggdump ot) | ||||
|  | ||||
| configure_file(gobble.opus . COPYONLY) | ||||
| configure_file(screamer . COPYONLY) | ||||
| configure_file(emptier . COPYONLY) | ||||
|  | ||||
| add_custom_target( | ||||
| 	check | ||||
|   | ||||
							
								
								
									
										17
									
								
								t/cli.cc
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								t/cli.cc
									
									
									
									
									
								
							| @@ -85,6 +85,11 @@ void check_good_arguments() | ||||
| 	if (opt.paths_in.size() != 3 || opt.paths_in[0] != "x" || opt.paths_in[1] != "y" || | ||||
| 	    opt.paths_in[2] != "z" || !opt.overwrite || !opt.in_place) | ||||
| 		throw failure("unexpected option parsing result for case #3"); | ||||
|  | ||||
| 	opt = parse({"opustags", "-ie", "x"}); | ||||
| 	if (opt.paths_in.size() != 1 || opt.paths_in[0] != "x" || | ||||
| 	    !opt.edit_interactively || !opt.overwrite || !opt.in_place) | ||||
| 		throw failure("unexpected option parsing result for case #4"); | ||||
| } | ||||
|  | ||||
| void check_bad_arguments() | ||||
| @@ -121,6 +126,18 @@ void check_bad_arguments() | ||||
| 	error_code_case({"opustags", "-S", "x"}, "Malformed tag: INVALID", ot::st::error, "attempt to read invalid argument with -S"); | ||||
| 	error_case({"opustags", "-o", "", "--output", "y", "z"}, | ||||
| 	           "Cannot specify --output more than once.", "double output with first filename empty"); | ||||
| 	error_case({"opustags", "-e", "-i", "x", "y"}, | ||||
| 	           "Exactly one input file must be specified.", "editing interactively two files at once"); | ||||
| 	error_case({"opustags", "--edit", "-S", "x"}, | ||||
| 	           "Cannot edit interactively when standard input or standard output are already used.", | ||||
| 	           "editing interactively with --set-all"); | ||||
| 	error_case({"opustags", "--edit", "-", "-o", "x"}, | ||||
| 	           "Cannot edit interactively when standard input or standard output are already used.", | ||||
| 	           "editing interactively from stdandard intput"); | ||||
| 	error_case({"opustags", "--edit", "x", "-o", "-"}, | ||||
| 	           "Cannot edit interactively when standard input or standard output are already used.", | ||||
| 	           "editing interactively to stdandard output"); | ||||
| 	error_case({"opustags", "--edit", "x"}, "Cannot edit interactively when no output is specified.", "editing without output"); | ||||
| } | ||||
|  | ||||
| static void check_delete_comments() | ||||
|   | ||||
							
								
								
									
										6
									
								
								t/emptier
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										6
									
								
								t/emptier
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| #!/bin/sh | ||||
| cat > "$1" << EOF | ||||
|  | ||||
| # Here’s a file with nothing but comments and newlines. | ||||
|  | ||||
| EOF | ||||
							
								
								
									
										18
									
								
								t/opustags.t
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								t/opustags.t
									
									
									
									
									
								
							| @@ -4,7 +4,7 @@ use strict; | ||||
| use warnings; | ||||
| use utf8; | ||||
|  | ||||
| use Test::More tests => 41; | ||||
| use Test::More tests => 45; | ||||
|  | ||||
| use Digest::MD5; | ||||
| use File::Basename; | ||||
| @@ -71,6 +71,7 @@ Options: | ||||
|   -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 EDITOR | ||||
|  | ||||
| See the man page for extensive documentation. | ||||
| EOF | ||||
| @@ -222,6 +223,21 @@ is(md5('out2.opus'), '0a4d20c287b2e46b26cb0eee353c2069', 'the tags were added co | ||||
| unlink('out.opus'); | ||||
| unlink('out2.opus'); | ||||
|  | ||||
| #################################################################################################### | ||||
| # Interactive edition | ||||
|  | ||||
| $ENV{EDITOR} = './screamer'; | ||||
| is_deeply(opustags(qw(gobble.opus --add artist=aaah -o screaming.opus -e)), ['', '', 0], 'edit a file with EDITOR'); | ||||
| is(md5('screaming.opus'), '682229df1df6b0ca147e2778737d449e', 'the tags were modified'); | ||||
|  | ||||
| $ENV{EDITOR} = './emptier'; | ||||
| is_deeply(opustags(qw(--add mystery=1 -i screaming.opus -e)), ['', "screaming.opus: error: Tag edition was cancelled because all the tags were deleted.\n", 256], 'edit a file with EDITOR'); | ||||
| is(md5('screaming.opus'), '682229df1df6b0ca147e2778737d449e', 'the tags were not modified'); | ||||
|  | ||||
| $ENV{EDITOR} = ''; | ||||
| unlink('screaming.opus'); | ||||
|  | ||||
|  | ||||
| #################################################################################################### | ||||
| # Test muxed streams | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								t/screamer
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										2
									
								
								t/screamer
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| #!/bin/sh | ||||
| exec sed -i -e 'y/a/A/' "$1" | ||||
		Reference in New Issue
	
	Block a user