Compare commits
78 Commits
Author | SHA1 | Date |
---|---|---|
Martchus | 12620c5768 | |
Martchus | 854bd13efc | |
Martchus | 22aa2f0851 | |
Martchus | abc80c24ff | |
Martchus | 6e66b6aa0b | |
Martchus | 934b955837 | |
Martchus | 959b0f0032 | |
Martchus | d8f542d3e4 | |
Martchus | a8281f323a | |
Martchus | b3f576a354 | |
Martchus | c26d3e7be8 | |
Martchus | 63ec653df5 | |
Martchus | 2cd47777a3 | |
Martchus | 88989ff986 | |
Martchus | e1e979f9f5 | |
Martchus | 04bd9563c0 | |
Martchus | dd0bbd74c5 | |
Martchus | f82bf04c66 | |
Martchus | a87e431e04 | |
Martchus | 72dcbbbd81 | |
Martchus | b5f9158106 | |
Martchus | f1b785337d | |
Martchus | 09e2fe20f7 | |
Martchus | c7df780281 | |
Martchus | fedc89b20e | |
Martchus | 6ba93048c6 | |
Martchus | e1fdfa744c | |
Martchus | 6f30fc309e | |
Martchus | d26a7afc47 | |
Martchus | 9a7108fe58 | |
Martchus | 6f7ee1ea69 | |
Martchus | a2fa2e5561 | |
Martchus | 4f2b8904fa | |
Martchus | f92d7b39dd | |
Martchus | e58f8a14a5 | |
Martchus | 1bb9f4b76e | |
Martchus | f4346fb8e6 | |
Martchus | 1cb00c7c41 | |
Martchus | 35dc6bca20 | |
Martchus | 2d5f586c88 | |
Martchus | 07406a1185 | |
Martchus | 503c8f2c88 | |
Martchus | 7f8cf5c44d | |
Martchus | 9d704ffc27 | |
Martchus | 1e45722ea1 | |
Martchus | 488684a318 | |
Martchus | 023b25e44b | |
Martchus | 5222082635 | |
Martchus | 63f0ab96a4 | |
Martchus | 64aabf6de3 | |
Martchus | c4f7d195a0 | |
Martchus | 7063f1bf03 | |
Martchus | 44d0ac21c2 | |
Martchus | d248c63279 | |
Martchus | e69278634f | |
Martchus | dace19b2bf | |
Martchus | a191aebd8a | |
Martchus | dbd4e71281 | |
Martchus | 485611141c | |
Martchus | 7533761d77 | |
Martchus | 9fd925a6bd | |
Martchus | cd1e0ce590 | |
Martchus | 73dc64ce6a | |
Martchus | 1d4c18f474 | |
Martchus | ccb516d47b | |
Martchus | 24b397b930 | |
Martchus | 9aca90538d | |
Martchus | 57b6d38e43 | |
Martchus | c3af3d43e6 | |
Martchus | 9cb8702d13 | |
Martchus | 8c2ab29927 | |
Martchus | 11d3cefbcf | |
Martchus | 22953ad0da | |
Martchus | 857917c23d | |
Martchus | 1017873c28 | |
Martchus | 1a94b5c85d | |
Martchus | 4a3aa9c1c1 | |
Martchus | 1cb6e06f31 |
|
@ -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)
|
||||
|
|
77
README.md
77
README.md
|
@ -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).
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
11
cli/helper.h
11
cli/helper.h
|
@ -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>;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -450,6 +450,7 @@ currently shown tag.</string>
|
|||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>renamePushButton</tabstop>
|
||||
<tabstop>tagSelectionComboBox</tabstop>
|
||||
<tabstop>keepPreviousValuesPushButton</tabstop>
|
||||
<tabstop>tagOptionsPushButton</tabstop>
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 "3/15" such as the track position are actually stored as such (and <i>not</i> 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">
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
156
tests/cli.cpp
156
tests/cli.cpp
|
@ -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
Loading…
Reference in New Issue