4 Commits
next ... 1.1.1

Author SHA1 Message Date
7174a1f2f2 bump to 1.1.1 2018-10-24 18:27:57 -04:00
1a8aaff933 Don't croak on overlong opustags 2018-10-02 18:48:09 -04:00
4973a4deab Become macOS compatible 2018-10-02 18:48:09 -04:00
8e9d98ac62 README: show alternatives 2017-10-01 12:26:56 +02:00
56 changed files with 670 additions and 3108 deletions

3
.gitignore vendored
View File

@ -1,3 +0,0 @@
/build
tests/catch.h
src/version.h

View File

@ -1,78 +0,0 @@
cmake_minimum_required (VERSION 2.8.8)
project (opustags)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED on)
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/modules/")
set(CMAKE_SOURCE_DIR "${CMAKE_BINARY_DIR}/../")
# ------------
# Dependencies
# ------------
find_package(Ogg REQUIRED)
include_directories(${Ogg_INCLUDE_DIR})
link_directories(${Ogg_LIBRARY_DIRS})
# --------------------
# Global build options
# --------------------
if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wextra")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wold-style-cast")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") # for MinGW-w64
endif()
# -------
# Version
# -------
execute_process(COMMAND git describe --tags --abbrev=0 OUTPUT_VARIABLE VERSION_SHORT OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(COMMAND git describe --always --dirty --long --tags OUTPUT_VARIABLE VERSION_LONG OUTPUT_STRIP_TRAILING_WHITESPACE)
if("${VERSION_SHORT}" STREQUAL "")
set(VERSION_SHORT "0.0")
set(VERSION_LONG "?")
endif()
configure_file("${CMAKE_SOURCE_DIR}/src/version.h.in" "${CMAKE_SOURCE_DIR}/src/version.h" @ONLY)
# ------------
# Source files
# ------------
file(GLOB_RECURSE common_sources "${CMAKE_SOURCE_DIR}/src/*.cc")
file(GLOB_RECURSE common_headers "${CMAKE_SOURCE_DIR}/src/*.h")
file(GLOB_RECURSE test_sources "${CMAKE_SOURCE_DIR}/tests/*.cc")
file(GLOB_RECURSE test_headers "${CMAKE_SOURCE_DIR}/tests/*.h")
list(REMOVE_ITEM common_sources "${CMAKE_SOURCE_DIR}/src/main.cc")
list(REMOVE_ITEM test_sources "${CMAKE_SOURCE_DIR}/tests/main.cc")
# -------------------
# 3rd party libraries
# -------------------
# Catch
set(CATCH_PATH "${CMAKE_SOURCE_DIR}/tests/catch.h")
if (NOT EXISTS "${CATCH_PATH}")
message("Downloading Catch...")
file(DOWNLOAD "http://raw.githubusercontent.com/philsquared/Catch/master/single_include/catch.hpp" "${CATCH_PATH}")
endif()
# ------------
# Installation
# ------------
install(FILES ${CMAKE_SOURCE_DIR}/doc/opustags.1 DESTINATION ${CMAKE_INSTALL_PREFIX}/man/man1)
install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/opustags DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
add_custom_target(uninstall COMMAND
rm -f "${CMAKE_INSTALL_PREFIX}/man/man1/opustags.1" &&
rm -f "${CMAKE_INSTALL_PREFIX}/bin/opustags" )
# -------------------
# Linking definitions
# -------------------
add_library(common OBJECT ${common_sources} ${common_headers})
add_executable(opustags "${CMAKE_SOURCE_DIR}/src/main.cc" $<TARGET_OBJECTS:common>)
add_executable(run_tests "${CMAKE_SOURCE_DIR}/tests/main.cc" $<TARGET_OBJECTS:common> ${test_sources} ${test_headers})
target_link_libraries(opustags ${OGG_LIBRARY})
target_link_libraries(run_tests ${OGG_LIBRARY})
target_include_directories(common BEFORE PUBLIC "${CMAKE_SOURCE_DIR}/src")
target_include_directories(opustags BEFORE PUBLIC "${CMAKE_SOURCE_DIR}/src")
target_include_directories(run_tests BEFORE PUBLIC "${CMAKE_SOURCE_DIR}/src")
target_include_directories(run_tests BEFORE PUBLIC "${CMAKE_SOURCE_DIR}/tests")

23
Makefile Normal file
View File

@ -0,0 +1,23 @@
DESTDIR=/usr/local
MANDEST=share/man
CFLAGS=-Wall
LDFLAGS=-logg
all: opustags
opustags: opustags.c
man: opustags.1
gzip <opustags.1 >opustags.1.gz
install: opustags man
mkdir -p $(DESTDIR)/bin $(DESTDIR)/$(MANDEST)/man1
install -m 755 opustags $(DESTDIR)/bin/
install -m 644 opustags.1.gz $(DESTDIR)/$(MANDEST)/man1/
uninstall:
rm -f $(DESTDIR)/bin/opustags
rm -f $(DESTDIR)/$(MANDEST)/man1/opustags.1.gz
clean:
rm -f opustags opustags.1.gz

View File

@ -3,43 +3,53 @@ opustags
View and edit Opus comments.
**Please note this project is old and not actively maintained.**
Maybe you should use something else.
It was built because at the time nothing supported Opus, but now things have
changed for the better.
For alternative, check out these projects:
- [EasyTAG](https://wiki.gnome.org/Apps/EasyTAG)
- [Beets](http://beets.io/)
- [Picard](https://picard.musicbrainz.org/)
- [puddletag](http://docs.puddletag.net/)
- [Quod Libet](https://quodlibet.readthedocs.io/en/latest/)
- [Goggles Music Manager](https://gogglesmm.github.io/)
See also these libraries if you need a lower-level access:
- [TagLib](http://taglib.org/)
- [mutagen](https://mutagen.readthedocs.io/en/latest/)
Requirements
------------
* A POSIX-compliant system,
* `libogg`.
* libogg.
Installing
----------
mkdir build && cd build
cmake ..
make
make install
make DESTDIR=/usr/local install
Documentation
-------------
Usage: opustags --help
opustags [OPTIONS] INPUT
opustags [OPTIONS] -o OUTPUT INPUT
opustags [OPTIONS] FILE
opustags OPTIONS FILE -o FILE
Options:
-h, --help print this help
-V, --version print version
-o, --output FILE write the modified tags to this file
-i, --in-place [SUFFIX] use a temporary file then replace the original file
-o, --output write the modified tags to a file
-y, --overwrite overwrite the output file if it already exists
--stream ID select stream for the next operations
-l, --list display a pretty listing of all tags
--no-color disable colors in --list output
-d, --delete FIELD delete all the fields of a specified type
-a, --add FIELD=VALUE add a field
-s, --set FIELD=VALUE delete then add a field
-D, --delete-all delete all the fields!
--full enable full file scan
--export dump the tags to standard output for --import
--import set the tags from scratch basing on stanard input
-e, --edit spawn the $EDITOR and apply --import on the result
-S, --set-all read the fields from stdin
See the man page, `opustags.1`, for extensive documentation.

View File

@ -1,209 +0,0 @@
.TH opustags 1 "2016"
.SH NAME
opustags \- Opus comment editor
.SH SYNOPSIS
.B opustags \-\-help
.br
.B opustags
.RI [ OPTIONS ]
.I INPUT
.br
.B opustags
.RI [ OPTIONS ]
.B \-o
.I OUTPUT INPUT
.SH DESCRIPTION
.PP
\fBopustags\fP can read and edit the comment header of an Opus file.
It has two modes of operation: read\-only for tag listing, and read\-write for
tag edition.
.PP
Edition mode is triggered by the \fB\-\-ouput\fP and \fB\-\-in\-place\fP
options. Otherwise, the default mode is listing.
.PP
Edition options are incompatible in listing mode, the same way as listing
options are incompatible in edition mode.
.PP
\fIINPUT\fP can either be the name of a file or \fB\-\fP to read from standard input.
.SS Listing
.PP
In listing mode, the tags are printed on standard output. Two listing formats
are available: pretty and raw. By default, pretty listing is implied if the
output is a terminal, and raw listing if it isn't.
.PP
Pretty listing is enabled with the option \fB\-\-list\fP. Colors are enabled
unless you use the \fB\-\-no\-color\fP option.
.PP
Dumping with \fB\-\-export\fP outputs the raw tags in UTF-8, for automatic
processing. You can restore exported tags with the \fB\-\-import\fP option.
.SS Editing
.PP
As for the edition mode, you need to specify an output file (or \fB\-\fP for
standard output). It must be different from the input file. You may want to
use \fB\-\-overwrite\fP if you know what you're doing. To overwrite the input
file, use \fB\-\-in\-place\fP.
.PP
Tag edition is done with the \fB\-\-add\fP, \fB\-\-delete\fP and \fB\-\-set\fP
options. You can use these options as many times as you wish.
.PP
You can delete all the tags with \fB\-\-delete\-all\fP. This operation can be
combined with \fB\-\-add\fP to set new tags without being bothered by the old
ones.
.PP
If you want to process tags yourself, you can use the \fB\-\-import\fP option
which will cause \fBopustags\fP to read tags from standard input. The format
is the same as the one used for output with \fB\-\-export\fP.
Note that this option implies \fB\-\-delete\-all\fP.
Also, input is read as UTF-8.
.PP
You can use \fB\-\-edit\fP to spawn your \fBEDITOR\fP and edit the tags
interactively. In read\-write mode, if no other option is specified, this one
is implied.
.SS Stream Selection
.PP
In case an Ogg file contains multiple streams, the \fB\-\-stream\fP option lets
you specify the particular streams to modify. You can modify several streams in
different ways by using this option more than once. Only operations specified
after this option and relevant, and until another \fB\-\-stream\fP option is
found.
.PP
opustags \-\-stream 1 \-\-stream 2 \-\-list # BAD
.PP
The above command would only show the second stream. To display both streams,
use instead:
.PP
opustags \-\-stream 1,2 \-\-list
.PP
See also the examples at the end of this page, and the documentation of the
\fB\-\-stream\fP option.
.PP
You can easily find the id's of the available streams by calling opustags with
no options to list the tags. Usually, you'll only have one stream, id 1.
.SH OPTIONS
.SS General
.TP
.B \-h, \-\-help
Display a brief description of the options.
.TP
.B \-V, \-\-version
Display the version of opustags.
.TP
.B \-\-stream \fIID\fP
Specify the stream that will be affected by the options following it. The
special id \fBall\fP selects all the Opus streams found, and is the default
value.
.sp
The identifer of a stream is determined from its position in the file. The
first stream found will have id 1, the second 2, and so on.
.sp
You can select more than one stream with this option by separated id's with commas. For example: \-\-stream 1,3. Ranges are not supported.
.sp
In edition mode, this option should be set if the input file contains more than
one Opus stream, otherwise a warning is generated. If in\-place modification is
selected, this warning becomes a fatal error.
.SS Listing
.TP
.B \-l, \-\-list
Display a pretty listing of all the tags found. This is the default option in
read\-only mode if the standard output is a terminal.
.TP
.B \-\-no\-color
By default, when listing tags with \fB\-\-list\fP, colors are used. Use this
option to disable it.
.TP
.B \-\-export
Dump the tags on standard output in a format compatible with \fB\-\-import\fP.
If only one stream is specified, the output format is compatible with other
tools such as vorbiscomment.
.sp
When exporting, output is encoded in UTF\-8 in
order not to lose information.
.TP
.B \-\-full
For performance reasons, only the beginning of the input is read, because
that's where tags are expected. If, however, you think you have an
unconventional file and you suspect that opustags is missing some streams, you
can use this option to force it to read the whole file.
.sp
This option shouldn't be needed, but if you do find files that require this
option, please submit a bug report (see the bottom of the page).
.SS Edition
.TP
.B \-o, \-\-output \fIFILE\fI
Edition mode. The input file will be read, its tags edited, then written to the
specified output file. If \fIFILE\fP is \fB\-\fP then the resulting Opus file
will be written to standard output. The output file can't be the same as the
input file, use \fB\-\-in\-place\fP instead. This option may be specified at
most once.
.TP
.B \-i, \-\-in\-place \fR[\fP\fISUFFIX\fP\fR]\fP
Use this when you want to modify the input file in\-place. This creates a
temporary file with the specified suffix (\fI.otmp\fP by default). This implies
\fB\-\-overwrite\fP in that if a file with the same temporary name already
exists, it will be overwritten without warning. Of course, this overwrites the
input file too. You cannot use this option when the input file is the standard
input.
.TP
.B \-y, \-\-overwrite
By default, opustags refuses to overwrite an already existent file. Use
this option to allow that.
.TP
.B \-d, \-\-delete \fIFIELD\fP
Delete all the tags whose field name is \fIFIELD\fP. Note that one tag key,
like \fIARTIST\fP, may appear more than once, in which case all of those are
deleted.
.TP
.B \-a, \-\-add \fIFIELD=VALUE\fP
Add a tag. It doesn't matter if a tag of the same type already exist (think
about the case where there are several artists).
.TP
.B \-s, \-\-set \fIFIELD=VALUE\fP
This option is provided for convenience. It deletes all the fields of the same
type that may already exist, then adds it with the wanted value. This is
strictly equivalent to \fB\-\-delete\fP \fIFIELD\fP \fB\-\-add\fP
\fIFIELD=VALUE\fP.
You can combine it with \fB\-\-add\fP to add more tags with that same \fIFIELD\fP.
.TP
.B \-D, \-\-delete\-all
Delete all the tags before adding any.
.TP
.B \-\-import
Set the tags from scratch. All the original tags are deleted and new ones are
read from standard input.
.sp
Each line must specify a \fIFIELD=VALUE\fP pair and be LF\-terminated (except
for the last line). Invalid lines are skipped and issue a warning. Blank lines
are ignored. Lines whose first non-blank character is \fB#\fP are ignored.
Blank characters at the beginning of a line are also skipped.
.sp
Input is read as UTF\-8, disregarding the current locale of your system.
.TP
.B \-e, \-\-edit
Spawn the program specified in the environment variable \fBEDITOR\fP to edit
tags interactively. If this variable can't be read or is empty, an error
message is displayed.
.sp
The expected format is the same as the one \fB\-\-import\fP expects.
.SH EXAMPLES
Here's how you would list all tags in a stream:
.PP
opustags in.ogg
.PP
Here's how you would edit two streams at once, setting the title and artist of
the first, and only the title of the second:
.PP
opustags \-\-stream 1 \-\-set TITLE=X \-\-set ARTIST=Y \-\-stream 2 \-\-set TITLE=Y in.ogg \-o out.ogg
.PP
Here's how you would set two artists:
.PP
opustags \-\-delete ARTIST \-\-add ARTIST=A \-\-add ARTIST=B in.ogg \-o out.ogg
.PP
Hoping that helped!
.SH SEE ALSO
.BR vorbiscomment (1),
.BR sed (1)
.SH AUTHORS
Frédéric Mangano <fmang+opustags@mg0.fr>,
rr\- <https://github.com/rr\->.
.PP
Please report issues on GitHub at <https://github.com/fmang/opustags/issues>.

View File

@ -1,23 +0,0 @@
# Base Io build system
# Written by Jeremy Tregunna <jeremy.tregunna@me.com>
#
# Find libogg.
FIND_PATH(OGG_INCLUDE_DIR ogg/ogg.h)
SET(OGG_NAMES ${OGG_NAMES} ogg libogg)
FIND_LIBRARY(OGG_LIBRARY NAMES ${OGG_NAMES} PATH)
IF(OGG_INCLUDE_DIR AND OGG_LIBRARY)
SET(OGG_FOUND TRUE)
ENDIF(OGG_INCLUDE_DIR AND OGG_LIBRARY)
IF(OGG_FOUND)
IF(NOT Ogg_FIND_QUIETLY)
MESSAGE(STATUS "Found Ogg: ${OGG_LIBRARY}")
ENDIF (NOT Ogg_FIND_QUIETLY)
ELSE(OGG_FOUND)
IF(Ogg_FIND_REQUIRED)
MESSAGE(FATAL_ERROR "Could not find ogg")
ENDIF(Ogg_FIND_REQUIRED)
ENDIF (OGG_FOUND)

108
opustags.1 Normal file
View File

@ -0,0 +1,108 @@
.TH opustags 1 "January 2013"
.SH NAME
opustags \- Opus comment editor
.SH SYNOPSIS
.B opustags --help
.br
.B opustags
.RI [ OPTIONS ]
.I INPUT
.br
.B opustags
.I OPTIONS
.B -o
.I OUTPUT INPUT
.SH DESCRIPTION
.PP
\fBopustags\fP can read and edit the comment header of an Opus file.
It basically has two modes: read-only and read-write (for tag edition).
.PP
In read-only mode, only the beginning of \fIINPUT\fP is read, and the tags are
printed on \fBstdout\fP.
\fIINPUT\fP can either be the name of a file or \fB-\fP to read from \fBstdin\fP.
You can use the options below to edit the tags before printing them.
This could be useful to preview some changes before writing them.
.PP
As for the edition mode, you need to specify an output file (or \fB-\fP for
\fBstdout\fP). It must be different from the input file.
You may want to use \fB--overwrite\fP if you know what youre doing.
To overwrite the input file, use \fB--in-place\fP.
.PP
Tag edition can be made with the \fB--add\fP, \fB--delete\fP and \fB--set\fP
options. They can be written in any order and dont conflict with each other.
However, they arent executed in any order: first the specified tags are
deleted, then the new tags are added. “Set” operations are mere convenience
for delete/add.
.PP
You can delete all the tags with \fB--delete-all\fP. This operation can be
combined with \fB--add\fP to set new tags without being bothered by the old
ones. Another way to do this is to use \fB--set-all\fP as explained below.
.PP
If you want to process tags yourself, you can use the \fB--set-all\fP option
which will cause \fBopustags\fP to read tags from \fBstdin\fP.
The format is the same as the one used for output; that is to say,
newline-separated \fIFIELD=Value\fP assignment. Note that this implies
\fB--delete-all\fP.
.PP
\fBWarning:\fP the Opus format specifications requires tags to be encoded in
\fBUTF-8\fP. This tool ignores the system locale, assuming the encoding is
set to UTF-8, and assume that tags are already encoded in UTF-8.
.SH OPTIONS
.TP
.B \-h, \-\-help
Display a brief description of the options.
.TP
.B \-o, \-\-output \fIFILE\fI
Edition mode. The input file will be read, its tags edited, then written to the
specified output file. If \fIFILE\fP is \fB-\fP then the resulting Opus file
will be written to \fBstdout\fP. As the input file is read incrementally, the
output file cant be the same as the input file.
.TP
.B \-i, \-\-in-place \fR[\fP\fISUFFIX\fP\fR]\fP
Use this when you want to modify the input file in-place. This creates a
temporary file with the specified suffix (.otmp by default). This implies
\fB--overwrite\fP in that if a file with the same temporary name already
exists, it will be overwritten without warning. Of course, this overwrites
the input file too. You cannot use this option when the input file is actually
\fBstdin\fP.
.TP
.B \-y, \-\-overwrite
By default, \fBopustags\fP refuses to overwrite an already existent file. Use
this option to allow that. Note that this doesnt allow in-place edition, the
output file needs to be different from the input file.
.TP
.B \-d, \-\-delete \fIFIELD\fP
Delete all the tags whose field name is \fIFIELD\fP (they may be several, though
usually there is only one of each type). You can use this option as many times
as you want.
.TP
.B \-a, \-\-add \fIFIELD=VALUE\fP
Add a tag. It doesnt matter if a tag of the same type already exist (think
the case where there are several artists). You can use this option as many
times as needed, with the same field names or not. When the \fB--delete\fP
is used with the same \fIFIELD\fP, only the older tags are deleted.
.TP
.B \-s, \-\-set \fIFIELD=VALUE\fP
This option is provided for convenience. It delete all the fields of the same
type that may already exist, then adds it with the wanted value.
This is strictly equivalent to \fB--delete\fP \fIFIELD\fP \fB--add\fP
\fIFIELD=VALUE\fP. You can combine it with \fB--add\fP to add tags of the same
type. As deletion occurs before adding, \fB--set\fP wont erase the tags
added with \fB--add\fP.
.TP
.B \-D, \-\-delete-all
Delete all the tags before adding any. When this option is specified, the
\fB--delete\fP options are ignored. Tags then can be added using \fB--add\fP
or \fB--set\fP, which, in that case, are equivalent.
.TP
.B \-S, \-\-set-all
Sets the tags from scratch. All the original tags are deleted and new ones are
read from \fBstdin\fP. Each line must specify a \fIFIELD=VALUE\fP pair and be
LF-terminated (except for the last line). Invalid lines are skipped and cause
a warning to be issued. Blank lines are ignored. This mode could be useful for
batch processing tags through an utility like \fBsed\fP.
.SH SEE ALSO
.BR vorbiscomment (1),
.BR sed (1)
.SH AUTHOR
Frédéric Mangano <fmang@mg0.fr>

513
opustags.c Normal file
View File

@ -0,0 +1,513 @@
#include <errno.h>
#include <getopt.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ogg/ogg.h>
#ifdef __APPLE__
#include <libkern/OSByteOrder.h>
#define htole32(x) OSSwapHostToLittleInt32(x)
#define le32toh(x) OSSwapLittleToHostInt32(x)
#endif
typedef struct {
uint32_t vendor_length;
const char *vendor_string;
uint32_t count;
uint32_t *lengths;
const char **comment;
} opus_tags;
int parse_tags(char *data, long len, opus_tags *tags){
long pos;
if(len < 8+4+4)
return -1;
if(strncmp(data, "OpusTags", 8) != 0)
return -1;
// Vendor
pos = 8;
tags->vendor_length = le32toh(*((uint32_t*) (data + pos)));
tags->vendor_string = data + pos + 4;
pos += 4 + tags->vendor_length;
if(pos + 4 > len)
return -1;
// Count
tags->count = le32toh(*((uint32_t*) (data + pos)));
if(tags->count == 0)
return 0;
tags->lengths = calloc(tags->count, sizeof(uint32_t));
if(tags->lengths == NULL)
return -1;
tags->comment = calloc(tags->count, sizeof(char*));
if(tags->comment == NULL){
free(tags->lengths);
return -1;
}
pos += 4;
// Comment
uint32_t i;
for(i=0; i<tags->count; i++){
tags->lengths[i] = le32toh(*((uint32_t*) (data + pos)));
tags->comment[i] = data + pos + 4;
pos += 4 + tags->lengths[i];
if(pos > len)
return -1;
}
if(pos < len)
fprintf(stderr, "warning: %ld unused bytes at the end of the OpusTags packet\n", len - pos);
return 0;
}
int render_tags(opus_tags *tags, ogg_packet *op){
// Note: op->packet must be manually freed.
op->b_o_s = 0;
op->e_o_s = 0;
op->granulepos = 0;
op->packetno = 1;
long len = 8 + 4 + tags->vendor_length + 4;
uint32_t i;
for(i=0; i<tags->count; i++)
len += 4 + tags->lengths[i];
op->bytes = len;
char *data = malloc(len);
if(!data)
return -1;
op->packet = (unsigned char*) data;
uint32_t n;
memcpy(data, "OpusTags", 8);
n = htole32(tags->vendor_length);
memcpy(data+8, &n, 4);
memcpy(data+12, tags->vendor_string, tags->vendor_length);
data += 12 + tags->vendor_length;
n = htole32(tags->count);
memcpy(data, &n, 4);
data += 4;
for(i=0; i<tags->count; i++){
n = htole32(tags->lengths[i]);
memcpy(data, &n, 4);
memcpy(data+4, tags->comment[i], tags->lengths[i]);
data += 4 + tags->lengths[i];
}
return 0;
}
int match_field(const char *comment, uint32_t len, const char *field){
size_t field_len;
for(field_len = 0; field[field_len] != '\0' && field[field_len] != '='; field_len++);
if(len <= field_len)
return 0;
if(comment[field_len] != '=')
return 0;
if(strncmp(comment, field, field_len) != 0)
return 0;
return 1;
}
void delete_tags(opus_tags *tags, const char *field){
uint32_t i;
for(i=0; i<tags->count; i++){
if(match_field(tags->comment[i], tags->lengths[i], field)){
tags->count--;
tags->lengths[i] = tags->lengths[tags->count];
tags->comment[i] = tags->comment[tags->count];
// No need to resize the arrays.
}
}
}
int add_tags(opus_tags *tags, const char **tags_to_add, uint32_t count){
if(count == 0)
return 0;
uint32_t *lengths = realloc(tags->lengths, (tags->count + count) * sizeof(uint32_t));
const char **comment = realloc(tags->comment, (tags->count + count) * sizeof(char*));
if(lengths == NULL || comment == NULL)
return -1;
tags->lengths = lengths;
tags->comment = comment;
uint32_t i;
for(i=0; i<count; i++){
tags->lengths[tags->count + i] = strlen(tags_to_add[i]);
tags->comment[tags->count + i] = tags_to_add[i];
}
tags->count += count;
return 0;
}
void print_tags(opus_tags *tags){
if(tags->count == 0)
puts("no tags");
int i;
for(i=0; i<tags->count; i++){
fwrite(tags->comment[i], 1, tags->lengths[i], stdout);
puts("");
}
}
void free_tags(opus_tags *tags){
if(tags->count > 0){
free(tags->lengths);
free(tags->comment);
}
}
int write_page(ogg_page *og, FILE *stream){
if(fwrite(og->header, 1, og->header_len, stream) < og->header_len)
return -1;
if(fwrite(og->body, 1, og->body_len, stream) < og->body_len)
return -1;
return 0;
}
const char *version = "opustags version 1.1.1\n";
const char *usage =
"Usage: opustags --help\n"
" opustags [OPTIONS] FILE\n"
" opustags OPTIONS FILE -o FILE\n";
const char *help =
"Options:\n"
" -h, --help print this help\n"
" -o, --output write the modified tags to a file\n"
" -i, --in-place [SUFFIX] use a temporary file then replace the original file\n"
" -y, --overwrite overwrite the output file if it already exists\n"
" -d, --delete FIELD delete all the fields of a specified type\n"
" -a, --add FIELD=VALUE add a field\n"
" -s, --set FIELD=VALUE delete then add a field\n"
" -D, --delete-all delete all the fields!\n"
" -S, --set-all read the fields from stdin\n";
struct option options[] = {
{"help", no_argument, 0, 'h'},
{"output", required_argument, 0, 'o'},
{"in-place", optional_argument, 0, 'i'},
{"overwrite", no_argument, 0, 'y'},
{"delete", required_argument, 0, 'd'},
{"add", required_argument, 0, 'a'},
{"set", required_argument, 0, 's'},
{"delete-all", no_argument, 0, 'D'},
{"set-all", no_argument, 0, 'S'},
{NULL, 0, 0, 0}
};
int main(int argc, char **argv){
if(argc == 1){
fputs(version, stdout);
fputs(usage, stdout);
return EXIT_SUCCESS;
}
char *path_in, *path_out = NULL, *inplace = NULL;
const char* to_add[argc];
const char* to_delete[argc];
int count_add = 0, count_delete = 0;
int delete_all = 0;
int set_all = 0;
int overwrite = 0;
int print_help = 0;
int c;
while((c = getopt_long(argc, argv, "ho:i::yd:a:s:DS", options, NULL)) != -1){
switch(c){
case 'h':
print_help = 1;
break;
case 'o':
path_out = optarg;
break;
case 'i':
inplace = optarg == NULL ? ".otmp" : optarg;
break;
case 'y':
overwrite = 1;
break;
case 'd':
if(strchr(optarg, '=') != NULL){
fprintf(stderr, "invalid field: '%s'\n", optarg);
return EXIT_FAILURE;
}
to_delete[count_delete++] = optarg;
break;
case 'a':
case 's':
if(strchr(optarg, '=') == NULL){
fprintf(stderr, "invalid comment: '%s'\n", optarg);
return EXIT_FAILURE;
}
to_add[count_add++] = optarg;
if(c == 's')
to_delete[count_delete++] = optarg;
break;
case 'S':
set_all = 1;
case 'D':
delete_all = 1;
break;
default:
return EXIT_FAILURE;
}
}
if(print_help){
puts(version);
puts(usage);
puts(help);
puts("See the man page for extensive documentation.");
return EXIT_SUCCESS;
}
if(optind != argc - 1){
fputs("invalid arguments\n", stderr);
return EXIT_FAILURE;
}
if(inplace && path_out){
fputs("cannot combine --in-place and --output\n", stderr);
return EXIT_FAILURE;
}
path_in = argv[optind];
if(path_out != NULL && strcmp(path_in, "-") != 0){
char canon_in[PATH_MAX+1], canon_out[PATH_MAX+1];
if(realpath(path_in, canon_in) && realpath(path_out, canon_out)){
if(strcmp(canon_in, canon_out) == 0){
fputs("error: the input and output files are the same\n", stderr);
return EXIT_FAILURE;
}
}
}
FILE *in;
if(strcmp(path_in, "-") == 0){
if(set_all){
fputs("can't open stdin for input when -S is specified\n", stderr);
return EXIT_FAILURE;
}
if(inplace){
fputs("cannot modify stdin 'in-place'\n", stderr);
return EXIT_FAILURE;
}
in = stdin;
}
else
in = fopen(path_in, "r");
if(!in){
perror("fopen");
return EXIT_FAILURE;
}
FILE *out = NULL;
if(inplace != NULL){
path_out = malloc(strlen(path_in) + strlen(inplace) + 1);
if(path_out == NULL){
fputs("failure to allocate memory\n", stderr);
fclose(in);
return EXIT_FAILURE;
}
strcpy(path_out, path_in);
strcat(path_out, inplace);
}
if(path_out != NULL){
if(strcmp(path_out, "-") == 0)
out = stdout;
else{
if(!overwrite && !inplace){
if(access(path_out, F_OK) == 0){
fprintf(stderr, "'%s' already exists (use -y to overwrite)\n", path_out);
fclose(in);
return EXIT_FAILURE;
}
}
out = fopen(path_out, "w");
if(!out){
perror("fopen");
fclose(in);
if(inplace)
free(path_out);
return EXIT_FAILURE;
}
}
}
ogg_sync_state oy;
ogg_stream_state os, enc;
ogg_page og;
ogg_packet op;
opus_tags tags;
ogg_sync_init(&oy);
char *buf;
size_t len;
char *error = NULL;
int packet_count = -1;
while(error == NULL){
// Read until we complete a page.
if(ogg_sync_pageout(&oy, &og) != 1){
if(feof(in))
break;
buf = ogg_sync_buffer(&oy, 65536);
if(buf == NULL){
error = "ogg_sync_buffer: out of memory";
break;
}
len = fread(buf, 1, 65536, in);
if(ferror(in))
error = strerror(errno);
ogg_sync_wrote(&oy, len);
if(ogg_sync_check(&oy) != 0)
error = "ogg_sync_check: internal error";
continue;
}
// We got a page.
// Short-circuit when the relevant packets have been read.
if(packet_count >= 2 && out){
if(write_page(&og, out) == -1){
error = "write_page: fwrite error";
break;
}
continue;
}
// Initialize the streams from the first page.
if(packet_count == -1){
if(ogg_stream_init(&os, ogg_page_serialno(&og)) == -1){
error = "ogg_stream_init: couldn't create a decoder";
break;
}
if(out){
if(ogg_stream_init(&enc, ogg_page_serialno(&og)) == -1){
error = "ogg_stream_init: couldn't create an encoder";
break;
}
}
packet_count = 0;
}
if(ogg_stream_pagein(&os, &og) == -1){
error = "ogg_stream_pagein: invalid page";
break;
}
// Read all the packets.
while(ogg_stream_packetout(&os, &op) == 1){
packet_count++;
if(packet_count == 1){ // Identification header
if(strncmp((char*) op.packet, "OpusHead", 8) != 0){
error = "opustags: invalid identification header";
break;
}
}
else if(packet_count == 2){ // Comment header
if(parse_tags((char*) op.packet, op.bytes, &tags) == -1){
error = "opustags: invalid comment header";
break;
}
if(delete_all)
tags.count = 0;
else{
int i;
for(i=0; i<count_delete; i++)
delete_tags(&tags, to_delete[i]);
}
char *raw_tags = NULL;
if(set_all){
raw_tags = malloc(16384);
if(raw_tags == NULL){
error = "malloc: not enough memory for buffering stdin";
free(raw_tags);
break;
}
else{
char *raw_comment[256];
size_t raw_len = fread(raw_tags, 1, 16383, stdin);
if(raw_len == 16383)
fputs("warning: truncating comment to 16 KiB\n", stderr);
raw_tags[raw_len] = '\0';
uint32_t raw_count = 0;
size_t field_len = 0;
int caught_eq = 0;
size_t i = 0;
char *cursor = raw_tags;
for(i=0; i <= raw_len && raw_count < 256; i++){
if(raw_tags[i] == '\n' || raw_tags[i] == '\0'){
if(field_len == 0)
continue;
if(caught_eq)
raw_comment[raw_count++] = cursor;
else
fputs("warning: skipping malformed tag\n", stderr);
cursor = raw_tags + i + 1;
field_len = 0;
caught_eq = 0;
raw_tags[i] = '\0';
continue;
}
if(raw_tags[i] == '=')
caught_eq = 1;
field_len++;
}
add_tags(&tags, (const char**) raw_comment, raw_count);
}
}
add_tags(&tags, to_add, count_add);
if(out){
ogg_packet packet;
render_tags(&tags, &packet);
if(ogg_stream_packetin(&enc, &packet) == -1)
error = "ogg_stream_packetin: internal error";
free(packet.packet);
}
else
print_tags(&tags);
free_tags(&tags);
if(raw_tags)
free(raw_tags);
if(error || !out)
break;
else
continue;
}
if(out){
if(ogg_stream_packetin(&enc, &op) == -1){
error = "ogg_stream_packetin: internal error";
break;
}
}
}
if(error != NULL)
break;
if(ogg_stream_check(&os) != 0)
error = "ogg_stream_check: internal error (decoder)";
// Write the page.
if(out){
ogg_stream_flush(&enc, &og);
if(write_page(&og, out) == -1)
error = "write_page: fwrite error";
else if(ogg_stream_check(&enc) != 0)
error = "ogg_stream_check: internal error (encoder)";
}
else if(packet_count >= 2) // Read-only mode
break;
}
if(packet_count >= 0){
ogg_stream_clear(&os);
if(out)
ogg_stream_clear(&enc);
}
ogg_sync_clear(&oy);
fclose(in);
if(out)
fclose(out);
if(!error && packet_count < 2)
error = "opustags: invalid file";
if(error){
fprintf(stderr, "%s\n", error);
if(path_out != NULL && out != stdout)
remove(path_out);
if(inplace)
free(path_out);
return EXIT_FAILURE;
}
else if(inplace){
if(rename(path_out, path_in) == -1){
perror("rename");
free(path_out);
return EXIT_FAILURE;
}
free(path_out);
}
return EXIT_SUCCESS;
}

View File

@ -1,83 +0,0 @@
#include "actions.h"
using namespace opustags;
void opustags::list_tags(ogg::Decoder &dec, ITagsHandler &handler, bool full)
{
std::map<long, int> sequence_numbers;
int stream_count = 0;
int remaining_streams = 0;
std::shared_ptr<ogg::Stream> s;
while (!handler.done()) {
s = dec.read_page();
if (s == nullptr)
break; // end of stream
switch (s->state) {
case ogg::HEADER_READY:
stream_count++;
sequence_numbers[s->stream.serialno] = stream_count;
handler.start_of_stream(stream_count, s->type);
if (!handler.relevant(stream_count))
s->downgrade();
remaining_streams++;
break;
case ogg::TAGS_READY:
handler.list(sequence_numbers[s->stream.serialno], s->tags);
s->downgrade(); // no more use for it
default:
remaining_streams--;
}
if (!full && remaining_streams <= 0) {
break;
// premature exit, but calls end_of_stream anyway
// we want our optimization to be transparent to the TagsHandler
}
}
handler.end_of_file();
}
void opustags::edit_tags(
ogg::Decoder &in, ogg::Encoder &out, ITagsHandler &handler)
{
std::map<long, int> sequence_numbers;
int stream_count = 0;
std::shared_ptr<ogg::Stream> s;
for (;;) {
s = in.read_page();
if (s == nullptr)
break; // end of stream
switch (s->state) {
case ogg::HEADER_READY:
stream_count++;
sequence_numbers[s->stream.serialno] = stream_count;
handler.start_of_stream(stream_count, s->type);
if (!handler.relevant(stream_count))
s->downgrade(); // makes it UNKNOWN
if (s->type == ogg::UNKNOWN_STREAM) {
out.write_raw_page(in.current_page);
} else {
out.forward(*s);
}
break;
case ogg::TAGS_READY:
handler.edit(sequence_numbers[s->stream.serialno], s->tags);
handler.list(sequence_numbers[s->stream.serialno], s->tags);
out.write_tags(s->stream.serialno, s->tags);
break;
case ogg::DATA_READY:
out.forward(*s);
break;
case ogg::RAW_READY:
out.write_raw_page(in.current_page);
break;
default:
;
}
}
handler.end_of_file();
}

View File

@ -1,33 +0,0 @@
#pragma once
#include "ogg.h"
#include "tags_handler.h"
namespace opustags {
// Decode a file and call the handler's list method every time a tags
// header is read. Set full to true if you want to make sure every single
// page of the stream is read.
//
// Use:
// std::ifstream in("in.ogg");
// ogg::Decoder dec(in);
// TagsLister lister(options);
// list_tags(dec, lister);
//
void list_tags(ogg::Decoder&, ITagsHandler &, bool full = false);
// Forward the input data to the output stream, transforming tags on-the-go
// with the handler's edit method.
//
// Use:
// std::ifstream in("in.ogg");
// ogg::Decoder dec(in);
// std::ofstream out("out.ogg");
// std::Encoder enc(out);
// TagsEditor editor(options);
// edit_tags(dec, enc, editor);
//
void edit_tags(ogg::Decoder &in, ogg::Encoder &out, ITagsHandler &);
}

View File

@ -1,29 +0,0 @@
#pragma once
namespace opustags {
enum class LogLevel {
LOG_NORMAL = 0,
LOG_VERBOSE = 1,
LOG_DEBUG = 2,
LOG_DEBUG_EXTRA = 3,
};
class Log
{
public:
Log(std::ostream &out);
LogLevel level;
Log& operator<<(LogLevel lvl);
template<class T> Log& operator<<(const T&);
private:
std::ostream &out;
};
extern Log log;
}

View File

@ -1,88 +0,0 @@
#include "actions.h"
#include "options.h"
#include "version.h"
#include <iostream>
#include <fstream>
#include <cstring>
static void show_usage(const bool include_help)
{
static const auto usage =
"Usage: opustags --help\n"
" opustags [OPTIONS] INPUT\n"
" opustags [OPTIONS] -o OUTPUT INPUT\n";
static const auto help =
"Options:\n"
" -h, --help print this help\n"
" -V, --version print version\n"
" -o, --output FILE write the modified tags to this file\n"
" -i, --in-place [SUFFIX] use a temporary file then replace the original file\n"
" -y, --overwrite overwrite the output file if it already exists\n"
" --stream ID select stream for the next operations\n"
" -l, --list display a pretty listing of all tags\n"
" --no-color disable colors in --list output\n"
" -d, --delete FIELD delete all the fields of a specified type\n"
" -a, --add FIELD=VALUE add a field\n"
" -s, --set FIELD=VALUE delete then add a field\n"
" -D, --delete-all delete all the fields!\n"
" --full enable full file scan\n"
" --export dump the tags to standard output for --import\n"
" --import set the tags from scratch basing on stanard input\n"
" -e, --edit spawn the $EDITOR and apply --import on the result\n";
std::cout << "opustags v" << opustags::version_short << "\n";
std::cout << usage;
if (include_help) {
std::cout << "\n";
std::cout << help;
}
}
static void show_version()
{
std::cout << "opustags v" << opustags::version_long << "\n";
}
int main(int argc, char **argv)
{
if (argc == 1) {
show_usage(false);
return EXIT_SUCCESS;
}
try {
auto options = opustags::parse_args(argc, argv);
if (options.show_help) {
show_usage(true);
return EXIT_SUCCESS;
}
if (options.show_version) {
show_version();
return EXIT_SUCCESS;
}
if (options.path_out.empty()) {
std::ifstream in(options.path_in);
opustags::ogg::Decoder dec(in);
list_tags(dec, options.tags_handler, options.full);
// TODO: report errors if user tries to edit the stream
} else {
std::ifstream in(options.path_in);
std::ofstream out(options.path_out);
opustags::ogg::Decoder dec(in);
opustags::ogg::Encoder enc(out);
edit_tags(dec, enc, options.tags_handler);
}
} catch (const std::exception &e) {
if (errno)
std::cerr << "fatal error: " << e.what() << " (" << strerror(errno) << ")" << std::endl;
else
std::cerr << "fatal error: " << e.what() << std::endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

View File

@ -1,308 +0,0 @@
#include "ogg.h"
#include <stdexcept>
#include <fstream>
#include <sstream>
#include <cstring>
#include <endian.h>
using namespace opustags;
////////////////////////////////////////////////////////////////////////////////
// ogg::Stream
ogg::Stream::Stream(int streamno)
{
state = ogg::BEGIN_OF_STREAM;
type = ogg::UNKNOWN_STREAM;
if (ogg_stream_init(&stream, streamno) != 0)
throw std::runtime_error("ogg_stream_init failed");
}
ogg::Stream::~Stream()
{
ogg_stream_clear(&stream);
}
void ogg::Stream::flush_packets()
{
ogg_packet op;
while (ogg_stream_packetout(&stream, &op) > 0);
}
bool ogg::Stream::page_in(ogg_page &og)
{
if (state != ogg::BEGIN_OF_STREAM && type == ogg::UNKNOWN_STREAM) {
state = ogg::RAW_READY;
return true;
}
flush_packets(); // otherwise packet_out keeps returning the same packet
if (ogg_stream_pagein(&stream, &og) != 0)
throw std::runtime_error("ogg_stream_pagein failed");
if (state == ogg::BEGIN_OF_STREAM || state == ogg::HEADER_READY) {
// We're expecting a header, so we parse it.
return handle_page();
} else {
// We're past the first two headers.
state = ogg::DATA_READY;
return true;
}
}
// Read the first packet of the page and parses it.
bool ogg::Stream::handle_page()
{
ogg_packet op;
int rc = ogg_stream_packetpeek(&stream, &op);
if (rc < 0)
throw std::runtime_error("ogg_stream_packetout failed");
else if (rc == 0) // insufficient data
return false; // asking for a new page
// We've read the first packet successfully.
// The headers are supposed to contain only one packet, so this is enough
// for us. Still, we could ensure there are no other packets.
handle_packet(op);
return true;
}
void ogg::Stream::handle_packet(const ogg_packet &op)
{
if (state == ogg::BEGIN_OF_STREAM)
parse_header(op);
else if (state == ogg::HEADER_READY)
parse_opustags(op);
// else shrug
}
void ogg::Stream::parse_header(const ogg_packet &op)
{
if (op.bytes >= 8 && memcmp(op.packet, "OpusHead", 8) == 0)
type = OPUS_STREAM;
else
type = UNKNOWN_STREAM;
state = HEADER_READY;
}
// For reference:
// https://tools.ietf.org/html/draft-ietf-codec-oggopus-14#section-5.2
void ogg::Stream::parse_opustags(const ogg_packet &op)
{
// This part is gonna be C-ish because I don't see how I'd do this in C++
// without being inefficient, both in volume of code and performance.
char *data = reinterpret_cast<char*>(op.packet);
long remaining = op.bytes;
if (remaining < 8 || memcmp(data, "OpusTags", 8) != 0)
throw std::runtime_error("expected OpusTags header");
data += 8;
remaining -= 8;
// Vendor string
if (remaining < 4)
throw std::runtime_error("no space for vendor string length");
uint32_t vendor_length = le32toh(*reinterpret_cast<uint32_t*>(data));
if (remaining - 4 < vendor_length)
throw std::runtime_error("invalid vendor string length");
tags.vendor = std::string(data + 4, vendor_length);
data += 4 + vendor_length;
remaining -= 4 + vendor_length;
// User comments count
if (remaining < 4)
throw std::runtime_error("no space for user comment list length");
long comment_count = le32toh(*reinterpret_cast<uint32_t*>(data));
data += 4;
remaining -= 4;
// Actual comments
// We iterate on a long type to prevent infinite looping when comment_count == UINT32_MAX.
for (long i = 0; i < comment_count; i++) {
if (remaining < 4)
throw std::runtime_error("no space for user comment length");
uint32_t comment_length = le32toh(*reinterpret_cast<uint32_t*>(data));
if (remaining - 4 < comment_length)
throw std::runtime_error("no space for comment contents");
tags.add(parse_tag(std::string(data + 4, comment_length)));
data += 4 + comment_length;
remaining -= 4 + comment_length;
}
// Extra data to keep if the least significant bit of the first byte is 1
if (remaining > 0 && (*data & 1) == 1 )
tags.extra = std::string(data, remaining);
state = TAGS_READY;
}
void ogg::Stream::downgrade()
{
type = ogg::UNKNOWN_STREAM;
if (state != ogg::BEGIN_OF_STREAM && state != ogg::END_OF_STREAM)
state = RAW_READY;
}
////////////////////////////////////////////////////////////////////////////////
// ogg::Decoder
ogg::Decoder::Decoder(std::istream &in)
: input(in)
{
if (!in)
throw std::runtime_error("invalid stream to decode");
input.exceptions(std::ifstream::badbit);
ogg_sync_init(&sync);
}
ogg::Decoder::~Decoder()
{
ogg_sync_clear(&sync);
}
std::shared_ptr<ogg::Stream> ogg::Decoder::read_page()
{
while (page_out()) {
int streamno = ogg_page_serialno(&current_page);
auto i = streams.find(streamno);
if (i == streams.end()) {
// we could check the page number to detect new streams (pageno = 0)
auto s = std::make_shared<Stream>(streamno);
i = streams.emplace(streamno, s).first;
}
if (i->second->page_in(current_page))
return i->second;
}
return nullptr; // end of stream
}
// Read the next page and return true on success, false on end of stream.
bool ogg::Decoder::page_out()
{
int rc;
for (;;) {
rc = ogg_sync_pageout(&sync, &current_page);
if (rc < 0) {
throw std::runtime_error("ogg_sync_pageout failed");
} else if (rc == 1) {
break; // page complete
} else if (!buff()) {
// more data required but end of file reached
// TODO check sync.unsynced flag in case we've got an incomplete page
return false;
}
}
return true;
}
// Read data from the stream into the sync's buffer.
bool ogg::Decoder::buff()
{
if (input.eof())
return false;
char *buf = ogg_sync_buffer(&sync, 65536);
if (buf == nullptr)
throw std::runtime_error("ogg_sync_buffer failed");
input.read(buf, 65536);
ogg_sync_wrote(&sync, input.gcount());
return true;
}
////////////////////////////////////////////////////////////////////////////////
// ogg::Encoder
ogg::Encoder::Encoder(std::ostream &out)
: output(out)
{
if (!output)
throw std::runtime_error("invalid stream to decode");
output.exceptions(std::ifstream::badbit);
}
ogg::Stream& ogg::Encoder::get_stream(int streamno)
{
auto i = streams.find(streamno);
if (i == streams.end()) {
auto s = std::make_shared<Stream>(streamno);
i = streams.emplace(streamno, s).first;
}
return *(i->second);
}
void ogg::Encoder::forward(ogg::Stream &in)
{
ogg::Stream *out = &get_stream(in.stream.serialno);
forward_stream(in, *out);
flush_stream(*out);
}
void ogg::Encoder::forward_stream(ogg::Stream &in, ogg::Stream &out)
{
int rc;
ogg_packet op;
for (;;) {
rc = ogg_stream_packetout(&in.stream, &op);
if (rc < 0) {
throw std::runtime_error("ogg_stream_packetout failed");
} else if (rc == 0) {
break;
} else {
if (ogg_stream_packetin(&out.stream, &op) != 0)
throw std::runtime_error("ogg_stream_packetin failed");
}
}
}
void ogg::Encoder::flush_stream(ogg::Stream &out)
{
ogg_page og;
if (ogg_stream_flush(&out.stream, &og))
write_raw_page(og);
}
void ogg::Encoder::write_raw_page(const ogg_page &og)
{
output.write(reinterpret_cast<const char*>(og.header), og.header_len);
output.write(reinterpret_cast<const char*>(og.body), og.body_len);
}
void ogg::Encoder::write_tags(int streamno, const Tags &tags)
{
ogg_packet op;
op.b_o_s = 0;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = 1; // checked on a file from ffmpeg
std::string data = render_opustags(tags);
op.bytes = data.size();
op.packet = reinterpret_cast<unsigned char*>(const_cast<char*>(data.data()));
std::shared_ptr<ogg::Stream> s = streams.at(streamno); // assume it exists
if (ogg_stream_packetin(&s->stream, &op) != 0)
throw std::runtime_error("ogg_stream_packetin failed");
flush_stream(*s);
}
std::string ogg::Encoder::render_opustags(const Tags &tags)
{
std::stringbuf s;
uint32_t length;
s.sputn("OpusTags", 8);
length = htole32(tags.vendor.size());
s.sputn(reinterpret_cast<char*>(&length), 4);
s.sputn(tags.vendor.data(), tags.vendor.size());
auto assocs = tags.get_all();
length = htole32(assocs.size());
s.sputn(reinterpret_cast<char*>(&length), 4);
for (const auto assoc : assocs) {
length = htole32(assoc.key.size() + 1 + assoc.value.size());
s.sputn(reinterpret_cast<char*>(&length), 4);
s.sputn(assoc.key.data(), assoc.key.size());
s.sputc('=');
s.sputn(assoc.value.data(), assoc.value.size());
}
s.sputn(tags.extra.data(), tags.extra.size());
return s.str();
}

129
src/ogg.h
View File

@ -1,129 +0,0 @@
#pragma once
#include "tags.h"
#include <iostream>
#include <map>
#include <memory>
#include <ogg/ogg.h>
namespace opustags {
namespace ogg
{
enum StreamState {
BEGIN_OF_STREAM,
HEADER_READY,
TAGS_READY,
DATA_READY,
RAW_READY,
END_OF_STREAM,
// Meaning of these states below, in Stream.
};
enum StreamType {
UNKNOWN_STREAM = 0,
OPUS_STREAM,
};
// An Ogg file may contain many logical bitstreams, and among them, many
// Opus streams. This class represents one stream, whether it is Opus or
// not.
struct Stream
{
Stream(int streamno);
Stream(const Stream&) = delete;
~Stream();
// Called by Decoder once a page was read.
// Returns true if it's ready, false if it expects more data.
// In the latter case, Decoder::read_page will keep reading.
bool page_in(ogg_page&);
// Make the stream behave as if it were unknown.
// As a consequence, no more effort would be made in extracting data.
void downgrade();
StreamState state;
StreamType type;
Tags tags;
// Here are the meanings of the state variable:
// BEGIN_OF_STREAM: the stream was just initialized,
// HEADER_READY: the header (first packet) was found,
// TAGS_READY: the tags are parsed and complete,
// DATA_READY: we've read a data page whose meaning is no concern to us,
// RAW_READY: we don't even know what kind of stream that is, so don't alter it,
// END_OF_STREAM: no more thing to read, not even the current page.
// From the state, we decide what to do with the Decoder's current_page.
// The difference between DATA_READY and RAW_DATA is that the former
// might require a reencoding of the current page. For example, if the
// tags grow and span over two pages, all the following pages are gonna
// need a new sequence number.
// For an Opus stream, the sequence will be:
// BEGIN_OF_STREAM → HEADER_READY → TAGS_READY → DATA_READY* → END_OF_STREAM
// For an unknown stream:
// BEGIN_OF_STREAM → HEADER_READY → RAW_READY* → END_OF_STREAM
ogg_stream_state stream;
private:
void flush_packets();
bool handle_page();
void handle_packet(const ogg_packet&);
void parse_header(const ogg_packet&);
void parse_opustags(const ogg_packet&);
};
struct Decoder
{
Decoder(std::istream&);
Decoder(const Decoder&) = delete;
~Decoder();
// Read a page, dispatch it, and return the stream it belongs to.
// The read page is given to Stream::page_in before this function
// returns.
// After the end of the file is reached, it returns NULL.
std::shared_ptr<Stream> read_page();
std::istream &input;
ogg_sync_state sync;
ogg_page current_page;
std::map<int, std::shared_ptr<Stream>> streams;
private:
bool page_out();
bool buff();
};
struct Encoder
{
Encoder(std::ostream&);
// Copy the input stream's current page.
void forward(Stream &in);
// Write the page without even ensuring its page number is correct.
// It would be an efficient way to copy a stream identically, and also
// needed for write_page.
void write_raw_page(const ogg_page&);
void write_tags(int streamno, const Tags&);
std::ostream &output;
// We're gonna need some ogg_stream_state for adjusting the page
// numbers and splitting large packets as it's gotta be done.
std::map<int, std::shared_ptr<Stream>> streams;
private:
Stream& get_stream(int streamno);
void forward_stream(Stream &in, Stream &out);
void flush_stream(Stream &out);
std::string render_opustags(const Tags &tags);
};
}
}

View File

@ -1,199 +0,0 @@
#include "options.h"
#include "tags_handlers/insertion_tags_handler.h"
#include "tags_handlers/modification_tags_handler.h"
#include "tags_handlers/external_edit_tags_handler.h"
#include "tags_handlers/export_tags_handler.h"
#include "tags_handlers/import_tags_handler.h"
#include "tags_handlers/listing_tags_handler.h"
#include "tags_handlers/removal_tags_handler.h"
#include <getopt.h>
#include <regex>
#include <sstream>
using namespace opustags;
ArgumentError::ArgumentError(const std::string &message)
: std::runtime_error(message.c_str())
{
}
Options::Options() :
show_help(false),
show_version(false),
overwrite(false),
full(false),
in_place(false)
{
}
Options opustags::parse_args(const int argc, char **argv)
{
static const auto short_def = "hVeo:i::yd:a:s:Dl";
static const option long_def[] = {
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'V'},
{"full", no_argument, 0, 0},
{"output", required_argument, 0, 'o'},
{"in-place", optional_argument, 0, 'i'},
{"overwrite", no_argument, 0, 'y'},
{"delete", required_argument, 0, 'd'},
{"add", required_argument, 0, 'a'},
{"stream", required_argument, 0, 0},
{"set", required_argument, 0, 's'},
{"list", no_argument, 0, 'l'},
{"delete-all", no_argument, 0, 'D'},
{"edit", no_argument, 0, 'e'},
{"import", no_argument, 0, 0},
{"export", no_argument, 0, 0},
// TODO: parse no-colors
{nullptr, 0, 0, 0}
};
// TODO: use --list as default switch
Options options;
std::vector<int> current_streamnos {StreamTagsHandler::ALL_STREAMS};
int option_index;
char c;
optind = 0;
while ((c = getopt_long(
argc, argv, short_def, long_def, &option_index)) != -1) {
const std::string arg(optarg == nullptr ? "" : optarg);
switch (c) {
case 'h':
options.show_help = true;
break;
case 'V':
options.show_version = true;
break;
case 'o':
if (arg.empty())
throw ArgumentError("Output path cannot be empty");
options.path_out = arg;
break;
case 'i':
// TODO: shouldn't we generate random file name here to which
// we apply the arg, rather than use the arg as a whole?
options.path_out = arg.empty() ? ".otmp" : arg;
options.in_place = true;
break;
case 'y':
options.overwrite = true;
break;
case 'd':
if (arg.find('=') != std::string::npos)
throw ArgumentError("Invalid field: '" + arg + "'");
for (const auto streamno : current_streamnos) {
options.tags_handler.add_handler(
std::make_shared<RemovalTagsHandler>(streamno, arg));
}
break;
case 'a':
case 's':
{
std::smatch match;
std::regex regex("^(\\w+)=(.*)$");
if (!std::regex_match(arg, match, regex))
throw ArgumentError("Invalid field: '" + arg + "'");
for (const auto streamno : current_streamnos) {
if (c == 's') {
options.tags_handler.add_handler(
std::make_shared<ModificationTagsHandler>(
streamno, match[1], match[2]));
} else {
options.tags_handler.add_handler(
std::make_shared<InsertionTagsHandler>(
streamno, match[1], match[2]));
}
}
break;
}
case 'l':
for (const auto streamno : current_streamnos) {
options.tags_handler.add_handler(
std::make_shared<ListingTagsHandler>(
streamno, std::cout));
}
break;
case 'e':
options.tags_handler.add_handler(
std::make_shared<ExternalEditTagsHandler>());
break;
case 'D':
for (const auto streamno : current_streamnos) {
options.tags_handler.add_handler(
std::make_shared<RemovalTagsHandler>(streamno));
}
break;
case 0:
{
std::string long_arg = long_def[option_index].name;
if (long_arg == "stream") {
int i;
current_streamnos.clear();
std::stringstream ss(optarg);
while (ss >> i) {
current_streamnos.push_back(i);
if (ss.peek() == ',')
ss.ignore();
}
}
else if (long_arg == "full")
options.full = true;
else if (long_arg == "export")
options.tags_handler.add_handler(
std::make_shared<ExportTagsHandler>(std::cout));
else if (long_arg == "import")
options.tags_handler.add_handler(
std::make_shared<ImportTagsHandler>(std::cin));
break;
}
default:
throw ArgumentError("Invalid flag");
}
}
std::vector<std::string> stray;
while (optind < argc)
stray.push_back(argv[optind++]);
if (!options.show_help && !options.show_version) {
if (stray.empty())
throw ArgumentError("Missing input path");
options.path_in = stray.at(0);
if (options.path_in.empty())
throw ArgumentError("Input path cannot be empty");
if (stray.size() > 1)
throw ArgumentError("Extra argument: " + stray.at(1));
if (options.path_out.empty()) {
options.tags_handler.add_handler(
std::make_shared<ListingTagsHandler>(
StreamTagsHandler::ALL_STREAMS, std::cout));
}
}
return options;
}

View File

@ -1,33 +0,0 @@
#pragma once
#include "tags_handlers/composite_tags_handler.h"
#include <stdexcept>
#include <map>
#include <vector>
namespace opustags
{
struct Options final
{
Options();
bool show_help;
bool show_version;
bool overwrite;
bool full;
bool in_place;
std::string path_in;
std::string path_out;
CompositeTagsHandler tags_handler;
};
struct ArgumentError : std::runtime_error
{
ArgumentError(const std::string &message);
};
Options parse_args(const int argc, char **argv);
}

View File

@ -1,103 +0,0 @@
#include "tags.h"
#include <algorithm>
using namespace opustags;
// ASCII only, but for tag keys it's good enough
static bool iequals(const std::string &a, const std::string &b)
{
if (a.size() != b.size())
return false;
for (size_t i = 0; i < a.size(); i++)
if (std::tolower(a[i]) != std::tolower(b[i]))
return false;
return true;
}
bool Tag::operator !=(const Tag &other_tag) const
{
return !operator ==(other_tag);
}
bool Tag::operator ==(const Tag &other_tag) const
{
return key == other_tag.key
&& value == other_tag.value;
}
Tags::Tags()
{
}
Tags::Tags(const std::vector<Tag> &tags) : tags(tags)
{
}
const std::vector<Tag> Tags::get_all() const
{
return tags;
}
std::string Tags::get(const std::string &key) const
{
for (auto &tag : tags)
if (iequals(tag.key, key))
return tag.value;
throw std::runtime_error("Tag '" + key + "' not found.");
}
void Tags::add(const Tag &tag)
{
tags.push_back(tag);
}
void Tags::add(const std::string &key, const std::string &value)
{
tags.push_back({key, value});
}
void Tags::clear()
{
tags.clear();
}
void Tags::remove(const std::string &key)
{
std::vector<Tag> new_tags;
std::copy_if(
tags.begin(),
tags.end(),
std::back_inserter(new_tags),
[&](const Tag &tag) { return !iequals(tag.key, key); });
tags = new_tags;
}
bool Tags::contains(const std::string &key) const
{
return std::count_if(
tags.begin(),
tags.end(),
[&](const Tag &tag) { return iequals(tag.key, key); }) > 0;
}
Tag opustags::parse_tag(const std::string &assoc)
{
size_t eq = assoc.find_first_of('=');
if (eq == std::string::npos)
throw std::runtime_error("misconstructed tag");
std::string name = assoc.substr(0, eq);
std::string value = assoc.substr(eq + 1);
return { name, value };
}
bool Tags::operator !=(const Tags &other_tags) const
{
return !operator ==(other_tags);
}
bool Tags::operator ==(const Tags &other_tags) const
{
return vendor == other_tags.vendor
&& extra == other_tags.extra
&& get_all() == other_tags.get_all();
}

View File

@ -1,49 +0,0 @@
#pragma once
#include <map>
#include <vector>
#include <utility>
namespace opustags {
struct Tag final
{
bool operator !=(const Tag &other_tag) const;
bool operator ==(const Tag &other_tag) const;
std::string key;
std::string value;
};
// A std::map adapter that keeps the order of insertion.
class Tags final
{
public:
Tags();
Tags(const std::vector<Tag> &tags);
const std::vector<Tag> get_all() const;
std::string get(const std::string &key) const;
void add(const Tag &tag);
void add(const std::string &key, const std::string &value);
void remove(const std::string &key);
bool contains(const std::string &key) const;
void clear();
bool operator !=(const Tags &other_tags) const;
bool operator ==(const Tags &other_tags) const;
// Additional fields are required to match the specs:
// https://tools.ietf.org/html/draft-ietf-codec-oggopus-14#section-5.2
std::string vendor;
std::string extra;
private:
std::vector<Tag> tags;
};
Tag parse_tag(const std::string &assoc); // KEY=value
}

View File

@ -1,50 +0,0 @@
#pragma once
#include "tags.h"
namespace opustags {
// TagsHandler define various operations related to tags and stream in
// order to control the main loop.
// In its implementation, it is expected to receive an option structure.
class ITagsHandler
{
public:
// Irrelevant streams don't even need to be parsed, so we can save some
// effort with this method.
// Returns true if the stream should be parsed, false if it should be
// ignored (list) or copied identically (edit).
virtual bool relevant(const int streamno) = 0;
// The list method is called by list_tags every time it has
// successfully parsed an OpusTags header.
virtual void list(const int streamno, const Tags &) = 0;
// Transform the tags at will.
// Returns true if the tags were indeed modified, false if they weren't.
// The latter case may be used for optimization.
virtual bool edit(const int streamno, Tags &) = 0;
// The work is done.
// When listing tags, once we've caught the streams we wanted, it's no
// use keeping reading the file for new streams. In that case, a true
// return value would abort any further processing.
virtual bool done() = 0;
// Signals a new stream was found.
// The meaning of type is in ogg::StreamType, but all you should assume
// is that when type is null (UNKNOWN_STREAM), list or edit won't be
// called.
virtual void start_of_stream(const int streamno, const int type) {}
// Signals the end of the file (and all the streams).
// If after this function is called, done() returns false, it's an
// error. However, it would be better to raise the error inside
// end_of_stream().
// For example, if you expect to find the stream #1 and reach the
// end-of-stream before finding it, better tell the user that you
// didn't do what he expected.
virtual void end_of_file() {}
};
}

View File

@ -1,44 +0,0 @@
#include "tags_handlers/composite_tags_handler.h"
using namespace opustags;
void CompositeTagsHandler::add_handler(std::shared_ptr<ITagsHandler> handler)
{
handlers.push_back(std::move(handler));
}
bool CompositeTagsHandler::relevant(const int streamno)
{
for (const auto &handler : handlers)
if (handler->relevant(streamno))
return true;
return false;
}
void CompositeTagsHandler::list(const int streamno, const Tags &tags)
{
for (const auto &handler : handlers)
handler->list(streamno, tags);
}
bool CompositeTagsHandler::edit(const int streamno, Tags &tags)
{
bool modified = false;
for (const auto &handler : handlers)
modified |= handler->edit(streamno, tags);
return modified;
}
bool CompositeTagsHandler::done()
{
for (const auto &handler : handlers)
if (!handler->done())
return false;
return true;
}
const std::vector<std::shared_ptr<ITagsHandler>>
CompositeTagsHandler::get_handlers() const
{
return handlers;
}

View File

@ -1,25 +0,0 @@
#pragma once
#include <memory>
#include <vector>
#include "tags_handler.h"
namespace opustags {
class CompositeTagsHandler final : public ITagsHandler
{
public:
void add_handler(std::shared_ptr<ITagsHandler> handler);
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
const std::vector<std::shared_ptr<ITagsHandler>> get_handlers() const;
private:
std::vector<std::shared_ptr<ITagsHandler>> handlers;
};
}

View File

@ -1,31 +0,0 @@
#include "tags_handlers/export_tags_handler.h"
using namespace opustags;
ExportTagsHandler::ExportTagsHandler(std::ostream &output_stream)
: output_stream(output_stream)
{
}
bool ExportTagsHandler::relevant(const int)
{
return true;
}
void ExportTagsHandler::list(const int streamno, const Tags &tags)
{
output_stream << "[Stream " << streamno << "]\n";
for (const auto tag : tags.get_all())
output_stream << tag.key << "=" << tag.value << "\n";
output_stream << "\n";
}
bool ExportTagsHandler::edit(const int, Tags &)
{
return false;
}
bool ExportTagsHandler::done()
{
return false;
}

View File

@ -1,22 +0,0 @@
#pragma once
#include <iostream>
#include "tags_handler.h"
namespace opustags {
class ExportTagsHandler : public ITagsHandler
{
public:
ExportTagsHandler(std::ostream &output_stream);
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
private:
std::ostream &output_stream;
};
}

View File

@ -1,22 +0,0 @@
#include "tags_handlers/external_edit_tags_handler.h"
using namespace opustags;
bool ExternalEditTagsHandler::relevant(const int streamno)
{
return false;
}
void ExternalEditTagsHandler::list(const int streamno, const Tags &)
{
}
bool ExternalEditTagsHandler::edit(const int streamno, Tags &)
{
return false;
}
bool ExternalEditTagsHandler::done()
{
return true;
}

View File

@ -1,17 +0,0 @@
#pragma once
#include <iostream>
#include "tags_handler.h"
namespace opustags {
class ExternalEditTagsHandler : public ITagsHandler
{
public:
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
};
}

View File

@ -1,67 +0,0 @@
#include <regex>
#include "tags_handlers/import_tags_handler.h"
using namespace opustags;
ImportTagsHandler::ImportTagsHandler(std::istream &input_stream)
: parsed(false), input_stream(input_stream)
{
}
bool ImportTagsHandler::relevant(const int)
{
return true;
}
void ImportTagsHandler::list(const int, const Tags &)
{
}
bool ImportTagsHandler::edit(const int streamno, Tags &tags)
{
// the reason why we do it this way is because the tests indirectly create
// this handler with std::cin, and we do not want the constructor to block!
parse_input_stream_if_needed();
const auto old_tags = tags;
tags.clear();
if (tag_map.find(streamno) != tag_map.end()) {
const auto &source_tags = tag_map.at(streamno);
for (const auto &source_tag : source_tags.get_all())
tags.add(source_tag.key, source_tag.value);
}
return old_tags != tags;
}
bool ImportTagsHandler::done()
{
return false;
}
void ImportTagsHandler::parse_input_stream_if_needed()
{
if (parsed)
return;
parsed = true;
const std::regex whitespace_regex("^\\s*$");
const std::regex stream_header_regex(
"^\\s*\\[stream\\s+(\\d+)\\]\\s*$", std::regex_constants::icase);
const std::regex tag_regex(
"^\\s*([a-z0-9]+)\\s*=\\s*(.*?)\\s*$", std::regex_constants::icase);
int current_stream_number = 1;
std::string line;
while (std::getline(input_stream, line)) {
std::smatch match;
if (std::regex_match(line, match, stream_header_regex)) {
current_stream_number = std::atoi(match[1].str().c_str());
} else if (std::regex_match(line, match, tag_regex)) {
tag_map[current_stream_number].add(match[1], match[2]);
} else if (!std::regex_match(line, match, whitespace_regex)) {
throw std::runtime_error("Malformed input data near line " + line);
}
}
}

View File

@ -1,27 +0,0 @@
#pragma once
#include <iostream>
#include <map>
#include "tags_handler.h"
namespace opustags {
class ImportTagsHandler : public ITagsHandler
{
public:
ImportTagsHandler(std::istream &input_stream);
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
private:
void parse_input_stream_if_needed();
bool parsed;
std::istream &input_stream;
std::map<int, Tags> tag_map;
};
}

View File

@ -1,28 +0,0 @@
#include "tags_handlers/insertion_tags_handler.h"
#include "tags_handlers_errors.h"
using namespace opustags;
InsertionTagsHandler::InsertionTagsHandler(
const int streamno,
const std::string &tag_key,
const std::string &tag_value)
: StreamTagsHandler(streamno), tag_key(tag_key), tag_value(tag_value)
{
}
std::string InsertionTagsHandler::get_tag_key() const
{
return tag_key;
}
std::string InsertionTagsHandler::get_tag_value() const
{
return tag_value;
}
bool InsertionTagsHandler::edit_impl(Tags &tags)
{
tags.add(tag_key, tag_value);
return true;
}

View File

@ -1,26 +0,0 @@
#pragma once
#include "tags_handlers/stream_tags_handler.h"
namespace opustags {
class InsertionTagsHandler : public StreamTagsHandler
{
public:
InsertionTagsHandler(
const int streamno,
const std::string &tag_key,
const std::string &tag_value);
std::string get_tag_key() const;
std::string get_tag_value() const;
protected:
bool edit_impl(Tags &) override;
private:
const std::string tag_key;
const std::string tag_value;
};
}

View File

@ -1,16 +0,0 @@
#include "tags_handlers/listing_tags_handler.h"
using namespace opustags;
ListingTagsHandler::ListingTagsHandler(
const int streamno,
std::ostream &output_stream)
: StreamTagsHandler(streamno), output_stream(output_stream)
{
}
void ListingTagsHandler::list_impl(const Tags &tags)
{
for (const auto &tag : tags.get_all())
output_stream << tag.key << "=" << tag.value << "\n";
}

View File

@ -1,20 +0,0 @@
#pragma once
#include <iostream>
#include "tags_handlers/stream_tags_handler.h"
namespace opustags {
class ListingTagsHandler : public StreamTagsHandler
{
public:
ListingTagsHandler(const int streamno, std::ostream &output_stream);
protected:
void list_impl(const Tags &) override;
private:
std::ostream &output_stream;
};
}

View File

@ -1,33 +0,0 @@
#include "tags_handlers/modification_tags_handler.h"
using namespace opustags;
ModificationTagsHandler::ModificationTagsHandler(
const int streamno,
const std::string &tag_key,
const std::string &tag_value)
: StreamTagsHandler(streamno), tag_key(tag_key), tag_value(tag_value)
{
}
std::string ModificationTagsHandler::get_tag_key() const
{
return tag_key;
}
std::string ModificationTagsHandler::get_tag_value() const
{
return tag_value;
}
bool ModificationTagsHandler::edit_impl(Tags &tags)
{
if (tags.contains(tag_key)) {
if (tags.get(tag_key) == tag_value)
return false;
tags.remove(tag_key);
}
tags.add(tag_key, tag_value);
return true;
}

View File

@ -1,26 +0,0 @@
#pragma once
#include "tags_handlers/stream_tags_handler.h"
namespace opustags {
class ModificationTagsHandler : public StreamTagsHandler
{
public:
ModificationTagsHandler(
const int streamno,
const std::string &tag_key,
const std::string &tag_value);
std::string get_tag_key() const;
std::string get_tag_value() const;
protected:
bool edit_impl(Tags &) override;
private:
const std::string tag_key;
const std::string tag_value;
};
}

View File

@ -1,35 +0,0 @@
#include "tags_handlers/removal_tags_handler.h"
#include "tags_handlers_errors.h"
using namespace opustags;
RemovalTagsHandler::RemovalTagsHandler(const int streamno)
: StreamTagsHandler(streamno)
{
}
RemovalTagsHandler::RemovalTagsHandler(
const int streamno, const std::string &tag_key)
: StreamTagsHandler(streamno), tag_key(tag_key)
{
}
std::string RemovalTagsHandler::get_tag_key() const
{
return tag_key;
}
bool RemovalTagsHandler::edit_impl(Tags &tags)
{
if (tag_key.empty()) {
const auto anything_removed = tags.get_all().size() > 0;
tags.clear();
return anything_removed;
} else {
if (!tags.contains(tag_key))
return false;
tags.remove(tag_key);
return true;
}
}

View File

@ -1,22 +0,0 @@
#pragma once
#include "tags_handlers/stream_tags_handler.h"
namespace opustags {
class RemovalTagsHandler : public StreamTagsHandler
{
public:
RemovalTagsHandler(const int streamno);
RemovalTagsHandler(const int streamno, const std::string &tag_key);
std::string get_tag_key() const;
protected:
bool edit_impl(Tags &) override;
private:
const std::string tag_key;
};
}

View File

@ -1,59 +0,0 @@
#include "tags_handlers/stream_tags_handler.h"
#include <iostream>
using namespace opustags;
const int StreamTagsHandler::ALL_STREAMS = -1;
StreamTagsHandler::StreamTagsHandler(const int streamno)
: streamno(streamno), work_finished(false)
{
}
int StreamTagsHandler::get_streamno() const
{
return streamno;
}
bool StreamTagsHandler::relevant(const int streamno)
{
return streamno == this->streamno || this->streamno == ALL_STREAMS;
}
void StreamTagsHandler::list(const int streamno, const Tags &tags)
{
if (!relevant(streamno))
return;
list_impl(tags);
work_finished = this->streamno != ALL_STREAMS;
}
bool StreamTagsHandler::edit(const int streamno, Tags &tags)
{
if (!relevant(streamno))
return false;
const auto ret = edit_impl(tags);
work_finished = this->streamno != ALL_STREAMS;
return ret;
}
bool StreamTagsHandler::done()
{
return work_finished;
}
void StreamTagsHandler::end_of_file()
{
if (!work_finished && streamno != ALL_STREAMS)
std::cerr << "warning: stream " << streamno << " wasn't found" << std::endl;
}
void StreamTagsHandler::list_impl(const Tags &)
{
}
bool StreamTagsHandler::edit_impl(Tags &)
{
return false;
}

View File

@ -1,33 +0,0 @@
#pragma once
#include "tags_handler.h"
namespace opustags {
// Base handler that holds the stream number it's supposed to work with
// and performs the usual boilerplate.
class StreamTagsHandler : public ITagsHandler
{
public:
static const int ALL_STREAMS;
StreamTagsHandler(const int streamno);
int get_streamno() const;
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
void end_of_file() override;
protected:
virtual void list_impl(const Tags &);
virtual bool edit_impl(Tags &);
private:
const int streamno;
bool work_finished;
};
}

View File

@ -1,13 +0,0 @@
#include "tags_handlers_errors.h"
using namespace opustags;
TagAlreadyExistsError::TagAlreadyExistsError(const std::string &tag_key)
: std::runtime_error("Tag already exists: " + tag_key)
{
}
TagDoesNotExistError::TagDoesNotExistError(const std::string &tag_key)
: std::runtime_error("Tag does not exist: " + tag_key)
{
}

View File

@ -1,17 +0,0 @@
#pragma once
#include <stdexcept>
namespace opustags {
struct TagAlreadyExistsError : std::runtime_error
{
TagAlreadyExistsError(const std::string &tag_key);
};
struct TagDoesNotExistError : std::runtime_error
{
TagDoesNotExistError(const std::string &tag_key);
};
}

View File

@ -1,10 +0,0 @@
#pragma once
#include <string>
namespace opustags {
static const std::string version_short = "@VERSION_SHORT@";
static const std::string version_long = "@VERSION_LONG@";
}

View File

@ -1,169 +0,0 @@
#include "actions.h"
#include "catch.h"
#include "ogg.h"
#include "tags_handlers/insertion_tags_handler.h"
#include "tags_handlers/stream_tags_handler.h"
#include <fstream>
#include <cstring>
using namespace opustags;
static bool same_streams(ogg::Decoder &a, ogg::Decoder &b)
{
std::shared_ptr<ogg::Stream> sa, sb;
ogg_packet pa, pb;
int ra, rb;
for (;;) {
sa = a.read_page();
sb = b.read_page();
if (!sa && !sb)
break; // both reached the end at the same time
if (!sa || !sb)
return false; // one stream is shorter than the other
if (sa->stream.serialno != sb->stream.serialno)
return false;
for (;;) {
ra = ogg_stream_packetout(&sa->stream, &pa);
rb = ogg_stream_packetout(&sb->stream, &pb);
if (ra != rb)
return false;
else if (ra == 0)
break;
else if (ra < 0)
throw std::runtime_error("ogg_stream_packetout failed");
// otherwise we got a valid packet on both sides
if (pa.bytes != pb.bytes || pa.b_o_s != pb.b_o_s || pa.e_o_s != pb.e_o_s)
return false;
if (pa.granulepos != pb.granulepos || pa.packetno != pb.packetno)
return false;
if (memcmp(pa.packet, pb.packet, pa.bytes) != 0)
return false;
}
}
return true;
}
static bool same_files(std::istream &a, std::istream &b)
{
static const size_t block = 1024;
char ba[block], bb[block];
while (a && b) {
a.read(ba, block);
b.read(bb, block);
if (a.gcount() != b.gcount())
return false;
if (memcmp(ba, bb, a.gcount()) != 0)
return false;
}
if (a || b)
return false; // one of them is shorter
return true;
}
TEST_CASE("editing an unknown stream should do nothing", "[actions]")
{
std::ifstream in("../tests/samples/beep.ogg");
std::stringstream out;
ogg::Decoder dec(in);
ogg::Encoder enc(out);
StreamTagsHandler editor(StreamTagsHandler::ALL_STREAMS);
edit_tags(dec, enc, editor);
in.clear();
in.seekg(0, in.beg);
out.clear();
out.seekg(0, out.beg);
REQUIRE(same_files(in, out));
}
TEST_CASE("fake editing of an Opus stream should preserve the stream", "[actions]")
{
std::ifstream in("../tests/samples/mystery-beep.ogg");
std::stringstream out;
ogg::Decoder dec(in);
ogg::Encoder enc(out);
StreamTagsHandler editor(StreamTagsHandler::ALL_STREAMS);
edit_tags(dec, enc, editor);
in.clear();
in.seekg(0, in.beg);
out.clear();
out.seekg(0, out.beg);
REQUIRE(same_files(in, out));
}
TEST_CASE("editing a specific stream", "[actions]")
{
std::ifstream in("../tests/samples/mystery-beep.ogg");
std::stringstream out;
{
ogg::Decoder dec(in);
ogg::Encoder enc(out);
InsertionTagsHandler editor(3, "pwnd", "yes");
edit_tags(dec, enc, editor);
}
in.clear();
in.seekg(0, in.beg);
out.clear();
out.seekg(0, out.beg);
{
ogg::Decoder a(in);
ogg::Decoder b(out);
std::shared_ptr<ogg::Stream> s2[6];
for (int i = 0; i < 6; i++) {
// get the headers
a.read_page();
s2[i] = b.read_page();
}
REQUIRE(s2[0]->type == ogg::OPUS_STREAM);
REQUIRE(s2[1]->type == ogg::UNKNOWN_STREAM);
REQUIRE(s2[2]->type == ogg::OPUS_STREAM);
REQUIRE(!s2[0]->tags.contains("pwnd"));
REQUIRE(s2[2]->tags.get("pwnd") == "yes");
REQUIRE(same_streams(a, b));
}
}
class GetLanguages : public StreamTagsHandler {
public:
GetLanguages() : StreamTagsHandler(StreamTagsHandler::ALL_STREAMS) {}
std::vector<std::string> languages;
protected:
void list_impl(const Tags &tags) {
languages.push_back(tags.get("LANGUAGE"));
}
};
TEST_CASE("listing tags", "[actions]")
{
std::ifstream in("../tests/samples/mystery-beep.ogg");
ogg::Decoder dec(in);
GetLanguages lister;
list_tags(dec, lister);
REQUIRE(lister.languages.size() == 2);
REQUIRE(lister.languages[0] == "und");
REQUIRE(lister.languages[1] == "und");
}
TEST_CASE("listing tags, full scan", "[actions]")
{
std::ifstream in("../tests/samples/mystery-beep.ogg");
ogg::Decoder dec(in);
GetLanguages lister;
list_tags(dec, lister, true);
REQUIRE(lister.languages.size() == 2);
REQUIRE(lister.languages[0] == "und");
REQUIRE(lister.languages[1] == "und");
}

View File

@ -1,2 +0,0 @@
#define CATCH_CONFIG_MAIN
#include "catch.h"

View File

@ -1,148 +0,0 @@
#include "ogg.h"
#include "catch.h"
#include <fstream>
using namespace opustags;
TEST_CASE("decoding a single-stream Ogg Opus file", "[ogg]")
{
std::ifstream src("../tests/samples/mystery.ogg");
ogg::Decoder dec(src);
std::shared_ptr<ogg::Stream> s = dec.read_page();
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
std::shared_ptr<ogg::Stream> s2 = dec.read_page();
REQUIRE(s2 == s);
REQUIRE(s->state == ogg::TAGS_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
REQUIRE(s->tags.get("encoder") == "Lavc57.24.102 libopus");
}
TEST_CASE("decoding garbage", "[ogg]")
{
std::ifstream src("Makefile");
ogg::Decoder dec(src);
REQUIRE_THROWS(dec.read_page());
}
TEST_CASE("decoding an Ogg Vorbis file", "[ogg]")
{
std::ifstream src("../tests/samples/beep.ogg");
ogg::Decoder dec(src);
std::shared_ptr<ogg::Stream> s = dec.read_page();
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::UNKNOWN_STREAM);
s = dec.read_page();
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::RAW_READY);
}
TEST_CASE("decoding a multi-stream file", "[ogg]")
{
std::ifstream src("../tests/samples/mystery-beep.ogg");
ogg::Decoder dec(src);
std::shared_ptr<ogg::Stream> first, second, third, s;
s = dec.read_page();
first = s;
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
s = dec.read_page();
second = s;
REQUIRE(s != nullptr);
REQUIRE(s != first);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::UNKNOWN_STREAM);
s = dec.read_page();
third = s;
REQUIRE(s != nullptr);
REQUIRE(s != first);
REQUIRE(s != second);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
s = dec.read_page();
REQUIRE(s == first);
REQUIRE(s->state == ogg::TAGS_READY);
s = dec.read_page();
REQUIRE(s == second);
REQUIRE(s->state == ogg::RAW_READY);
s = dec.read_page();
REQUIRE(s == third);
REQUIRE(s->state == ogg::TAGS_READY);
}
static void craft_stream(std::ostream &out, const std::string &tags_data)
{
ogg::Encoder enc(out);
ogg_stream_state os;
ogg_page og;
ogg_packet op;
ogg_stream_init(&os, 0);
op.packet = reinterpret_cast<unsigned char*>(const_cast<char*>("OpusHead"));
op.bytes = 8;
op.b_o_s = 1;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = 0;
ogg_stream_packetin(&os, &op);
ogg_stream_flush(&os, &og);
enc.write_raw_page(og);
op.packet = reinterpret_cast<unsigned char*>(const_cast<char*>(tags_data.data()));
op.bytes = tags_data.size();
op.b_o_s = 0;
op.e_o_s = 1;
op.granulepos = 0;
op.packetno = 1;
ogg_stream_packetin(&os, &op);
ogg_stream_flush(&os, &og);
enc.write_raw_page(og);
ogg_stream_clear(&os);
}
const char *evil_tags =
"OpusTags"
"\x00\x00\x00\x00" "" /* vendor */
"\x01\x00\x00\x00" /* one comment */
"\xFA\x00\x00\x00" "TITLE=Evil"
/* ^ should be \x0A as the length of the comment is 10 */
;
TEST_CASE("decoding a malicious Ogg Opus file", "[ogg]")
{
std::stringstream buf;
craft_stream(buf, std::string(evil_tags, 8 + 4 + 4 + 4 + 10));
buf.seekg(0, buf.beg);
ogg::Decoder dec(buf);
std::shared_ptr<ogg::Stream> s = dec.read_page();
REQUIRE(s != nullptr);
REQUIRE(s->state == ogg::HEADER_READY);
REQUIRE(s->type == ogg::OPUS_STREAM);
REQUIRE_THROWS(dec.read_page());
}
TEST_CASE("decoding a bad stream", "[ogg]")
{
std::ifstream in("uioprheuio");
REQUIRE_THROWS(std::make_shared<ogg::Decoder>(in));
}
// Encoding is trickier, and might as well be done in actions_test.cc, given
// opustags::edit_tags covers all of Encoder's regular code.

View File

@ -1,205 +0,0 @@
#include "options.h"
#include "catch.h"
#include "tags_handlers/export_tags_handler.h"
#include "tags_handlers/external_edit_tags_handler.h"
#include "tags_handlers/import_tags_handler.h"
#include "tags_handlers/insertion_tags_handler.h"
#include "tags_handlers/listing_tags_handler.h"
#include "tags_handlers/modification_tags_handler.h"
#include "tags_handlers/removal_tags_handler.h"
#include <memory>
using namespace opustags;
static std::unique_ptr<char[]> string_to_uptr(const std::string &str)
{
auto ret = std::make_unique<char[]>(str.size() + 1);
for (size_t i = 0; i < str.size(); i++)
ret[i] = str[i];
ret[str.size()] = 0;
return ret;
}
static Options retrieve_options(
std::vector<std::string> args, bool fake_input_path = true)
{
// need to pass non-const char*, but we got const objects. make copies
std::vector<std::unique_ptr<char[]>> arg_holders;
arg_holders.push_back(string_to_uptr("fake/path/to/program"));
for (size_t i = 0; i < args.size(); i++)
arg_holders.push_back(string_to_uptr(args[i]));
if (fake_input_path)
arg_holders.push_back(string_to_uptr("fake/path/to/input"));
auto plain_args = std::make_unique<char*[]>(arg_holders.size());
for (size_t i = 0; i < arg_holders.size(); i++)
plain_args[i] = arg_holders[i].get();
return parse_args(arg_holders.size(), plain_args.get());
}
// retrieve a specific TagsHandler from the CompositeTagsHandler in options
template<typename T> static T *get_handler(
const Options &options, const size_t index)
{
const auto handlers = options.tags_handler.get_handlers();
const auto handler = dynamic_cast<T*>(handlers.at(index).get());
REQUIRE(handler);
return handler;
}
TEST_CASE("option parsing", "[options]")
{
SECTION("--help") {
REQUIRE(retrieve_options({"--help"}).show_help);
REQUIRE(retrieve_options({"-h"}).show_help);
REQUIRE(!retrieve_options({}).show_help);
}
SECTION("--version") {
REQUIRE(retrieve_options({"--version"}).show_version);
REQUIRE(retrieve_options({"-V"}).show_version);
REQUIRE(!retrieve_options({}).show_version);
}
SECTION("--overwrite") {
REQUIRE(retrieve_options({"--overwrite"}).overwrite);
REQUIRE(retrieve_options({"-y"}).overwrite);
REQUIRE(!retrieve_options({}).overwrite);
}
SECTION("--full") {
REQUIRE(retrieve_options({"--full"}).full);
REQUIRE(!retrieve_options({}).full);
}
SECTION("--in-place") {
REQUIRE(!retrieve_options({}).in_place);
REQUIRE(retrieve_options({}).path_out.empty());
REQUIRE(retrieve_options({"--in-place", "ABC"}, false).in_place);
REQUIRE(
retrieve_options({"--in-place", "ABC"}, false).path_out == ".otmp");
REQUIRE(retrieve_options({"--in-place"}).in_place);
REQUIRE(retrieve_options({"--in-place"}).path_out == ".otmp");
REQUIRE(retrieve_options({"--in-place=ABC"}).in_place);
REQUIRE(retrieve_options({"--in-place=ABC"}).path_out == "ABC");
REQUIRE(retrieve_options({"-i", "ABC"}, false).in_place);
REQUIRE(retrieve_options({"-i", "ABC"}, false).path_out == ".otmp");
REQUIRE(retrieve_options({"-i"}).in_place);
REQUIRE(retrieve_options({"-i"}).path_out == ".otmp");
REQUIRE(retrieve_options({"-iABC"}).in_place);
REQUIRE(retrieve_options({"-iABC"}).path_out == "ABC");
}
SECTION("input") {
REQUIRE_THROWS(retrieve_options({}, false));
REQUIRE_THROWS(retrieve_options({""}, false));
REQUIRE(retrieve_options({"input"}, false).path_in == "input");
REQUIRE_THROWS(retrieve_options({"input", "extra argument"}, false));
}
SECTION("--output") {
REQUIRE(retrieve_options({"--output", "ABC"}).path_out == "ABC");
REQUIRE(retrieve_options({"-o", "ABC"}).path_out == "ABC");
REQUIRE_THROWS(retrieve_options({"--output", ""}));
REQUIRE_THROWS(retrieve_options({"-o", ""}));
}
SECTION("--import") {
get_handler<ImportTagsHandler>(retrieve_options({"--import"}), 0);
}
SECTION("--export") {
get_handler<ExportTagsHandler>(retrieve_options({"--export"}), 0);
}
SECTION("--edit") {
get_handler<ExternalEditTagsHandler>(retrieve_options({"--edit"}), 0);
get_handler<ExternalEditTagsHandler>(retrieve_options({"-e"}), 0);
}
SECTION("--list") {
get_handler<ListingTagsHandler>(retrieve_options({"--list"}), 0);
get_handler<ListingTagsHandler>(retrieve_options({"-l"}), 0);
// TODO:
// test enabling / disabling colors, which should be
// contained inside the state of ListingTagsHandler
// TODO:
// it should be the default operation for readonly mode - test this too
}
SECTION("--delete-all") {
REQUIRE(get_handler<RemovalTagsHandler>(
retrieve_options({"--delete-all"}), 0)->get_tag_key().empty());
REQUIRE(get_handler<RemovalTagsHandler>(
retrieve_options({"-D"}), 0)->get_tag_key().empty());
}
SECTION("--delete") {
const auto opts = retrieve_options({"--delete", "A", "-d", "B"});
REQUIRE(get_handler<RemovalTagsHandler>(opts, 0)->get_tag_key() == "A");
REQUIRE(get_handler<RemovalTagsHandler>(opts, 1)->get_tag_key() == "B");
REQUIRE_THROWS(retrieve_options({"--delete", "invalid="}));
}
SECTION("--add") {
const auto opts = retrieve_options({"--add", "A=1", "-a", "B=2"});
const auto handler1 = get_handler<InsertionTagsHandler>(opts, 0);
const auto handler2 = get_handler<InsertionTagsHandler>(opts, 1);
REQUIRE(handler1->get_tag_key() == "A");
REQUIRE(handler1->get_tag_value() == "1");
REQUIRE(handler2->get_tag_key() == "B");
REQUIRE(handler2->get_tag_value() == "2");
REQUIRE_THROWS(retrieve_options({"--add", "invalid"}));
}
SECTION("--set") {
const auto opts = retrieve_options({"--set", "A=1", "-s", "B=2"});
const auto handler1 = get_handler<ModificationTagsHandler>(opts, 0);
const auto handler2 = get_handler<ModificationTagsHandler>(opts, 1);
REQUIRE(handler1->get_tag_key() == "A");
REQUIRE(handler1->get_tag_value() == "1");
REQUIRE(handler2->get_tag_key() == "B");
REQUIRE(handler2->get_tag_value() == "2");
REQUIRE_THROWS(retrieve_options({"--set", "invalid"}));
}
SECTION("--stream") {
// by default, use all the streams
REQUIRE(
get_handler<RemovalTagsHandler>(retrieve_options({"-d", "xyz"}), 0)
->get_streamno() == StreamTagsHandler::ALL_STREAMS);
// ...unless the user supplies an option to use a specific stream
REQUIRE(
get_handler<RemovalTagsHandler>(
retrieve_options({"--stream", "1", "-d", "xyz"}), 0)
->get_streamno() == 1);
// ...which can be combined multiple times
{
const auto opts = retrieve_options({
"--stream", "1",
"-d", "xyz",
"--stream", "2",
"-d", "abc"});
const auto handler1 = get_handler<RemovalTagsHandler>(opts, 0);
const auto handler2 = get_handler<RemovalTagsHandler>(opts, 1);
REQUIRE(handler1->get_streamno() == 1);
REQUIRE(handler2->get_streamno() == 2);
}
// ...or contain comma separated values
{
const auto opts = retrieve_options({
"--stream", "1,2", "-d", "xyz"});
const auto handler1 = get_handler<RemovalTagsHandler>(opts, 0);
const auto handler2 = get_handler<RemovalTagsHandler>(opts, 1);
REQUIRE(handler1->get_streamno() == 1);
REQUIRE(handler2->get_streamno() == 2);
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,96 +0,0 @@
#include "tags_handlers/composite_tags_handler.h"
#include "catch.h"
using namespace opustags;
namespace
{
struct DummyTagsHandler : ITagsHandler
{
DummyTagsHandler();
bool relevant(const int streamno) override;
void list(const int streamno, const Tags &) override;
bool edit(const int streamno, Tags &) override;
bool done() override;
bool relevant_ret, edit_ret, done_ret, list_fired;
};
}
DummyTagsHandler::DummyTagsHandler()
: relevant_ret(true), edit_ret(true), done_ret(true), list_fired(false)
{
}
bool DummyTagsHandler::relevant(const int)
{
return relevant_ret;
}
void DummyTagsHandler::list(const int, const Tags &)
{
list_fired = true;
}
bool DummyTagsHandler::edit(const int, Tags &)
{
return edit_ret;
}
bool DummyTagsHandler::done()
{
return done_ret;
}
TEST_CASE("composite tags handler", "[tags_handlers]")
{
auto handler1 = std::make_shared<DummyTagsHandler>();
auto handler2 = std::make_shared<DummyTagsHandler>();
CompositeTagsHandler composite_handler;
composite_handler.add_handler(handler1);
composite_handler.add_handler(handler2);
SECTION("relevance") {
const int dummy_streamno = 1;
handler1->relevant_ret = true;
handler2->relevant_ret = true;
REQUIRE(composite_handler.relevant(dummy_streamno));
handler1->relevant_ret = false;
REQUIRE(composite_handler.relevant(dummy_streamno));
handler2->relevant_ret = false;
REQUIRE(!composite_handler.relevant(dummy_streamno));
}
SECTION("listing") {
const int dummy_streamno = 1;
Tags dummy_tags;
REQUIRE(!handler1->list_fired);
REQUIRE(!handler2->list_fired);
composite_handler.list(dummy_streamno, dummy_tags);
REQUIRE(handler1->list_fired);
REQUIRE(handler2->list_fired);
}
SECTION("editing") {
const int dummy_streamno = 1;
Tags dummy_tags;
handler1->edit_ret = true;
handler2->edit_ret = true;
REQUIRE(composite_handler.edit(dummy_streamno, dummy_tags));
handler1->edit_ret = false;
REQUIRE(composite_handler.edit(dummy_streamno, dummy_tags));
handler2->edit_ret = false;
REQUIRE(!composite_handler.edit(dummy_streamno, dummy_tags));
}
SECTION("finish") {
handler1->done_ret = true;
handler2->done_ret = true;
REQUIRE(composite_handler.done());
handler1->done_ret = false;
REQUIRE(!composite_handler.done());
handler2->done_ret = false;
REQUIRE(!composite_handler.done());
}
}

View File

@ -1,23 +0,0 @@
#include "tags_handlers/export_tags_handler.h"
#include "catch.h"
#include <sstream>
using namespace opustags;
TEST_CASE("export tags handler", "[tags_handlers]")
{
std::stringstream ss;
ExportTagsHandler handler(ss);
handler.list(1, {{{"a", "value1"}, {"b", "value2"}}});
handler.list(2, {{{"c", "value3"}, {"d", "value4"}}});
REQUIRE(ss.str() == R"([Stream 1]
a=value1
b=value2
[Stream 2]
c=value3
d=value4
)");
}

View File

@ -1,142 +0,0 @@
#include "tags_handlers/import_tags_handler.h"
#include "catch.h"
#include <sstream>
using namespace opustags;
TEST_CASE("import tags handler", "[tags_handlers]")
{
SECTION("tags for streams not mentioned in import get emptied")
{
Tags tags = {{{"remove", "me"}}};
std::stringstream ss;
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().empty());
}
SECTION("streams that do not exist in the input file are ignored")
{
// TODO: rather than ignoring, at least print a warning
// requires #6
Tags tags;
std::stringstream ss;
ss << "[Stream 5]\nkey = value\n";
ImportTagsHandler handler(ss);
REQUIRE(!handler.edit(1, tags));
REQUIRE(tags.get_all().empty());
}
SECTION("adding unique tags")
{
Tags tags;
std::stringstream ss;
ss << "[Stream 1]\nkey = value\nkey2 = value2\n";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get("key") == "value");
REQUIRE(tags.get("key2") == "value2");
}
SECTION("adding tags with the same key")
{
Tags tags;
std::stringstream ss;
ss << "[Stream 1]\nkey = value1\nkey = value2\n";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get_all().at(0).key == "key");
REQUIRE(tags.get_all().at(1).key == "key");
REQUIRE(tags.get_all().at(0).value == "value1");
REQUIRE(tags.get_all().at(1).value == "value2");
}
SECTION("overwriting existing tags")
{
Tags tags = {{{"remove", "me"}}};
std::stringstream ss;
ss << "[Stream 1]\nkey = value\nkey2 = value2\n";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get("key") == "value");
REQUIRE(tags.get("key2") == "value2");
}
SECTION("various whitespace issues are worked around")
{
Tags tags;
std::stringstream ss;
ss << " [StrEaM 1] \n\n key = value \nkey2=value2\n";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get("key") == "value");
REQUIRE(tags.get("key2") == "value2");
}
SECTION("not specifying stream assumes first stream")
{
Tags tags;
std::stringstream ss;
ss << "key=value";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 1);
REQUIRE(tags.get("key") == "value");
}
SECTION("multiple streams")
{
Tags tags1;
Tags tags2;
std::stringstream ss;
ss << "[stream 1]\nkey=value\n[stream 2]\nkey2=value2";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags1));
REQUIRE(handler.edit(2, tags2));
REQUIRE(tags1.get_all().size() == 1);
REQUIRE(tags2.get_all().size() == 1);
REQUIRE(tags1.get("key") == "value");
REQUIRE(tags2.get("key2") == "value2");
}
SECTION("sections listed twice are concatenated")
{
Tags tags;
std::stringstream ss;
ss << "[stream 1]\nkey=value\n"
"[stream 2]\nkey=irrelevant\n"
"[Stream 1]\nkey2=value2";
ImportTagsHandler handler(ss);
REQUIRE(handler.edit(1, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get("key") == "value");
REQUIRE(tags.get("key2") == "value2");
}
SECTION("weird input throws errors - malformed section headers")
{
Tags tags;
std::stringstream ss;
ss << "[stream huh]\n";
REQUIRE_THROWS({
ImportTagsHandler handler(ss);
handler.edit(1, tags);
});
}
SECTION("weird input throws errors - malformed lines")
{
Tags tags;
std::stringstream ss;
ss << "tag huh\n";
REQUIRE_THROWS({
ImportTagsHandler handler(ss);
handler.edit(1, tags);
});
}
}

View File

@ -1,22 +0,0 @@
#include "tags_handlers/insertion_tags_handler.h"
#include "catch.h"
using namespace opustags;
TEST_CASE("insertion tags handler", "[tags_handlers]")
{
Tags tags;
const auto streamno = 1;
const auto expected_tag_key = "tag_key";
const auto expected_tag_value = "tag_value";
InsertionTagsHandler handler(
streamno, expected_tag_key, expected_tag_value);
REQUIRE(handler.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 1);
REQUIRE(tags.get(expected_tag_key) == expected_tag_value);
REQUIRE(handler.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get(expected_tag_key) == expected_tag_value);
}

View File

@ -1,22 +0,0 @@
#include "tags_handlers/listing_tags_handler.h"
#include "catch.h"
#include <sstream>
using namespace opustags;
TEST_CASE("listing tags handler", "[tags_handlers]")
{
const auto streamno = 1;
Tags tags;
tags.add("z", "value1");
tags.add("a", "value2");
tags.add("y", "value3");
tags.add("c", "value4");
std::stringstream ss;
ListingTagsHandler handler(streamno, ss);
handler.list(streamno, tags);
REQUIRE(ss.str() == "z=value1\na=value2\ny=value3\nc=value4\n");
}

View File

@ -1,33 +0,0 @@
#include "tags_handlers/modification_tags_handler.h"
#include "catch.h"
using namespace opustags;
TEST_CASE("modification tags handler", "[tags_handlers]")
{
const auto streamno = 1;
const auto first_tag_key = "tag_key";
const auto other_tag_key = "other_tag_key";
const auto dummy_value = "dummy";
const auto new_value = "dummy 2";
Tags tags;
tags.add(first_tag_key, dummy_value);
REQUIRE(tags.get_all().size() == 1);
// setting nonexistent keys adds them
ModificationTagsHandler handler1(streamno, other_tag_key, dummy_value);
REQUIRE(handler1.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get(other_tag_key) == dummy_value);
// setting existing keys overrides their values
ModificationTagsHandler handler2(streamno, other_tag_key, new_value);
REQUIRE(handler2.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 2);
REQUIRE(tags.get(other_tag_key) == new_value);
// setting existing keys reports no modifications if values are the same
REQUIRE(!handler2.edit(streamno, tags));
}

View File

@ -1,43 +0,0 @@
#include "tags_handlers/removal_tags_handler.h"
#include "catch.h"
using namespace opustags;
TEST_CASE("removal tags handler", "[tags_handlers]")
{
const auto streamno = 1;
SECTION("removing a single tag")
{
const auto expected_tag_key = "tag_key";
const auto other_tag_key = "other_tag_key";
const auto dummy_value = "dummy";
RemovalTagsHandler handler(streamno, expected_tag_key);
Tags tags;
tags.add(expected_tag_key, dummy_value);
tags.add(other_tag_key, dummy_value);
REQUIRE(tags.get_all().size() == 2);
REQUIRE(handler.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 1);
REQUIRE(tags.contains(other_tag_key));
REQUIRE(!handler.edit(streamno, tags));
}
SECTION("removing all tags")
{
RemovalTagsHandler handler(streamno);
Tags tags;
tags.add("z", "value1");
tags.add("a", "value2");
tags.add("y", "value3");
tags.add("c", "value4");
REQUIRE(tags.get_all().size() == 4);
REQUIRE(handler.edit(streamno, tags));
REQUIRE(tags.get_all().size() == 0);
REQUIRE(!handler.edit(streamno, tags));
}
}

View File

@ -1,92 +0,0 @@
#include "tags_handlers/stream_tags_handler.h"
#include "catch.h"
using namespace opustags;
namespace
{
class DummyTagsHandler final : public StreamTagsHandler
{
public:
DummyTagsHandler(const int streamno);
protected:
void list_impl(const Tags &) override;
bool edit_impl(Tags &) override;
public:
bool list_fired, edit_fired;
};
}
DummyTagsHandler::DummyTagsHandler(const int streamno)
: StreamTagsHandler(streamno), list_fired(false), edit_fired(false)
{
}
void DummyTagsHandler::list_impl(const Tags &)
{
list_fired = true;
}
bool DummyTagsHandler::edit_impl(Tags &)
{
edit_fired = true;
return true;
}
TEST_CASE("stream-based tags handler", "[tags_handlers]")
{
SECTION("concrete stream") {
const auto relevant_stream_number = 1;
const auto irrelevant_stream_number = 2;
Tags dummy_tags;
DummyTagsHandler handler(relevant_stream_number);
SECTION("relevance") {
REQUIRE(!handler.relevant(irrelevant_stream_number));
REQUIRE(handler.relevant(relevant_stream_number));
}
SECTION("listing") {
handler.list(irrelevant_stream_number, dummy_tags);
REQUIRE(!handler.list_fired);
handler.list(relevant_stream_number, dummy_tags);
REQUIRE(handler.list_fired);
}
SECTION("Editing") {
REQUIRE(!handler.edit(irrelevant_stream_number, dummy_tags));
REQUIRE(!handler.edit_fired);
REQUIRE(handler.edit(relevant_stream_number, dummy_tags));
REQUIRE(handler.edit_fired);
}
SECTION("finish through listing") {
REQUIRE(!handler.edit(irrelevant_stream_number, dummy_tags));
REQUIRE(!handler.done());
REQUIRE(handler.edit(relevant_stream_number, dummy_tags));
REQUIRE(handler.done());
}
SECTION("finish through editing") {
handler.list(irrelevant_stream_number, dummy_tags);
REQUIRE(!handler.done());
handler.list(relevant_stream_number, dummy_tags);
REQUIRE(handler.done());
}
}
SECTION("any stream") {
Tags dummy_tags;
DummyTagsHandler handler(StreamTagsHandler::ALL_STREAMS);
REQUIRE(handler.relevant(1));
REQUIRE(handler.relevant(2));
REQUIRE(handler.relevant(3));
REQUIRE(!handler.done());
handler.list(1, dummy_tags);
REQUIRE(!handler.done());
handler.list(2, dummy_tags);
REQUIRE(!handler.done());
}
}

View File

@ -1,85 +0,0 @@
#include "tags.h"
#include "catch.h"
using namespace opustags;
TEST_CASE("tag manipulation", "[tags]")
{
SECTION("basic operations") {
Tags tags;
REQUIRE(!tags.contains("a"));
tags.add("a", "1");
REQUIRE(tags.get("a") == "1");
REQUIRE(tags.contains("a"));
tags.remove("a");
REQUIRE(!tags.contains("a"));
REQUIRE_THROWS(tags.get("a"));
}
SECTION("clearing") {
Tags tags;
tags.add("a", "1");
tags.add("b", "2");
REQUIRE(tags.get_all().size() == 2);
tags.clear();
REQUIRE(tags.get_all().empty());
}
SECTION("maintaing order of insertions") {
Tags tags;
tags.add("z", "1");
tags.add("y", "2");
tags.add("x", "3");
tags.add("y", "4");
REQUIRE(tags.get_all().size() == 4);
REQUIRE(tags.get_all()[0].key == "z");
REQUIRE(tags.get_all()[1].key == "y");
REQUIRE(tags.get_all()[2].key == "x");
REQUIRE(tags.get_all()[3].key == "y");
tags.remove("z");
REQUIRE(tags.get_all().size() == 3);
REQUIRE(tags.get_all()[0].key == "y");
REQUIRE(tags.get_all()[1].key == "x");
REQUIRE(tags.get_all()[2].key == "y");
tags.add("gamma", "5");
REQUIRE(tags.get_all().size() == 4);
REQUIRE(tags.get_all()[0].key == "y");
REQUIRE(tags.get_all()[1].key == "x");
REQUIRE(tags.get_all()[2].key == "y");
REQUIRE(tags.get_all()[3].key == "gamma");
}
SECTION("key to multiple values") {
// ARTIST is set once per artist.
// https://www.xiph.org/vorbis/doc/v-comment.html
Tags tags;
tags.add("ARTIST", "You");
tags.add("ARTIST", "Me");
REQUIRE(tags.get_all().size() == 2);
}
SECTION("removing multivalues should remove all of them") {
Tags tags;
tags.add("ARTIST", "You");
tags.add("ARTIST", "Me");
tags.remove("ARTIST");
REQUIRE(tags.get_all().empty());
}
SECTION("tag parsing") {
Tag t = parse_tag("TITLE=Foo=Bar");
REQUIRE(t.key == "TITLE");
REQUIRE(t.value == "Foo=Bar");
}
SECTION("case insensitiveness for keys") {
Tags tags;
tags.add("TITLE", "Boop");
REQUIRE(tags.get("title") == "Boop");
tags.remove("titLE");
REQUIRE(tags.get_all().empty());
}
}