Compare commits

...

108 Commits

Author SHA1 Message Date
Martchus 12620c5768 Ensure desktop file name is set when initializing GUI 2024-04-12 01:08:59 +02:00
Martchus 854bd13efc Update translations 2024-02-28 21:43:03 +01:00
Martchus 22aa2f0851 Handle TRACKTOTAL/DISCTOTAL/PARTTOTAL fields in Vorbis Comments
See the corresponding tagparser commit
2024-02-28 21:39:52 +01:00
Martchus abc80c24ff Mention key used for signing binaries in README 2024-02-07 19:25:15 +01:00
Martchus 6e66b6aa0b Make code in `runProcess` compile with Qt 5 2024-01-26 12:26:32 +01:00
Martchus 934b955837 Update copyright date 2024-01-23 00:27:34 +01:00
Martchus 959b0f0032 Allow running a sub process via the script API 2024-01-20 17:30:48 +01:00
Martchus d8f542d3e4 Update translations 2024-01-17 14:34:43 +01:00
Martchus a8281f323a Fix consistency issue when declaring/defining field mapping array 2024-01-17 14:34:30 +01:00
Martchus b3f576a354 Fix typo in README 2024-01-08 14:42:02 +01:00
Martchus c26d3e7be8 Fix indentation in `gui/entertargetdialog.ui` 2024-01-07 23:37:49 +01:00
Martchus 63ec653df5 Add and update tab-stops explicitly where default order is wrong 2024-01-07 23:36:27 +01:00
Martchus 2cd47777a3 Add mapping for "Publisher webpage" in CLI 2024-01-04 20:54:43 +01:00
Martchus 88989ff986 Extend script API; allow copying tags from other files 2023-12-30 03:06:58 +01:00
Martchus e1e979f9f5 Avoid use of `jq`-argument `--argjson` as it has been removed
Apprently `--slurpfile` is supposed to be used instead.
2023-12-29 18:08:25 +01:00
Martchus 04bd9563c0 Support "Publisher webpage field" 2023-12-29 17:27:55 +01:00
Martchus dd0bbd74c5 Update use of `albumArtist` in examples of renaming utility 2023-12-29 15:47:32 +01:00
Martchus f82bf04c66 State minimum required Windows 10 version 2023-11-18 21:42:26 +01:00
Martchus a87e431e04 Update outdated README section about DPI awareness under Windows 2023-11-18 21:28:11 +01:00
Martchus 72dcbbbd81 Allow renaming file via `set --script …`
This allows doing what the GUI renaming utilitiy does on the command-line.
If only renaming is wanted
2023-11-10 15:46:45 +01:00
Martchus b5f9158106 Bump patch version 2023-11-10 14:35:52 +01:00
Martchus f1b785337d Mention settings to improve performance 2023-10-31 21:25:25 +01:00
Martchus 09e2fe20f7 Mention Chocolatey package in README 2023-10-20 17:38:41 +02:00
Martchus c7df780281 Mention problem with antivirus software in README 2023-10-20 17:38:41 +02:00
Martchus fedc89b20e Update release date 2023-09-05 10:46:09 +02:00
Martchus 6ba93048c6 Apply clang-format 2023-08-20 20:28:53 +02:00
Martchus e1fdfa744c Add test for script processing 2023-08-20 20:26:17 +02:00
Martchus 6f30fc309e Improve README section about script processing 2023-08-19 18:35:12 +02:00
Martchus d26a7afc47 Change `--java-script` to just `--script`
This should be specific enough in this context and is more consistent with
`--script-settings`.
2023-08-19 00:18:27 +02:00
Martchus 9a7108fe58 Fix `CliTests::testFileLayoutOptions()` under Windows 2023-08-19 00:10:13 +02:00
Martchus 6f7ee1ea69 Fix `CliTests::testExtraction()` under Windows 2023-08-18 23:56:49 +02:00
Martchus a2fa2e5561 Fix `CliTests::testHandlingAttachments()` under Windows 2023-08-18 23:39:35 +02:00
Martchus 4f2b8904fa Fix `CliTests::testMultipleFiles()` under Windows 2023-08-18 23:34:59 +02:00
Martchus f92d7b39dd Allow running CLI tests under Windows as well
Not all tests pass yet but it is a start
2023-08-18 23:25:57 +02:00
Martchus e58f8a14a5 Make setting covers via CLI work with Windows drive letters
Interpret a single letter plus colon as drive specification followed by the
actual path instead of splitting it and considering the path the cover
type.

See https://github.com/Martchus/tageditor/issues/109
2023-08-09 23:54:01 +02:00
Martchus 1bb9f4b76e Update translations 2023-08-09 23:27:42 +02:00
Martchus f4346fb8e6 Allow preserving the muxing/writing application
See https://github.com/Martchus/tageditor/issues/108
2023-08-09 23:27:34 +02:00
Martchus 1cb00c7c41 Rename `mediafileinfoobject.cpp` to `scriptapi.cpp`
The file contains various wrapper "objects" so it makes sense to use a more
generic name.
2023-08-09 22:38:02 +02:00
Martchus 35dc6bca20 Update translations 2023-08-09 01:29:41 +02:00
Martchus 2d5f586c88 Fix condition for skipping lyrics download from Tekstowo if already done 2023-08-09 01:29:04 +02:00
Martchus 07406a1185 Make `set-tags.js` a bit more generic by disabling certain parts by default 2023-08-09 01:01:23 +02:00
Martchus 503c8f2c88 Extend advanced renaming script to put bootlegs and singles into special folder 2023-08-09 00:56:37 +02:00
Martchus 7f8cf5c44d Add exception for "true" in umlaut fixing function 2023-08-08 23:19:29 +02:00
Martchus 9d704ffc27 Fix crash when opening meta-data search 2023-08-08 23:18:42 +02:00
Martchus 1e45722ea1 Avoid running into Qt assertion when destructing TagFieldEdit 2023-08-08 22:47:53 +02:00
Martchus 488684a318 Show muxing/writing application when displaying file info 2023-08-08 17:24:37 +02:00
Martchus 023b25e44b Improve example JavaScript example and README section 2023-08-08 01:05:09 +02:00
Martchus 5222082635 Fix crashes when invoking DB query functions with invalid index 2023-08-08 00:53:57 +02:00
Martchus 63f0ab96a4 Fix/improve code for DB query widget 2023-08-07 23:03:13 +02:00
Martchus 64aabf6de3 Restore compatibility with Qt 5 2023-08-07 22:43:37 +02:00
Martchus c4f7d195a0 Allow adding cover via JavaScript 2023-08-07 18:41:19 +02:00
Martchus 7063f1bf03 Fix creation of song description from JavaScript object
It cannot just use `toString()` as this would turn e.g. `undefined` into
the string ´"undefined"` instead of an empty string.
2023-08-07 17:34:06 +02:00
Martchus 44d0ac21c2 Improve logging changes when applying changes in JavaScript
* Don't log non-printable characters
* Make it clear when an explicitly overridden field value is identical to
  the initial value
2023-08-07 17:32:38 +02:00
Martchus d248c63279 Allow passing settings to JavaScript 2023-08-07 17:30:25 +02:00
Martchus e69278634f Overhaul meta-data search
* Improve coding style
* Remove useless code comments
* Hide legacy providers by default
* Add Tekstowo to have at least one functioning provider for lyrics again
* Enable query logging only if an environment variable is set
* Use Tekstowo in example JavaScript
2023-08-06 20:02:00 +02:00
Martchus dace19b2bf Improve example JavaScript
* Move querying lyrics into separate module
* Avoid out of service message from being used as lyrics
2023-08-06 17:10:21 +02:00
Martchus a191aebd8a Allow basic use of db query via JavaScript 2023-08-05 02:06:26 +02:00
Martchus dbd4e71281 Set total number of tracks in example JavaScript 2023-08-04 00:28:48 +02:00
Martchus 485611141c Track "position in set" changes via JavaScript correctly 2023-08-04 00:19:10 +02:00
Martchus 7533761d77 Make function to read directory from JavaScript actually return anything 2023-08-04 00:18:13 +02:00
Martchus 9fd925a6bd Improve path-related properties of MediaFileInfoObject 2023-08-03 23:32:33 +02:00
Martchus cd1e0ce590 Expose DB query functions to JavaScript
Those are likely not very usable as-is because they return a model that is
not yet written for use within the QML engine.
2023-08-02 18:26:01 +02:00
Martchus 73dc64ce6a Expose target to JavaScript 2023-08-02 18:08:35 +02:00
Martchus 1d4c18f474 Allow setting the save path via JavaScript 2023-08-02 17:56:20 +02:00
Martchus ccb516d47b Avoid implicit type conversion 2023-08-02 17:56:01 +02:00
Martchus 24b397b930 Add functions to read dirs/files from the JavaScript 2023-08-02 17:55:36 +02:00
Martchus 9aca90538d Execute JavaScript after tags have been added/removed 2023-08-02 17:42:47 +02:00
Martchus 57b6d38e43 Simplify dealing with fields in JavaScript
* Expose PositionInSet as Object (that is still convertable to String)
* Allow assigning field values directly without having to have a
  TagValueObject and without having to have an Array
2023-08-01 00:50:33 +02:00
Martchus c3af3d43e6 Allow dealing with multiple fields values in JavaScript 2023-07-31 23:53:35 +02:00
Martchus 9cb8702d13 Fix use of QT_BEGIN_NAMESPACE when declaring qHash() function 2023-07-30 16:57:19 +02:00
Martchus 8c2ab29927 Make JavaScript processing code compatible with Qt 5 2023-07-30 16:55:35 +02:00
Martchus 11d3cefbcf Bump tagparser version for JavaScript processing feature 2023-07-30 16:25:36 +02:00
Martchus 22953ad0da Allow tag processing via JavaScript 2023-07-30 16:15:19 +02:00
Martchus 857917c23d Bump minor version 2023-07-30 15:58:09 +02:00
Martchus 1017873c28 Apply clang-format 2023-07-25 23:34:39 +02:00
Martchus 1a94b5c85d Update translations 2023-07-23 22:18:12 +02:00
Martchus 4a3aa9c1c1 Fix warnings about implicit conversions when compiling against Qt 6 2023-07-23 22:17:47 +02:00
Martchus 1cb6e06f31 Avoid CMake deprecation warning by bumping version 2023-07-23 20:59:39 +02:00
Martchus b63521330f Update release date 2023-06-08 15:31:43 +02:00
Martchus 7d2fe59996 Workaround lupdate limitation
Defining a function like this apparently leads to
`Qualifying with unknown namespace/class` so let's just drop the namespace.
2023-06-07 23:18:04 +02:00
Martchus 5286273cc5 Bump patch version 2023-06-07 23:16:51 +02:00
Martchus 313b2c60ac Update release date 2023-06-06 11:05:17 +02:00
Martchus 2cd10b7a66 Update to tagparser 12 2023-05-16 22:01:51 +02:00
Martchus a0b9b1f7ba Fix typo 2023-05-08 11:19:13 +02:00
Martchus 791e169dc6 Sync README with tagparser 2023-05-08 11:19:13 +02:00
Martchus 0dd79f5613 Update translations 2023-05-07 22:58:23 +02:00
Martchus 43b46bd312 Update README according to latest changes 2023-05-07 22:58:09 +02:00
Martchus 1b7bc82787 Use group box consistently in auto correction page 2023-05-07 22:24:32 +02:00
Martchus a941ec42c1 Improve wording in a few places of the UI 2023-05-07 22:18:31 +02:00
Martchus a6231b8442 Add the tag type the "not supported for" message refers to 2023-05-03 21:09:36 +02:00
Martchus a18d268ea1 Use `TESTUTILS_ASSERT_EXEC` macro consistently in all CLI tests 2023-05-03 21:04:05 +02:00
Martchus 0e36eec6c7 Suppress "field not supported" for ID3v1 fields if writing ID3v2 as well 2023-05-03 21:00:59 +02:00
Martchus 27a1d6b81e Actually add the `--pedantic` to the `set` operation 2023-05-03 20:41:05 +02:00
Martchus 7a11b944b9 Update link to section about dark mode under Windows
The phrase "not supported" is at this point no longer fitting.
2023-05-03 20:10:27 +02:00
Martchus bc73842ae9 Add test for the --no-color, --validate and --pedantic options 2023-04-29 18:31:21 +02:00
Martchus 40acb0bdec Fix --no-color argument 2023-04-29 12:25:10 +02:00
Martchus ff6cb7d143 Update translations 2023-04-25 23:17:11 +02:00
Martchus 0caff70ebb Apply clang-format 2023-04-25 23:16:52 +02:00
Martchus b7016f98a2 Add pedantic argument to allow returning a non-zero exit code in case of errors
This is especially useful to check whether a file is complete, e.g. one might
use `tageditor info --validate --pedantic --files …` to check whether the
specified files are ok. (If they were truncated there's be an error about it
and the command would return a non-zero exit code. Without pedantic this would
just return in a non-zero exit code if the file couldn't be parsed at all.)
2023-04-25 23:16:15 +02:00
Martchus 1e77e0b9e1 Bump minor version 2023-04-25 23:10:15 +02:00
Martchus 59ca34983e Update release date 2023-04-04 21:06:11 +02:00
Martchus 6ab558b7b1 Update translations 2023-04-02 18:33:50 +02:00
Martchus 930ae2a14b Avoid warning mismatching sign of comparison 2023-03-26 22:43:40 +02:00
Martchus d6e70764c4 Update style sheets on palette change 2023-03-26 22:19:27 +02:00
Martchus b023c26205 Avoid use of non-standard escape character to avoid MSVC warning about it 2023-03-26 21:59:31 +02:00
Martchus 1835ebfdd1 Apply Qt settings immediately 2023-03-26 21:48:25 +02:00
Martchus 2abd4b191c Improve notes regarding symlink handling on Windows
* Mention developer mode
* Use single `git config` command before cloning
2023-03-13 19:56:33 +01:00
Martchus 8612827e1f Bump patch version 2023-03-13 19:56:31 +01:00
70 changed files with 3908 additions and 1454 deletions

View File

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.1.0 FATAL_ERROR)
cmake_minimum_required(VERSION 3.17.0 FATAL_ERROR)
# meta data
project(tageditor)
@ -13,9 +13,9 @@ set(META_APP_DESCRIPTION
set(META_GUI_OPTIONAL true)
set(META_JS_SRC_DIR renamingutility)
set(META_VERSION_MAJOR 3)
set(META_VERSION_MINOR 7)
set(META_VERSION_PATCH 8)
set(META_RELEASE_DATE "2023-03-07")
set(META_VERSION_MINOR 9)
set(META_VERSION_PATCH 1)
set(META_RELEASE_DATE "2023-09-05")
set(META_ADD_DEFAULT_CPP_UNIT_TEST_APPLICATION ON)
# add project files
@ -52,6 +52,7 @@ set(WIDGETS_HEADER_FILES
dbquery/musicbrainz.h
dbquery/makeitpersonal.h
dbquery/lyricswikia.h
dbquery/tekstowo.h
gui/dbquerywidget.h
misc/networkaccessmanager.h
renamingutility/filesystemitem.h
@ -82,6 +83,7 @@ set(WIDGETS_SRC_FILES
dbquery/musicbrainz.cpp
dbquery/makeitpersonal.cpp
dbquery/lyricswikia.cpp
dbquery/tekstowo.cpp
gui/dbquerywidget.cpp
misc/networkaccessmanager.cpp
renamingutility/filesystemitem.cpp
@ -115,6 +117,7 @@ set(WIDGETS_UI_FILES
set(TEST_HEADER_FILES)
set(TEST_SRC_FILES tests/cli.cpp)
set(EXCLUDED_FILES cli/scriptapi.h cli/scriptapi.cpp)
set(TS_FILES translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts)
@ -184,7 +187,7 @@ set(REQUIRED_ICONS
set(CONFIGURATION_PACKAGE_SUFFIX
""
CACHE STRING "sets the suffix for find_package() calls to packages configured via c++utilities")
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.21.0 REQUIRED)
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.23.0 REQUIRED)
use_cpp_utilities()
include(BasicConfig)
@ -196,12 +199,12 @@ if (WIDGETS_GUI OR QUICK_GUI)
set(CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES
"${CONFIGURATION_PACKAGE_SUFFIX}"
CACHE STRING "sets the suffix for qtutilities")
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.11.0 REQUIRED)
find_package(qtutilities${CONFIGURATION_PACKAGE_SUFFIX_QTUTILITIES} 6.12.0 REQUIRED)
use_qt_utilities()
endif ()
# find tagparser
find_package(tagparser${CONFIGURATION_PACKAGE_SUFFIX} 11.5.0 REQUIRED)
find_package(tagparser${CONFIGURATION_PACKAGE_SUFFIX} 12.2.0 REQUIRED)
use_tag_parser()
# enable experimental JSON export
@ -234,13 +237,21 @@ endif ()
# add Qt modules which can not be detected automatically
list(APPEND ADDITIONAL_QT_MODULES Concurrent Network)
# include modules to apply configuration
# configure usage of Qt
if (WIDGETS_GUI OR QUICK_GUI)
include(QtGuiConfig)
include(QtJsProviderConfig)
include(QtWebViewProviderConfig)
include(QtConfig)
endif ()
# configure JavaScript processing for the CLI
if (JS_PROVIDER STREQUAL Qml)
list(APPEND HEADER_FILES cli/scriptapi.h)
list(APPEND SRC_FILES cli/scriptapi.cpp)
endif ()
# include modules to apply configuration
include(WindowsResources)
include(TestTarget)
include(AppTarget)

138
README.md
View File

@ -7,11 +7,19 @@ The tag editor can read and write the following tag formats:
* iTunes-style MP4/M4A tags (MP4-DASH is supported)
* ID3v1 and ID3v2 tags
* conversion between ID3v1 and different versions of ID3v2 is possible
* mainly for use in MP3 files but can be added to any kind of file
* Vorbis, Opus and FLAC comments in Ogg streams
* cover art via "METADATA_BLOCK_PICTURE" is supported
* Vorbis comments and "METADATA_BLOCK_PICTURE" in raw FLAC streams
* Matroska/WebM tags and attachments
Further remarks:
* Unsupported file contents (such as unsupported tag formats) are *generally* preserved as-is.
* Note that APE tags are *not* supported. APE tags in the beginning of a file are strongly
unrecommended and thus discarded when applying changes. APE tags at the end of the file
are preserved as-is when applying changes.
## Additional features
The tag editor can also display technical information such as the ID, format,
language, bitrate, duration, size, timestamps, sampling frequency, FPS and other information of the tracks.
@ -85,6 +93,19 @@ much as possible, set the following in the GUI's "File layout" settings:
When using the CLI, you just need to add `--max-padding 429496729` to the CLI arguments (and avoid any of the other
arguments mentioned in previous sections).
### Improve performance
Editing big files (especially Matroska files) can take some time. To improve the performance, put the index at the
end of the file (CLI option `--index-pos back`) because then the size of the index will never have to be recalculated.
Also follow the advice from the "Backup/temporary files" section to force rewriting and to put the temporary directory
on the same filesystem as the file you are editing. Forcing a rewrite can improve the performance because then the tag
editor will not even try to see whether it could be avoided and can thus skip computations that can take a notable
time for big Matroska files.
Of course being able to avoid a rewrite would still be more optimal. Checkout the previous section for how to achieve
that. To improve performance further when avoiding a rewrite, put the tag at the end (CLI option `--tag-pos back`).
Then the tag editor will not even try to put tags at the front and can thus skip a few computations. (Avoiding a
rewrite is still not a good idea in general.)
## Download
### Source
See the release section on GitHub.
@ -121,12 +142,19 @@ See the release section on GitHub.
the package `libopengl0` is installed on Debian/Ubuntu)
* Supports X11 and Wayland (set the environment variable `QT_QPA_PLATFORM=xcb` to disable
native Wayland support if it does not work on your system)
* Binaries are signed with the GPG key
[`B9E36A7275FC61B464B67907E06FE8F53CDC6A4C`](https://keyserver.ubuntu.com/pks/lookup?search=B9E36A7275FC61B464B67907E06FE8F53CDC6A4C&fingerprint=on&op=index).
* Windows
* for binaries checkout the [release section on GitHub](https://github.com/Martchus/tageditor/releases)
* Windows SmartScreen will likely block the execution (you'll get a window saying "Windows protected your PC");
right click on the executable, select properties and tick the checkbox to allow the execution
* the Qt 6 based version is stable and preferable but only supports Windows 10 and newer
* Antivirus software often **wrongly** considers the executable harmful. This is a known problem. Please don't create
issues about it.
* the Qt 6 based version is stable and preferable but only supports Windows 10 version 1809 and newer
* the Qt 5 based version should still work on older versions down to Windows 7 although this is not regularly checked
* Binaries are signed with the GPG key
[`B9E36A7275FC61B464B67907E06FE8F53CDC6A4C`](https://keyserver.ubuntu.com/pks/lookup?search=B9E36A7275FC61B464B67907E06FE8F53CDC6A4C&fingerprint=on&op=index).
* there is also a [Chocolatey package](https://community.chocolatey.org/packages/tageditor) maintained by bcurran3
* for mingw-w64 PKGBUILDs checkout [my GitHub repository](https://github.com/Martchus/PKGBUILDs)
## Usage
@ -144,10 +172,13 @@ The basic workflow is quite simple:
You can set the behaviour of the editor to keep previous values, so you don't have to enter
information like album name or artist for all files in an album again and again.
Note that the GUI does *not* support setting multiple values of the same field (besides covers of
#### Limitations
The GUI does *not* support setting multiple values of the same field (besides covers of
different types). If a file already contains fields with multiple values, the additional values
are discarded. Use the CLI if support for multiple values per field is required but note that not
all tag formats support this anyways.
are discarded. Use the CLI if support for multiple values per field is required. Not all tag formats
support this anyways, though.
The GUI does *not* support batch processing. I recommend using the CLI for this.
#### Screenshots
##### Main window under Openbox/qt5ct with Breeze theme/icons
@ -203,6 +234,9 @@ tageditor <operation> [options]
Checkout the available operations and options with `--help`. For a list of all available field names, track
attribute names and modifier, use the CLI option `--print-field-names`.
Note that Windows users must use `tageditor-cli.exe` instead of `tageditor.exe` or use Mintty as terminal.
Checkout the "Windows-specific issues" section for details.
#### Examples
Here are some Bash examples which illustrate getting and setting tag information:
@ -330,6 +364,66 @@ Here are some Bash examples which illustrate getting and setting tag information
- This is only supported by the tag formats ID3v2 and Vorbis Comment. The type and description are ignored
when dealing with a different format.
* Sets fields by running a script to compute changes dynamically:
```
tageditor set --pedantic debug --script path/to/script.js -f foo.mp3
```
- This feature is still experimental. The script API is still subject to change.
- The script needs to be ECMAScript as supported by the Qt framework.
- This feature requires the tag editor to be configured with Qt QML as JavaScript provider at
compile time. Checkout the build instructions under "Building with Qt GUI" for details.
- The script needs to export a `main()` function. This function is invoked for every file and
passed an object representing the current file as first argument.
- Checkout the file `testfiles/set-tags.js` in this repository for an example that applies basic
fixes and tries to fetch lyrics and cover art when according settings are passed (e.g.
`--script-settings addCover=1 addLyrics=1`).
- For debugging, the option `--pedantic debug` is very useful. You may also add
`--script-settings dryRun=1` and check for that setting within the script as shown in the
mentioned example script.
- Common tag fields are exposed as object properties as shown in the mentioned example.
- Only properties for fields that are supported by the tag are added to the "fields" object.
- Adding properties of unsupported fields manually does not work; those will just be ignored.
- The properties for fields that are absent in the tag have an empty array assigned. You may
also assign an empty array to fields to delete them.
- The content of binary fields is exposed as `ArrayBuffer`. Use must also use an `ArrayBuffer`
to set the value of binary fields such as the cover.
- The content of other fields is mostly exposed as `String` or `Number`. You must also use
one of those types to set the value of those fields. The string-representation of the
assigned content will then be converted automatically to what's needed internally.
- The `utility` object exposes useful methods, e.g. for logging and controlling the event loop.
- Checkout the file `testfiles/http.js` in this repository for an example of using XHR and
controlling the event loop.
- The script runs after tags are added/removed (according to options like `--id3v1-usage`).
So the tags present during script execution don't necessarily represent tags that are actually
already present in the file.
- The script is executed before any other modifications are applied. So if you also specify
values as usual (via `--values`) then these values have precedence over values set by the
script.
- It is also possible to rename the file (via e.g. `file.rename(newPath)`). This will be done
immediately and also if `main()` returns a falsy value (so it is possible to only rename a
file without modifying it by returning a falsy value). If the specified path is relative, it
is interpreted relative to current directory of the file (and *not* to the current working
directory of the tag editor).
- It is also possible to open another file via `utility.openFile(path)`. This makes it possible
to copy tags over from another file, e.g. to insert tags back from original files that have
been lost when converting to a different format. The mentioned example script `set-tags.js`
also demonstrates this for covers and lyrics when according script settings are passed (e.g.
`--script-settings addCover=1 originalDir=… originalExt=…`).
##### Further useful commands
* Let the tag editor return with a non-zero exit code even if only non-fatal problems have been encountered
* when saving a file:
```
tageditor set ... --pedantic warning -f ...
```
* when printing technical information to validate the structure of a file:
```
tageditor info --pedantic warning --validate -f ...
```
- This is especially useful for MP4 and Matroska files where the tag editor will be able to emit
warnings and critical messages when those files are truncated or have a broken index.
## Text encoding / unicode support
1. It is possible to set the preferred encoding used *within* the tags via CLI option ``--encoding``
and in the GUI settings.
@ -391,15 +485,18 @@ When enabled, the following additional dependencies are required (only at build-
For the latest version from Git clone the following repositories:
```
cd "$SOURCES"
git config core.symlinks true # only required on Windows
git clone https://github.com/Martchus/cpp-utilities.git c++utilities
git clone https://github.com/Martchus/tagparser.git
git clone https://github.com/Martchus/qtutilities.git # only required for Qt GUI
git clone https://github.com/Martchus/reflective-rapidjson.git # only required for JSON export
git clone https://github.com/Martchus/tageditor.git
git clone -c core.symlinks=true https://github.com/Martchus/subdirs.git
git clone https://github.com/Martchus/subdirs.git
```
Note that `-c core.symlinks=true` is only required under Windows to handle symlinks correctly.
This requires a recent Git version and a filesystem which supports symlinks (NTFS works).
Note that `git config core.symlinks=true` is only required under Windows to handle symlinks correctly.
This requires a recent Git version and a filesystem which supports symlinks (NTFS works). Additionally,
you need to
[enable Windows Developer Mode](https://learn.microsoft.com/en-us/gaming/game-bar/guide/developer-mode).
If you run into "not found" errors on symlink creation use `git reset --hard` within the repository to
fix this.
2. Configure the build
@ -438,26 +535,31 @@ When enabled, the following additional dependencies are required (only at build-
* More TODOs and bugs are tracked in the [issue section at GitHub](https://github.com/Martchus/tageditor/issues).
### Windows-specific issues
The following caveats apply to Windows' default terminal emulator `cmd.exe`. I recommend to use Mintty (e.g. via MSYS2) instead.
The following caveats can be worked around by using the CLI-wrapper instead of the main executable. This is the
file that ends with `-cli.exe`. Alternatively you may use Mintty (e.g. via MSYS2) which is also not affected by
those issues:
* The console's codepage is set to UTF-8 to ensure point *3.* of the "Text encoding" section is handled correctly. Use
`set ENABLE_CP_UTF8=0` if this is not wanted.
* To enable console output for Tag Editor which is built as a GUI application it is attaching to the parent
processes' console. However, this prevents redirections to work. If redirections are needed, use
`set ENABLE_CONSOLE=0` to disable that behavior.
* The console's codepage is set to UTF-8 to ensure point *3.* of the "Text encoding" section is handled correctly.
This may not work well under older Windows versions. Use `set ENABLE_CP_UTF8=0` if this is not wanted.
* The main application is built as a GUI application. To nevertheless enable console output it is attaching to the
parent processes' console. However, this prevents redirections to work in most cases. If redirections are needed,
use `set ENABLE_CONSOLE=0` to disable that behavior.
---
The dark mode introduced with Windows 10 is not supported but this can be
[worked around](https://github.com/Martchus/syncthingtray#workaround-missing-support-for-windows-10-dark-mode).
The dark mode introduced with Windows 10 is not affecting traditional desktop applications but
[can be enabled](https://github.com/Martchus/syncthingtray#tweak-gui-settings-for-dark-mode-under-windows)
by selecting the Fusion style.
---
Per monitor DPI awareness (v2) is not working out of the box but experimental support
[can be enabled](https://github.com/Martchus/syncthingtray#enable-experimental-support-for-windows-per-monitor-dpi-awareness-v2).
Tag Editor supports
[PMv2](https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows#per-monitor-and-per-monitor-v2-dpi-awareness)
out of the box as of Qt 6. You may tweak settings according to the
[Qt documentation](https://doc.qt.io/qt-6/highdpi.html#configuring-windows).
## Copyright notice and license
Copyright © 2015-2023 Marius Kittler
Copyright © 2015-2024 Marius Kittler
All code is licensed under [GPL-2-or-later](LICENSE).

View File

@ -209,6 +209,8 @@ const char *KnownFieldModel::fieldName(KnownField field)
return QT_TR_NOOP("License");
case KnownField::TermsOfUse:
return QT_TR_NOOP("Terms of use");
case KnownField::PublisherWebpage:
return QT_TR_NOOP("Publisher webpage");
default:
return "";
}
@ -327,6 +329,7 @@ KnownFieldModel::KnownFieldModel(QObject *parent, DefaultSelection defaultSelect
mkItem(KnownField::ProductionCopyright, Qt::Unchecked),
mkItem(KnownField::License, Qt::Unchecked),
mkItem(KnownField::TermsOfUse, Qt::Unchecked),
mkItem(KnownField::PublisherWebpage, Qt::Unchecked),
});
// clang-format on
}

View File

@ -29,9 +29,10 @@ using namespace QtUtilities;
namespace Cli {
SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg)
SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg, Argument &pedanticArg)
: filesArg(filesArg)
, verboseArg(verboseArg)
, pedanticArg(pedanticArg)
, quietArg("quiet", 'q', "suppress printing progress information")
, docTitleArg("doc-title", 'd', "specifies the document title (has no affect if not supported by the container)",
{ "title of first segment", "title of second segment" })
@ -82,6 +83,13 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg)
, backupDirArg("temp-dir", '\0', "specifies the directory for temporary/backup files", { "path" })
, layoutOnlyArg("layout-only", 'l', "confirms layout-only changes")
, preserveModificationTimeArg("preserve-modification-time", '\0', "preserves the file's modification time")
, preserveMuxingAppArg("preserve-muxing-app", '\0', "preserves the file's muxing app meta-data value")
, preserveWritingAppArg("preserve-writing-app", '\0', "preserves the file's writing app meta-data value")
, preserveTotalFieldsArg("preserve-total-fields", '\0',
"preserves the TRACKTOTAL/DISCTOTAL/PARTTOTAL fields in Vorbis Comments (which are otherwise automatically included into the "
"TRACKNUMBER/DISCNUMBER/PARTNUMBER fields)")
, jsArg("script", 'j', "modifies tag fields via the specified JavaScript", { "path" })
, jsSettingsArg("script-settings", '\0', "passes settings to the JavaScript specified via --script", { "key=value" })
, setTagInfoArg("set", 's', "sets the specified tag information and attachments")
{
docTitleArg.setRequiredValueCount(Argument::varValueCount);
@ -119,6 +127,9 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg)
valuesArg.setPreDefinedCompletionValues(Cli::fieldNamesForSet);
valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
outputFilesArg.setRequiredValueCount(Argument::varValueCount);
jsArg.setValueCompletionBehavior(ValueCompletionBehavior::Files);
jsSettingsArg.setValueCompletionBehavior(ValueCompletionBehavior::AppendEquationSign);
jsSettingsArg.setRequiredValueCount(Argument::varValueCount);
setTagInfoArg.setCallback(std::bind(Cli::setTagInfo, std::cref(*this)));
setTagInfoArg.setExample(PROJECT_NAME
" set title=\"Title of \"{1st,2nd,3rd}\" file\" title=\"Title of \"{4..16}\"th file\" album=\"The Album\" -f /some/dir/*.m4a\n" PROJECT_NAME
@ -134,7 +145,8 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg)
&id3v2UsageArg, &id3InitOnCreateArg, &id3TransferOnRemovalArg, &mergeMultipleSuccessiveTagsArg, &id3v2VersionArg, &encodingArg,
&removeTargetArg, &addAttachmentArg, &updateAttachmentArg, &removeAttachmentArg, &removeExistingAttachmentsArg, &minPaddingArg,
&maxPaddingArg, &prefPaddingArg, &tagPosArg, &indexPosArg, &forceRewriteArg, &backupDirArg, &layoutOnlyArg, &preserveModificationTimeArg,
&verboseArg, &quietArg, &outputFilesArg });
&preserveMuxingAppArg, &preserveWritingAppArg, &preserveTotalFieldsArg, &jsArg, &jsSettingsArg, &verboseArg, &pedanticArg, &quietArg,
&outputFilesArg });
}
} // namespace Cli
@ -146,12 +158,17 @@ int main(int argc, char *argv[])
CMD_UTILS_CONVERT_ARGS_TO_UTF8;
SET_APPLICATION_INFO;
QT_CONFIG_ARGUMENTS qtConfigArgs;
HelpArgument helpArg(parser);
NoColorArgument noColorArg;
ConfigValueArgument timeSpanFormatArg("time-span-format", '\0', "specifies the output format for time spans", { "measures/colons/seconds" });
timeSpanFormatArg.setPreDefinedCompletionValues("measures colons seconds");
// verbose option
ConfigValueArgument verboseArg("verbose", 'v', "be verbose, print debug and info messages");
// pedantic options
ConfigValueArgument pedanticArg("pedantic", '\0',
"return non-zero exit code if a non-fatal problem has been encountered that is at least as severe as the specified severity (or critical if "
"none specified)",
{ "critical/warning/info/debug" });
pedanticArg.setRequiredValueCount(Argument::varValueCount);
pedanticArg.setPreDefinedCompletionValues("error warning info debug");
// input/output file/files
ConfigValueArgument fileArg("file", 'f', "specifies the path of the file to be opened", { "path" });
ConfigValueArgument defaultFileArg(fileArg);
@ -167,8 +184,9 @@ int main(int argc, char *argv[])
ConfigValueArgument validateArg(
"validate", 'c', "validates the file integrity as accurately as possible; the structure of the file will be parsed completely");
OperationArgument displayFileInfoArg("info", 'i', "displays general file information", PROJECT_NAME " info -f /some/dir/*.m4a");
displayFileInfoArg.setCallback(std::bind(Cli::displayFileInfo, _1, std::cref(filesArg), std::cref(verboseArg), std::cref(validateArg)));
displayFileInfoArg.setSubArguments({ &filesArg, &validateArg, &verboseArg });
displayFileInfoArg.setCallback(
std::bind(Cli::displayFileInfo, _1, std::cref(filesArg), std::cref(verboseArg), std::cref(pedanticArg), std::cref(validateArg)));
displayFileInfoArg.setSubArguments({ &filesArg, &validateArg, &verboseArg, &pedanticArg });
// display tag info
ConfigValueArgument fieldsArg("fields", 'n', "specifies the field names to be displayed", { "title", "album", "artist", "trackpos" });
fieldsArg.setRequiredValueCount(Argument::varValueCount);
@ -177,11 +195,11 @@ int main(int argc, char *argv[])
OperationArgument displayTagInfoArg("get", 'g', "displays the values of all specified tag fields (displays all fields if none specified)",
PROJECT_NAME " get title album artist -f /some/dir/*.m4a");
ConfigValueArgument showUnsupportedArg("show-unsupported", 'u', "shows unsupported fields (has only effect when no field names specified)");
displayTagInfoArg.setCallback(
std::bind(Cli::displayTagInfo, std::cref(fieldsArg), std::cref(showUnsupportedArg), std::cref(filesArg), std::cref(verboseArg)));
displayTagInfoArg.setSubArguments({ &fieldsArg, &showUnsupportedArg, &filesArg, &verboseArg });
displayTagInfoArg.setCallback(std::bind(Cli::displayTagInfo, std::cref(fieldsArg), std::cref(showUnsupportedArg), std::cref(filesArg),
std::cref(verboseArg), std::cref(pedanticArg)));
displayTagInfoArg.setSubArguments({ &fieldsArg, &showUnsupportedArg, &filesArg, &verboseArg, &pedanticArg });
// set tag info
Cli::SetTagInfoArgs setTagInfoArgs(filesArg, verboseArg);
Cli::SetTagInfoArgs setTagInfoArgs(filesArg, verboseArg, pedanticArg);
// extract cover
ConfigValueArgument fieldArg("field", 'n', "specifies the field to be extracted", { "field name" });
fieldArg.setImplicit(true);
@ -210,7 +228,7 @@ int main(int argc, char *argv[])
qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&defaultFileArg);
qtConfigArgs.qtWidgetsGuiArg().addSubArgument(&renamingUtilityArg);
parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayFileInfoArg, &displayTagInfoArg,
&setTagInfoArgs.setTagInfoArg, &extractFieldArg, &exportArg, &genInfoArg, &timeSpanFormatArg, &noColorArg, &helpArg });
&setTagInfoArgs.setTagInfoArg, &extractFieldArg, &exportArg, &genInfoArg, &timeSpanFormatArg, &parser.noColorArg(), &parser.helpArg() });
// parse given arguments
parser.parseArgs(argc, argv, ParseArgumentBehavior::CheckConstraints | ParseArgumentBehavior::ExitOnFailure);

View File

@ -129,6 +129,9 @@ void restore()
v.tagPocessing.autoTagManagement = settings.value(QStringLiteral("autotagmanagement"), v.tagPocessing.autoTagManagement).toBool();
v.tagPocessing.preserveModificationTime
= settings.value(QStringLiteral("preservemodificationtime"), v.tagPocessing.preserveModificationTime).toBool();
v.tagPocessing.preserveMuxingApp = settings.value(QStringLiteral("preservemuxingapp"), v.tagPocessing.preserveMuxingApp).toBool();
v.tagPocessing.preserveWritingApp = settings.value(QStringLiteral("preservewritingapp"), v.tagPocessing.preserveWritingApp).toBool();
v.tagPocessing.convertTotalFields = settings.value(QStringLiteral("converttotalfields"), v.tagPocessing.convertTotalFields).toBool();
settings.beginGroup(QStringLiteral("id3v1"));
switch (settings.value(QStringLiteral("usage"), 0).toInt()) {
case 1:
@ -206,6 +209,7 @@ void restore()
v.dbQuery.musicBrainzUrl = settings.value(QStringLiteral("musicbrainzurl")).toString();
v.dbQuery.lyricsWikiaUrl = settings.value(QStringLiteral("lyricwikiurl")).toString();
v.dbQuery.makeItPersonalUrl = settings.value(QStringLiteral("makeitpersonalurl")).toString();
v.dbQuery.tekstowoUrl = settings.value(QStringLiteral("tekstowourl")).toString();
v.dbQuery.coverArtArchiveUrl = settings.value(QStringLiteral("coverartarchiveurl")).toString();
settings.endGroup();
@ -265,6 +269,9 @@ void save()
settings.setValue(QStringLiteral("unsupportedfieldhandling"), static_cast<int>(v.tagPocessing.unsupportedFieldHandling));
settings.setValue(QStringLiteral("autotagmanagement"), v.tagPocessing.autoTagManagement);
settings.setValue(QStringLiteral("preservemodificationtime"), v.tagPocessing.preserveModificationTime);
settings.setValue(QStringLiteral("preservemuxingapp"), v.tagPocessing.preserveMuxingApp);
settings.setValue(QStringLiteral("preservewritingapp"), v.tagPocessing.preserveWritingApp);
settings.setValue(QStringLiteral("converttotalfields"), v.tagPocessing.convertTotalFields);
settings.beginGroup(QStringLiteral("id3v1"));
settings.setValue(QStringLiteral("usage"), static_cast<int>(v.tagPocessing.creationSettings.id3v1usage));
settings.endGroup();
@ -302,6 +309,7 @@ void save()
settings.setValue(QStringLiteral("musicbrainzurl"), v.dbQuery.musicBrainzUrl);
settings.setValue(QStringLiteral("lyricwikiurl"), v.dbQuery.lyricsWikiaUrl);
settings.setValue(QStringLiteral("makeitpersonalurl"), v.dbQuery.makeItPersonalUrl);
settings.setValue(QStringLiteral("tekstowourl"), v.dbQuery.tekstowoUrl);
settings.setValue(QStringLiteral("coverartarchiveurl"), v.dbQuery.coverArtArchiveUrl);
settings.endGroup();

View File

@ -80,6 +80,9 @@ struct TagProcessing {
UnsupportedFieldHandling unsupportedFieldHandling = UnsupportedFieldHandling::Ignore;
bool autoTagManagement = true;
bool preserveModificationTime = false;
bool preserveMuxingApp = false;
bool preserveWritingApp = false;
bool convertTotalFields = true;
TagParser::TagCreationSettings creationSettings;
FileLayout fileLayout;
};
@ -100,6 +103,7 @@ struct DbQuery {
QString coverArtArchiveUrl;
QString lyricsWikiaUrl;
QString makeItPersonalUrl;
QString tekstowoUrl;
};
struct RenamingUtility {

View File

@ -8,7 +8,7 @@ using namespace TagParser;
namespace Cli {
namespace FieldMapping {
static constexpr auto fieldMapping = std::array<Mapping, 99>{ {
static constexpr auto fieldMapping = MappingType{ {
{ "Title", KnownField::Title },
{ "Album", KnownField::Album },
{ "Artist", KnownField::Artist },
@ -108,6 +108,7 @@ static constexpr auto fieldMapping = std::array<Mapping, 99>{ {
{ "ProductionCopyright", KnownField::ProductionCopyright },
{ "License", KnownField::License },
{ "TermsOfUse", KnownField::TermsOfUse },
{ "PublisherWebpage", KnownField::PublisherWebpage },
} };
const char *fieldDenotation(TagParser::KnownField knownField)
@ -142,7 +143,7 @@ TagParser::KnownField knownField(const char *fieldDenotation, std::size_t fieldD
return KnownField::Invalid;
}
const std::array<Mapping, 99> &mapping()
const MappingType &mapping()
{
return fieldMapping;
}

View File

@ -18,7 +18,8 @@ struct Mapping {
const char *fieldDenotation(TagParser::KnownField knownField);
TagParser::KnownField knownField(const char *fieldDenotation, std::size_t fieldDenotationSize);
const std::array<Mapping, 99> &mapping();
using MappingType = std::array<Mapping, 100>;
const MappingType &mapping();
} // namespace FieldMapping
} // namespace Cli

View File

@ -125,7 +125,7 @@ void InterruptHandler::handler(int signum)
}
#if !defined(PLATFORM_WINDOWS) || defined(PLATFORM_MINGW)
if (EscapeCodes::enabled) {
write(STDOUT_FILENO, "\e[1;33mWarning:\e[0m \e[1mSignal received, trying to abort ongoing process ...\e[0m\n", 82);
write(STDOUT_FILENO, "\033[1;33mWarning:\033[0m \033[1mSignal received, trying to abort ongoing process ...\033[0m\n", 82);
} else {
write(STDOUT_FILENO, "Warning: Signal received, trying to abort ongoing process ...\n", 63);
}
@ -160,40 +160,58 @@ string incremented(const string &str, unsigned int toIncrement)
return res;
}
void printDiagMessages(const Diagnostics &diag, const char *head, bool beVerbose)
void printDiagMessages(const Diagnostics &diag, const char *head, bool beVerbose, const CppUtilities::Argument *pedanticArg)
{
if (diag.empty()) {
return;
}
if (!beVerbose) {
for (const auto &message : diag) {
switch (message.level()) {
case DiagLevel::Debug:
case DiagLevel::Information:
break;
default:
goto printDiagMsg;
}
// set exit code to failure if there are diag messages considered bad enough
auto minLevel = beVerbose ? DiagLevel::Information : DiagLevel::Warning;
auto badExitLevel = DiagLevel::Fatal;
if (pedanticArg && pedanticArg->isPresent()) {
const auto &values = pedanticArg->values();
if (values.empty() || values.front() == "error"sv || values.front() == "critical"sv) {
badExitLevel = DiagLevel::Critical;
} else if (values.front() == "warning"sv) {
badExitLevel = DiagLevel::Warning;
} else if (values.front() == "info"sv) {
badExitLevel = minLevel = DiagLevel::Information;
} else {
badExitLevel = minLevel = DiagLevel::Debug;
}
return;
}
printDiagMsg:
// set exit code if there are severe enough messages and check whether there's something to print
auto hasAnythingToPrint = false;
for (const auto &message : diag) {
if (message.level() >= badExitLevel) {
exitCode = EXIT_PARSING_FAILURE;
}
if (message.level() >= minLevel) {
hasAnythingToPrint = true;
}
if (exitCode != EXIT_SUCCESS && hasAnythingToPrint) {
break;
}
}
// print diag messages if there's anything to print
if (!hasAnythingToPrint) {
return;
}
if (head) {
cerr << " - " << head << endl;
}
for (const auto &message : diag) {
if (message.level() < minLevel) {
continue;
}
switch (message.level()) {
case DiagLevel::Debug:
if (!beVerbose) {
continue;
}
cerr << " Debug ";
break;
case DiagLevel::Information:
if (!beVerbose) {
continue;
}
cerr << " Information ";
break;
case DiagLevel::Warning:
@ -204,20 +222,25 @@ printDiagMsg:
setStyle(cerr, TextAttribute::Reset);
break;
case DiagLevel::Critical:
case DiagLevel::Fatal:
setStyle(cerr, Color::Red, ColorContext::Foreground, TextAttribute::Bold);
setStyle(cerr, TextAttribute::Reset);
setStyle(cerr, TextAttribute::Bold);
cerr << " Error ";
setStyle(cerr, TextAttribute::Reset);
if (message.level() == DiagLevel::Fatal && exitCode == EXIT_SUCCESS) {
exitCode = EXIT_PARSING_FAILURE;
}
break;
case DiagLevel::Fatal:
setStyle(cerr, Color::Red, ColorContext::Foreground, TextAttribute::Bold);
setStyle(cerr, TextAttribute::Reset);
setStyle(cerr, TextAttribute::Bold);
cerr << " Fatal ";
setStyle(cerr, TextAttribute::Reset);
break;
default:;
}
cerr << message.creationTime().toString(DateTimeOutputFormat::TimeOnly) << " ";
cerr << message.context() << ": ";
if (!message.context().empty()) {
cerr << message.context() << ": ";
}
cerr << message.message() << '\n';
}
}

View File

@ -54,6 +54,17 @@ const std::vector<std::string_view> &id3v2CoverTypeNames();
CoverType id3v2CoverType(std::string_view coverName);
std::string_view id3v2CoverName(CoverType coverType);
template <class TagType>
bool fieldPredicate(CoverType coverType, std::optional<std::string_view> description,
const std::pair<typename TagType::IdentifierType, typename TagType::FieldType> &pair)
{
const auto &[fieldId, field] = pair;
const auto typeMatches
= field.isTypeInfoAssigned() ? (field.typeInfo() == static_cast<typename TagType::FieldType::TypeInfoType>(coverType)) : (coverType == 0);
const auto descMatches = !description.has_value() || field.value().description() == description.value();
return typeMatches && descMatches;
}
class FieldId {
friend struct std::hash<FieldId>;
@ -258,7 +269,8 @@ constexpr bool isDigit(char c)
std::string incremented(const std::string &str, unsigned int toIncrement = 1);
void printDiagMessages(const TagParser::Diagnostics &diag, const char *head = nullptr, bool beVerbose = false);
void printDiagMessages(
const TagParser::Diagnostics &diag, const char *head = nullptr, bool beVerbose = false, const CppUtilities::Argument *pedanticArg = nullptr);
void printProperty(const char *propName, std::string_view value, const char *suffix = nullptr, CppUtilities::Indentation indentation = 4);
void printProperty(const char *propName, ElementPosition elementPosition, const char *suffix = nullptr, CppUtilities::Indentation indentation = 4);

View File

@ -6,6 +6,13 @@
#endif
#include "../application/knownfieldmodel.h"
// includes for JavaScript support of set operation
#ifdef TAGEDITOR_USE_JSENGINE
#include "./scriptapi.h"
#endif
// includes for generating HTML info
#if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK)
#include "../misc/htmlinfo.h"
#include "../misc/utility.h"
@ -39,6 +46,16 @@
#include <c++utilities/io/path.h>
#include <c++utilities/misc/parseerror.h>
// includes for JavaScript support of set operation
#ifdef TAGEDITOR_USE_JSENGINE
#include <QCoreApplication>
#include <QFile>
#include <QJSValue>
#include <QQmlEngine>
#include <QTextStream>
#endif
// includes for generating HTML info
#if defined(TAGEDITOR_GUI_QTWIDGETS) || defined(TAGEDITOR_GUI_QTQUICK)
#include <QDir>
#include <qtutilities/misc/conversion.h>
@ -85,7 +102,7 @@ namespace Cli {
" purchasingdate recordinglocation compositionlocation composernationality\n" \
" playcounter measure tuning isrc mcdi isbn barcode catalognumber labelcode\n" \
" lccn imdb tmdb tvdb purchaseitem purchaseinfo purchaseowner purchaseprice\n" \
" purchasecurrency copyright productioncopyright license termsofuse"
" purchasecurrency copyright productioncopyright license termsofuse publisherwebpage"
#define TRACK_ATTRIBUTE_NAMES "name tracknumber enabled=yes enabled=no forced=yes forced=no default=yes default=no"
@ -167,7 +184,8 @@ void generateFileInfo(const ArgumentOccurrence &, const Argument &inputFileArg,
#endif
}
void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const Argument &verboseArg, const Argument &validateArg)
void displayFileInfo(
const ArgumentOccurrence &, const Argument &filesArg, const Argument &verboseArg, const Argument &pedanticArg, const Argument &validateArg)
{
CMD_UTILS_START_CONSOLE;
@ -203,7 +221,7 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const
printProperty("Duration", duration);
printProperty("Overall avg. bitrate", bitrateToString(fileInfo.overallAverageBitrate()));
}
if (const auto container = fileInfo.container()) {
if (const auto *const container = fileInfo.container()) {
size_t segmentIndex = 0;
for (const auto &title : container->titles()) {
if (segmentIndex) {
@ -222,6 +240,12 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const
printProperty("Modification time", container->modificationTime());
printProperty("Tag position", container->determineTagPosition(diag));
printProperty("Index position", container->determineIndexPosition(diag));
if (const auto &muxingApps = container->muxingApplications(); !muxingApps.empty()) {
printProperty("Muxing application", joinStrings(muxingApps, ", "));
}
if (const auto &writingApps = container->writingApplications(); !writingApps.empty()) {
printProperty("Writing application", joinStrings(writingApps, ", "));
}
}
if (fileInfo.paddingSize()) {
printProperty("Padding", dataSizeToString(fileInfo.paddingSize()));
@ -348,12 +372,13 @@ void displayFileInfo(const ArgumentOccurrence &, const Argument &filesArg, const
exitCode = EXIT_IO_FAILURE;
}
printDiagMessages(diag, "Diagnostic messages:", verboseArg.isPresent());
printDiagMessages(diag, "Diagnostic messages:", verboseArg.isPresent(), &pedanticArg);
cout << endl;
}
}
void displayTagInfo(const Argument &fieldsArg, const Argument &showUnsupportedArg, const Argument &filesArg, const Argument &verboseArg)
void displayTagInfo(
const Argument &fieldsArg, const Argument &showUnsupportedArg, const Argument &filesArg, const Argument &verboseArg, const Argument &pedanticArg)
{
CMD_UTILS_START_CONSOLE;
@ -366,7 +391,8 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &showUnsupportedAr
// parse specified fields
const auto fields = parseFieldDenotations(fieldsArg, true);
MediaFileInfo fileInfo;
auto fileInfo = MediaFileInfo();
fileInfo.setFileHandlingFlags(fileInfo.fileHandlingFlags() | MediaFileHandlingFlags::ConvertTotalFields);
for (const char *file : filesArg.values()) {
Diagnostics diag;
AbortableProgressFeedback progress; // FIXME: actually use the progress object
@ -412,7 +438,7 @@ void displayTagInfo(const Argument &fieldsArg, const Argument &showUnsupportedAr
cerr << Phrases::Error << "An IO error occurred when reading the file \"" << file << "\"." << Phrases::EndFlush;
exitCode = EXIT_IO_FAILURE;
}
printDiagMessages(diag, "Diagnostic messages:", verboseArg.isPresent());
printDiagMessages(diag, "Diagnostic messages:", verboseArg.isPresent(), &pedanticArg);
cout << endl;
}
}
@ -429,17 +455,6 @@ struct Id3v2Cover {
std::optional<std::string_view> description;
};
template <class TagType>
bool fieldPredicate(CoverType coverType, std::optional<std::string_view> description,
const std::pair<typename TagType::IdentifierType, typename TagType::FieldType> &pair)
{
const auto &[fieldId, field] = pair;
const auto typeMatches
= field.isTypeInfoAssigned() ? (field.typeInfo() == static_cast<typename TagType::FieldType::TypeInfoType>(coverType)) : (coverType == 0);
const auto descMatches = !description.has_value() || field.value().description() == description.value();
return typeMatches && descMatches;
}
template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vector<Id3v2Cover> &&values)
{
auto &fields = tag->fields();
@ -469,6 +484,125 @@ template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vect
}
}
#ifdef TAGEDITOR_USE_JSENGINE
class JavaScriptProcessor {
public:
explicit JavaScriptProcessor(const SetTagInfoArgs &args);
JavaScriptProcessor(const JavaScriptProcessor &) = delete;
QJSValue callMain(MediaFileInfo &mediaFileInfo, Diagnostics &diag);
private:
static void addWarnings(Diagnostics &diag, const std::string &context, const QList<QQmlError> &warnings);
const SetTagInfoArgs &args;
int argc;
QCoreApplication app;
Diagnostics diag;
QQmlEngine engine; // not using QJSEngine as otherwise XMLHttpRequest is not available
QJSValue module, main;
UtilityObject *utility;
};
/*!
* \brief Initializes JavaScript processing for the specified \a args.
* \remarks
* - Comes with its own QCoreApplication. Only for use within the CLI parts of the app!
* - Exits the app on fatal errors.
* - Logs status/problems directly in accordance with other parts of the CLI.
*/
JavaScriptProcessor::JavaScriptProcessor(const SetTagInfoArgs &args)
: args(args)
, argc(0)
, app(argc, nullptr)
, utility(new UtilityObject(&engine))
{
// print status message
const auto jsPath = args.jsArg.firstValue();
if (!jsPath) {
return;
}
if (!args.quietArg.isPresent()) {
std::cout << TextAttribute::Bold << "Loading JavaScript file \"" << jsPath << "\" ..." << Phrases::EndFlush;
}
// print warnings later via the usual helper function for consistent formatting
engine.setOutputWarningsToStandardError(false);
QObject::connect(&engine, &QQmlEngine::warnings, &engine,
[this, context = std::string("loading JavaScript")](const auto &warnings) { addWarnings(diag, context, warnings); });
// assign utility object and load specified JavaScript file as module
engine.globalObject().setProperty(QStringLiteral("utility"), engine.newQObject(utility));
module = engine.importModule(QString::fromUtf8(jsPath));
if (module.isError()) {
std::cerr << Phrases::Error << "Unable to load the specified JavaScript file \"" << jsPath << "\":" << Phrases::End;
std::cerr << "Uncaught exception at line " << module.property(QStringLiteral("lineNumber")).toInt() << ':' << ' '
<< module.toString().toStdString() << '\n';
std::exit(EXIT_FAILURE);
}
main = module.property(QStringLiteral("main"));
if (!main.isCallable()) {
std::cerr << Phrases::Error << "The specified JavaScript file \"" << jsPath << "\" does not export a main() function." << Phrases::End;
std::exit(EXIT_FAILURE);
}
// assign settings specified via CLI argument
auto settings = engine.newObject();
if (args.jsSettingsArg.isPresent()) {
for (const auto *const setting : args.jsSettingsArg.values()) {
if (const auto *const value = std::strchr(setting, '=')) {
settings.setProperty(QString::fromUtf8(setting, value - setting), QJSValue(QString::fromUtf8(value + 1)));
} else {
settings.setProperty(QString::fromUtf8(setting), QJSValue());
}
}
}
engine.globalObject().setProperty(QStringLiteral("settings"), settings);
// print warnings
printDiagMessages(diag, "Diagnostic messages:", args.verboseArg.isPresent(), &args.pedanticArg);
diag.clear();
}
/*!
* \brief Calls the JavaScript's main() function for the specified \a mediaFileInfo populating \a diag.
* \returns Returns what the main() function has returned.
*/
QJSValue JavaScriptProcessor::callMain(MediaFileInfo &mediaFileInfo, Diagnostics &diag)
{
auto fileInfoObject = MediaFileInfoObject(mediaFileInfo, diag, &engine, args.quietArg.isPresent());
auto fileInfoObjectValue = engine.newQObject(&fileInfoObject);
auto context = argsToString("executing JavaScript for ", mediaFileInfo.fileName());
utility->setDiag(&context, &diag);
QObject::connect(
&engine, &QQmlEngine::warnings, &fileInfoObject, [&diag, &context](const auto &warnings) { addWarnings(diag, context, warnings); });
diag.emplace_back(DiagLevel::Information, "entering main() function", context);
auto res = main.call(QJSValueList({ fileInfoObjectValue }));
if (res.isError()) {
diag.emplace_back(DiagLevel::Fatal,
argsToString(res.toString().toStdString(), " at line ", res.property(QStringLiteral("lineNumber")).toInt(), '.'), context);
} else if (!res.isUndefined()) {
diag.emplace_back(DiagLevel::Information, argsToString("done with return value: ", res.toString().toStdString()), context);
} else {
diag.emplace_back(DiagLevel::Debug, "done without return value", context);
}
return res;
}
/*!
* \brief Adds the \a warnings to the specified \a diag object with the specified \a context.
*/
void JavaScriptProcessor::addWarnings(Diagnostics &diag, const string &context, const QList<QQmlError> &warnings)
{
for (const auto &warning : warnings) {
diag.emplace_back(DiagLevel::Warning, warning.toString().toStdString(), context);
}
}
#endif
/*!
* \brief Implements the "set"-operation of the CLI.
*/
void setTagInfo(const SetTagInfoArgs &args)
{
CMD_UTILS_START_CONSOLE;
@ -494,7 +628,7 @@ void setTagInfo(const SetTagInfoArgs &args)
&& (!args.updateAttachmentArg.isPresent() || args.updateAttachmentArg.values().empty())
&& (!args.removeAttachmentArg.isPresent() || args.removeAttachmentArg.values().empty())
&& (!args.docTitleArg.isPresent() || args.docTitleArg.values().empty()) && !args.id3v1UsageArg.isPresent() && !args.id3v2UsageArg.isPresent()
&& !args.id3v2VersionArg.isPresent()) {
&& !args.id3v2VersionArg.isPresent() && !args.jsArg.isPresent()) {
if (!args.layoutOnlyArg.isPresent()) {
std::cerr << Phrases::Error << "No fields/attachments have been specified." << Phrases::End
<< "note: This is usually a mistake. Use --layout-only to prevent this error and apply file layout options only." << endl;
@ -554,12 +688,13 @@ void setTagInfo(const SetTagInfoArgs &args)
}
// parse other settings
const TagTextEncoding denotedEncoding = parseEncodingDenotation(args.encodingArg, TagTextEncoding::Utf8);
const auto denotedEncoding = parseEncodingDenotation(args.encodingArg, TagTextEncoding::Utf8);
settings.id3v1usage = parseUsageDenotation(args.id3v1UsageArg, TagUsage::KeepExisting);
settings.id3v2usage = parseUsageDenotation(args.id3v2UsageArg, TagUsage::Always);
// setup media file info
MediaFileInfo fileInfo;
auto fileInfo = MediaFileInfo();
auto tags = std::vector<Tag *>();
fileInfo.setMinPadding(parseUInt64(args.minPaddingArg, 0));
fileInfo.setMaxPadding(parseUInt64(args.maxPaddingArg, 0));
fileInfo.setPreferredPadding(parseUInt64(args.prefPaddingArg, 0));
@ -569,16 +704,42 @@ void setTagInfo(const SetTagInfoArgs &args)
fileInfo.setForceIndexPosition(args.forceIndexPosArg.isPresent());
fileInfo.setForceRewrite(args.forceRewriteArg.isPresent());
fileInfo.setWritingApplication(APP_NAME " v" APP_VERSION);
if (args.preserveMuxingAppArg.isPresent()) {
fileInfo.setFileHandlingFlags(fileInfo.fileHandlingFlags() | MediaFileHandlingFlags::PreserveMuxingApplication);
}
if (args.preserveWritingAppArg.isPresent()) {
fileInfo.setFileHandlingFlags(fileInfo.fileHandlingFlags() | MediaFileHandlingFlags::PreserveWritingApplication);
}
if (!args.preserveTotalFieldsArg.isPresent()) {
fileInfo.setFileHandlingFlags(fileInfo.fileHandlingFlags() | MediaFileHandlingFlags::ConvertTotalFields);
}
// set backup path
if (args.backupDirArg.isPresent()) {
fileInfo.setBackupDirectory(std::string(args.backupDirArg.values().front()));
}
// initialize JavaScript processing if --java-script argument is present
#ifdef TAGEDITOR_USE_JSENGINE
auto js = args.jsArg.isPresent() ? std::make_unique<JavaScriptProcessor>(args) : std::unique_ptr<JavaScriptProcessor>();
#else
if (args.jsArg.isPresent()) {
std::cerr << Phrases::Error << "A JavaScript has been specified but support for this has been disabled at compile-time." << Phrases::EndFlush;
std::exit(EXIT_FAILURE);
}
#endif
// iterate through all specified files
const auto quiet = args.quietArg.isPresent();
auto fileIndex = 0u;
static auto context = std::string("setting tags");
const auto continueWithNextFile = [&](Diagnostics &diag) {
printDiagMessages(diag, "Diagnostic messages:", args.verboseArg.isPresent(), &args.pedanticArg);
++fileIndex;
if (currentOutputFile != noMoreOutputFiles) {
++currentOutputFile;
}
};
for (const char *file : args.filesArg.values()) {
auto diag = Diagnostics();
auto parsingProgress = AbortableProgressFeedback(); // FIXME: actually use the progress object
@ -594,15 +755,14 @@ void setTagInfo(const SetTagInfoArgs &args)
fileInfo.parseAttachments(diag, parsingProgress);
// remove tags with the specified targets
auto tags = std::vector<Tag *>();
if (!targetsToRemove.empty()) {
tags.clear();
fileInfo.tags(tags);
for (auto *const tag : tags) {
if (find(targetsToRemove.cbegin(), targetsToRemove.cend(), tag->target()) != targetsToRemove.cend()) {
fileInfo.removeTag(tag);
}
}
tags.clear();
}
// select the relevant values for the current file index
@ -667,12 +827,35 @@ void setTagInfo(const SetTagInfoArgs &args)
}
}
// process tag fields via the specified JavaScript
#ifdef TAGEDITOR_USE_JSENGINE
if (js) {
const auto res = js->callMain(fileInfo, diag);
if (res.isError() || diag.has(DiagLevel::Fatal)) {
if (!quiet) {
std::cout << " - Skipping file due to fatal error when executing JavaScript.\n";
}
continueWithNextFile(diag);
continue;
}
if (!res.isUndefined() && !res.toBool()) {
if (!quiet) {
std::cout << " - Skipping file because JavaScript returned a falsy value other than undefined.\n";
}
continueWithNextFile(diag);
continue;
}
}
#endif
// alter tags
tags.clear();
fileInfo.tags(tags);
if (tags.empty()) {
diag.emplace_back(DiagLevel::Critical, "Can not create appropriate tags for file.", context);
} else {
// iterate through all tags
const auto willWriteAnId3v2Tag = fileInfo.hasId3v2Tag();
for (auto *tag : tags) {
// clear current values if option is present
if (args.removeOtherFieldsArg.isPresent()) {
@ -724,8 +907,14 @@ void setTagInfo(const SetTagInfoArgs &args)
continue;
}
// add value from file
const auto parts = splitStringSimple<std::vector<std::string_view>>(relevantDenotedValue->value, ":", 3);
const auto path = parts.empty() ? std::string_view() : parts.front();
const auto denotedValue = relevantDenotedValue->value;
const auto firstPartIsDriveLetter = denotedValue.size() >= 2 && denotedValue[1] == ':' ? 1u : 0u;
const auto maxParts = std::size_t(3u + firstPartIsDriveLetter);
const auto parts = splitStringSimple<std::vector<std::string_view>>(denotedValue, ":", static_cast<int>(maxParts));
const auto path = parts.empty()
? std::string_view()
: (firstPartIsDriveLetter ? std::string_view(denotedValue.data(), parts[0].size() + parts[1].size() + 1)
: parts.front());
const auto fieldType = denotedScope.field.knownFieldForTag(tag, tagType);
const auto dataType = fieldType == KnownField::Cover ? TagDataType::Picture : TagDataType::Text;
try {
@ -744,25 +933,26 @@ void setTagInfo(const SetTagInfoArgs &args)
value.setMimeType(coverFileInfo.mimeType());
}
auto description = std::optional<std::string_view>();
if (parts.size() > 2) {
value.setDescription(parts[2], TagTextEncoding::Utf8);
description = parts[2];
if (parts.size() > 2u + firstPartIsDriveLetter) {
description = parts[2 + firstPartIsDriveLetter];
value.setDescription(description.value(), TagTextEncoding::Utf8);
}
if (parts.size() > 1 && fieldType == KnownField::Cover
if (parts.size() > 1u + firstPartIsDriveLetter && fieldType == KnownField::Cover
&& (tagType == TagType::Id3v2Tag || tagType == TagType::VorbisComment)) {
const auto coverType = id3v2CoverType(parts[1]);
const auto typeSpec = parts[1 + firstPartIsDriveLetter];
const auto coverType = id3v2CoverType(typeSpec);
if (coverType == invalidCoverType) {
diag.emplace_back(DiagLevel::Warning,
argsToString("Specified cover type \"", parts[1], "\" is invalid. Ignoring the specified field/value."),
argsToString("Specified cover type \"", typeSpec, "\" is invalid. Ignoring the specified field/value."),
context);
} else {
convertedId3v2CoverValues.emplace_back(std::move(value), coverType, description);
}
} else {
if (parts.size() > 1) {
if (parts.size() > 1u + firstPartIsDriveLetter) {
diag.emplace_back(
tag->type() == TagType::Id3v1Tag && fileInfo.hasId3v2Tag() ? DiagLevel::Information : DiagLevel::Warning,
argsToString("Ignoring cover type \"", parts[1], "\" for ", tag->typeName(),
argsToString("Ignoring cover type \"", parts[1 + firstPartIsDriveLetter], "\" for ", tag->typeName(),
". It is only supported by the cover field and the tag formats ID3v2 and Vorbis Comment."),
context);
}
@ -779,12 +969,16 @@ void setTagInfo(const SetTagInfoArgs &args)
argsToString("An IO error occurred when parsing the specified file \"", path, "\": ", e.what()), context);
}
}
// finally set the values
// finally set/clear the values
try {
// add error if field is not supported unless it is just ID3v1 and we are writing an ID3v2 tag as well
if ((!convertedValues.empty() || convertedId3v2CoverValues.empty())
&& !denotedScope.field.setValues(tag, tagType, convertedValues)) {
&& !denotedScope.field.setValues(tag, tagType, convertedValues)
&& (tagType != TagType::Id3v1Tag || !willWriteAnId3v2Tag)) {
diag.emplace_back(DiagLevel::Critical,
argsToString("Unable set field \"", denotedScope.field.name(), "\": setting field is not supported"), context);
argsToString(
"Unable set field \"", denotedScope.field.name(), "\": field is not supported for ", tag->typeName()),
context);
}
} catch (const ConversionException &e) {
diag.emplace_back(DiagLevel::Critical,
@ -951,14 +1145,7 @@ void setTagInfo(const SetTagInfoArgs &args)
<< Phrases::EndFlush;
exitCode = EXIT_IO_FAILURE;
}
printDiagMessages(diag, "Diagnostic messages:", args.verboseArg.isPresent());
// continue with next file
++fileIndex;
if (currentOutputFile != noMoreOutputFiles) {
++currentOutputFile;
}
continueWithNextFile(diag);
}
}
@ -1214,4 +1401,5 @@ void applyGeneralConfig(const Argument &timeSapnFormatArg)
{
timeSpanOutputFormat = parseTimeSpanOutputFormat(timeSapnFormatArg, TimeSpanOutputFormat::WithMeasures);
}
} // namespace Cli

View File

@ -13,9 +13,10 @@ class Argument;
namespace Cli {
struct SetTagInfoArgs {
SetTagInfoArgs(CppUtilities::Argument &filesArg, CppUtilities::Argument &verboseArg);
SetTagInfoArgs(CppUtilities::Argument &filesArg, CppUtilities::Argument &verboseArg, CppUtilities::Argument &pedanticArg);
CppUtilities::Argument &filesArg;
CppUtilities::Argument &verboseArg;
CppUtilities::Argument &pedanticArg;
CppUtilities::ConfigValueArgument quietArg;
CppUtilities::ConfigValueArgument docTitleArg;
CppUtilities::ConfigValueArgument removeOtherFieldsArg;
@ -47,6 +48,11 @@ struct SetTagInfoArgs {
CppUtilities::ConfigValueArgument backupDirArg;
CppUtilities::ConfigValueArgument layoutOnlyArg;
CppUtilities::ConfigValueArgument preserveModificationTimeArg;
CppUtilities::ConfigValueArgument preserveMuxingAppArg;
CppUtilities::ConfigValueArgument preserveWritingAppArg;
CppUtilities::ConfigValueArgument preserveTotalFieldsArg;
CppUtilities::ConfigValueArgument jsArg;
CppUtilities::ConfigValueArgument jsSettingsArg;
CppUtilities::OperationArgument setTagInfoArg;
};
@ -56,11 +62,11 @@ extern int exitCode;
void applyGeneralConfig(const CppUtilities::Argument &timeSapnFormatArg);
void printFieldNames(const CppUtilities::ArgumentOccurrence &occurrence);
void displayFileInfo(const CppUtilities::ArgumentOccurrence &, const CppUtilities::Argument &filesArg, const CppUtilities::Argument &verboseArg,
const CppUtilities::Argument &validateArg);
const CppUtilities::Argument &pedanticArg, const CppUtilities::Argument &validateArg);
void generateFileInfo(const CppUtilities::ArgumentOccurrence &, const CppUtilities::Argument &inputFileArg,
const CppUtilities::Argument &outputFileArg, const CppUtilities::Argument &validateArg);
void displayTagInfo(const CppUtilities::Argument &fieldsArg, const CppUtilities::Argument &showUnsupportedArg, const CppUtilities::Argument &filesArg,
const CppUtilities::Argument &verboseArg);
const CppUtilities::Argument &verboseArg, const CppUtilities::Argument &pedanticArg);
void setTagInfo(const Cli::SetTagInfoArgs &args);
void extractField(const CppUtilities::Argument &fieldArg, const CppUtilities::Argument &attachmentArg, const CppUtilities::Argument &inputFilesArg,
const CppUtilities::Argument &outputFileArg, const CppUtilities::Argument &indexArg, const CppUtilities::Argument &verboseArg);

730
cli/scriptapi.cpp Normal file
View File

@ -0,0 +1,730 @@
#include "./scriptapi.h"
#include "./fieldmapping.h"
#include "./helper.h"
#include "../application/knownfieldmodel.h"
#include "../dbquery/dbquery.h"
#include "../misc/utility.h"
#include <tagparser/abstracttrack.h>
#include <tagparser/exceptions.h>
#include <tagparser/id3/id3v2tag.h>
#include <tagparser/mediafileinfo.h>
#include <tagparser/progressfeedback.h>
#include <tagparser/signature.h>
#include <tagparser/tag.h>
#include <tagparser/tagvalue.h>
#include <tagparser/vorbis/vorbiscomment.h>
#include <qtutilities/misc/conversion.h>
#include <c++utilities/conversion/binaryconversion.h>
#include <c++utilities/conversion/conversionexception.h>
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/io/path.h>
#include <c++utilities/tests/testutils.h>
#include <qtutilities/misc/compat.h>
#include <QBuffer>
#include <QByteArray>
#include <QCoreApplication>
#include <QDir>
#include <QHash>
#include <QImage>
#include <QJSEngine>
#include <QJSValueIterator>
#include <QRegularExpression>
#include <filesystem>
#include <iostream>
#include <limits>
#include <type_traits>
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QT_BEGIN_NAMESPACE
uint qHash(const TagParser::KnownField key, uint seed) noexcept
{
return ::qHash(static_cast<std::underlying_type_t<TagParser::KnownField>>(key), seed);
}
QT_END_NAMESPACE
#endif
namespace Cli {
constexpr auto nativeUtf16Encoding = TagParser::TagTextEncoding::
#if defined(CONVERSION_UTILITIES_BYTE_ORDER_LITTLE_ENDIAN)
Utf16LittleEndian
#else
Utf16BigEndian
#endif
;
const std::string UtilityObject::s_defaultContext = std::string("executing JavaScript");
UtilityObject::UtilityObject(QJSEngine *engine)
: QObject(engine)
, m_engine(engine)
, m_context(nullptr)
, m_diag(nullptr)
{
}
void UtilityObject::log(const QString &message)
{
std::cout << message.toStdString() << std::endl;
}
void UtilityObject::diag(const QString &level, const QString &message, const QString &context)
{
if (!m_diag) {
return;
}
static const auto mapping = QHash<QString, TagParser::DiagLevel>({
{ QStringLiteral("fatal"), TagParser::DiagLevel::Fatal },
{ QStringLiteral("critical"), TagParser::DiagLevel::Critical },
{ QStringLiteral("error"), TagParser::DiagLevel::Critical },
{ QStringLiteral("warning"), TagParser::DiagLevel::Warning },
{ QStringLiteral("info"), TagParser::DiagLevel::Information },
{ QStringLiteral("information"), TagParser::DiagLevel::Information },
{ QStringLiteral("debug"), TagParser::DiagLevel::Debug },
});
m_diag->emplace_back(mapping.value(level.toLower(), TagParser::DiagLevel::Debug), message.toStdString(),
context.isEmpty() ? (m_context ? *m_context : s_defaultContext) : context.toStdString());
}
int UtilityObject::exec()
{
return QCoreApplication::exec();
}
void UtilityObject::exit(int retcode)
{
QCoreApplication::exit(retcode);
}
QJSValue UtilityObject::readEnvironmentVariable(const QString &variable, const QJSValue &defaultValue) const
{
const auto variableUtf8 = variable.toUtf8();
if (qEnvironmentVariableIsSet(variableUtf8.data())) {
return QJSValue(qEnvironmentVariable(variableUtf8.data()));
} else {
return QJSValue(defaultValue);
}
}
QJSValue UtilityObject::readDirectory(const QString &path)
{
auto dir = QDir(path);
return dir.exists() ? m_engine->toScriptValue(dir.entryList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) : QJSValue();
}
QJSValue UtilityObject::readFile(const QString &path)
{
if (auto file = QFile(path); file.open(QFile::ReadOnly)) {
if (auto data = file.readAll(); file.error() != QFile::NoError) {
return m_engine->toScriptValue(std::move(data));
}
}
return QJSValue();
}
QJSValue UtilityObject::openFile(const QString &path)
{
if (!m_diag) {
return QJSValue();
}
auto mediaFileInfo = std::make_unique<TagParser::MediaFileInfo>(path.toStdString());
auto feedback = TagParser::AbortableProgressFeedback();
try {
mediaFileInfo->open(true);
mediaFileInfo->parseEverything(*m_diag, feedback);
} catch (const std::exception &e) {
m_diag->emplace_back(TagParser::DiagLevel::Critical, CppUtilities::argsToString("Unable to open \"", mediaFileInfo->path(), "\": ", e.what()),
m_context ? *m_context : s_defaultContext);
return QJSValue();
}
auto mediaFileInfoObj = new MediaFileInfoObject(std::move(mediaFileInfo), *m_diag, m_engine, false, m_engine);
return m_engine->newQObject(mediaFileInfoObj);
}
QJSValue UtilityObject::runProcess(const QString &path, const QJSValue &args, int timeout)
{
auto res = m_engine->newObject();
#ifdef CPP_UTILITIES_HAS_EXEC_APP
auto pathUtf8 = path.toUtf8();
auto argsUtf8 = QByteArrayList();
auto argsUtf8Array = std::vector<const char *>();
if (args.isArray()) {
const auto size = args.property(QStringLiteral("length")).toUInt();
argsUtf8.reserve(size);
argsUtf8Array.reserve(static_cast<std::size_t>(size) + 2);
for (auto i = quint32(); i != size; ++i) {
argsUtf8.append(args.property(i).toString().toUtf8());
}
}
argsUtf8Array.emplace_back(pathUtf8.data());
for (const auto &argUtf8 : argsUtf8) {
argsUtf8Array.emplace_back(argUtf8.data());
}
argsUtf8Array.emplace_back(nullptr);
auto output = std::string(), errors = std::string();
try {
auto exitStatus = CppUtilities::execHelperAppInSearchPath(pathUtf8.data(), argsUtf8Array.data(), output, errors, false, timeout);
#ifndef CPP_UTILITIES_BOOST_PROCESS
if (WIFEXITED(exitStatus)) {
exitStatus = WEXITSTATUS(exitStatus);
res.setProperty(QStringLiteral("status"), exitStatus);
}
#else
res.setProperty(QStringLiteral("status"), exitStatus);
#endif
} catch (const std::runtime_error &e) {
res.setProperty(QStringLiteral("error"), QString::fromUtf8(e.what()));
}
res.setProperty(QStringLiteral("stdout"), QString::fromStdString(output));
res.setProperty(QStringLiteral("stderr"), QString::fromStdString(errors));
#endif
return res;
}
QString UtilityObject::formatName(const QString &str) const
{
return Utility::formatName(str);
}
QString UtilityObject::fixUmlauts(const QString &str) const
{
return Utility::fixUmlauts(str);
}
QJSValue UtilityObject::queryMusicBrainz(const QJSValue &songDescription)
{
return m_engine->newQObject(QtGui::queryMusicBrainz(makeSongDescription(songDescription)));
}
QJSValue UtilityObject::queryLyricsWikia(const QJSValue &songDescription)
{
return m_engine->newQObject(QtGui::queryLyricsWikia(makeSongDescription(songDescription)));
}
QJSValue UtilityObject::queryMakeItPersonal(const QJSValue &songDescription)
{
return m_engine->newQObject(QtGui::queryMakeItPersonal(makeSongDescription(songDescription)));
}
QJSValue UtilityObject::queryTekstowo(const QJSValue &songDescription)
{
return m_engine->newQObject(QtGui::queryTekstowo(makeSongDescription(songDescription)));
}
QByteArray UtilityObject::convertImage(const QByteArray &imageData, const QSize &maxSize, const QString &format)
{
auto image = QImage::fromData(imageData);
if (image.isNull()) {
return imageData;
}
if (!maxSize.isNull() && (image.width() > maxSize.width() || image.height() > maxSize.height())) {
image = image.scaled(maxSize.width(), maxSize.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
if (image.isNull()) {
return imageData;
}
}
auto newData = QByteArray();
auto buffer = QBuffer(&newData);
auto res = buffer.open(QIODevice::WriteOnly) && image.save(&buffer, format.isEmpty() ? "JPEG" : format.toUtf8().data());
return res ? newData : imageData;
}
static QString propertyString(const QJSValue &obj, const QString &propertyName)
{
const auto val = obj.property(propertyName);
return val.isUndefined() || val.isNull() ? QString() : val.toString();
}
QtGui::SongDescription UtilityObject::makeSongDescription(const QJSValue &obj)
{
auto desc = QtGui::SongDescription();
desc.songId = propertyString(obj, QStringLiteral("songId"));
desc.title = propertyString(obj, QStringLiteral("title"));
desc.album = propertyString(obj, QStringLiteral("album"));
desc.albumId = propertyString(obj, QStringLiteral("albumId"));
desc.artist = propertyString(obj, QStringLiteral("artist"));
desc.artistId = propertyString(obj, QStringLiteral("artistId"));
desc.year = propertyString(obj, QStringLiteral("year"));
return desc;
}
PositionInSetObject::PositionInSetObject(TagParser::PositionInSet value, TagValueObject *relatedValue, QJSEngine *engine, QObject *parent)
: QObject(parent)
, m_v(value)
, m_relatedValue(relatedValue)
{
Q_UNUSED(engine)
}
PositionInSetObject::PositionInSetObject(const PositionInSetObject &other)
: QObject(other.parent())
, m_v(other.m_v)
, m_relatedValue(nullptr)
{
}
PositionInSetObject::~PositionInSetObject()
{
}
qint32 PositionInSetObject::position() const
{
return m_v.position();
}
void PositionInSetObject::setPosition(qint32 position)
{
if (m_relatedValue) {
m_relatedValue->flagChange();
}
m_v.setPosition(position);
}
qint32 PositionInSetObject::total() const
{
return m_v.total();
}
void PositionInSetObject::setTotal(qint32 total)
{
if (m_relatedValue) {
m_relatedValue->flagChange();
}
m_v.setTotal(total);
}
QString PositionInSetObject::toString() const
{
return QString::fromStdString(m_v.toString());
}
TagValueObject::TagValueObject(const TagParser::TagValue &value, QJSEngine *engine, QObject *parent)
: QObject(parent)
, m_engine(engine)
, m_initial(true)
{
setContentFromTagValue(value);
}
TagValueObject::~TagValueObject()
{
}
const QString &TagValueObject::type() const
{
return m_type;
}
const QJSValue &TagValueObject::content() const
{
return m_content;
}
const QJSValue &TagValueObject::initialContent() const
{
return m_initial ? m_content : m_initialContent;
}
void TagValueObject::setContent(const QJSValue &content)
{
flagChange();
m_content = content;
}
void TagValueObject::setContentFromTagValue(const TagParser::TagValue &value)
{
const auto type = TagParser::tagDataTypeString(value.type());
m_type = QString::fromUtf8(type.data(), Utility::sizeToInt(type.size()));
switch (value.type()) {
case TagParser::TagDataType::Undefined:
break;
case TagParser::TagDataType::Text:
case TagParser::TagDataType::Popularity:
case TagParser::TagDataType::DateTime:
case TagParser::TagDataType::DateTimeExpression:
case TagParser::TagDataType::TimeSpan:
m_content = Utility::tagValueToQString(value);
break;
case TagParser::TagDataType::Integer:
m_content = value.toInteger();
break;
case TagParser::TagDataType::UnsignedInteger:
if (auto v = value.toUnsignedInteger(); v < std::numeric_limits<uint>::max()) {
m_content = QJSValue(static_cast<uint>(v));
} else {
m_content = QString::number(v);
}
break;
case TagParser::TagDataType::Binary:
case TagParser::TagDataType::Picture:
m_content = m_engine->toScriptValue(QByteArray(value.dataPointer(), Utility::sizeToInt(value.dataSize())));
break;
case TagParser::TagDataType::PositionInSet:
m_content = m_engine->newQObject(new PositionInSetObject(value.toPositionInSet(), this, m_engine, this));
break;
default:
m_content = QJSValue::NullValue;
}
}
bool TagValueObject::isInitial() const
{
return m_initial;
}
TagParser::TagValue TagValueObject::toTagValue(TagParser::TagTextEncoding encoding) const
{
auto res = TagParser::TagValue();
if (m_content.isUndefined() || m_content.isNull()) {
return res;
}
if (const auto variant = m_content.toVariant(); variant.userType() == QMetaType::QByteArray) {
const auto bytes = variant.toByteArray();
const auto container = TagParser::parseSignature(bytes.data(), static_cast<std::size_t>(bytes.size()));
res.assignData(bytes.data(), static_cast<std::size_t>(bytes.size()), TagParser::TagDataType::Binary);
res.setMimeType(TagParser::containerMimeType(container));
} else {
const auto str = m_content.toString();
res.assignText(reinterpret_cast<const char *>(str.utf16()), static_cast<std::size_t>(str.size()) * (sizeof(ushort) / sizeof(char)),
nativeUtf16Encoding, encoding);
}
return res;
}
void TagValueObject::flagChange()
{
if (m_initial) {
if (const auto *const positionInSet = qobject_cast<const PositionInSetObject *>(m_content.toQObject())) {
m_initialContent = m_engine->newQObject(new PositionInSetObject(*positionInSet));
} else {
m_initialContent = m_content;
}
m_initial = false;
}
}
void TagValueObject::restore()
{
if (!m_initial) {
m_content = m_initialContent;
m_initial = true;
}
}
void TagValueObject::clear()
{
setContent(QJSValue());
}
QString TagValueObject::toString() const
{
return m_content.toString();
}
TagObject::TagObject(TagParser::Tag &tag, TagParser::Diagnostics &diag, QJSEngine *engine, QObject *parent)
: QObject(parent)
, m_tag(tag)
, m_diag(diag)
, m_engine(engine)
{
}
TagObject::~TagObject()
{
}
QString TagObject::type() const
{
return Utility::qstr(m_tag.typeName());
}
QJSValue TagObject::target() const
{
const auto &target = m_tag.target();
auto obj = m_engine->newObject();
obj.setProperty(QStringLiteral("level"), m_engine->toScriptValue(target.level()));
obj.setProperty(QStringLiteral("levelName"), QJSValue(QString::fromStdString(target.levelName())));
obj.setProperty(QStringLiteral("tracks"), m_engine->toScriptValue(QList<std::uint64_t>(target.tracks().cbegin(), target.tracks().cend())));
obj.setProperty(QStringLiteral("chapters"), m_engine->toScriptValue(QList<std::uint64_t>(target.chapters().cbegin(), target.chapters().cend())));
obj.setProperty(QStringLiteral("editions"), m_engine->toScriptValue(QList<std::uint64_t>(target.editions().cbegin(), target.editions().cend())));
obj.setProperty(
QStringLiteral("attachments"), m_engine->toScriptValue(QList<std::uint64_t>(target.attachments().cbegin(), target.attachments().cend())));
return obj;
}
QString TagObject::propertyNameForField(TagParser::KnownField field)
{
static const auto reverseMapping = [] {
auto reverse = QHash<TagParser::KnownField, QString>();
for (const auto &mapping : FieldMapping::mapping()) {
auto d = QString::fromUtf8(mapping.knownDenotation);
if (d.isUpper()) {
d = d.toLower(); // turn abbreviations into just lower case
} else {
d.front() = d.front().toLower();
}
reverse[mapping.knownField] = std::move(d);
}
return reverse;
}();
return reverseMapping.value(field, QString());
}
std::string TagObject::printJsValue(const QJSValue &value)
{
const auto str = value.toString();
for (const auto c : str) {
if (!c.isPrint()) {
return "[binary]";
}
}
return str.toStdString();
}
QJSValue &TagObject::fields()
{
if (!m_fields.isUndefined()) {
return m_fields;
}
static const auto fieldRegex = QRegularExpression(QStringLiteral("\\s(\\w)"));
m_fields = m_engine->newObject();
for (auto field = TagParser::firstKnownField; field != TagParser::KnownField::Invalid; field = TagParser::nextKnownField(field)) {
if (!m_tag.supportsField(field)) {
continue;
}
if (const auto propertyName = propertyNameForField(field); !propertyName.isEmpty()) {
const auto values = m_tag.values(field);
const auto size = Utility::sizeToInt<quint32>(values.size());
auto array = m_engine->newArray(size);
for (auto i = quint32(); i != size; ++i) {
array.setProperty(i, m_engine->newQObject(new TagValueObject(m_tag.value(field), m_engine, this)));
}
m_fields.setProperty(propertyName, array);
}
}
return m_fields;
}
/// \brief Sets the first of the specified \a values as front-cover with no description replacing any existing cover values.
template <class TagType> static void setId3v2CoverValues(TagType *tag, std::vector<TagParser::TagValue> &&values)
{
auto &fields = tag->fields();
const auto id = tag->fieldId(TagParser::KnownField::Cover);
const auto range = fields.equal_range(id);
const auto first = range.first;
constexpr auto coverType = CoverType(3); // assume front cover
constexpr auto description = std::optional<std::string_view>(); // assume no description
for (auto &tagValue : values) {
// check whether there is already a tag value with the current type and description
auto pair = std::find_if(first, range.second, std::bind(&fieldPredicate<TagType>, coverType, description, std::placeholders::_1));
if (pair != range.second) {
// there is already a tag value with the current type and description
// -> update this value
pair->second.setValue(tagValue);
// check whether there are more values with the current type and description
while ((pair = std::find_if(++pair, range.second, std::bind(&fieldPredicate<TagType>, coverType, description, std::placeholders::_1)))
!= range.second) {
// -> remove these values as we only support one value of a type/description in the same tag
pair->second.setValue(TagParser::TagValue());
}
} else if (!tagValue.isEmpty()) {
using FieldType = typename TagType::FieldType;
auto newField = FieldType(id, tagValue);
newField.setTypeInfo(static_cast<typename FieldType::TypeInfoType>(coverType));
fields.insert(std::pair(id, std::move(newField)));
}
break; // allow only setting one value for now
}
}
void TagObject::applyChanges()
{
auto context = !m_tag.target().isEmpty() || m_tag.type() == TagParser::TagType::MatroskaTag
? CppUtilities::argsToString(m_tag.typeName(), " targeting ", m_tag.targetString())
: std::string(m_tag.typeName());
m_diag.emplace_back(TagParser::DiagLevel::Debug, "applying changes", std::move(context));
if (m_fields.isUndefined()) {
return;
}
const auto encoding = m_tag.proposedTextEncoding();
for (auto field = TagParser::firstKnownField; field != TagParser::KnownField::Invalid; field = TagParser::nextKnownField(field)) {
if (!m_tag.supportsField(field)) {
continue;
}
const auto propertyName = propertyNameForField(field);
if (propertyName.isEmpty()) {
continue;
}
auto propertyValue = m_fields.property(propertyName);
if (!propertyValue.isArray()) {
auto array = m_engine->newArray(1);
array.setProperty(0, propertyValue);
propertyValue = std::move(array);
}
const auto size = propertyValue.property(QStringLiteral("length")).toUInt();
const auto initialValues = m_tag.values(field);
auto values = std::vector<TagParser::TagValue>();
values.reserve(size);
for (auto i = quint32(); i != size; ++i) {
const auto arrayElement = propertyValue.property(i);
if (arrayElement.isUndefined() || arrayElement.isNull()) {
continue;
}
auto *tagValueObj = qobject_cast<TagValueObject *>(arrayElement.toQObject());
if (!tagValueObj) {
tagValueObj = new TagValueObject(i < initialValues.size() ? *initialValues[i] : TagParser::TagValue(), m_engine, this);
tagValueObj->setContent(arrayElement);
propertyValue.setProperty(i, m_engine->newQObject(tagValueObj));
}
if (tagValueObj->isInitial()) {
if (i < initialValues.size()) {
values.emplace_back(*initialValues[i]);
}
continue;
}
auto &value = values.emplace_back(tagValueObj->toTagValue(encoding));
m_diag.emplace_back(TagParser::DiagLevel::Debug,
value.isNull()
? CppUtilities::argsToString(" - delete ", propertyName.toStdString(), '[', i, ']')
: (tagValueObj->initialContent().isUndefined()
? CppUtilities::argsToString(
" - set ", propertyName.toStdString(), '[', i, "] to '", printJsValue(tagValueObj->content()), '\'')
: ((tagValueObj->content().equals(tagValueObj->initialContent()))
? CppUtilities::argsToString(" - set ", propertyName.toStdString(), '[', i, "] to '",
printJsValue(tagValueObj->content()), "\' (no change)")
: CppUtilities::argsToString(" - change ", propertyName.toStdString(), '[', i, "] from '",
printJsValue(tagValueObj->initialContent()), "' to '", printJsValue(tagValueObj->content()), '\''))),
std::string());
}
// assign cover values of ID3v2/VorbisComment tags as front-cover with no description
if (field == TagParser::KnownField::Cover && !values.empty()) {
switch (m_tag.type()) {
case TagParser::TagType::Id3v2Tag:
setId3v2CoverValues(static_cast<TagParser::Id3v2Tag *>(&m_tag), std::move(values));
continue;
case TagParser::TagType::VorbisComment:
setId3v2CoverValues(static_cast<TagParser::VorbisComment *>(&m_tag), std::move(values));
continue;
default:;
}
}
m_tag.setValues(field, values);
}
}
MediaFileInfoObject::MediaFileInfoObject(
TagParser::MediaFileInfo &mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, bool quiet, QObject *parent)
: QObject(parent)
, m_f(mediaFileInfo)
, m_diag(diag)
, m_engine(engine)
, m_quiet(quiet)
{
}
MediaFileInfoObject::MediaFileInfoObject(
std::unique_ptr<TagParser::MediaFileInfo> &&mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, bool quiet, QObject *parent)
: MediaFileInfoObject(*mediaFileInfo.get(), diag, engine, quiet, parent)
{
m_f_owned = std::move(mediaFileInfo);
}
MediaFileInfoObject::~MediaFileInfoObject()
{
}
QString MediaFileInfoObject::path() const
{
return QString::fromStdString(m_f.path());
}
bool MediaFileInfoObject::isPathRelative() const
{
return QFileInfo(path()).isRelative();
}
QString MediaFileInfoObject::name() const
{
return QString::fromStdString(m_f.fileName());
}
QString MediaFileInfoObject::nameWithoutExtension() const
{
return QString::fromStdString(m_f.fileName(true));
}
QString MediaFileInfoObject::extension() const
{
return QString::fromStdString(m_f.extension());
}
QString MediaFileInfoObject::containingDirectory() const
{
return QString::fromStdString(m_f.containingDirectory());
}
QString MediaFileInfoObject::savePath() const
{
return QtUtilities::fromNativeFileName(m_f.saveFilePath());
}
void MediaFileInfoObject::setSavePath(const QString &path)
{
const auto nativePath = QtUtilities::toNativeFileName(path);
m_f.setSaveFilePath(std::string_view(nativePath.data(), static_cast<std::size_t>(nativePath.size())));
}
QList<Cli::TagObject *> &MediaFileInfoObject::tags()
{
if (!m_tags.isEmpty()) {
return m_tags;
}
auto tags = m_f.tags();
m_tags.reserve(Utility::sizeToInt(tags.size()));
for (auto *const tag : tags) {
m_tags << new TagObject(*tag, m_diag, m_engine, this);
}
return m_tags;
}
void MediaFileInfoObject::applyChanges()
{
for (auto *const tag : m_tags) {
tag->applyChanges();
}
}
bool MediaFileInfoObject::rename(const QString &newPath)
{
const auto from = m_f.path();
const auto fromNative = std::filesystem::path(CppUtilities::makeNativePath(from));
const auto toRelUtf8 = newPath.toUtf8();
const auto toRelView = CppUtilities::PathStringView(toRelUtf8.data(), static_cast<std::size_t>(toRelUtf8.size()));
const auto toNative = fromNative.parent_path().append(CppUtilities::makeNativePath(toRelView));
const auto toView = CppUtilities::extractNativePath(toNative.native());
try {
m_f.stream().close();
std::filesystem::rename(fromNative, toNative);
m_f.reportPathChanged(toView);
m_f.stream().open(m_f.path(), std::ios_base::in | std::ios_base::out | std::ios_base::binary);
} catch (const std::runtime_error &e) {
m_diag.emplace_back(TagParser::DiagLevel::Critical, e.what(), CppUtilities::argsToString("renaming \"", from, "\" to \"", toView));
return false;
}
if (!m_quiet) {
std::cout << " - Renamed \"" << from << "\" to \"" << toView << "\"\n";
}
return true;
}
} // namespace Cli

238
cli/scriptapi.h Normal file
View File

@ -0,0 +1,238 @@
#ifndef CLI_SCRIPT_API_H
#define CLI_SCRIPT_API_H
#include <tagparser/positioninset.h>
#include <QtGlobal>
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
namespace TagParser {
enum class KnownField : unsigned int;
}
QT_BEGIN_NAMESPACE
uint qHash(const TagParser::KnownField key, uint seed = 0) noexcept;
QT_END_NAMESPACE
#endif
#include <QJSValue>
#include <QObject>
QT_FORWARD_DECLARE_CLASS(QJSEngine)
namespace TagParser {
class Diagnostics;
class MediaFileInfo;
class Tag;
class TagValue;
enum class KnownField : unsigned int;
enum class TagTextEncoding : unsigned int;
} // namespace TagParser
namespace QtGui {
struct SongDescription;
}
namespace Cli {
/*!
* \brief The UtilityObject class wraps useful functions of Qt, TagParser and the Utility namespace for use within QML.
*/
class UtilityObject : public QObject {
Q_OBJECT
public:
explicit UtilityObject(QJSEngine *engine);
void setDiag(const std::string *context, TagParser::Diagnostics *diag);
public Q_SLOTS:
void log(const QString &message);
void diag(const QString &level, const QString &message, const QString &context = QString());
int exec();
void exit(int retcode = 0);
QJSValue readEnvironmentVariable(const QString &variable, const QJSValue &defaultValue = QJSValue()) const;
QJSValue readDirectory(const QString &path);
QJSValue readFile(const QString &path);
QJSValue openFile(const QString &path);
QJSValue runProcess(const QString &path, const QJSValue &args, int timeout = -1);
QString formatName(const QString &str) const;
QString fixUmlauts(const QString &str) const;
QJSValue queryMusicBrainz(const QJSValue &songDescription);
QJSValue queryLyricsWikia(const QJSValue &songDescription);
QJSValue queryMakeItPersonal(const QJSValue &songDescription);
QJSValue queryTekstowo(const QJSValue &songDescription);
QByteArray convertImage(const QByteArray &imageData, const QSize &maxSize, const QString &format = QString());
private:
static QtGui::SongDescription makeSongDescription(const QJSValue &obj);
QJSEngine *m_engine;
const std::string *m_context;
static const std::string s_defaultContext;
TagParser::Diagnostics *m_diag;
};
inline void UtilityObject::setDiag(const std::string *context, TagParser::Diagnostics *diag)
{
m_context = context;
m_diag = diag;
}
class TagValueObject;
/*!
* \brief The PositionInSetObject class wraps a TagParser::PositionInSet for use within QML.
*/
class PositionInSetObject : public QObject {
Q_OBJECT
Q_PROPERTY(qint32 position READ position WRITE setPosition)
Q_PROPERTY(qint32 total READ total WRITE setTotal)
public:
explicit PositionInSetObject(TagParser::PositionInSet value, TagValueObject *relatedValue, QJSEngine *engine, QObject *parent);
explicit PositionInSetObject(const PositionInSetObject &other);
~PositionInSetObject() override;
qint32 position() const;
void setPosition(qint32 position);
qint32 total() const;
void setTotal(qint32 total);
public Q_SLOTS:
QString toString() const;
private:
TagParser::PositionInSet m_v;
TagValueObject *m_relatedValue;
};
/*!
* \brief The TagValueObject class wraps a TagParser::TagValue for use within QML.
*/
class TagValueObject : public QObject {
Q_OBJECT
Q_PROPERTY(QString type READ type)
Q_PROPERTY(QJSValue content READ content WRITE setContent)
Q_PROPERTY(QJSValue initialContent READ initialContent)
Q_PROPERTY(bool initial READ isInitial)
public:
explicit TagValueObject(const TagParser::TagValue &value, QJSEngine *engine, QObject *parent);
~TagValueObject() override;
const QString &type() const;
const QJSValue &content() const;
void setContent(const QJSValue &content);
void setContentFromTagValue(const TagParser::TagValue &value);
const QJSValue &initialContent() const;
bool isInitial() const;
TagParser::TagValue toTagValue(TagParser::TagTextEncoding encoding) const;
public Q_SLOTS:
void restore();
void clear();
QString toString() const;
void flagChange();
private:
QJSEngine *m_engine;
QString m_type;
QJSValue m_content;
QJSValue m_initialContent;
bool m_initial;
};
/*!
* \brief The TagObject class wraps a TagParser::Tag for use within QML.
*/
class TagObject : public QObject {
Q_OBJECT
Q_PROPERTY(QString type READ type)
Q_PROPERTY(QJSValue target READ target)
Q_PROPERTY(QJSValue fields READ fields)
public:
explicit TagObject(TagParser::Tag &tag, TagParser::Diagnostics &diag, QJSEngine *engine, QObject *parent);
~TagObject() override;
TagParser::Tag &tag();
QString type() const;
QJSValue target() const;
QJSValue &fields();
public Q_SLOTS:
void applyChanges();
private:
static QString propertyNameForField(TagParser::KnownField field);
std::string printJsValue(const QJSValue &value);
TagParser::Tag &m_tag;
TagParser::Diagnostics &m_diag;
QJSEngine *m_engine;
QString m_type;
QJSValue m_fields;
};
inline TagParser::Tag &TagObject::tag()
{
return m_tag;
}
/*!
* \brief The MediaFileInfoObject class wraps a TagParser::MediaFileInfo for use within QML.
*/
class MediaFileInfoObject : public QObject {
Q_OBJECT
Q_PROPERTY(QString path READ path)
Q_PROPERTY(bool pathRelative READ isPathRelative)
Q_PROPERTY(QString name READ name)
Q_PROPERTY(QString nameWithoutExtension READ nameWithoutExtension)
Q_PROPERTY(QString extension READ extension)
Q_PROPERTY(QString containingDirectory READ containingDirectory)
Q_PROPERTY(QString savePath READ savePath WRITE setSavePath)
Q_PROPERTY(QList<TagObject *> tags READ tags)
public:
explicit MediaFileInfoObject(
TagParser::MediaFileInfo &mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine, bool quiet, QObject *parent = nullptr);
explicit MediaFileInfoObject(std::unique_ptr<TagParser::MediaFileInfo> &&mediaFileInfo, TagParser::Diagnostics &diag, QJSEngine *engine,
bool quiet, QObject *parent = nullptr);
~MediaFileInfoObject() override;
TagParser::MediaFileInfo &fileInfo();
QString path() const;
bool isPathRelative() const;
QString name() const;
QString nameWithoutExtension() const;
QString extension() const;
QString containingDirectory() const;
QString savePath() const;
void setSavePath(const QString &path);
QList<TagObject *> &tags();
public Q_SLOTS:
void applyChanges();
bool rename(const QString &newPath);
private:
TagParser::MediaFileInfo &m_f;
std::unique_ptr<TagParser::MediaFileInfo> m_f_owned;
TagParser::Diagnostics &m_diag;
QJSEngine *m_engine;
QList<TagObject *> m_tags;
bool m_quiet;
};
inline TagParser::MediaFileInfo &MediaFileInfoObject::fileInfo()
{
return m_f;
}
} // namespace Cli
#endif // CLI_SCRIPT_API_H

View File

@ -3,6 +3,8 @@
#include "../misc/networkaccessmanager.h"
#include "../misc/utility.h"
#include "resources/config.h"
#include <tagparser/signature.h>
#include <tagparser/tag.h>
#include <tagparser/tagvalue.h>
@ -25,7 +27,7 @@ SongDescription::SongDescription(const QString &songId)
}
std::list<QString> QueryResultsModel::s_coverNames = std::list<QString>();
map<QString, QByteArray> QueryResultsModel::s_coverData = map<QString, QByteArray>();
std::map<QString, QByteArray> QueryResultsModel::s_coverData = std::map<QString, QByteArray>();
QueryResultsModel::QueryResultsModel(QObject *parent)
: QAbstractTableModel(parent)
@ -104,7 +106,7 @@ QVariant QueryResultsModel::data(const QModelIndex &index, int role) const
if (!index.isValid() || index.row() >= m_results.size()) {
return QVariant();
}
const SongDescription &res = m_results.at(index.row());
const auto &res = m_results.at(index.row());
switch (role) {
case Qt::DisplayRole:
switch (index.column()) {
@ -146,7 +148,7 @@ QVariant QueryResultsModel::data(const QModelIndex &index, int role) const
Qt::ItemFlags QueryResultsModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags flags = Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
auto flags = Qt::ItemFlags(Qt::ItemNeverHasChildren | Qt::ItemIsSelectable | Qt::ItemIsEnabled);
if (index.isValid()) {
flags |= Qt::ItemIsUserCheckable;
}
@ -189,7 +191,7 @@ QVariant QueryResultsModel::headerData(int section, Qt::Orientation orientation,
int QueryResultsModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_results.size();
return parent.isValid() ? 0 : Utility::containerSizeToInt(m_results.size());
}
int QueryResultsModel::columnCount(const QModelIndex &parent) const
@ -209,6 +211,14 @@ const QByteArray *QueryResultsModel::cover(const QModelIndex &index) const
return nullptr;
}
QByteArray QueryResultsModel::coverValue(const QModelIndex &index) const
{
if (const auto *c = cover(index)) {
return *c;
}
return QByteArray();
}
/*!
* \brief Fetches the cover the specified \a index.
* \returns
@ -240,6 +250,14 @@ const QString *QueryResultsModel::lyrics(const QModelIndex &index) const
return nullptr;
}
QString QueryResultsModel::lyricsValue(const QModelIndex &index) const
{
if (const auto *l = lyrics(index)) {
return *l;
}
return QString();
}
/*!
* \brief Fetches the lyrics the specified \a index.
* \returns
@ -295,6 +313,16 @@ void HttpResultsModel::handleInitialReplyFinished()
setResultsAvailable(true); // update status, emit resultsAvailable()
}
#ifdef CPP_UTILITIES_DEBUG_BUILD
void HttpResultsModel::logReply(QNetworkReply *reply)
{
static const auto enableQueryLogging = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_ENABLE_QUERY_LOGGING");
if (enableQueryLogging) {
std::cerr << "HTTP query: " << reply->url().toString().toUtf8().data() << std::endl;
}
}
#endif
QNetworkReply *HttpResultsModel::evaluateReplyResults(QNetworkReply *reply, QByteArray &data, bool alwaysFollowRedirection)
{
// delete reply (later)
@ -312,8 +340,10 @@ QNetworkReply *HttpResultsModel::evaluateReplyResults(QNetworkReply *reply, QByt
m_errorList << tr("Server replied no data.");
}
#ifdef CPP_UTILITIES_DEBUG_BUILD
cerr << "Results from HTTP query:" << endl;
cerr << data.data() << endl;
static const auto enableQueryLogging = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_ENABLE_QUERY_LOGGING");
if (enableQueryLogging) {
std::cerr << "Results from HTTP query:\n" << data.data() << '\n';
}
#endif
return nullptr;
}
@ -350,7 +380,7 @@ void HttpResultsModel::abort()
void HttpResultsModel::handleCoverReplyFinished(QNetworkReply *reply, const QString &albumId, int row)
{
QByteArray data;
auto data = QByteArray();
if (auto *const newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&HttpResultsModel::handleCoverReplyFinished, this, newReply, albumId, row));
return;

View File

@ -22,7 +22,7 @@ TAGEDITOR_ENUM_CLASS KnownField : unsigned int;
namespace QtGui {
struct SongDescription {
SongDescription(const QString &songId = QString());
explicit SongDescription(const QString &songId = QString());
QString songId;
QString title;
@ -42,6 +42,9 @@ struct SongDescription {
class QueryResultsModel : public QAbstractTableModel {
Q_OBJECT
Q_PROPERTY(QStringList errorList READ errorList)
Q_PROPERTY(bool areResultsAvailable READ areResultsAvailable)
Q_PROPERTY(bool isFetchingCover READ isFetchingCover)
public:
enum Column {
@ -68,11 +71,13 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
const QByteArray *cover(const QModelIndex &index) const;
virtual bool fetchCover(const QModelIndex &index);
Q_INVOKABLE QByteArray coverValue(const QModelIndex &index) const;
Q_INVOKABLE virtual bool fetchCover(const QModelIndex &index);
const QString *lyrics(const QModelIndex &index) const;
virtual bool fetchLyrics(const QModelIndex &index);
virtual void abort();
virtual QUrl webUrl(const QModelIndex &index);
Q_INVOKABLE QString lyricsValue(const QModelIndex &index) const;
Q_INVOKABLE virtual bool fetchLyrics(const QModelIndex &index);
Q_INVOKABLE virtual void abort();
Q_INVOKABLE virtual QUrl webUrl(const QModelIndex &index);
Q_SIGNALS:
void resultsAvailable();
@ -80,7 +85,7 @@ Q_SIGNALS:
void lyricsAvailable(const QModelIndex &index);
protected:
QueryResultsModel(QObject *parent = nullptr);
explicit QueryResultsModel(QObject *parent = nullptr);
void setResultsAvailable(bool resultsAvailable);
void setFetchingCover(bool fetchingCover);
@ -119,7 +124,7 @@ public:
void abort() override;
protected:
HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
explicit HttpResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
template <class Object, class Function> void addReply(QNetworkReply *reply, Object object, Function handler);
template <class Function> void addReply(QNetworkReply *reply, Function handler);
virtual void parseInitialResults(const QByteArray &data) = 0;
@ -130,17 +135,20 @@ protected:
private Q_SLOTS:
void handleInitialReplyFinished();
#ifdef CPP_UTILITIES_DEBUG_BUILD
void logReply(QNetworkReply *reply);
#endif
protected:
QList<QNetworkReply *> m_replies;
const SongDescription m_initialDescription;
SongDescription m_initialDescription;
};
template <class Object, class Function> inline void HttpResultsModel::addReply(QNetworkReply *reply, Object object, Function handler)
{
(m_replies << reply), connect(reply, &QNetworkReply::finished, object, handler);
#ifdef CPP_UTILITIES_DEBUG_BUILD
std::cerr << "HTTP query: " << reply->url().toString().toUtf8().data() << std::endl;
logReply(reply);
#endif
}
@ -152,7 +160,7 @@ template <class Function> inline void HttpResultsModel::addReply(QNetworkReply *
{
(m_replies << reply), connect(reply, &QNetworkReply::finished, handler);
#ifdef CPP_UTILITIES_DEBUG_BUILD
std::cerr << "HTTP query: " << reply->url().toString().toUtf8().data() << std::endl;
logReply(reply);
#endif
}
@ -160,6 +168,7 @@ QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription);
QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription);
QNetworkReply *queryCoverArtArchive(const QString &albumId);
QueryResultsModel *queryMakeItPersonal(SongDescription &&songDescription);
QueryResultsModel *queryTekstowo(SongDescription &&songDescription);
} // namespace QtGui

View File

@ -2,6 +2,7 @@
#include "../application/settings.h"
#include "../misc/networkaccessmanager.h"
#include "../misc/utility.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
@ -18,7 +19,7 @@ using namespace Utility;
namespace QtGui {
static const QString defaultLyricsWikiaUrl(QStringLiteral("https://lyrics.fandom.com"));
static const auto defaultLyricsWikiaUrl = QStringLiteral("https://lyrics.fandom.com");
static QUrl lyricsWikiaApiUrl()
{
@ -42,15 +43,12 @@ LyricsWikiaResultsModel::LyricsWikiaResultsModel(SongDescription &&initialSongDe
bool LyricsWikiaResultsModel::fetchCover(const QModelIndex &index)
{
// FIXME: avoid code duplication with musicbrainz.cpp
// find song description
if (index.parent().isValid() || index.row() >= m_results.size()) {
if (index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) {
return true;
}
SongDescription &desc = m_results[index.row()];
// skip if cover is already available
auto &desc = m_results[index.row()];
if (!desc.cover.isEmpty()) {
return true;
}
@ -86,13 +84,12 @@ bool LyricsWikiaResultsModel::fetchCover(const QModelIndex &index)
bool LyricsWikiaResultsModel::fetchLyrics(const QModelIndex &index)
{
// find song description
if (index.parent().isValid() || index.row() >= m_results.size()) {
if (index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) {
return true;
}
SongDescription &desc = m_results[index.row()];
// skip if lyrics already present
auto &desc = m_results[index.row()];
if (!desc.lyrics.isEmpty()) {
return true;
}
@ -112,25 +109,24 @@ bool LyricsWikiaResultsModel::fetchLyrics(const QModelIndex &index)
void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
{
// prepare parsing LyricsWikia meta data
beginResetModel();
m_results.clear();
QXmlStreamReader xmlReader(data);
// parse XML tree
auto xmlReader = QXmlStreamReader(data);
// clang-format off
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("getArtistResponse") {
QString artist;
auto artist = QString();
children {
iftag("artist") {
artist = text;
} eliftag("albums") {
children {
iftag("albumResult") {
QString album, year;
QList<SongDescription> songs;
auto album = QString(), year = QString();
auto songs = QList<SongDescription>();
children {
iftag("album") {
album = text;
@ -141,7 +137,7 @@ void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
iftag("item") {
songs << SongDescription();
songs.back().title = text;
songs.back().track = songs.size();
songs.back().track = Utility::containerSizeToInt(songs.size());
}
else_skip
}
@ -157,7 +153,7 @@ void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
&& (!m_initialDescription.track || m_initialDescription.track == song.track)) {
song.album = album;
song.year = year;
song.totalTracks = songs.size();
song.totalTracks = Utility::containerSizeToInt(songs.size());
m_results << std::move(song);
}
}
@ -168,7 +164,7 @@ void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
}
else_skip
}
for (SongDescription &song : m_results) {
for (auto &song : m_results) {
// set the arist which is the same for all results
song.artist = artist;
// set the album ID (album is identified by its artist, year and name)
@ -190,14 +186,13 @@ void LyricsWikiaResultsModel::parseInitialResults(const QByteArray &data)
m_errorList << xmlReader.errorString();
}
// promote changes
endResetModel();
}
QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription &songDescription)
{
// compose URL
QUrlQuery query;
auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("func"), QStringLiteral("getSong"));
query.addQueryItem(QStringLiteral("action"), QStringLiteral("lyrics"));
query.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));
@ -208,22 +203,21 @@ QNetworkReply *LyricsWikiaResultsModel::requestSongDetails(const SongDescription
// specifying album seems to have no effect but also doesn't hurt
query.addQueryItem(QStringLiteral("album"), songDescription.album);
}
QUrl url(lyricsWikiaApiUrl());
auto url = lyricsWikiaApiUrl();
url.setQuery(query);
return Utility::networkAccessManager().get(QNetworkRequest(url));
}
QNetworkReply *LyricsWikiaResultsModel::requestAlbumDetails(const SongDescription &songDescription)
{
QUrl url(lyricsWikiaApiUrl());
auto url = lyricsWikiaApiUrl();
url.setPath(QStringLiteral("/wiki/") + songDescription.albumId);
return Utility::networkAccessManager().get(QNetworkRequest(url));
}
void LyricsWikiaResultsModel::handleSongDetailsFinished(QNetworkReply *reply, int row)
{
QByteArray data;
auto data = QByteArray();
if (auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&LyricsWikiaResultsModel::handleSongDetailsFinished, this, newReply, row));
} else if (!data.isEmpty()) {
@ -233,23 +227,21 @@ void LyricsWikiaResultsModel::handleSongDetailsFinished(QNetworkReply *reply, in
void LyricsWikiaResultsModel::parseSongDetails(int row, const QByteArray &data)
{
// find associated result/desc
if (row >= m_results.size()) {
m_errorList << tr("Internal error: context for song details reply invalid");
setResultsAvailable(true);
return;
}
SongDescription &assocDesc = m_results[row];
QUrl parsedUrl;
// parse XML tree
auto &assocDesc = m_results[row];
auto parsedUrl = QUrl();
auto xmlReader = QXmlStreamReader(data);
// clang-format off
QXmlStreamReader xmlReader(data);
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("LyricsResult") {
SongDescription parsedDesc;
auto parsedDesc = SongDescription();
children {
iftag("artist") {
parsedDesc.artist = text;
@ -296,7 +288,7 @@ void LyricsWikiaResultsModel::parseSongDetails(int row, const QByteArray &data)
.arg(assocDesc.artist, assocDesc.title);
}
// -> do not use parsed URL "as-is" in any case to avoid unintended requests
QUrl requestUrl(lyricsWikiaApiUrl());
auto requestUrl = lyricsWikiaApiUrl();
requestUrl.setPath(parsedUrl.path());
// -> initialize the actual request
auto *const reply = Utility::networkAccessManager().get(QNetworkRequest(requestUrl));
@ -305,7 +297,7 @@ void LyricsWikiaResultsModel::parseSongDetails(int row, const QByteArray &data)
void LyricsWikiaResultsModel::handleLyricsReplyFinished(QNetworkReply *reply, int row)
{
QByteArray data;
auto data = QByteArray();
if (auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&LyricsWikiaResultsModel::handleLyricsReplyFinished, this, newReply, row));
return;
@ -320,18 +312,15 @@ void LyricsWikiaResultsModel::handleLyricsReplyFinished(QNetworkReply *reply, in
void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data)
{
// find associated result/desc
if (row >= m_results.size()) {
m_errorList << tr("Internal error: context for LyricsWikia page reply invalid");
setResultsAvailable(true);
return;
}
SongDescription &assocDesc = m_results[row];
// convert data to QString
const QString html(data);
// parse lyrics from HTML
auto &assocDesc = m_results[row];
const auto html = QString(data);
const auto lyricsStart = html.indexOf(QLatin1String("<div class='lyricbox'>"));
if (lyricsStart < 0) {
m_errorList << tr("Song details requested for %1/%2 do not contain lyrics").arg(assocDesc.artist, assocDesc.title);
@ -339,7 +328,7 @@ void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data
return;
}
const auto lyricsEnd = html.indexOf(QLatin1String("<div class='lyricsbreak'></div>"), lyricsStart);
QTextDocument textDoc;
auto textDoc = QTextDocument();
textDoc.setHtml(html.mid(lyricsStart, (lyricsEnd > lyricsStart) ? (lyricsEnd - lyricsStart) : -1));
assocDesc.lyrics = textDoc.toPlainText();
@ -349,7 +338,7 @@ void LyricsWikiaResultsModel::parseLyricsResults(int row, const QByteArray &data
void LyricsWikiaResultsModel::handleAlbumDetailsReplyFinished(QNetworkReply *reply, int row)
{
QByteArray data;
auto data = QByteArray();
if (auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&LyricsWikiaResultsModel::handleAlbumDetailsReplyFinished, this, newReply, row));
} else {
@ -366,24 +355,21 @@ void LyricsWikiaResultsModel::parseAlbumDetailsAndFetchCover(int row, const QByt
return;
}
// find associated result/desc
if (row >= m_results.size()) {
m_errorList << tr("Internal error: context for LyricsWikia page reply invalid");
setFetchingCover(false);
setResultsAvailable(true);
return;
}
SongDescription &assocDesc = m_results[row];
// convert data to QString
const QString html(data);
// parse cover URL from HTML
const int coverDivStart = html.indexOf(QLatin1String("<div class=\"plainlinks\" style=\"clear:right; float:right;")) + 56;
auto &assocDesc = m_results[row];
const auto html = QString(data);
const auto coverDivStart = html.indexOf(QLatin1String("<div class=\"plainlinks\" style=\"clear:right; float:right;")) + 56;
if (coverDivStart > 56) {
const int coverHrefStart = html.indexOf(QLatin1String("href=\""), coverDivStart) + 6;
const auto coverHrefStart = html.indexOf(QLatin1String("href=\""), coverDivStart) + 6;
if (coverHrefStart > coverDivStart + 6) {
const int coverHrefEnd = html.indexOf(QLatin1String("\""), coverHrefStart);
const auto coverHrefEnd = html.indexOf(QLatin1String("\""), coverHrefStart);
if (coverHrefEnd > 0) {
assocDesc.coverUrl = html.mid(coverHrefStart, coverHrefEnd - coverHrefStart);
}
@ -405,35 +391,32 @@ void LyricsWikiaResultsModel::parseAlbumDetailsAndFetchCover(int row, const QByt
QUrl LyricsWikiaResultsModel::webUrl(const QModelIndex &index)
{
if (index.parent().isValid() || index.row() >= results().size()) {
if (index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) {
return QUrl();
}
SongDescription &desc = m_results[index.row()];
auto &desc = m_results[index.row()];
lazyInitializeLyricsWikiaSongId(desc);
// return URL
QUrl url(lyricsWikiaApiUrl());
auto url = lyricsWikiaApiUrl();
url.setPath(QStringLiteral("/wiki/") + desc.songId);
return url;
}
QueryResultsModel *queryLyricsWikia(SongDescription &&songDescription)
{
// compose URL
QUrlQuery query;
auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("func"), QStringLiteral("getArtist"));
query.addQueryItem(QStringLiteral("fmt"), QStringLiteral("xml"));
query.addQueryItem(QStringLiteral("fixXML"), QString());
query.addQueryItem(QStringLiteral("artist"), songDescription.artist);
QUrl url(lyricsWikiaApiUrl());
auto url = lyricsWikiaApiUrl();
url.setQuery(query);
return new LyricsWikiaResultsModel(std::move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url)));
// NOTE: Only getArtist seems to work, so artist must be specified and filtering must
// be done manually when parsing results.
// make request
return new LyricsWikiaResultsModel(std::move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url)));
}
} // namespace QtGui

View File

@ -11,7 +11,7 @@ class LyricsWikiaResultsModel : public HttpResultsModel {
Q_OBJECT
public:
LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
explicit LyricsWikiaResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchCover(const QModelIndex &index) override;
bool fetchLyrics(const QModelIndex &index) override;
QUrl webUrl(const QModelIndex &index) override;

View File

@ -37,32 +37,25 @@ bool MakeItPersonalResultsModel::fetchLyrics(const QModelIndex &index)
void MakeItPersonalResultsModel::parseInitialResults(const QByteArray &data)
{
// prepare parsing meta data
beginResetModel();
m_results.clear();
SongDescription desc = m_initialDescription;
auto desc = m_initialDescription;
desc.songId = m_initialDescription.artist + m_initialDescription.title;
desc.artistId = m_initialDescription.artist;
desc.lyrics = QString::fromUtf8(data).trimmed();
if (desc.lyrics != QLatin1String("Sorry, We don't have lyrics for this song yet.")) {
m_results << std::move(desc);
}
// promote changes
endResetModel();
}
QueryResultsModel *queryMakeItPersonal(SongDescription &&songDescription)
{
// compose URL
QUrlQuery query;
auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("artist"), songDescription.artist);
query.addQueryItem(QStringLiteral("title"), songDescription.title);
QUrl url(makeItPersonalApiUrl());
auto url = makeItPersonalApiUrl();
url.setQuery(query);
// make request
return new MakeItPersonalResultsModel(std::move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url)));
}

View File

@ -11,7 +11,7 @@ class MakeItPersonalResultsModel : public HttpResultsModel {
Q_OBJECT
public:
MakeItPersonalResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
explicit MakeItPersonalResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchLyrics(const QModelIndex &index) override;
protected:

View File

@ -40,15 +40,12 @@ MusicBrainzResultsModel::MusicBrainzResultsModel(SongDescription &&initialSongDe
bool MusicBrainzResultsModel::fetchCover(const QModelIndex &index)
{
// FIXME: avoid code duplication with lyricswikia.cpp
// find song description
if (index.parent().isValid() || index.row() >= m_results.size()) {
if (index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) {
return true;
}
SongDescription &desc = m_results[index.row()];
// skip if cover is already available
auto &desc = m_results[index.row()];
if (!desc.cover.isEmpty()) {
return true;
}
@ -76,26 +73,25 @@ bool MusicBrainzResultsModel::fetchCover(const QModelIndex &index)
QUrl MusicBrainzResultsModel::webUrl(const QModelIndex &index)
{
if (index.parent().isValid() || index.row() >= results().size()) {
if (index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) {
return QUrl();
}
return QUrl(QStringLiteral("https://musicbrainz.org/recording/") + results().at(index.row()).songId);
return QUrl(QStringLiteral("https://musicbrainz.org/recording/") + m_results.at(index.row()).songId);
}
void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
{
// prepare parsing MusicBrainz meta data
beginResetModel();
m_results.clear();
// store all song information (called recordings by MusicBrainz)
vector<SongDescription> recordings;
auto recordings = std::vector<SongDescription>();
// store all albums/collections (called releases by MusicBrainz) for a song
unordered_map<QString, vector<SongDescription>> releasesByRecording;
auto releasesByRecording = std::unordered_map<QString, std::vector<SongDescription>>();
// parse XML tree
QXmlStreamReader xmlReader(data);
// clang-format off
auto xmlReader = QXmlStreamReader(data);
// clang-format off
#include <qtutilities/misc/xmlparsermacros.h>
children {
iftag("metadata") {
@ -103,7 +99,7 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
iftag("recording-list") {
children {
iftag("recording") {
SongDescription currentDescription(attribute("id").toString());
auto currentDescription = SongDescription(attribute("id").toString());
children {
iftag("title") {
currentDescription.title = text;
@ -130,7 +126,7 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
} eliftag("release-list") {
children {
iftag("release") {
SongDescription releaseInfo;
auto releaseInfo = SongDescription();
releaseInfo.albumId = attribute("id").toString();
children {
iftag("title") {
@ -220,7 +216,7 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
// populate results
// -> create a song for each recording/release combination and group those songs by their releases sorted ascendingly from oldest to latest
map<QString, vector<SongDescription>> recordingsByRelease;
auto recordingsByRelease = std::map<QString, std::vector<SongDescription>>();
for (const auto &recording : recordings) {
const auto &releases = releasesByRecording[recording.songId];
for (const auto &release : releases) {
@ -272,7 +268,6 @@ void MusicBrainzResultsModel::parseInitialResults(const QByteArray &data)
m_errorList << xmlReader.errorString();
}
// promote changes
endResetModel();
}
// clang-format on
@ -281,8 +276,7 @@ QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription)
{
static const auto defaultMusicBrainzUrl(QStringLiteral("https://musicbrainz.org/ws/2/recording/"));
// compose parts
QStringList parts;
auto parts = QStringList();
parts.reserve(4);
if (!songDescription.title.isEmpty()) {
parts << QChar('\"') % songDescription.title % QChar('\"');
@ -297,15 +291,12 @@ QueryResultsModel *queryMusicBrainz(SongDescription &&songDescription)
parts << QStringLiteral("number:") + QString::number(songDescription.track);
}
// compose URL
const auto &musicBrainzUrl = Settings::values().dbQuery.musicBrainzUrl;
QUrl url(musicBrainzUrl.isEmpty() ? defaultMusicBrainzUrl : (musicBrainzUrl + QStringLiteral("/recording/")));
QUrlQuery query;
auto url = QUrl(musicBrainzUrl.isEmpty() ? defaultMusicBrainzUrl : (musicBrainzUrl + QStringLiteral("/recording/")));
auto query = QUrlQuery();
query.addQueryItem(QStringLiteral("query"), parts.join(QStringLiteral(" AND ")));
url.setQuery(query);
// make request
QNetworkRequest request(url);
auto request = QNetworkRequest(url);
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("Mozilla/5.0 (X11; Linux x86_64; rv:54.0) Gecko/20100101 Firefox/54.0"));
return new MusicBrainzResultsModel(std::move(songDescription), Utility::networkAccessManager().get(request));
}

View File

@ -15,7 +15,7 @@ private:
enum What { MusicBrainzMetaData, CoverArt };
public:
MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
explicit MusicBrainzResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchCover(const QModelIndex &index) override;
QUrl webUrl(const QModelIndex &index) override;

163
dbquery/tekstowo.cpp Normal file
View File

@ -0,0 +1,163 @@
#include "./tekstowo.h"
#include "../application/settings.h"
#include "../misc/networkaccessmanager.h"
#include "../misc/utility.h"
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QTextDocumentFragment>
#include <QUrl>
#include <functional>
using namespace std;
using namespace std::placeholders;
using namespace Utility;
namespace QtGui {
static const auto defaultTekstowoUrl = QStringLiteral("https://www.tekstowo.pl");
static QUrl tekstowoUrl()
{
const auto &url = Settings::values().dbQuery.tekstowoUrl;
return QUrl(url.isEmpty() ? defaultTekstowoUrl : url);
}
TekstowoResultsModel::TekstowoResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply)
: HttpResultsModel(std::move(initialSongDescription), reply)
{
}
bool TekstowoResultsModel::fetchLyrics(const QModelIndex &index)
{
if ((index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) || !m_results[index.row()].lyrics.isEmpty()) {
return true;
}
const auto url = webUrl(index);
if (url.isEmpty()) {
m_errorList << tr("Unable to fetch lyrics: web URL is unknown.");
emit resultsAvailable();
return true;
}
auto *reply = Utility::networkAccessManager().get(QNetworkRequest(url));
addReply(reply, bind(&TekstowoResultsModel::handleLyricsReplyFinished, this, reply, index.row()));
return false;
}
void TekstowoResultsModel::parseInitialResults(const QByteArray &data)
{
beginResetModel();
m_results.clear();
auto dropLast = false;
auto hasExactMatch = false;
auto exactMatch = QList<SongDescription>::size_type();
for (auto index = exactMatch; index >= 0;) {
const auto linkStart = data.indexOf("<a href=\"/piosenka,", index);
if (linkStart < 0) {
break;
}
const auto hrefStart = linkStart + 9;
const auto hrefEnd = data.indexOf("\"", hrefStart + 1);
if (hrefEnd <= hrefStart) {
break;
}
const auto linkEnd = data.indexOf("</a>", hrefEnd);
if (linkEnd < linkStart) {
break;
}
index = linkEnd + 4;
auto linkText = QTextDocumentFragment::fromHtml(QString::fromUtf8(data.begin() + linkStart, linkEnd + 3 - linkStart)).toPlainText().trimmed();
auto titleStart = linkText.indexOf(QLatin1String(" - "));
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
auto &songDetails = dropLast ? m_results.back() : m_results.emplace_back();
#else
if (!dropLast) {
m_results.append(SongDescription());
}
auto &songDetails = m_results.back();
#endif
songDetails.songId = QTextDocumentFragment::fromHtml(QString::fromUtf8(data.begin() + hrefStart, hrefEnd - hrefStart)).toPlainText();
if (titleStart > -1) {
songDetails.artist = linkText.mid(0, titleStart);
if (songDetails.artist != m_initialDescription.artist) {
dropLast = true;
continue;
}
songDetails.title = linkText.mid(titleStart + 3);
} else {
songDetails.title = std::move(linkText);
}
if (!hasExactMatch && songDetails.title == m_initialDescription.title) {
hasExactMatch = true;
exactMatch = m_results.size() - 1;
}
dropLast = false;
}
if (dropLast) {
m_results.pop_back();
}
// ensure the first exact match for the song title is placed first
if (hasExactMatch && exactMatch != 0) {
std::swap(m_results[exactMatch], m_results[0]);
}
endResetModel();
}
void TekstowoResultsModel::handleLyricsReplyFinished(QNetworkReply *reply, int row)
{
auto data = QByteArray();
if (auto *newReply = evaluateReplyResults(reply, data, true)) {
addReply(newReply, bind(&TekstowoResultsModel::handleLyricsReplyFinished, this, newReply, row));
return;
}
if (!data.isEmpty()) {
parseLyricsResults(row, data);
}
if (!m_resultsAvailable) {
setResultsAvailable(true);
}
}
void TekstowoResultsModel::parseLyricsResults(int row, const QByteArray &data)
{
if (row >= m_results.size()) {
m_errorList << tr("Internal error: context for Tekstowo page reply invalid");
setResultsAvailable(true);
return;
}
auto lyricsStart = data.indexOf("<div class=\"inner-text\">");
if (lyricsStart < 0) {
const auto &assocDesc = m_results[row];
m_errorList << tr("Song details requested for %1/%2 do not contain lyrics").arg(assocDesc.artist, assocDesc.title);
setResultsAvailable(true);
return;
}
const auto lyricsEnd = data.indexOf("</div>", lyricsStart += 24); // hopefully lyrics don't contain nested </div>
m_results[row].lyrics = QTextDocumentFragment::fromHtml(
QString::fromUtf8(data.data() + lyricsStart, lyricsEnd > -1 ? lyricsEnd - lyricsStart : data.size() - lyricsStart))
.toPlainText()
.trimmed();
setResultsAvailable(true);
emit lyricsAvailable(index(row, 0));
}
QUrl TekstowoResultsModel::webUrl(const QModelIndex &index)
{
if (index.parent().isValid() || !index.isValid() || index.row() >= m_results.size()) {
return QUrl();
}
auto url = tekstowoUrl();
url.setPath(m_results[index.row()].songId);
return url;
}
QueryResultsModel *queryTekstowo(SongDescription &&songDescription)
{
auto url = tekstowoUrl();
url.setPath(QStringLiteral("/szukaj,wykonawca,%1,tytul,%2.html").arg(songDescription.artist, songDescription.title));
return new TekstowoResultsModel(std::move(songDescription), Utility::networkAccessManager().get(QNetworkRequest(url)));
}
} // namespace QtGui

28
dbquery/tekstowo.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef QTGUI_TEKSTOWO_H
#define QTGUI_TEKSTOWO_H
#include "./dbquery.h"
#include <map>
namespace QtGui {
class TekstowoResultsModel : public HttpResultsModel {
Q_OBJECT
public:
explicit TekstowoResultsModel(SongDescription &&initialSongDescription, QNetworkReply *reply);
bool fetchLyrics(const QModelIndex &index) override;
QUrl webUrl(const QModelIndex &index) override;
protected:
void parseInitialResults(const QByteArray &data) override;
private:
void handleLyricsReplyFinished(QNetworkReply *reply, int row);
void parseLyricsResults(int row, const QByteArray &data);
};
} // namespace QtGui
#endif // QTGUI_TEKSTOWO_H

View File

@ -73,6 +73,13 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>addPushButton</tabstop>
<tabstop>extractPushButton</tabstop>
<tabstop>restorePushButton</tabstop>
<tabstop>clearPushButton</tabstop>
<tabstop>treeView</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -1,5 +1,7 @@
#include "./attachmentsmodel.h"
#include "../misc/utility.h"
#include <tagparser/abstractattachment.h>
#include <c++utilities/conversion/stringconversion.h>
@ -233,7 +235,7 @@ int AttachmentsModel::rowCount(const QModelIndex &parent) const
if (parent.isValid()) {
return 0;
} else {
return m_attachments.size();
return Utility::containerSizeToInt(m_attachments.size());
}
}
@ -251,7 +253,7 @@ void AttachmentsModel::revert()
for (auto &item : m_attachments) {
item.revert();
}
emit dataChanged(index(0, 0), index(m_attachments.size() - 1, 0), QVector<int>() << Qt::CheckStateRole);
emit dataChanged(index(0, 0), index(Utility::containerSizeToInt(m_attachments.size()) - 1, 0), QVector<int>() << Qt::CheckStateRole);
}
bool AttachmentsModel::submit()
@ -268,7 +270,7 @@ void AttachmentsModel::repealSelection()
for (auto &item : m_attachments) {
item.setActivated(false);
}
emit dataChanged(index(0, 0), index(m_attachments.size() - 1, 0), QVector<int>() << Qt::CheckStateRole);
emit dataChanged(index(0, 0), index(Utility::containerSizeToInt(m_attachments.size()) - 1, 0), QVector<int>() << Qt::CheckStateRole);
}
}
@ -283,7 +285,7 @@ AbstractAttachment *AttachmentsModel::attachment(const QModelIndex &index)
void AttachmentsModel::addAttachment(int row, AbstractAttachment *attachment, bool activated, const QString &location)
{
if (row < 0 || row > m_attachments.size()) {
row = m_attachments.size();
row = Utility::containerSizeToInt(m_attachments.size());
}
beginInsertRows(QModelIndex(), row, row);
m_attachments.insert(row, AttachmentItem(attachment, activated, location));

View File

@ -1,5 +1,7 @@
#include "./codeedit.h"
#include "../misc/utility.h"
#include <QTextBlock>
#include <QTextDocumentFragment>
@ -49,7 +51,7 @@ void CodeEdit::handleReturn(QKeyEvent *)
if (index < line.size() && line.at(index) == QChar('}')) {
if (index > 0) {
int beg = index;
index -= m_indentation.size();
index -= Utility::containerSizeToInt(m_indentation.size());
cursor.select(QTextCursor::BlockUnderCursor);
cursor.deleteChar();
cursor.insertBlock();

View File

@ -7,6 +7,7 @@
#include "../dbquery/dbquery.h"
#include "../misc/utility.h"
#include "resources/config.h"
#include "ui_dbquerywidget.h"
#include <tagparser/tag.h>
@ -47,13 +48,16 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent)
, m_coverIndex(-1)
, m_lyricsIndex(-1)
, m_menu(new QMenu(parent))
, m_insertPresentDataAction(nullptr)
, m_searchMusicBrainzAction(nullptr)
, m_searchLyricsWikiaAction(nullptr)
, m_searchMakeItPersonalAction(nullptr)
, m_searchTekstowoAction(nullptr)
, m_lastSearchAction(nullptr)
, m_refreshAutomaticallyAction(nullptr)
{
m_ui->setupUi(this);
#ifdef Q_OS_WIN32
setStyleSheet(dialogStyle());
#else
setStyleSheet(QStringLiteral("QGroupBox { color: palette(text); background-color: palette(base); }"));
#endif
updateStyleSheet();
m_ui->notificationLabel->setText(tr("Search hasn't been started"));
m_ui->notificationLabel->setContext(tr("MusicBrainz/LyricsWikia notifications"));
@ -76,20 +80,27 @@ DbQueryWidget::DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent)
// setup menu
const auto searchIcon = QIcon::fromTheme(QStringLiteral("search"));
const auto enableLegacyProvider = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_ENABLE_LEGACY_METADATA_PROVIDERS");
m_menu->setTitle(tr("New search"));
m_menu->setIcon(searchIcon);
m_searchMusicBrainzAction = m_lastSearchAction = m_menu->addAction(tr("Query MusicBrainz"));
m_searchMusicBrainzAction->setIcon(searchIcon);
m_searchMusicBrainzAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_M));
connect(m_searchMusicBrainzAction, &QAction::triggered, this, &DbQueryWidget::searchMusicBrainz);
m_searchLyricsWikiaAction = m_menu->addAction(tr("Query LyricsWikia"));
m_searchLyricsWikiaAction->setIcon(searchIcon);
m_searchLyricsWikiaAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_L));
connect(m_searchLyricsWikiaAction, &QAction::triggered, this, &DbQueryWidget::searchLyricsWikia);
m_searchMakeItPersonalAction = m_menu->addAction(tr("Query makeitpersonal"));
m_searchMakeItPersonalAction->setIcon(searchIcon);
m_searchMakeItPersonalAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_K));
connect(m_searchMakeItPersonalAction, &QAction::triggered, this, &DbQueryWidget::searchMakeItPersonal);
if (enableLegacyProvider) {
m_searchLyricsWikiaAction = m_menu->addAction(tr("Query LyricsWikia"));
m_searchLyricsWikiaAction->setIcon(searchIcon);
m_searchLyricsWikiaAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_L));
connect(m_searchLyricsWikiaAction, &QAction::triggered, this, &DbQueryWidget::searchLyricsWikia);
m_searchMakeItPersonalAction = m_menu->addAction(tr("Query makeitpersonal"));
m_searchMakeItPersonalAction->setIcon(searchIcon);
m_searchMakeItPersonalAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_K));
connect(m_searchMakeItPersonalAction, &QAction::triggered, this, &DbQueryWidget::searchMakeItPersonal);
}
m_searchTekstowoAction = m_menu->addAction(tr("Query Tekstowo"));
m_searchTekstowoAction->setIcon(searchIcon);
m_searchTekstowoAction->setShortcut(QKeySequence(Qt::CTRL, Qt::Key_T));
connect(m_searchTekstowoAction, &QAction::triggered, this, &DbQueryWidget::searchTekstowo);
m_menu->addSeparator();
m_insertPresentDataAction = m_menu->addAction(tr("Use present data as search criteria"));
m_insertPresentDataAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
@ -119,19 +130,18 @@ DbQueryWidget::~DbQueryWidget()
values().dbQuery.override = m_ui->overrideCheckBox->isChecked();
}
void DbQueryWidget::insertSearchTermsFromTagEdit(TagEdit *tagEdit, bool songSpecific)
void DbQueryWidget::insertSearchTermsFromTagEdit(TagEdit *tagEdit)
{
if (!tagEdit) {
return;
}
bool somethingChanged = false;
// be always song-specific when querying makeitpersonal
songSpecific = m_lastSearchAction == m_searchMakeItPersonalAction;
auto somethingChanged = false;
auto lyricsCentricProvider
= (m_lastSearchAction == m_searchTekstowoAction) || (m_searchMakeItPersonalAction && m_lastSearchAction == m_searchMakeItPersonalAction);
// set album and artist
if (m_lastSearchAction != m_searchMakeItPersonalAction) {
if (!lyricsCentricProvider) {
const auto newAlbum = tagValueToQString(tagEdit->value(KnownField::Album));
if (m_ui->albumLineEdit->text() != newAlbum) {
m_ui->albumLineEdit->setText(newAlbum);
@ -144,26 +154,16 @@ void DbQueryWidget::insertSearchTermsFromTagEdit(TagEdit *tagEdit, bool songSpec
somethingChanged = true;
}
if (!songSpecific) {
return;
}
// set title and track number
const auto newTitle = tagValueToQString(tagEdit->value(KnownField::Title));
if (m_ui->titleLineEdit->text() != newTitle) {
m_ui->titleLineEdit->setText(newTitle);
somethingChanged = true;
}
if (m_lastSearchAction != m_searchMakeItPersonalAction) {
const auto newTrackNumber = tagEdit->trackNumber();
if (m_ui->trackSpinBox->value() != newTrackNumber) {
m_ui->trackSpinBox->setValue(newTrackNumber);
if (lyricsCentricProvider) {
if (const auto newTitle = tagValueToQString(tagEdit->value(KnownField::Title)); m_ui->titleLineEdit->text() != newTitle) {
m_ui->titleLineEdit->setText(newTitle);
somethingChanged = true;
}
}
// refresh automatically if enabled and something has changed
if (somethingChanged && m_refreshAutomaticallyAction->isChecked()) {
if (somethingChanged && m_refreshAutomaticallyAction && m_refreshAutomaticallyAction->isChecked()) {
m_lastSearchAction->trigger();
}
}
@ -250,6 +250,30 @@ void DbQueryWidget::searchMakeItPersonal()
useQueryResults(queryMakeItPersonal(currentSongDescription()));
}
void DbQueryWidget::searchTekstowo()
{
m_lastSearchAction = m_searchTekstowoAction;
// check whether enough search terms are supplied
if (m_ui->artistLineEdit->text().isEmpty() || m_ui->titleLineEdit->text().isEmpty()) {
m_ui->notificationLabel->setNotificationType(NotificationType::Critical);
m_ui->notificationLabel->setText(tr("Insufficient search criteria supplied - artist and title are mandatory"));
return;
}
// delete current model
m_ui->resultsTreeView->setModel(nullptr);
delete m_model;
// show status
m_ui->notificationLabel->setNotificationType(NotificationType::Progress);
m_ui->notificationLabel->setText(tr("Retrieving lyrics from Tekstowo ..."));
setStatus(false);
// do actual query
useQueryResults(queryTekstowo(currentSongDescription()));
}
void DbQueryWidget::abortSearch()
{
if (!m_model) {
@ -283,7 +307,8 @@ void DbQueryWidget::showResults()
if (m_model->results().isEmpty()) {
m_ui->notificationLabel->setText(tr("No results available"));
} else {
m_ui->notificationLabel->setText(tr("%1 result(s) available", nullptr, m_model->results().size()).arg(m_model->results().size()));
m_ui->notificationLabel->setText(
tr("%1 result(s) available", nullptr, Utility::containerSizeToInt(m_model->results().size())).arg(m_model->results().size()));
}
} else {
m_ui->notificationLabel->setNotificationType(NotificationType::Critical);
@ -334,7 +359,9 @@ void DbQueryWidget::setStatus(bool aborted)
{
m_ui->abortPushButton->setVisible(!aborted);
m_searchMusicBrainzAction->setEnabled(aborted);
m_searchLyricsWikiaAction->setEnabled(aborted);
if (m_searchLyricsWikiaAction) {
m_searchLyricsWikiaAction->setEnabled(aborted);
}
m_ui->applyPushButton->setVisible(aborted);
}
@ -714,6 +741,18 @@ void DbQueryWidget::showLyricsFromIndex(const QModelIndex &index)
}
}
bool DbQueryWidget::event(QEvent *event)
{
const auto res = QWidget::event(event);
switch (event->type()) {
case QEvent::PaletteChange:
updateStyleSheet();
break;
default:;
}
return res;
}
void DbQueryWidget::clearSearchCriteria()
{
m_ui->titleLineEdit->clear();
@ -722,6 +761,15 @@ void DbQueryWidget::clearSearchCriteria()
m_ui->trackSpinBox->setValue(0);
}
void DbQueryWidget::updateStyleSheet()
{
#ifdef Q_OS_WINDOWS
setStyleSheet(dialogStyleForPalette(palette()));
#else
setStyleSheet(QStringLiteral("QGroupBox { color: palette(text); background-color: palette(base); }"));
#endif
}
bool DbQueryWidget::eventFilter(QObject *obj, QEvent *event)
{
if (obj == m_ui->searchGroupBox) {

View File

@ -31,7 +31,7 @@ public:
explicit DbQueryWidget(TagEditorWidget *tagEditorWidget, QWidget *parent = nullptr);
~DbQueryWidget() override;
void insertSearchTermsFromTagEdit(TagEdit *tagEdit, bool songSpecific = false);
void insertSearchTermsFromTagEdit(TagEdit *tagEdit);
SongDescription currentSongDescription() const;
void applyResults(TagEdit *tagEdit, const QModelIndex &resultIndex);
@ -39,6 +39,7 @@ public Q_SLOTS:
void searchMusicBrainz();
void searchLyricsWikia();
void searchMakeItPersonal();
void searchTekstowo();
void abortSearch();
void applySelectedResults();
void applySpecifiedResults(const QModelIndex &modelIndex);
@ -49,6 +50,7 @@ public Q_SLOTS:
void clearSearchCriteria();
private Q_SLOTS:
void updateStyleSheet();
void showResults();
void setStatus(bool aborted);
void fileStatusChanged(bool opened, bool hasTags);
@ -65,6 +67,7 @@ private Q_SLOTS:
void showLyricsFromIndex(const QModelIndex &index);
protected:
bool event(QEvent *event) override;
bool eventFilter(QObject *obj, QEvent *event) override;
private:
@ -80,6 +83,7 @@ private:
QAction *m_searchMusicBrainzAction;
QAction *m_searchLyricsWikiaAction;
QAction *m_searchMakeItPersonalAction;
QAction *m_searchTekstowoAction;
QAction *m_lastSearchAction;
QAction *m_refreshAutomaticallyAction;
QPoint m_contextMenuPos;

View File

@ -382,6 +382,19 @@
<header location="global">qtutilities/widgets/clearspinbox.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>abortPushButton</tabstop>
<tabstop>searchPushButton</tabstop>
<tabstop>applyPushButton</tabstop>
<tabstop>trackSpinBox</tabstop>
<tabstop>titleLineEdit</tabstop>
<tabstop>albumLineEdit</tabstop>
<tabstop>artistLineEdit</tabstop>
<tabstop>fieldsListView</tabstop>
<tabstop>overrideCheckBox</tabstop>
<tabstop>autoInsertCheckBox</tabstop>
<tabstop>resultsTreeView</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -12,7 +12,7 @@
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Auto correction/completion will be applied when showing the selected tag fields after &lt;span style=&quot; font-style:italic;&quot;&gt;loading&lt;/span&gt; a file but &lt;span style=&quot; font-style:italic;&quot;&gt;not&lt;/span&gt; before saving. So the tag values you see when opening a file are already corrected. However, when clicking the save button the tags are stored as they are with no further correction applied.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Auto correction/completion will be applied when showing the selected tag fields after &lt;span style=&quot; font-style:italic;&quot;&gt;loading&lt;/span&gt; a file but &lt;span style=&quot; font-style:italic;&quot;&gt;not&lt;/span&gt; before saving. This means the tag values you see when opening a file are already corrected. This also means when clicking the &amp;quot;Save&amp;quot; button the tags are saved as they are shown in the editor at this point with no further correction applied.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
@ -97,32 +97,52 @@
</widget>
</item>
<item>
<widget class="QLabel" name="fieldsLabel">
<property name="styleSheet">
<string notr="true">font-weight: bold;</string>
</property>
<property name="text">
<string>Fields</string>
</property>
</widget>
</item>
<item>
<widget class="QListView" name="fieldsListView">
<property name="showDropIndicator" stdset="0">
<bool>true</bool>
</property>
<property name="flow">
<enum>QListView::LeftToRight</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>true</bool>
</property>
<property name="spacing">
<number>3</number>
</property>
<property name="wordWrap">
<bool>true</bool>
<widget class="QGroupBox" name="fieldsGroupBox">
<property name="title">
<string>Fields to apply the auto correction on</string>
</property>
<layout class="QVBoxLayout" name="fieldsVerticalLayout">
<property name="leftMargin">
<number>1</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>1</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListView" name="fieldsListView">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="lineWidth">
<number>0</number>
</property>
<property name="showDropIndicator" stdset="0">
<bool>true</bool>
</property>
<property name="flow">
<enum>QListView::LeftToRight</enum>
</property>
<property name="isWrapping" stdset="0">
<bool>true</bool>
</property>
<property name="spacing">
<number>3</number>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
@ -140,6 +160,16 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>insertTitleFromFilenameCheckBox</tabstop>
<tabstop>trimWhitespacesCheckBox</tabstop>
<tabstop>formatNamesCheckBox</tabstop>
<tabstop>fixUmlautsCheckBox</tabstop>
<tabstop>customSubstitutionGroupBox</tabstop>
<tabstop>regularExpressionLineEdit</tabstop>
<tabstop>replacementLineEdit</tabstop>
<tabstop>fieldsListView</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -3,7 +3,7 @@
<class>QtGui::EditorDbQueryOptionsPage</class>
<widget class="QWidget" name="QtGui::EditorDbQueryOptionsPage">
<property name="windowTitle">
<string>MusicBrainz</string>
<string>Metadata search</string>
</property>
<property name="styleSheet">
<string notr="true">QGroupBox { font-weight: bold };</string>
@ -60,6 +60,11 @@
<header location="global">qtutilities/widgets/clearlineedit.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>musicBrainzUrlLineEdit</tabstop>
<tabstop>lyricWikiUrlLineEdit</tabstop>
<tabstop>coverArtArchiveUrlLineEdit</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -68,10 +68,7 @@ EnterTargetDialog::EnterTargetDialog(QWidget *parent)
{
// setup UI
m_ui->setupUi(this);
// apply style sheets
#ifdef Q_OS_WIN32
setStyleSheet(dialogStyle());
#endif
updateStyleSheet();
// setup views
m_ui->tracksListView->setModel(m_tracksModel);
m_ui->chaptersListView->setModel(m_chaptersModel);
@ -94,6 +91,13 @@ void EnterTargetDialog::updateLevelNamePlaceholderText(int i)
i >= 0 ? tagTargetLevelName(containerTargetLevel(m_currentContainerFormat, static_cast<std::uint32_t>(i))) : std::string_view()));
}
void EnterTargetDialog::updateStyleSheet()
{
#ifdef Q_OS_WINDOWS
setStyleSheet(dialogStyleForPalette(palette()));
#endif
}
TagParser::TagTarget EnterTargetDialog::target() const
{
TagTarget target;
@ -141,4 +145,16 @@ void EnterTargetDialog::setTarget(const TagTarget &target, const MediaFileInfo *
}
}
bool EnterTargetDialog::event(QEvent *event)
{
const auto res = QDialog::event(event);
switch (event->type()) {
case QEvent::PaletteChange:
updateStyleSheet();
break;
default:;
}
return res;
}
} // namespace QtGui

View File

@ -33,8 +33,12 @@ public:
TagParser::TagTarget target() const;
void setTarget(const TagParser::TagTarget &target, const TagParser::MediaFileInfo *file = nullptr);
protected:
bool event(QEvent *event) override;
private Q_SLOTS:
void updateLevelNamePlaceholderText(int i);
void updateStyleSheet();
private:
std::unique_ptr<Ui::EnterTargetDialog> m_ui;

View File

@ -189,7 +189,17 @@
<extends>QLineEdit</extends>
<header location="global">qtutilities/widgets/clearlineedit.h</header>
</customwidget>
</customwidgets>
</customwidgets>
<tabstops>
<tabstop>levelSpinBox</tabstop>
<tabstop>levelNameLineEdit</tabstop>
<tabstop>tracksListView</tabstop>
<tabstop>chaptersListView</tabstop>
<tabstop>editionsListView</tabstop>
<tabstop>attachmentsListView</tabstop>
<tabstop>abortPushButton</tabstop>
<tabstop>confirmPushButton</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -340,6 +340,13 @@ void FileInfoModel::updateCache()
containerHelper.appendRow(tr("Document read version"), container->doctypeReadVersion());
containerHelper.appendRow(tr("Tag position"), Utility::elementPositionToQString(container->determineTagPosition(diag)));
containerHelper.appendRow(tr("Index position"), Utility::elementPositionToQString(container->determineIndexPosition(diag)));
const auto *const constContainer = container;
if (const auto &muxingApps = constContainer->muxingApplications(); !muxingApps.empty()) {
containerHelper.appendRow(tr("Muxing application"), qstr(joinStrings(muxingApps, ", ")));
}
if (const auto &writingApps = constContainer->writingApplications(); !writingApps.empty()) {
containerHelper.appendRow(tr("Writing application"), qstr(joinStrings(writingApps, ", ")));
}
}
containerHelper.appendRow(tr("Padding size"), m_file->paddingSize());

View File

@ -239,6 +239,20 @@ another position would prevent rewriting the entire file</string>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>forceRewriteCheckBox</tabstop>
<tabstop>minPaddingSpinBox</tabstop>
<tabstop>maxPaddingSpinBox</tabstop>
<tabstop>preferredPaddingSpinBox</tabstop>
<tabstop>tagPosBeforeDataRadioButton</tabstop>
<tabstop>tagPosAfterDataRadioButton</tabstop>
<tabstop>tagPosKeepRadioButton</tabstop>
<tabstop>tagPosForceCheckBox</tabstop>
<tabstop>indexPosBeforeDataRadioButton</tabstop>
<tabstop>indexPosAfterDataRadioButton</tabstop>
<tabstop>indexPosKeepRadioButton</tabstop>
<tabstop>indexPosForceCheckBox</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -169,6 +169,14 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>widthSpinBox</tabstop>
<tabstop>heightSpinBox</tabstop>
<tabstop>formatComboBox</tabstop>
<tabstop>aspectRatioComboBox</tabstop>
<tabstop>abortPushButton</tabstop>
<tabstop>confirmPushButton</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -7,12 +7,13 @@
#include "resources/config.h"
#include "resources/qtconfig.h"
#include <QApplication> // ensure QGuiApplication is defined before resources.h for desktop file name
#include <qtutilities/resources/importplugin.h>
#include <qtutilities/resources/qtconfigarguments.h>
#include <qtutilities/resources/resources.h>
#include <qtutilities/settingsdialog/qtsettings.h>
#include <QApplication>
#include <QMessageBox>
ENABLE_QT_RESOURCES_OF_STATIC_DEPENDENCIES
@ -42,6 +43,7 @@ int runWidgetsGui(int argc, char *argv[], const QtConfigArguments &qtConfigArgs,
// apply settings specified via command line args after the settings chosen in the GUI to give the CLI options precedence
auto &settings = Settings::values();
settings.qt.disableNotices();
settings.qt.apply();
qtConfigArgs.applySettings(settings.qt.hasCustomFont());

View File

@ -1,5 +1,7 @@
#include "./javascripthighlighter.h"
#include "../misc/utility.h"
namespace QtGui {
JavaScriptHighlighter::JavaScriptHighlighter(QTextDocument *parent)
@ -55,8 +57,8 @@ void JavaScriptHighlighter::highlightBlock(const QString &text)
const auto &expression(rule.pattern);
auto match = expression.match(text);
while (match.hasMatch()) {
const auto index = match.capturedStart();
const auto length = match.capturedLength();
const auto index = Utility::containerSizeToInt(match.capturedStart());
const auto length = Utility::containerSizeToInt(match.capturedLength());
setFormat(index, length, rule.format);
match = expression.match(text, index + length);
}
@ -65,21 +67,21 @@ void JavaScriptHighlighter::highlightBlock(const QString &text)
auto startIndex = 0;
if (previousBlockState() != 1) {
startIndex = m_commentStartExpression.match(text).capturedStart();
startIndex = Utility::containerSizeToInt(m_commentStartExpression.match(text).capturedStart());
}
while (startIndex >= 0) {
const auto endMatch = m_commentEndExpression.match(text, startIndex);
const auto endIndex = endMatch.capturedStart();
const auto endIndex = Utility::containerSizeToInt(endMatch.capturedStart());
const auto commentLength = [&] {
if (endIndex >= 0) {
return endIndex - startIndex + endMatch.capturedLength();
return endIndex - startIndex + Utility::containerSizeToInt(endMatch.capturedLength());
}
setCurrentBlockState(1);
return text.length() - startIndex;
return Utility::containerSizeToInt(text.length()) - startIndex;
}();
setFormat(startIndex, commentLength, m_multiLineCommentFormat);
startIndex = m_commentStartExpression.match(text, startIndex + commentLength).capturedStart();
startIndex = Utility::containerSizeToInt(m_commentStartExpression.match(text, startIndex + commentLength).capturedStart());
}
}

View File

@ -76,11 +76,7 @@ MainWindow::MainWindow(QWidget *parent)
{
// setup UI
m_ui->setupUi(this);
#ifdef Q_OS_WIN32
setStyleSheet(dialogStyle() + QStringLiteral("#tagEditorWidget { color: palette(text); background-color: palette(base); }"));
#else
setStyleSheet(dialogStyle());
#endif
updateStyleSheet();
// restore geometry and state
const auto &settings = Settings::values();
@ -166,7 +162,7 @@ QString MainWindow::currentDirectory() const
/*!
* \brief Sets the directory the file browser is showing.
* If a file is specified the file will be opended.
* If a file is specified the file will be opened.
*/
void MainWindow::setCurrentDirectory(const QString &path)
{
@ -243,6 +239,9 @@ bool MainWindow::event(QEvent *event)
{
auto &settings = Settings::values();
switch (event->type()) {
case QEvent::PaletteChange:
updateStyleSheet();
break;
case QEvent::Close:
if (m_ui->tagEditorWidget->isFileOperationOngoing()) {
event->ignore();
@ -348,6 +347,18 @@ void MainWindow::handleCurrentPathChanged(const QString &newPath)
activateWindow();
}
/*!
* \brief Updates the style sheet.
*/
void MainWindow::updateStyleSheet()
{
#ifdef Q_OS_WINDOWS
setStyleSheet(dialogStyleForPalette(palette()) + QStringLiteral("#tagEditorWidget { color: palette(text); background-color: palette(base); }"));
#else
setStyleSheet(dialogStyleForPalette(palette()));
#endif
}
/*!
* \brief Spawns an external player for the current file.
*/

View File

@ -67,6 +67,7 @@ private Q_SLOTS:
void showSaveAsDlg();
void handleFileStatusChange(bool opened, bool hasTag);
void handleCurrentPathChanged(const QString &newPath);
void updateStyleSheet();
// settings
void showNewWindow();

View File

@ -191,9 +191,9 @@ void NotificationLabel::applyMaxLineCount()
return;
}
int newStart = 0;
auto newStart = QString::size_type();
for (; m_currentLineCount > m_maxLineCount; --m_currentLineCount) {
const int nextBullet = m_text.indexOf(s_bulletLine, newStart);
const auto nextBullet = m_text.indexOf(s_bulletLine, newStart);
if (nextBullet < 0) {
break;
}

View File

@ -520,7 +520,8 @@ void PicturePreviewSelection::extractSelected()
QMessageBox::warning(this, QCoreApplication::applicationName(), tr("Unable to open output file."));
return;
}
if (value.dataSize() <= numeric_limits<qint64>::max() && file.write(value.dataPointer(), static_cast<qint64>(value.dataSize())) > 0) {
if (value.dataSize() <= static_cast<std::size_t>(numeric_limits<qint64>::max())
&& file.write(value.dataPointer(), static_cast<qint64>(value.dataSize())) > 0) {
QMessageBox::information(this, QCoreApplication::applicationName(), tr("The cover has extracted."));
} else {
QMessageBox::warning(this, QCoreApplication::applicationName(), tr("Unable to write to output file."));
@ -601,8 +602,8 @@ void PicturePreviewSelection::convertSelected()
m_imageConversionDialog = new QDialog(this);
m_imageConversionUI = make_unique<Ui::ImageConversionDialog>();
m_imageConversionUI->setupUi(m_imageConversionDialog);
#ifdef Q_OS_WIN32
m_imageConversionDialog->setStyleSheet(dialogStyle());
#ifdef Q_OS_WINDOWS
m_imageConversionDialog->setStyleSheet(dialogStyleForPalette(palette()));
#endif
m_imageConversionUI->formatComboBox->addItems({ tr("JPEG"), tr("PNG") });
m_imageConversionUI->aspectRatioComboBox->addItems({ tr("Ignore"), tr("Keep"), tr("Keep by expanding") });
@ -638,6 +639,22 @@ void PicturePreviewSelection::setCoverButtonsHidden(bool hideCoverButtons)
m_ui->coverButtonsWidget->setHidden(hideCoverButtons);
}
bool PicturePreviewSelection::event(QEvent *event)
{
const auto res = QWidget::event(event);
#ifdef Q_OS_WINDOWS
switch (event->type()) {
case QEvent::PaletteChange:
if (m_imageConversionDialog) {
m_imageConversionDialog->setStyleSheet(dialogStyleForPalette(palette()));
}
break;
default:;
}
#endif
return res;
}
void PicturePreviewSelection::changeEvent(QEvent *event)
{
switch (event->type()) {

View File

@ -68,6 +68,7 @@ Q_SIGNALS:
void pictureChanged();
protected:
bool event(QEvent *event) override;
void changeEvent(QEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;

View File

@ -37,9 +37,7 @@ RenameFilesDialog::RenameFilesDialog(QWidget *parent)
{
setAttribute(Qt::WA_QuitOnClose, false);
m_ui->setupUi(this);
#ifdef Q_OS_WIN32
setStyleSheet(dialogStyle() + QStringLiteral("QSplitter:handle { background-color: palette(base); }"));
#endif
updateStyleSheet();
// setup javascript editor and script file selection
m_ui->javaScriptPlainTextEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
m_highlighter = new JavaScriptHighlighter(m_ui->javaScriptPlainTextEdit->document());
@ -114,6 +112,9 @@ bool RenameFilesDialog::event(QEvent *event)
{
auto &settings = Settings::values().renamingUtility;
switch (event->type()) {
case QEvent::PaletteChange:
updateStyleSheet();
break;
case QEvent::Close:
// save settings
settings.scriptSource = m_ui->sourceFileStackedWidget->currentIndex();
@ -402,4 +403,11 @@ void RenameFilesDialog::setScriptModified(bool scriptModified)
m_scriptModified = scriptModified;
}
void RenameFilesDialog::updateStyleSheet()
{
#ifdef Q_OS_WINDOWS
setStyleSheet(dialogStyleForPalette(palette()) + QStringLiteral("QSplitter:handle { background-color: palette(base); }"));
#endif
}
} // namespace QtGui

View File

@ -52,6 +52,7 @@ private Q_SLOTS:
void abortClose();
void toggleScriptSource();
void setScriptModified(bool scriptModified);
void updateStyleSheet();
private:
std::unique_ptr<Ui::RenameFilesDialog> m_ui;

View File

@ -755,6 +755,19 @@
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>includeSubdirsCheckBox</tabstop>
<tabstop>pasteScriptPushButton</tabstop>
<tabstop>toggleScriptSourcePushButton</tabstop>
<tabstop>javaScriptPlainTextEdit</tabstop>
<tabstop>currentTreeView</tabstop>
<tabstop>previewTreeView</tabstop>
<tabstop>abortClosePushButton</tabstop>
<tabstop>generatePreviewPushButton</tabstop>
<tabstop>applyChangingsPushButton</tabstop>
<tabstop>scriptFilePathLineEdit</tabstop>
<tabstop>selectScriptFilePushButton</tabstop>
</tabstops>
<resources>
<include location="../resources/icons.qrc"/>
</resources>

View File

@ -358,6 +358,9 @@ bool TagProcessingGeneralOptionPage::apply()
}
settings.autoTagManagement = ui()->autoTagManagementCheckBox->isChecked();
settings.preserveModificationTime = ui()->preserveModificationTimeCheckBox->isChecked();
settings.preserveMuxingApp = ui()->preserveMuxingAppCheckBox->isChecked();
settings.preserveWritingApp = ui()->preserveWritingAppCheckBox->isChecked();
settings.convertTotalFields = ui()->convertTotalFieldsCheckBox->isChecked();
}
return true;
}
@ -393,6 +396,9 @@ void TagProcessingGeneralOptionPage::reset()
}
ui()->autoTagManagementCheckBox->setChecked(settings.autoTagManagement);
ui()->preserveModificationTimeCheckBox->setChecked(settings.preserveModificationTime);
ui()->preserveMuxingAppCheckBox->setChecked(settings.preserveMuxingApp);
ui()->preserveWritingAppCheckBox->setChecked(settings.preserveWritingApp);
ui()->convertTotalFieldsCheckBox->setChecked(settings.convertTotalFields);
}
}
@ -680,8 +686,7 @@ SettingsDialog::SettingsDialog(QWidget *parent)
setWindowIcon(QIcon::fromTheme(
QStringLiteral("preferences-other"), QIcon(QStringLiteral(":/tageditor/icons/hicolor/32x32/settingscategories/preferences-other.svg"))));
// some settings could be applied without restarting the application, good idea?
//connect(this, &Dialogs::SettingsDialog::applied, bind(&Dialogs::QtSettings::apply, &Settings::qtSettings()));
connect(this, &SettingsDialog::applied, std::bind(&QtSettings::apply, &::Settings::values().qt));
}
SettingsDialog::~SettingsDialog()

View File

@ -362,14 +362,14 @@ void TagEditorWidget::updateTagEditsAndAttachmentEdits(bool updateUi, PreviousVa
// add/update TagEdit widgets
if (m_tags.size()) {
// create a lists of the targets and tags
QList<TagTarget> targets;
QList<QList<Tag *>> tagsByTarget;
for (Tag *tag : m_tags) {
const TagTarget &target = tag->target();
int index = targets.indexOf(target);
auto targets = QList<TagTarget>();
auto tagsByTarget = QList<QList<Tag *>>();
for (auto *const tag : m_tags) {
const auto &target = tag->target();
auto index = targets.indexOf(target);
if (index < 0) {
targets << target;
tagsByTarget << (QList<Tag *>() << tag);
tagsByTarget << QList<Tag *>({ tag });
} else {
tagsByTarget[index] << tag;
}
@ -379,7 +379,7 @@ void TagEditorWidget::updateTagEditsAndAttachmentEdits(bool updateUi, PreviousVa
switch (Settings::values().editor.multipleTagHandling) {
case Settings::MultipleTagHandling::SingleEditorPerTarget:
// iterate through all targets in both cases
for (int targetIndex = 0, targetCount = targets.size(); targetIndex < targetCount; ++targetIndex) {
for (auto targetIndex = QList<TagTarget>::size_type(), targetCount = targets.size(); targetIndex < targetCount; ++targetIndex) {
fetchNextEdit();
edit->setTags(tagsByTarget.at(targetIndex), updateUi); // set all tags with the same target to a single edit
if (!hasAutoCorrectionBeenApplied) {
@ -390,7 +390,7 @@ void TagEditorWidget::updateTagEditsAndAttachmentEdits(bool updateUi, PreviousVa
break;
case Settings::MultipleTagHandling::SeparateEditors:
// iterate through all targets in both cases
for (int targetIndex = 0, targetCount = targets.size(); targetIndex < targetCount; ++targetIndex) {
for (auto targetIndex = QList<TagTarget>::size_type(), targetCount = targets.size(); targetIndex < targetCount; ++targetIndex) {
for (Tag *tag : tagsByTarget.at(targetIndex)) {
fetchNextEdit();
edit->setTag(tag, updateUi); // use a separate edit for each tag
@ -835,6 +835,11 @@ bool TagEditorWidget::startParsing(const QString &path, bool forceRefresh)
m_currentDir = fileInfo.absolutePath();
m_fileName = fileInfo.fileName();
}
// set flags that are also important when parsing
auto &generalSettings = Settings::values().tagPocessing;
auto flags = m_fileInfo.fileHandlingFlags();
CppUtilities::modFlagEnum(flags, MediaFileHandlingFlags::ConvertTotalFields, generalSettings.convertTotalFields);
m_fileInfo.setFileHandlingFlags(flags);
// write diagnostics to m_diagReparsing if making results are available
m_makingResultsAvailable &= sameFile;
Diagnostics &diag = m_makingResultsAvailable ? m_diagReparsing : m_diag;
@ -1155,7 +1160,8 @@ bool TagEditorWidget::startSaving()
m_fileWatcher->removePath(m_currentPath);
// use current configuration
const auto &settings = Settings::values();
const auto &fileLayoutSettings = settings.tagPocessing.fileLayout;
const auto &generalSettings = settings.tagPocessing;
const auto &fileLayoutSettings = generalSettings.fileLayout;
m_fileInfo.setForceRewrite(fileLayoutSettings.forceRewrite);
m_fileInfo.setTagPosition(fileLayoutSettings.preferredTagPosition);
m_fileInfo.setForceTagPosition(fileLayoutSettings.forceTagPosition);
@ -1165,6 +1171,11 @@ bool TagEditorWidget::startSaving()
m_fileInfo.setMaxPadding(fileLayoutSettings.maxPadding);
m_fileInfo.setPreferredPadding(fileLayoutSettings.preferredPadding);
m_fileInfo.setBackupDirectory(settings.editor.backupDirectory);
auto flags = m_fileInfo.fileHandlingFlags();
CppUtilities::modFlagEnum(flags, MediaFileHandlingFlags::PreserveMuxingApplication, generalSettings.preserveMuxingApp);
CppUtilities::modFlagEnum(flags, MediaFileHandlingFlags::PreserveWritingApplication, generalSettings.preserveWritingApp);
CppUtilities::modFlagEnum(flags, MediaFileHandlingFlags::ConvertTotalFields, generalSettings.convertTotalFields);
m_fileInfo.setFileHandlingFlags(flags);
const auto startThread = [this, preserveModificationTime = settings.tagPocessing.preserveModificationTime] {
// define functions to show the saving progress and to actually applying the changes
auto showPercentage([this](AbortableProgressFeedback &progress) {
@ -1380,7 +1391,7 @@ void TagEditorWidget::closeFile()
updateFileStatusStatus();
}
void QtGui::TagEditorWidget::renameFile()
void TagEditorWidget::renameFile()
{
if (isFileOperationOngoing()) {
emit statusMessage(tr("Unable to rename the file because the current process hasn't been finished yet."));

View File

@ -450,6 +450,7 @@ currently shown tag.</string>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>renamePushButton</tabstop>
<tabstop>tagSelectionComboBox</tabstop>
<tabstop>keepPreviousValuesPushButton</tabstop>
<tabstop>tagOptionsPushButton</tabstop>

View File

@ -81,6 +81,14 @@ TagFieldEdit::TagFieldEdit(const QList<TagParser::Tag *> &tags, TagParser::Known
updateValue();
}
TagFieldEdit::~TagFieldEdit()
{
// delete those actions before entering base class destructors as we connect signal handlers when those actions are
// destructed and calling those handlers is gonna break otherwise
delete m_lockAction;
delete m_restoreAction;
}
/*!
* \brief Assigns the specified \a tags and sets the specified \a fields using the given \a previousValueHandling.
*

View File

@ -39,6 +39,7 @@ class TagFieldEdit : public QWidget {
public:
explicit TagFieldEdit(const QList<TagParser::Tag *> &tags, TagParser::KnownField field, QWidget *parent = nullptr);
~TagFieldEdit() override;
const QList<TagParser::Tag *> &tags() const;
TagParser::KnownField field() const;

View File

@ -106,7 +106,7 @@
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>If enabled, appropriate tags will be created and removed according to the settings automatically when opening a file. Otherwise you have to do this manually (eg. adding an ID3 tag if none is present yet) and settings like ID3 usage have no effect.</string>
<string>If enabled, appropriate tags will be created and removed according to the settings automatically when opening a file. Otherwise you have to do this manually (e.g. adding an ID3 tag if none is present yet) and the settings for ID3 usage and targets have no effect.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
@ -139,6 +139,39 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Miscellaneous</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QCheckBox" name="preserveMuxingAppCheckBox">
<property name="text">
<string>Preserve muxing application</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="preserveWritingAppCheckBox">
<property name="text">
<string>Preserve writing application</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="convertTotalFieldsCheckBox">
<property name="toolTip">
<string>Ensures fields usually holding values like &quot;3/15&quot; such as the track position are actually stored as such (and &lt;i&gt;not&lt;/i&gt; as two separate fields for the position and total values). This is required for the tag editor to support handling such separately stored total values at all. So far this only affects Vorbis Comments where it will convert the fields TRACKTOTAL/DISCTOTAL/PARTTOTAL to be included in the TRACKNUMBER/DISCNUMBER/PARTNUMBER fields.</string>
</property>
<property name="text">
<string>Convert total fields (see tooltip for details)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
@ -157,7 +190,7 @@
<resources/>
<connections/>
<buttongroups>
<buttongroup name="unsupportedButtonGroup"/>
<buttongroup name="preferredTextEncodingButtonGroup"/>
<buttongroup name="unsupportedButtonGroup"/>
</buttongroups>
</ui>

View File

@ -958,6 +958,13 @@ public:
}
rowMaker.mkRow(QCoreApplication::translate("HtmlInfo", "Tag position"), container->determineTagPosition(m_diagReparsing));
rowMaker.mkRow(QCoreApplication::translate("HtmlInfo", "Index position"), container->determineIndexPosition(m_diagReparsing));
const auto *const constContainer = container;
if (const auto &muxingApps = constContainer->muxingApplications(); !muxingApps.empty()) {
rowMaker.mkRow(QCoreApplication::translate("HtmlInfo", "Muxing application"), qstr(joinStrings(muxingApps, ", ")));
}
if (const auto &writingApps = constContainer->writingApplications(); !writingApps.empty()) {
rowMaker.mkRow(QCoreApplication::translate("HtmlInfo", "Writing application"), qstr(joinStrings(writingApps, ", ")));
}
}
if (m_file.paddingSize()) {
rowMaker.mkRow(QCoreApplication::translate("HtmlInfo", "Padding size"),

View File

@ -165,10 +165,10 @@ QString elementPositionToQString(ElementPosition elementPosition)
QString formatName(const QString &str, bool underscoreToWhitespace)
{
QString res;
auto res = QString();
res.reserve(str.size());
bool whitespace = true;
for (int i = 0, size = str.size(); i != size; ++i) {
auto whitespace = true;
for (auto i = QString::size_type(), size = str.size(); i != size; ++i) {
const QChar current = str.at(i);
if (current.isSpace() || current == QChar('(') || current == QChar('[')) {
whitespace = true;
@ -203,8 +203,9 @@ QString formatName(const QString &str, bool underscoreToWhitespace)
QString fixUmlauts(const QString &str)
{
auto words = str.split(QChar(' '));
static const QLatin1String exceptions[] = { QLatin1String("reggae"), QLatin1String("blues"), QLatin1String("auer"), QLatin1String("aues"),
QLatin1String("manuel"), QLatin1String("duet"), QLatin1String("duel"), QLatin1String("neue"), QLatin1String("prologue") };
static const QLatin1String exceptions[]
= { QLatin1String("reggae"), QLatin1String("blues"), QLatin1String("auer"), QLatin1String("aues"), QLatin1String("manuel"),
QLatin1String("duet"), QLatin1String("duel"), QLatin1String("neue"), QLatin1String("prologue"), QLatin1String("true") };
static const QLatin1String pairs[6][2] = { { QLatin1String("ae"), QLatin1String("\xe4") }, { QLatin1String("ue"), QLatin1String("\xfc") },
{ QLatin1String("oe"), QLatin1String("\xf6") }, { QLatin1String("Ae"), QLatin1String("\xc4") },
{ QLatin1String("Ue"), QLatin1String("\xdc") }, { QLatin1String("Oe"), QLatin1String("\xd6") } };
@ -228,7 +229,7 @@ void parseFileName(const QString &fileName, QString &title, int &trackNumber)
{
title = fileName.trimmed();
trackNumber = 0;
int lastPoint = title.lastIndexOf(QChar('.'));
auto lastPoint = title.lastIndexOf(QChar('.'));
if (lastPoint > 0) {
title.truncate(lastPoint);
} else if (lastPoint == 0) {
@ -236,13 +237,13 @@ void parseFileName(const QString &fileName, QString &title, int &trackNumber)
}
static const QLatin1String delims[] = { QLatin1String(" - "), QLatin1String(", "), QLatin1String("-"), QLatin1String(" ") };
for (const auto &delim : delims) {
int lastDelimIndex = 0;
int delimIndex = title.indexOf(delim);
auto lastDelimIndex = QString::size_type();
auto delimIndex = title.indexOf(delim);
while (delimIndex > lastDelimIndex) {
bool ok = false;
trackNumber = QtUtilities::midRef(title, lastDelimIndex, delimIndex - lastDelimIndex).toInt(&ok);
if (ok) {
int titleStart = delimIndex + delim.size();
auto titleStart = delimIndex + delim.size();
for (const auto &delim2 : delims) {
if (QtUtilities::midRef(title, titleStart).startsWith(delim2)) {
titleStart += delim2.size();

View File

@ -4,6 +4,10 @@
#include <tagparser/tagvalue.h>
#include <QString>
#include <QStringList>
#include <limits>
#include <type_traits>
QT_FORWARD_DECLARE_CLASS(QDir)
QT_FORWARD_DECLARE_CLASS(QAbstractItemModel)
@ -30,9 +34,18 @@ void parseFileName(const QString &fileName, QString &title, int &trackNumber);
QString printModel(QAbstractItemModel *model);
void printModelIndex(const QModelIndex &index, QString &res, int level);
constexpr int sizeToInt(std::size_t size)
template <typename IntType = int> constexpr IntType sizeToInt(std::size_t size)
{
return size > std::numeric_limits<int>::max() ? std::numeric_limits<int>::max() : static_cast<int>(size);
return size > std::numeric_limits<IntType>::max() ? std::numeric_limits<IntType>::max() : static_cast<IntType>(size);
}
constexpr int containerSizeToInt(typename QStringList::size_type size)
{
if constexpr (std::is_same_v<decltype(size), int>) {
return size;
} else {
return size > std::numeric_limits<int>::max() ? std::numeric_limits<int>::max() : static_cast<int>(size);
}
}
constexpr int trQuandity(quint64 quandity)

View File

@ -1,6 +1,8 @@
#ifndef FILESYSTEMITEM_H
#define FILESYSTEMITEM_H
#include "../misc/utility.h"
#include <QList>
#include <QString>
@ -188,7 +190,7 @@ inline void FileSystemItem::setCheckable(bool checkable)
inline int FileSystemItem::row() const
{
return m_parent ? m_parent->children().indexOf(const_cast<FileSystemItem *>(this)) : -1;
return m_parent ? Utility::containerSizeToInt(m_parent->children().indexOf(const_cast<FileSystemItem *>(this))) : -1;
}
} // namespace RenamingUtility

View File

@ -1,6 +1,8 @@
#include "./filesystemitemmodel.h"
#include "./filesystemitem.h"
#include "../misc/utility.h"
#include <QApplication>
#include <QBrush>
#include <QFont>
@ -232,7 +234,7 @@ QModelIndex FileSystemItemModel::counterpart(const QModelIndex &index, int colum
int FileSystemItemModel::rowCount(const QModelIndex &parent) const
{
if (const auto *const parentItem = (parent.isValid() ? reinterpret_cast<FileSystemItem *>(parent.internalPointer()) : m_rootItem)) {
return parentItem->children().size();
return Utility::containerSizeToInt(parentItem->children().size());
} else {
return 0;
}

View File

@ -294,7 +294,7 @@ void RenamingEngine::applyChangings(FileSystemItem *parentItem)
applyChangings(item);
}
}
m_itemsProcessed += parentItem->children().size();
m_itemsProcessed += Utility::containerSizeToInt(parentItem->children().size());
emit progress(m_itemsProcessed, m_errorsOccured);
}

View File

@ -137,7 +137,7 @@ if (fileInfo.currentSuffix === "tmp") {
var fields = []
// get the artist (preferably album artist), remove invalid characters and add it to fields array
var artist = validFileName(tag.albumartist || tag.artist)
var artist = validFileName(tag.albumArtist || tag.artist)
if (includeArtist && !isPartOfCollection(tag) && notEmpty(artist)) {
fields.push(trailingBracketsStripped(firstValue(artist)))
}
@ -214,7 +214,12 @@ tageditor.rename(newName)
var path = []
if (distDir) {
path.push(distDir)
var artist = validDirectoryName(firstValue(tag.albumartist || tag.artist))
if (tag.comment.includes("bootleg")) {
path.push("bootlegs");
} else if (tag.comment.includes("single")) {
path.push("singles");
}
var artist = validDirectoryName(firstValue(tag.albumArtist || tag.artist))
if (isPartOfCollection(tag)) {
path.push(collectionsDir)
} else if (notEmpty(artist) && !isMiscFile(tag)) {

View File

@ -22,7 +22,7 @@ if (!fileInfo.hasAudioTracks && !fileInfo.hasVideoTracks) {
}
// make new filename
const fieldsToInclude = [tag.albumartist || tag.artist, tag.album, tag.trackPos || infoFromFileName.trackPos, tag.title || infoFromFileName.title]
const fieldsToInclude = [tag.albumArtist || tag.artist, tag.album, tag.trackPos || infoFromFileName.trackPos, tag.title || infoFromFileName.title]
let newName = ""
for (let field of fieldsToInclude) {
field = field.toString()

63
testfiles/helpers.js Normal file
View File

@ -0,0 +1,63 @@
const fileCache = {};
export function isString(value) {
return typeof(value) === "string" || value instanceof String;
}
export function isTruthy(value) {
return value && value !== "false" && value !== "0";
}
export function logTagInfo(file, tag) {
// log tag type and supported fields
const fields = tag.fields;
utility.diag("debug", tag.type, "tag");
utility.diag("debug", Object.keys(fields).sort().join(", "), "supported fields");
// log fields for debugging purposes
for (const [key, values] of Object.entries(fields)) {
for (const value of values) {
const content = value.content;
if (content !== undefined && content !== null && !(content instanceof ArrayBuffer)) {
utility.diag("debug", content, key + " (" + value.type + ")");
}
}
}
}
export function readFieldContents(file, fieldName) {
const values = [];
for (const tag of file.tags) {
if (tag.type === "ID3v1 tag") {
// due to its limitations ID3v1 tags may have truncated contents; so just ignore them
// for the sake of this script
continue;
}
for (const value of tag.fields[fieldName]) {
const content = value.content;
if (content !== undefined && content !== null) {
values.push(content);
}
}
if (values.length) {
// just return the contents from the first tag that has any for now
break;
}
}
return values;
}
export function cacheValue(cache, key, generator) {
const cachedValue = cache[key];
return cachedValue ? cachedValue : (cache[key] = generator());
}
export function openOriginalFile(file) {
const originalDir = settings.originalDir;
const originalExt = settings.originalExt || file.extension;
if (originalDir && file.pathRelative) {
const name = file.nameWithoutExtension;
const path = [originalDir, file.containingDirectory, name + originalExt].join("/");
return cacheValue(fileCache, path, () => utility.openFile(path));
}
}

23
testfiles/http.js Normal file
View File

@ -0,0 +1,23 @@
export function query(method, url) {
let request = new XMLHttpRequest();
let result = null;
request.onreadystatechange = function() {
if (request.readyState === XMLHttpRequest.DONE) {
utility.exit(0);
result = {
status: request.status,
headers: request.getAllResponseHeaders(),
contentType: request.responseType,
content: request.response
};
}
};
request.open(method, url);
request.send();
utility.exec();
return result;
}
export function get(url) {
return query("GET", url);
}

View File

@ -0,0 +1,78 @@
import * as helpers from "helpers.js"
const lyricsCache = {};
const coverCache = {};
const albumColumn = 1;
export function queryLyrics(searchCriteria) {
return helpers.cacheValue(lyricsCache, searchCriteria.title + "_" + searchCriteria.artist, () => {
utility.log(" - Querying lyrics for '" + searchCriteria.title + "' from '" + searchCriteria.artist + "' ...");
return queryLyricsFromProviders(["Tekstowo", "MakeItPersonal"], searchCriteria)
});
}
export function queryCover(searchCriteria) {
return helpers.cacheValue(coverCache, searchCriteria.album + "_" + searchCriteria.artist, () => {
utility.log(" - Querying cover art for '" + searchCriteria.album + "' from '" + searchCriteria.artist + "' ...");
return queryCoverFromProvider("MusicBrainz", searchCriteria);
});
}
function waitFor(signal) {
signal.connect(() => { utility.exit(); });
utility.exec();
}
function queryLyricsFromProvider(provider, searchCriteria) {
const model = utility["query" + provider](searchCriteria);
if (!model.areResultsAvailable) {
waitFor(model.resultsAvailable);
}
if (!model.fetchLyrics(model.index(0, 0))) {
waitFor(model.lyricsAvailable);
}
const lyrics = model.lyricsValue(model.index(0, 0));
if (lyrics && lyrics.startsWith("Bots have beat this API")) {
return undefined;
}
return lyrics;
}
function queryLyricsFromProviders(providers, searchCriteria) {
for (const provider of providers) {
const res = queryLyricsFromProvider(provider, searchCriteria);
if (res) {
return res;
}
}
}
function queryCoverFromProvider(provider, searchCriteria) {
const context = searchCriteria.album + " from " + searchCriteria.artist;
const model = utility["query" + provider](searchCriteria);
if (!model.areResultsAvailable) {
waitFor(model.resultsAvailable);
}
const albumUpper = searchCriteria.album.toUpperCase();
utility.diag("debug", model.rowCount(), "rows");
let row = 0, rowCount = model.rowCount();
for (; row != rowCount; ++row) {
const album = model.data(model.index(row, albumColumn));
if (album && album.toUpperCase() === albumUpper) {
break;
}
}
if (row === rowCount) {
utility.diag("debug", "unable to find meta-data on " + provider, context);
return undefined;
}
if (!model.fetchCover(model.index(row, 0))) {
waitFor(model.coverAvailable);
}
let cover = model.coverValue(model.index(row, 0));
if (cover instanceof ArrayBuffer) {
utility.diag("debug", "found cover", context);
cover = utility.convertImage(cover, Qt.size(512, 512), "JPEG");
}
return cover;
}

View File

@ -0,0 +1,24 @@
import * as helpers from "helpers.js"
export function main(file) {
utility.diag("debug", Object.keys(settings).join(", "), "settings");
for (const tag of file.tags) {
changeTagFields(file, tag);
}
file.applyChanges();
return !helpers.isTruthy(settings.dryRun);
}
function addTestFields(file, tag) {
const fields = tag.fields;
for (const [key, value] of Object.entries(settings)) {
if (key.startsWith("set:")) {
fields[key.substr(4)] = value;
}
}
}
function changeTagFields(file, tag) {
helpers.logTagInfo(file, tag);
addTestFields(file, tag);
}

141
testfiles/set-tags.js Normal file
View File

@ -0,0 +1,141 @@
import * as helpers from "helpers.js"
import * as metadatasearch from "metadatasearch.js"
export function main(file) {
// iterate though all tags of the file to change fields in all of them
for (const tag of file.tags) {
changeTagFields(file, tag);
}
// submit changes from the JavaScript-context to the tag editor application; does not save changes to disk yet
file.applyChanges();
// return a falsy value to skip the file after all
return !helpers.isTruthy(settings.dryRun);
}
const mainTextFields = ["title", "artist", "album", "genre"];
const personalFields = ["comment", "rating"];
function applyFixesToMainFields(file, tag) {
const fields = tag.fields;
for (const key of mainTextFields) {
for (const value of fields[key]) {
if (helpers.isString(value.content)) {
value.content = value.content.trim();
if (helpers.isTruthy(settings.fixUmlauts)) {
value.content = utility.fixUmlauts(value.content);
}
if (helpers.isTruthy(settings.formatNames)) {
value.content = utility.formatName(value.content);
}
}
}
}
}
function clearPersonalFields(file, tag) {
const fields = tag.fields;
for (const key of personalFields) {
fields[key] = [];
}
}
function addTotalNumberOfTracks(file, tag) {
const track = tag.fields.track;
if (track.find(value => !value.content.total) === undefined) {
return; // skip if already assigned
}
const extension = file.extension;
const dirItems = utility.readDirectory(file.containingDirectory) || [];
const total = dirItems.filter(fileName => fileName.endsWith(extension)).length;
if (!total) {
return;
}
for (const value of track) {
value.content.total |= total;
}
}
function addFieldFromOriginalFile(file, tag, fieldName) {
const originalFile = helpers.openOriginalFile(file);
if (!originalFile) {
return false;
}
utility.diag("debug", "Trying to take over " + fieldName + " from \"" + originalFile.path + "\".");
const contents = helpers.readFieldContents(originalFile, fieldName);
if (!contents.length) {
utility.diag("debug", "No " + fieldName + " found in original file.");
return false;
}
tag.fields[fieldName] = contents;
return true;
}
function addLyrics(file, tag) {
const fields = tag.fields;
if (fields.lyrics.length) {
return; // skip if already assigned
}
if (addFieldFromOriginalFile(file, tag, "lyrics")) {
return; // skip fetching via meta-data search if lyrics could be taken over from original file
}
const firstTitle = fields.title?.[0]?.content;
const firstArtist = fields.artist?.[0]?.content;
if (firstTitle && firstArtist) {
fields.lyrics = metadatasearch.queryLyrics({title: firstTitle, artist: firstArtist});
}
}
function addCover(file, tag) {
const fields = tag.fields;
if (fields.cover.length) {
return; // skip if already assigned
}
if (addFieldFromOriginalFile(file, tag, "cover")) {
// ensure the cover's resolution is below a certain size to avoid bloating the file
const convertedCovers = [];
const maxSizeInt = parseInt(settings.coverMaxSize || 512);
const maxSize = Qt.size(maxSizeInt, maxSizeInt);
for (const cover of fields.cover) {
convertedCovers.push(utility.convertImage(cover, maxSize));
}
fields.cover = convertedCovers;
return; // skip fetching via meta-data search if cover could be taken over from original file
}
const firstAlbum = fields.album?.[0]?.content?.replace(/ \(.*\)/, '');
const firstArtist = fields.artist?.[0]?.content;
if (firstAlbum && firstArtist) {
fields.cover = metadatasearch.queryCover({album: firstAlbum, artist: firstArtist});
}
}
function addMiscFields(file, tag) {
// assume the number of disks is always one for now
const fields = tag.fields;
if (!fields.disk.length) {
fields.disk = "1/1";
}
const dir = file.containingDirectory.toLowerCase();
if (dir.includes("bootleg")) {
fields.comment = "bootleg";
} else if (dir.includes("singles")) {
fields.comment = "single";
}
}
function changeTagFields(file, tag) {
helpers.logTagInfo(file, tag);
// change/add various fields; these values can still be overridden by specifying fields normally as CLI args
applyFixesToMainFields(file, tag);
clearPersonalFields(file, tag);
addTotalNumberOfTracks(file, tag);
addMiscFields(file, tag);
if (helpers.isTruthy(settings.addLyrics)) {
addLyrics(file, tag);
}
if (helpers.isTruthy(settings.addCover)) {
addCover(file, tag);
}
}

View File

@ -1,3 +1,5 @@
#include "../cli/mainfeatures.h"
#include "resources/config.h"
#include <c++utilities/conversion/stringbuilder.h>
@ -13,6 +15,7 @@
#include <cstdlib>
#include <cstring>
#include <filesystem>
namespace CppUtilities {
@ -36,6 +39,13 @@ using namespace CppUtilities;
#include <fstream>
#include <iostream>
#ifdef stdout
#undef stdout
#endif
#ifdef stderr
#undef stderr
#endif
using namespace std;
using namespace CppUtilities::Literals;
using namespace TagParser;
@ -48,7 +58,7 @@ enum class TagStatus { Original, TestMetaDataPresent, Removed };
*/
class CliTests : public TestFixture {
CPPUNIT_TEST_SUITE(CliTests);
#ifdef PLATFORM_UNIX
#if defined(PLATFORM_UNIX) || defined(CPP_UTILITIES_HAS_EXEC_APP)
CPPUNIT_TEST(testBasicReading);
CPPUNIT_TEST(testBasicWriting);
CPPUNIT_TEST(testModifyingCover);
@ -67,6 +77,7 @@ class CliTests : public TestFixture {
CPPUNIT_TEST(testReadingAndWritingDocumentTitle);
CPPUNIT_TEST(testFileLayoutOptions);
CPPUNIT_TEST(testJsonExport);
CPPUNIT_TEST(testScriptProcessing);
#endif
CPPUNIT_TEST_SUITE_END();
@ -74,7 +85,7 @@ public:
void setUp() override;
void tearDown() override;
#ifdef PLATFORM_UNIX
#if defined(PLATFORM_UNIX) || defined(CPP_UTILITIES_HAS_EXEC_APP)
void testBasicReading();
void testBasicWriting();
void testModifyingCover();
@ -93,6 +104,7 @@ public:
void testReadingAndWritingDocumentTitle();
void testFileLayoutOptions();
void testJsonExport();
void testScriptProcessing();
#endif
private:
@ -108,17 +120,34 @@ void CliTests::tearDown()
{
}
#ifdef PLATFORM_UNIX
#if defined(PLATFORM_UNIX) || defined(CPP_UTILITIES_HAS_EXEC_APP)
template <typename StringType, bool negateErrorCond = false>
bool testContainsSubstrings(const StringType &str, std::initializer_list<const typename StringType::value_type *> substrings)
{
vector<const typename StringType::value_type *> failedSubstrings;
typename StringType::size_type currentPos = 0;
#if defined(PLATFORM_WINDOWS)
auto substringsWindows = std::vector<std::string>();
substringsWindows.reserve(substrings.size());
for (const auto *const substring : substrings) {
findAndReplace(substringsWindows.emplace_back(substring), "\n", "\r\n");
}
#endif
auto failedSubstrings = std::vector<const typename StringType::value_type *>();
auto currentPos = typename StringType::size_type();
#if defined(PLATFORM_WINDOWS)
auto currentSubstr = substrings.begin();
for (const auto &substr : substringsWindows) {
if ((currentPos = str.find(substr, currentPos)) == StringType::npos) {
failedSubstrings.emplace_back(*currentSubstr);
}
currentPos += substr.size();
++currentSubstr;
#else
for (const auto *substr : substrings) {
if ((currentPos = str.find(substr, currentPos)) == StringType::npos) {
failedSubstrings.emplace_back(substr);
}
currentPos += std::strlen(substr);
#endif
}
bool res = failedSubstrings.empty();
@ -257,12 +286,12 @@ void CliTests::testModifyingCover()
const auto lyrics = "lyrics>=" + lyricsFile;
const char *const args1[] = { "tageditor", "get", "-f", mp3File1.data(), nullptr };
const char *const args2[] = { "tageditor", "set", otherCover.data(), frontCover0.data(), frontCover1.data(), backCover0.data(), lyrics.data(),
"-f", mp3File1.data(), nullptr };
CPPUNIT_ASSERT_EQUAL(0, execApp(args2, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
"--pedantic", "-f", mp3File1.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT_MESSAGE("covers added",
testContainsSubstrings(stdout,
{ " - \e[1mID3v2 tag (version 2.3.0)\e[0m\n", " Lyrics I\nam\nno\nsong\nwriter\n",
{ " - \033[1mID3v2 tag (version 2.3.0)\033[0m\n", " Lyrics I\nam\nno\nsong\nwriter\n",
" Cover (other) can't display image/png as string (use --extract)\n"
" Cover (front-cover) can't display image/png as string (use --extract)\n"
" description: foo\n"
@ -273,12 +302,12 @@ void CliTests::testModifyingCover()
// test whether empty trailing ":" does *not* affect all descriptions
const char *const args3[] = { "tageditor", "set", "cover0=:front-cover:", "-f", mp3File1.data(), nullptr };
CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
TESTUTILS_ASSERT_EXEC(args3);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT_MESSAGE("covers not altered",
testContainsSubstrings(stdout,
{
" - \e[1mID3v2 tag (version 2.3.0)\e[0m\n",
" - \033[1mID3v2 tag (version 2.3.0)\033[0m\n",
" Cover (other) can't display image/png as string (use --extract)\n"
" Cover (front-cover) can't display image/png as string (use --extract)\n"
" description: foo\n"
@ -290,13 +319,13 @@ void CliTests::testModifyingCover()
// remove all front covers by omitting trailing ":"
const char *const args4[] = { "tageditor", "set", "cover0=:front-cover", "-f", mp3File1.data(), nullptr };
CPPUNIT_ASSERT_EQUAL(0, execApp(args4, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
TESTUTILS_ASSERT_EXEC(args4);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT_EQUAL_MESSAGE("front covers removed", std::string::npos, stdout.find("front-cover"));
CPPUNIT_ASSERT_MESSAGE("other covers not altered",
testContainsSubstrings(stdout,
{
" - \e[1mID3v2 tag (version 2.3.0)\e[0m\n",
" - \033[1mID3v2 tag (version 2.3.0)\033[0m\n",
" Cover (other) can't display image/png as string (use --extract)\n"
" Cover (back-cover) can't display image/png as string (use --extract)\n",
}));
@ -304,8 +333,8 @@ void CliTests::testModifyingCover()
// remove all covers
const char *const args5[] = { "tageditor", "set", "cover0=", "-f", mp3File1.data(), nullptr };
CPPUNIT_ASSERT_EQUAL(0, execApp(args5, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
TESTUTILS_ASSERT_EXEC(args5);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT_EQUAL_MESSAGE("All covers removed", std::string::npos, stdout.find("Cover"));
CPPUNIT_ASSERT_EQUAL(0, remove(mp3File1Backup.data()));
@ -427,7 +456,7 @@ void CliTests::testId3SpecificOptions()
const char *const args1[] = { "tageditor", "get", "-f", mp3File1.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mID3v1 tag\e[0m\n"
{ " - \033[1mID3v1 tag\033[0m\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
@ -435,7 +464,7 @@ void CliTests::testId3SpecificOptions()
" Comment ExactAudioCopy v0.95b4\n"
" Track 4\n"
" Record date 1984\n",
" - \e[1mID3v2 tag (version 2.3.0)\e[0m\n"
" - \033[1mID3v2 tag (version 2.3.0)\033[0m\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
@ -451,7 +480,7 @@ void CliTests::testId3SpecificOptions()
TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mID3v2 tag (version 2.4.0)\e[0m\n"
{ " - \033[1mID3v2 tag (version 2.4.0)\033[0m\n"
" Title Cohesion\n"
" Album Double Nickels On The Dime\n"
" Artist Minutemen\n"
@ -466,10 +495,10 @@ void CliTests::testId3SpecificOptions()
// convert remaining ID3v2 tag to version 2, add an ID3v1 tag again and set a field with unicode char by the way
const char *const args3[] = { "tageditor", "set", "album=Dóuble Nickels On The Dime", "track=5/10", "disk=2/3", "duration=1:45:15",
"--id3v1-usage", "always", "--id3v2-version", "2", "--id3-init-on-create", "-f", mp3File1.data(), nullptr };
CPPUNIT_ASSERT_EQUAL(0, execApp(args3, stdout, stderr));
CPPUNIT_ASSERT_EQUAL(0, execApp(args1, stdout, stderr));
TESTUTILS_ASSERT_EXEC(args3);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mID3v1 tag\e[0m\n"
{ " - \033[1mID3v1 tag\033[0m\n"
" Title Cohesion\n"
" Album Dóuble Nickels On The Dime\n"
" Artist Minutemen\n"
@ -477,7 +506,7 @@ void CliTests::testId3SpecificOptions()
" Comment ExactAudioCopy v0.95b4\n"
" Track 5\n"
" Record date 1984\n",
" - \e[1mID3v2 tag (version 2.2.0)\e[0m\n"
" - \033[1mID3v2 tag (version 2.2.0)\033[0m\n"
" Title Cohesion\n"
" Album Dóuble Nickels On The Dime\n"
" Artist Minutemen\n"
@ -577,28 +606,28 @@ void CliTests::testMultipleFiles()
TESTUTILS_ASSERT_EXEC(args2);
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\n"
{ " - \033[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\033[0m\n"
" Title MKV testfiles\n"
" Comment Matroska Validation File1, basic MPEG4.2 and MP3 with only SimpleBlock\n"
" Total parts 3\n"
" Release date 2010\n"
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" - \033[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\033[0m\n"
" Title test1\n"
" Part 1",
" - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\n"
" - \033[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\033[0m\n"
" Title MKV testfiles\n"
" Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC\n"
" Total parts 3\n"
" Release date 2010\n"
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" - \033[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\033[0m\n"
" Title test2\n"
" Part 2",
" - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\n"
" - \033[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\033[0m\n"
" Title MKV testfiles\n"
" Comment Matroska Validation File 3, header stripping on the video track and no SimpleBlock\n"
" Total parts 3\n"
" Release date 2010",
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" - \033[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\033[0m\n"
" Title test3\n"
" Part 3" }));
@ -616,13 +645,16 @@ void CliTests::testMultipleFiles()
*/
void CliTests::testOutputFile()
{
cout << "\nReading and writing multiple files at once with output files specified" << endl;
string stdout, stderr;
const string mkvFile1(workingCopyPath("matroska_wave1/test1.mkv"));
const string mkvFile2(workingCopyPath("matroska_wave1/test2.mkv"));
std::cout << "\nReading and writing multiple files at once with output files specified" << std::endl;
auto stdout = std::string(), stderr = std::string();
const auto mkvFile1(workingCopyPath("matroska_wave1/test1.mkv"));
const auto mkvFile2(workingCopyPath("matroska_wave1/test2.mkv"));
const auto tempDir = std::filesystem::temp_directory_path();
const auto tempFile1 = (tempDir / "test1.mkv").string();
const auto tempFile2 = (tempDir / "test2.mkv").string();
const char *const args1[] = { "tageditor", "set", "target-level=30", "title=test1", "title=test2", "-f", mkvFile1.data(), mkvFile2.data(), "-o",
"/tmp/test1.mkv", "/tmp/test2.mkv", nullptr };
tempFile1.data(), tempFile2.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args1);
// original files have not been modified
@ -633,18 +665,18 @@ void CliTests::testOutputFile()
CPPUNIT_ASSERT(stdout.find("Title test2") == string::npos);
// specified output files contain new titles
const char *const args3[] = { "tageditor", "get", "-f", "/tmp/test1.mkv", "/tmp/test2.mkv", nullptr };
const char *const args3[] = { "tageditor", "get", "-f", tempFile1.data(), tempFile2.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args3);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
{ " - \033[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\033[0m\n"
" Title test1\n",
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" - \033[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\033[0m\n"
" Title test2\n" }));
CPPUNIT_ASSERT_EQUAL(0, remove(mkvFile1.data()));
CPPUNIT_ASSERT_EQUAL(0, remove(mkvFile2.data()));
CPPUNIT_ASSERT_EQUAL(0, remove("/tmp/test1.mkv"));
CPPUNIT_ASSERT_EQUAL(0, remove("/tmp/test2.mkv"));
CPPUNIT_ASSERT_EQUAL(0, remove(tempFile1.data()));
CPPUNIT_ASSERT_EQUAL(0, remove(tempFile2.data()));
}
/*!
@ -667,7 +699,7 @@ void CliTests::testBackupDir()
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{
" - \e[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\e[0m\n"
" - \033[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\033[0m\n"
" Title test1\n",
}));
@ -776,19 +808,19 @@ void CliTests::testHandlingAttachments()
CPPUNIT_ASSERT_EQUAL(0, remove(mkvFile1Backup.data()));
// extract assigned attachment again
const char *const args4[]
= { "tageditor", "extract", "--attachment", "name=test2.mkv", "-f", mkvFile1.data(), "-o", "/tmp/extracted.mkv", nullptr };
const auto tmpFile = (std::filesystem::temp_directory_path() / "extracted.mkv").string();
const char *const args4[] = { "tageditor", "extract", "--attachment", "name=test2.mkv", "-f", mkvFile1.data(), "-o", tmpFile.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args4);
fstream origFile, extFile;
origFile.exceptions(ios_base::failbit | ios_base::badbit), extFile.exceptions(ios_base::failbit | ios_base::badbit);
origFile.open(mkvFile2.data() + 5, ios_base::in | ios_base::binary), extFile.open("/tmp/extracted.mkv", ios_base::in | ios_base::binary);
origFile.open(mkvFile2.data() + 5, ios_base::in | ios_base::binary), extFile.open(tmpFile.data(), ios_base::in | ios_base::binary);
origFile.seekg(0, ios_base::end), extFile.seekg(0, ios_base::end);
std::int64_t origFileSize = origFile.tellg(), extFileSize = extFile.tellg();
CPPUNIT_ASSERT_EQUAL(origFileSize, extFileSize);
for (origFile.seekg(0), extFile.seekg(0); origFileSize > 0; --origFileSize) {
CPPUNIT_ASSERT_EQUAL(origFile.get(), extFile.get());
}
remove("/tmp/extracted.mkv");
remove(tmpFile.data());
// remove assigned attachment
const char *const args5[] = { "tageditor", "set", "--remove-attachment", "name=test2.mkv", "-f", mkvFile1.data(), nullptr };
@ -809,12 +841,12 @@ void CliTests::testDisplayingInfo()
cout << "\nDisplaying general file info" << endl;
string stdout, stderr;
// test Matroska file
const string mkvFile(testFilePath("matroska_wave1/test2.mkv"));
const char *const args1[] = { "tageditor", "info", "-f", mkvFile.data(), nullptr };
// test valid Matroska file
const auto mkvFile1 = testFilePath("matroska_wave1/test2.mkv");
const char *const args1[] = { "tageditor", "info", "--pedantic", "-f", mkvFile1.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args1);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mContainer format: Matroska\e[0m\n"
{ " - \033[1mContainer format: Matroska\033[0m\n"
" Size 20.16 MiB\n"
" Mime-type video/x-matroska\n"
" Duration 47 s 509 ms\n"
@ -826,7 +858,7 @@ void CliTests::testDisplayingInfo()
" Document version 2\n"
" Tag position before data\n"
" Index position before data\n",
" - \e[1mTracks: H.264-Main@L3.1-576p / AAC-LC-2ch\e[0m\n"
" - \033[1mTracks: H.264-Main@L3.1-576p / AAC-LC-2ch\033[0m\n"
" ID 1863976627\n"
" Type Video\n"
" Format Advanced Video Coding Main Profile\n"
@ -841,12 +873,50 @@ void CliTests::testDisplayingInfo()
" Channel config 2 channels: front-left, front-right\n"
" Sampling frequency 48000 Hz" }));
// test MP4 file with AAC track using SBR and PS extensions
const string mp4File(testFilePath("mtx-test-data/aac/he-aacv2-ps.m4a"));
const char *const args2[] = { "tageditor", "info", "-f", mp4File.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args2);
// test broken Matroska file
const auto mkvFile2 = testFilePath("matroska_wave1/test4.mkv");
const char *const args2[] = { "tageditor", "--no-color", "info", "--validate", "--pedantic", "-f", mkvFile2.data(), nullptr };
TESTUTILS_ASSERT_EXEC_EXIT_STATUS(args2, EXIT_PARSING_FAILURE);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mContainer format: MPEG-4 Part 14\e[0m\n"
{
" - Container format: Matroska\n"
" Size 20.33 MiB\n"
" Mime-type video/x-matroska\n"
" Document type matroska\n"
" Read version 1\n"
" Version 1\n"
" Document read version 1\n"
" Document version 1\n",
" - Tracks: Theora-720p / Vorbis-2ch\n"
" ID 1368622492\n"
" Type Video\n"
" Format Theora\n"
" Raw format ID V_THEORA\n"
" FPS 24\n"
" Pixel size width: 1280, height: 720\n"
" Display size width: 1280, height: 720\n"
" Labeled as default",
" ID 3171450505\n"
" Type Audio\n"
" Format Vorbis\n"
" Raw format ID A_VORBIS\n"
" Channel count 2\n"
" Sampling frequency 48000 Hz\n"
" Labeled as default\n",
}));
CPPUNIT_ASSERT(testContainsSubstrings(stderr,
{
" - Diagnostic messages:\n",
"parsing EBML element header: EBML ID length at 35 is not supported, trying to skip.",
"parsing header of EBML element 0x1549A966 \"segment info\" at 169: 134 bytes have been skipped",
}));
// test MP4 file with AAC track using SBR and PS extensions
const auto mp4File1 = testFilePath("mtx-test-data/aac/he-aacv2-ps.m4a");
const char *const args3[] = { "tageditor", "info", "-f", mp4File1.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args3);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \033[1mContainer format: MPEG-4 Part 14\033[0m\n"
" Size 898.34 KiB\n"
" Mime-type audio/mp4\n"
" Duration 3 min\n"
@ -854,7 +924,7 @@ void CliTests::testDisplayingInfo()
" Document type mp42\n"
" Creation time 2014-12-10 16:22:41\n"
" Modification time 2014-12-10 16:22:41\n",
" - \e[1mTracks: HE-AAC-2ch\e[0m\n"
" - \033[1mTracks: HE-AAC-2ch\033[0m\n"
" ID 1\n"
" Name soun\n"
" Type Audio\n"
@ -894,7 +964,7 @@ void CliTests::testSettingTrackMetaData()
TESTUTILS_ASSERT_EXEC(args1);
TESTUTILS_ASSERT_EXEC(args2);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mContainer format: Matroska\e[0m\n"
{ " - \033[1mContainer format: Matroska\033[0m\n"
" Size 20.16 MiB\n"
" Mime-type video/x-matroska\n"
" Duration 47 s 509 ms\n"
@ -906,7 +976,7 @@ void CliTests::testSettingTrackMetaData()
" Document version 2\n"
" Tag position before data\n"
" Index position before data\n",
" - \e[1mTracks: H.264-Main@L3.1-576p / AAC-LC-2ch-ger\e[0m\n"
" - \033[1mTracks: H.264-Main@L3.1-576p / AAC-LC-2ch-ger\033[0m\n"
" ID 1863976627\n"
" Name video track\n"
" Type Video\n"
@ -926,7 +996,7 @@ void CliTests::testSettingTrackMetaData()
" Labeled as default, forced" }));
TESTUTILS_ASSERT_EXEC(args3);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\e[0m\n"
{ " - \033[1mMatroska tag targeting \"level 50 'album, opera, concert, movie, episode'\"\033[0m\n"
" Title title of tag\n"
" Artist setting tag value again\n"
" Comment Matroska Validation File 2, 100,000 timecode scale, odd aspect ratio, and CRC-32. Codecs are AVC and AAC\n"
@ -935,7 +1005,7 @@ void CliTests::testSettingTrackMetaData()
const char *const args4[] = { "tageditor", "info", "-f", mp4File.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args4);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \e[1mContainer format: MPEG-4 Part 14\e[0m\n"
{ " - \033[1mContainer format: MPEG-4 Part 14\033[0m\n"
" Size 898.49 KiB\n"
" Mime-type audio/mp4\n"
" Duration 3 min\n"
@ -943,7 +1013,7 @@ void CliTests::testSettingTrackMetaData()
" Document type mp42\n"
" Creation time 2014-12-10 16:22:41\n"
" Modification time 2014-12-10 16:22:41\n",
" - \e[1mTracks: HE-AAC-2ch-eng\e[0m\n"
" - \033[1mTracks: HE-AAC-2ch-eng\033[0m\n"
" ID 1\n"
" Name sbr and ps\n"
" Type Audio\n"
@ -975,16 +1045,17 @@ void CliTests::testSettingTrackMetaData()
*/
void CliTests::testExtraction()
{
cout << "\nExtraction" << endl;
string stdout, stderr;
const string mp4File1(testFilePath("mtx-test-data/alac/othertest-itunes.m4a"));
std::cout << "\nExtraction" << std::endl;
auto stdout = std::string(), stderr = std::string();
const auto mp4File1 = testFilePath("mtx-test-data/alac/othertest-itunes.m4a");
const auto tempFile = (std::filesystem::temp_directory_path() / "extracted.jpeg").string();
// test extraction of cover
const char *const args1[] = { "tageditor", "extract", "cover", "-f", mp4File1.data(), "-o", "/tmp/extracted.jpeg", nullptr };
const char *const args1[] = { "tageditor", "extract", "cover", "-f", mp4File1.data(), "-o", tempFile.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args1);
Diagnostics diag;
AbortableProgressFeedback progress;
MediaFileInfo extractedInfo("/tmp/extracted.jpeg"sv);
auto diag = Diagnostics();
auto progress = AbortableProgressFeedback();
auto extractedInfo = MediaFileInfo(tempFile);
extractedInfo.open(true);
extractedInfo.parseContainerFormat(diag, progress);
CPPUNIT_ASSERT_EQUAL(static_cast<std::uint64_t>(22771), extractedInfo.size());
@ -992,17 +1063,19 @@ void CliTests::testExtraction()
extractedInfo.invalidate();
// test assignment of cover by the way
const string mp4File2(workingCopyPath("mtx-test-data/aac/he-aacv2-ps.m4a"));
const char *const args2[] = { "tageditor", "set", "cover=/tmp/extracted.jpeg", "-f", mp4File2.data(), nullptr };
const auto mp4File2 = workingCopyPath("mtx-test-data/aac/he-aacv2-ps.m4a");
const auto coverArg = argsToString("cover=", tempFile);
const char *const args2[] = { "tageditor", "set", coverArg.data(), "-f", mp4File2.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args2);
const char *const args3[] = { "tageditor", "extract", "cover", "-f", mp4File2.data(), "-o", "/tmp/extracted.jpeg", nullptr };
CPPUNIT_ASSERT_EQUAL(0, remove("/tmp/extracted.jpeg"));
const char *const args3[] = { "tageditor", "extract", "cover", "-f", mp4File2.data(), "-o", tempFile.data(), nullptr };
CPPUNIT_ASSERT_EQUAL(0, remove(tempFile.data()));
TESTUTILS_ASSERT_EXEC(args3);
extractedInfo.open(true);
extractedInfo.parseContainerFormat(diag, progress);
CPPUNIT_ASSERT_EQUAL(static_cast<std::uint64_t>(22771), extractedInfo.size());
CPPUNIT_ASSERT(ContainerFormat::Jpeg == extractedInfo.containerFormat());
CPPUNIT_ASSERT_EQUAL(0, remove("/tmp/extracted.jpeg"));
extractedInfo.close();
CPPUNIT_ASSERT_EQUAL(0, remove(tempFile.data()));
CPPUNIT_ASSERT_EQUAL(0, remove(mp4File2.data()));
CPPUNIT_ASSERT_EQUAL(0, remove((mp4File2 + ".bak").data()));
CPPUNIT_ASSERT_EQUAL(Diagnostics(), diag);
@ -1058,16 +1131,16 @@ void CliTests::testFileLayoutOptions()
const char *const args5[] = { "tageditor", "get", "-f", mp4File2.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args5);
CPPUNIT_ASSERT(stdout.find(" - \e[1mMP4/iTunes tag\e[0m\n"
" Title You Shook Me All Night Long\n"
" Album Who Made Who\n"
" Artist ACDC\n"
" Genre Rock\n"
" Track 2/9\n"
" Encoder Nero AAC codec / 1.5.3.0, remuxed with Lavf57.56.100\n"
" Record date 1986\n"
" Encoder settings ndaudio 1.5.3.0 / -q 0.34")
!= string::npos);
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ " - \033[1mMP4/iTunes tag\033[0m\n"
" Title You Shook Me All Night Long\n"
" Album Who Made Who\n"
" Artist ACDC\n"
" Genre Rock\n"
" Track 2/9\n"
" Encoder Nero AAC codec / 1.5.3.0, remuxed with Lavf57.56.100\n"
" Record date 1986\n"
" Encoder settings ndaudio 1.5.3.0 / -q 0.34" }));
remove((mp4File2 + ".bak").data());
const char *const args6[] = { "tageditor", "set", "--index-pos", "front", "--force", "--layout-only", "-f", mp4File2.data(), nullptr };
@ -1091,12 +1164,12 @@ void CliTests::testJsonExport()
cout << "\nJSON export" << endl;
string stdout, stderr;
const auto file(testFilePath("matroska_wave1/test3.mkv"));
const auto expectedJsonPath(testFilePath("matroska_wave1-test3.json"));
const auto file = testFilePath("matroska_wave1/test3.mkv");
const auto expectedJson = readFile(testFilePath("matroska_wave1-test3.json"));
const char *const args[] = { "tageditor", "export", "--pretty", "-f", file.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args);
const char *const jqArgs[]
= { "jq", "--argfile", "expected", expectedJsonPath.data(), "--argjson", "actual", stdout.data(), "-n", "$actual == $expected", nullptr };
= { "jq", "--argjson", "expected", expectedJson.data(), "--argjson", "actual", stdout.data(), "-n", "$actual == $expected", nullptr };
const auto *const logJsonExport = std::getenv(PROJECT_VARNAME_UPPER "_LOG_JQ_INVOCATION");
execHelperAppInSearchPath("jq", jqArgs, stdout, stderr, !logJsonExport || !std::strlen(logJsonExport));
CPPUNIT_ASSERT_EQUAL(""s, stderr);
@ -1104,4 +1177,41 @@ void CliTests::testJsonExport()
#endif // TAGEDITOR_JSON_EXPORT
}
#endif // PLATFORM_UNIX
/*!
* \brief Tests the --script parameter of the set operation.
*/
void CliTests::testScriptProcessing()
{
#ifndef TAGEDITOR_USE_JSENGINE
std::cout << "\nSkipping script processing (feature not enabled)" << std::endl;
#else
std::cout << "\nScript processing" << endl;
auto stdout = std::string(), stderr = std::string();
const auto file = workingCopyPath("mtx-test-data/alac/othertest-itunes.m4a");
const auto script = testFilePath("script-processing-test.js");
const char *args[] = { "tageditor", "set", "--pedantic", "debug", "--script", script.data(), "--script-settings", "set:title=foo",
"set:artist=bar", "dryRun=false", "-f", file.data(), nullptr };
TESTUTILS_ASSERT_EXEC_EXIT_STATUS(args, EXIT_PARSING_FAILURE);
CPPUNIT_ASSERT(testContainsSubstrings(stderr,
{ "executing JavaScript for othertest-itunes.m4a: entering main() function", "settings: set:title, set:artist, dryRun", "tag: MP4/iTunes tag",
"supported fields: album, albumArtist, arranger, ", "soundEngineer, title, track", "MP4/iTunes tag: applying changes",
" - change title[0] from 'Sad Song' to 'foo'", " - change artist[0] from 'Oasis' to 'bar'",
"executing JavaScript for othertest-itunes.m4a: done with return value: true", "Changes are about to be applied" }));
CPPUNIT_ASSERT(testContainsSubstrings(
stdout, { "Loading JavaScript file", script.data(), "Setting tag information for", file.data(), "Changes have been applied." }));
args[9] = "dryRun=true";
TESTUTILS_ASSERT_EXEC_EXIT_STATUS(args, EXIT_PARSING_FAILURE);
CPPUNIT_ASSERT(testContainsSubstrings(stderr,
{ "executing JavaScript for othertest-itunes.m4a: entering main() function", "MP4/iTunes tag: applying changes",
" - set title[0] to 'foo' (no change)", " - set artist[0] to 'bar' (no change)",
"executing JavaScript for othertest-itunes.m4a: done with return value: false" }));
CPPUNIT_ASSERT_EQUAL(std::string::npos, stderr.find("Changes are about to be applied"));
CPPUNIT_ASSERT(testContainsSubstrings(stdout,
{ "Loading JavaScript file", script.data(), "Setting tag information for", file.data(),
" - Skipping file because JavaScript returned a falsy value other than undefined." }));
#endif
}
#endif // defined(PLATFORM_UNIX) || defined(CPP_UTILITIES_HAS_EXEC_APP)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff