mirror of
https://github.com/fmang/opustags.git
synced 2025-07-06 17:47:51 +02:00
Compare commits
102 Commits
Author | SHA1 | Date | |
---|---|---|---|
2ef9a825da | |||
4de88d0ed2 | |||
0624376fcc | |||
52e4a8ca58 | |||
dd0656cb07 | |||
d3b4a389bc | |||
410708e252 | |||
dad987a8da | |||
76bf95a74c | |||
2d7f812119 | |||
1ff2553f5f | |||
eb968dc513 | |||
a21331057b | |||
5ae31c9bc9 | |||
2957fa3538 | |||
74b9cade48 | |||
8e9204420b | |||
7b616aa671 | |||
159340926a | |||
e1d954388e | |||
d48573ceef | |||
d8dcc38777 | |||
7d20fc70b2 | |||
a210d1229e | |||
e60f7f84a0 | |||
a3daa0f108 | |||
84a8d14ae0 | |||
74904fd516 | |||
fc2a4cb41c | |||
1c2232c197 | |||
f26de884aa | |||
0d91429435 | |||
54571e8bc3 | |||
a06f337a63 | |||
b5d2e03a7b | |||
f726eaeb91 | |||
8f5a6bb534 | |||
7f7766f175 | |||
be3984423f | |||
2443490b0b | |||
a96cc9c222 | |||
cd550d8d80 | |||
4cd0e34d0d | |||
0e2b1fec9c | |||
817ba5cba6 | |||
20663a847f | |||
40bbc90786 | |||
d21517de94 | |||
a78906a8d4 | |||
32c5e2b0a6 | |||
449235ed5a | |||
661c469bd6 | |||
7f984e1492 | |||
4f1070f272 | |||
b4dc544031 | |||
5e19657d25 | |||
898846f1f4 | |||
c19233236a | |||
84a0ce55af | |||
95ddd2e7da | |||
9fd629bcc3 | |||
53fbf533fb | |||
0e3dfbe381 | |||
8364667b4c | |||
4d9ee3bf88 | |||
f941464c61 | |||
f469d76e62 | |||
acbf99d276 | |||
fe7f23576a | |||
c101041bd7 | |||
e5e7952b89 | |||
221c314625 | |||
7b6ad95b35 | |||
24c2dae49e | |||
02c966bacb | |||
4f5d33491b | |||
d9f123c84c | |||
a67f8c1472 | |||
d7b187ec59 | |||
fca4f81eac | |||
b362f6565f | |||
6832cbf3f5 | |||
2ee7702b9d | |||
aeca60d128 | |||
1d8d96cdee | |||
094bed2b35 | |||
b62ac98ca5 | |||
12327e6f68 | |||
326c164e09 | |||
a84aeaea96 | |||
8d1ea8d32d | |||
ff17d35531 | |||
c2d6763e2b | |||
6baf120a94 | |||
f1109dd04f | |||
390c9268a7 | |||
a8a6552f29 | |||
e198b5f9ef | |||
e474ec8420 | |||
15c9f187a5 | |||
3a320a7b39 | |||
5cdcb5457f |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/build
|
||||
tests/catch.h
|
||||
src/version.h
|
78
CMakeLists.txt
Normal file
78
CMakeLists.txt
Normal file
@ -0,0 +1,78 @@
|
||||
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
23
Makefile
@ -1,23 +0,0 @@
|
||||
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
|
42
README.md
42
README.md
@ -3,53 +3,43 @@ 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 DESTDIR=/usr/local install
|
||||
make install
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Usage: opustags --help
|
||||
opustags [OPTIONS] FILE
|
||||
opustags OPTIONS FILE -o FILE
|
||||
opustags [OPTIONS] INPUT
|
||||
opustags [OPTIONS] -o OUTPUT INPUT
|
||||
|
||||
Options:
|
||||
-h, --help print this help
|
||||
-o, --output write the modified tags to a file
|
||||
-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
|
||||
-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!
|
||||
-S, --set-all read the fields from stdin
|
||||
--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
|
||||
|
||||
See the man page, `opustags.1`, for extensive documentation.
|
||||
|
209
doc/opustags.1
Normal file
209
doc/opustags.1
Normal file
@ -0,0 +1,209 @@
|
||||
.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>.
|
23
modules/FindOgg.cmake
Normal file
23
modules/FindOgg.cmake
Normal file
@ -0,0 +1,23 @@
|
||||
# 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
108
opustags.1
@ -1,108 +0,0 @@
|
||||
.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 you’re 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 don’t conflict with each other.
|
||||
However, they aren’t 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 can’t 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 doesn’t 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 doesn’t 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 won’t 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
513
opustags.c
@ -1,513 +0,0 @@
|
||||
#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;
|
||||
}
|
83
src/actions.cc
Normal file
83
src/actions.cc
Normal file
@ -0,0 +1,83 @@
|
||||
#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();
|
||||
}
|
33
src/actions.h
Normal file
33
src/actions.h
Normal file
@ -0,0 +1,33 @@
|
||||
#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 &);
|
||||
|
||||
}
|
29
src/log.h
Normal file
29
src/log.h
Normal file
@ -0,0 +1,29 @@
|
||||
#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;
|
||||
|
||||
}
|
88
src/main.cc
Normal file
88
src/main.cc
Normal file
@ -0,0 +1,88 @@
|
||||
#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;
|
||||
}
|
308
src/ogg.cc
Normal file
308
src/ogg.cc
Normal file
@ -0,0 +1,308 @@
|
||||
#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(¤t_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, ¤t_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
Normal file
129
src/ogg.h
Normal file
@ -0,0 +1,129 @@
|
||||
#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);
|
||||
};
|
||||
}
|
||||
}
|
199
src/options.cc
Normal file
199
src/options.cc
Normal file
@ -0,0 +1,199 @@
|
||||
#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;
|
||||
}
|
33
src/options.h
Normal file
33
src/options.h
Normal file
@ -0,0 +1,33 @@
|
||||
#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);
|
||||
}
|
103
src/tags.cc
Normal file
103
src/tags.cc
Normal file
@ -0,0 +1,103 @@
|
||||
#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();
|
||||
}
|
49
src/tags.h
Normal file
49
src/tags.h
Normal file
@ -0,0 +1,49 @@
|
||||
#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
|
||||
|
||||
}
|
50
src/tags_handler.h
Normal file
50
src/tags_handler.h
Normal file
@ -0,0 +1,50 @@
|
||||
#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() {}
|
||||
};
|
||||
|
||||
}
|
44
src/tags_handlers/composite_tags_handler.cc
Normal file
44
src/tags_handlers/composite_tags_handler.cc
Normal file
@ -0,0 +1,44 @@
|
||||
#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;
|
||||
}
|
25
src/tags_handlers/composite_tags_handler.h
Normal file
25
src/tags_handlers/composite_tags_handler.h
Normal file
@ -0,0 +1,25 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
31
src/tags_handlers/export_tags_handler.cc
Normal file
31
src/tags_handlers/export_tags_handler.cc
Normal file
@ -0,0 +1,31 @@
|
||||
#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;
|
||||
}
|
22
src/tags_handlers/export_tags_handler.h
Normal file
22
src/tags_handlers/export_tags_handler.h
Normal file
@ -0,0 +1,22 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
22
src/tags_handlers/external_edit_tags_handler.cc
Normal file
22
src/tags_handlers/external_edit_tags_handler.cc
Normal file
@ -0,0 +1,22 @@
|
||||
#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;
|
||||
}
|
17
src/tags_handlers/external_edit_tags_handler.h
Normal file
17
src/tags_handlers/external_edit_tags_handler.h
Normal file
@ -0,0 +1,17 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
67
src/tags_handlers/import_tags_handler.cc
Normal file
67
src/tags_handlers/import_tags_handler.cc
Normal file
@ -0,0 +1,67 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
27
src/tags_handlers/import_tags_handler.h
Normal file
27
src/tags_handlers/import_tags_handler.h
Normal file
@ -0,0 +1,27 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
28
src/tags_handlers/insertion_tags_handler.cc
Normal file
28
src/tags_handlers/insertion_tags_handler.cc
Normal file
@ -0,0 +1,28 @@
|
||||
#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;
|
||||
}
|
26
src/tags_handlers/insertion_tags_handler.h
Normal file
26
src/tags_handlers/insertion_tags_handler.h
Normal file
@ -0,0 +1,26 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
16
src/tags_handlers/listing_tags_handler.cc
Normal file
16
src/tags_handlers/listing_tags_handler.cc
Normal file
@ -0,0 +1,16 @@
|
||||
#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";
|
||||
}
|
20
src/tags_handlers/listing_tags_handler.h
Normal file
20
src/tags_handlers/listing_tags_handler.h
Normal file
@ -0,0 +1,20 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
33
src/tags_handlers/modification_tags_handler.cc
Normal file
33
src/tags_handlers/modification_tags_handler.cc
Normal file
@ -0,0 +1,33 @@
|
||||
#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;
|
||||
}
|
26
src/tags_handlers/modification_tags_handler.h
Normal file
26
src/tags_handlers/modification_tags_handler.h
Normal file
@ -0,0 +1,26 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
35
src/tags_handlers/removal_tags_handler.cc
Normal file
35
src/tags_handlers/removal_tags_handler.cc
Normal file
@ -0,0 +1,35 @@
|
||||
#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;
|
||||
}
|
||||
}
|
22
src/tags_handlers/removal_tags_handler.h
Normal file
22
src/tags_handlers/removal_tags_handler.h
Normal file
@ -0,0 +1,22 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
59
src/tags_handlers/stream_tags_handler.cc
Normal file
59
src/tags_handlers/stream_tags_handler.cc
Normal file
@ -0,0 +1,59 @@
|
||||
#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;
|
||||
}
|
33
src/tags_handlers/stream_tags_handler.h
Normal file
33
src/tags_handlers/stream_tags_handler.h
Normal file
@ -0,0 +1,33 @@
|
||||
#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;
|
||||
};
|
||||
|
||||
}
|
13
src/tags_handlers_errors.cc
Normal file
13
src/tags_handlers_errors.cc
Normal file
@ -0,0 +1,13 @@
|
||||
#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)
|
||||
{
|
||||
}
|
17
src/tags_handlers_errors.h
Normal file
17
src/tags_handlers_errors.h
Normal file
@ -0,0 +1,17 @@
|
||||
#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);
|
||||
};
|
||||
|
||||
}
|
10
src/version.h.in
Normal file
10
src/version.h.in
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace opustags {
|
||||
|
||||
static const std::string version_short = "@VERSION_SHORT@";
|
||||
static const std::string version_long = "@VERSION_LONG@";
|
||||
|
||||
}
|
169
tests/actions_test.cc
Normal file
169
tests/actions_test.cc
Normal file
@ -0,0 +1,169 @@
|
||||
#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");
|
||||
}
|
2
tests/main.cc
Normal file
2
tests/main.cc
Normal file
@ -0,0 +1,2 @@
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch.h"
|
148
tests/ogg_test.cc
Normal file
148
tests/ogg_test.cc
Normal file
@ -0,0 +1,148 @@
|
||||
#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.
|
205
tests/options_test.cc
Normal file
205
tests/options_test.cc
Normal file
@ -0,0 +1,205 @@
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
BIN
tests/samples/beep.ogg
Normal file
BIN
tests/samples/beep.ogg
Normal file
Binary file not shown.
BIN
tests/samples/mystery-beep.ogg
Normal file
BIN
tests/samples/mystery-beep.ogg
Normal file
Binary file not shown.
BIN
tests/samples/mystery.ogg
Normal file
BIN
tests/samples/mystery.ogg
Normal file
Binary file not shown.
96
tests/tags_handlers/composite_tags_handler_test.cc
Normal file
96
tests/tags_handlers/composite_tags_handler_test.cc
Normal file
@ -0,0 +1,96 @@
|
||||
#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());
|
||||
}
|
||||
}
|
23
tests/tags_handlers/export_tags_handler_test.cc
Normal file
23
tests/tags_handlers/export_tags_handler_test.cc
Normal file
@ -0,0 +1,23 @@
|
||||
#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
|
||||
|
||||
)");
|
||||
}
|
142
tests/tags_handlers/import_tags_handler_test.cc
Normal file
142
tests/tags_handlers/import_tags_handler_test.cc
Normal file
@ -0,0 +1,142 @@
|
||||
#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);
|
||||
});
|
||||
}
|
||||
}
|
22
tests/tags_handlers/insertion_tags_handler_test.cc
Normal file
22
tests/tags_handlers/insertion_tags_handler_test.cc
Normal file
@ -0,0 +1,22 @@
|
||||
#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);
|
||||
}
|
22
tests/tags_handlers/listing_tags_handler_test.cc
Normal file
22
tests/tags_handlers/listing_tags_handler_test.cc
Normal file
@ -0,0 +1,22 @@
|
||||
#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");
|
||||
}
|
33
tests/tags_handlers/modification_tags_handler_test.cc
Normal file
33
tests/tags_handlers/modification_tags_handler_test.cc
Normal file
@ -0,0 +1,33 @@
|
||||
#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));
|
||||
}
|
43
tests/tags_handlers/removal_tags_handler_test.cc
Normal file
43
tests/tags_handlers/removal_tags_handler_test.cc
Normal file
@ -0,0 +1,43 @@
|
||||
#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));
|
||||
}
|
||||
}
|
92
tests/tags_handlers/stream_tags_handler_test.cc
Normal file
92
tests/tags_handlers/stream_tags_handler_test.cc
Normal file
@ -0,0 +1,92 @@
|
||||
#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());
|
||||
}
|
||||
}
|
85
tests/tags_test.cc
Normal file
85
tests/tags_test.cc
Normal file
@ -0,0 +1,85 @@
|
||||
#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());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user