Compare commits

...

78 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
62 changed files with 3427 additions and 1175 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 8)
set(META_VERSION_MINOR 9)
set(META_VERSION_PATCH 1)
set(META_RELEASE_DATE "2023-06-08")
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)
@ -201,7 +204,7 @@ if (WIDGETS_GUI OR QUICK_GUI)
endif ()
# find tagparser
find_package(tagparser${CONFIGURATION_PACKAGE_SUFFIX} 12.0.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)

View File

@ -93,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.
@ -129,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
@ -344,6 +364,53 @@ 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:
@ -486,11 +553,13 @@ 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

@ -83,6 +83,13 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg, Argumen
, 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);
@ -120,6 +127,9 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg, Argumen
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
@ -135,7 +145,8 @@ SetTagInfoArgs::SetTagInfoArgs(Argument &filesArg, Argument &verboseArg, Argumen
&id3v2UsageArg, &id3InitOnCreateArg, &id3TransferOnRemovalArg, &mergeMultipleSuccessiveTagsArg, &id3v2VersionArg, &encodingArg,
&removeTargetArg, &addAttachmentArg, &updateAttachmentArg, &removeAttachmentArg, &removeExistingAttachmentsArg, &minPaddingArg,
&maxPaddingArg, &prefPaddingArg, &tagPosArg, &indexPosArg, &forceRewriteArg, &backupDirArg, &layoutOnlyArg, &preserveModificationTimeArg,
&verboseArg, &pedanticArg, &quietArg, &outputFilesArg });
&preserveMuxingAppArg, &preserveWritingAppArg, &preserveTotalFieldsArg, &jsArg, &jsSettingsArg, &verboseArg, &pedanticArg, &quietArg,
&outputFilesArg });
}
} // namespace Cli

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

@ -222,17 +222,25 @@ void printDiagMessages(const Diagnostics &diag, const char *head, bool beVerbose
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);
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>;

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"
@ -204,7 +221,7 @@ void displayFileInfo(
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) {
@ -223,6 +240,12 @@ void displayFileInfo(
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()));
@ -368,7 +391,8 @@ void displayTagInfo(
// 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
@ -431,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();
@ -471,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;
@ -496,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;
@ -556,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));
@ -571,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
@ -596,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
@ -669,7 +827,29 @@ 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);
@ -727,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 {
@ -747,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);
}
@ -958,14 +1145,7 @@ void setTagInfo(const SetTagInfoArgs &args)
<< Phrases::EndFlush;
exitCode = EXIT_IO_FAILURE;
}
printDiagMessages(diag, "Diagnostic messages:", args.verboseArg.isPresent(), &args.pedanticArg);
// continue with next file
++fileIndex;
if (currentOutputFile != noMoreOutputFiles) {
++currentOutputFile;
}
continueWithNextFile(diag);
}
}
@ -1221,4 +1401,5 @@ void applyGeneralConfig(const Argument &timeSapnFormatArg)
{
timeSpanOutputFormat = parseTimeSpanOutputFormat(timeSapnFormatArg, TimeSpanOutputFormat::WithMeasures);
}
} // namespace Cli

View File

@ -48,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;
};

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,6 +48,13 @@ 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);
updateStyleSheet();
@ -72,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")));
@ -115,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);
@ -140,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();
}
}
@ -246,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) {
@ -279,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);
@ -330,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);
}

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);
@ -82,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

@ -160,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

@ -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

@ -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

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

@ -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

@ -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);
}
}

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) {

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

@ -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">

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

@ -15,6 +15,7 @@
#include <cstdlib>
#include <cstring>
#include <filesystem>
namespace CppUtilities {
@ -38,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;
@ -50,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);
@ -69,6 +77,7 @@ class CliTests : public TestFixture {
CPPUNIT_TEST(testReadingAndWritingDocumentTitle);
CPPUNIT_TEST(testFileLayoutOptions);
CPPUNIT_TEST(testJsonExport);
CPPUNIT_TEST(testScriptProcessing);
#endif
CPPUNIT_TEST_SUITE_END();
@ -76,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();
@ -95,6 +104,7 @@ public:
void testReadingAndWritingDocumentTitle();
void testFileLayoutOptions();
void testJsonExport();
void testScriptProcessing();
#endif
private:
@ -110,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();
@ -618,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
@ -635,7 +665,7 @@ 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,
{ " - \033[1mMatroska tag targeting \"level 30 'track, song, chapter'\"\033[0m\n"
@ -645,8 +675,8 @@ void CliTests::testOutputFile()
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()));
}
/*!
@ -778,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 };
@ -1015,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());
@ -1032,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);
@ -1098,16 +1131,16 @@ void CliTests::testFileLayoutOptions()
const char *const args5[] = { "tageditor", "get", "-f", mp4File2.data(), nullptr };
TESTUTILS_ASSERT_EXEC(args5);
CPPUNIT_ASSERT(stdout.find(" - \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")
!= 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 };
@ -1131,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);
@ -1144,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