Compare commits

...

20 Commits

Author SHA1 Message Date
Jonas Kvinge 63a9676c42 Remove lazy loading and FTS
Fixes #392
2024-05-12 21:43:11 +02:00
Jonas Kvinge dff3ae7410 CI: Remove --skip-broken from dnf for Fedora 2024-05-12 21:41:38 +02:00
Jonas Kvinge 76614bcde0 Only apply collection directories changes on save 2024-05-12 21:40:51 +02:00
Jonas Kvinge 2953f9eefc ParserBase: Use original paths 2024-05-12 21:38:59 +02:00
Jonas Kvinge 4a24605361 FileViewList: Use original paths instead of canonical paths 2024-05-12 21:38:15 +02:00
Jonas Kvinge decabe8d47 CommandlineOptions: Use original paths instead of canonical paths 2024-05-12 21:37:54 +02:00
Jonas Kvinge 51adcf0f1e MainWindow: Use original paths instead of canonical paths 2024-05-12 21:37:32 +02:00
Jonas Kvinge 2a6a07fef6 CollectionBackendTest: Remove use of QFileInfo::canonicalFilePath 2024-05-12 21:36:52 +02:00
Jonas Kvinge 315cf63118 CI: Remove Windows 32 bit 2024-05-11 23:23:56 +02:00
Wedone e28d362aad Update fr.po 2024-05-04 16:22:51 +02:00
Jonas Kvinge e0d9b8f715 ResizableTextEdit: Remove tab 2024-05-04 16:21:59 +02:00
Jonas Kvinge eff6b75c43 nsi: Remove /SOLID from SetCompressor for debug 2024-04-24 02:25:05 +02:00
Robert Gingras e8be0adf37 TagReaderTagLib: Remove redundant ID3v2 validity check
TagLib will have created a valid ID3v2 tag on this file by this point in the code, due to the way it handles the tag() method for WAV::File. Thus the null pointer check is redundant and the hasID3v2() call is at best redundant and at worst will cause tags to not save when they otherwise should have
2024-04-24 01:23:51 +02:00
Robert Gingras 9f4a82bb62 TagReaderTagLib: Remove file_mpeg argument from the SetEmbeddedArt ID3v2 overload 2024-04-24 01:23:51 +02:00
Robert Gingras 8d7e14f21d Song: Added WAV to list of supported filetypes 2024-04-24 01:23:51 +02:00
Robert Gingras d03d3622aa TagReaderTagLib: Have RIFF WAV files save ID3v2 tags, when applicable 2024-04-24 01:23:51 +02:00
Robert Gingras 821c32992d TagReaderTagLib: Refactor ID3v2 saving to a dedicated function 2024-04-24 01:23:51 +02:00
Robert Gingras b52cf9f3cd TagReaderTagLib: Reposition ParseID3v2Tag 2024-04-24 01:23:51 +02:00
Robert Gingras ab8e687f96 TagReaderTagLib: Add id3v2 parsing for RIFF WAV files 2024-04-24 01:23:51 +02:00
Robert Gingras 90703703aa TagReaderTagLib: Make id3v2 parsing reusable 2024-04-24 01:23:51 +02:00
59 changed files with 1430 additions and 1510 deletions

View File

@ -185,7 +185,7 @@ jobs:
run: dnf -y upgrade
- name: Install dependencies
run: >
dnf -y --skip-broken install
dnf -y install
@development-tools
redhat-lsb-core
which
@ -985,7 +985,7 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: [ 'i686', 'x86_64' ]
arch: [ 'x86_64' ]
buildtype: [ 'debug', 'release' ]
container:
image: jonaski/strawberry-mxe-${{matrix.arch}}-${{matrix.buildtype}}
@ -1156,8 +1156,8 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: [ 'x86', 'x86_64' ]
buildtype: [ 'debug', 'release' ]
arch: [ 'x86_64' ]
buildtype: [ 'release' ]
steps:
- name: Set prefix path

View File

@ -476,25 +476,6 @@ if(NOT CMAKE_CROSSCOMPILING)
"
QT_SQLITE_TEST
)
if(QT_SQLITE_TEST)
# Check that we have sqlite3 with FTS5
check_cxx_source_runs("
#include <QSqlDatabase>
#include <QSqlQuery>
int main() {
QSqlDatabase db = QSqlDatabase::addDatabase(\"QSQLITE\");
db.setDatabaseName(\":memory:\");
if (!db.open()) { return 1; }
QSqlQuery q(db);
q.prepare(\"CREATE VIRTUAL TABLE test_fts USING fts5(test, tokenize = 'unicode61 remove_diacritics 0');\");
if (!q.exec()) return 1;
}
"
SQLITE_FTS5_TEST
)
endif()
unset(CMAKE_REQUIRED_FLAGS)
unset(CMAKE_REQUIRED_LIBRARIES)
endif()
# Set up definitions
@ -555,11 +536,7 @@ if(QT_VERSION_MAJOR EQUAL 5)
endif()
if(NOT CMAKE_CROSSCOMPILING)
if(QT_SQLITE_TEST)
if(NOT SQLITE_FTS5_TEST)
message(WARNING "sqlite must be enabled with FTS5. See: https://www.sqlite.org/fts5.html")
endif()
else()
if(NOT QT_SQLITE_TEST)
message(WARNING "The Qt sqlite driver test failed.")
endif()
endif()

View File

@ -76,7 +76,7 @@ To build Strawberry from source you need the following installed on your system
* [Boost](https://www.boost.org/)
* [GLib](https://developer.gnome.org/glib/)
* [Qt 6 or Qt 5.12 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
* [SQLite 3.9 or newer with FTS5](https://www.sqlite.org)
* [SQLite 3.9 or newer](https://www.sqlite.org)
* [Protobuf](https://developers.google.com/protocol-buffers/)
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
* [D-Bus (Required on Linux)](https://www.freedesktop.org/wiki/Software/dbus/)

View File

@ -109,7 +109,11 @@
Unicode True
!ifdef debug
SetCompressor lzma
!else
SetCompressor /SOLID lzma
!endif
!include "MUI2.nsh"
!include "FileAssociation.nsh"

View File

@ -328,137 +328,8 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
}
else if (TagLib::MPEG::File *file_mpeg = dynamic_cast<TagLib::MPEG::File*>(fileref->file())) {
if (file_mpeg->ID3v2Tag()) {
const TagLib::ID3v2::FrameListMap &map = file_mpeg->ID3v2Tag()->frameListMap();
if (map.contains("TPOS")) disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
if (map.contains("TCOM")) TStringToStdString(map["TCOM"].front()->toString(), song->mutable_composer());
// content group
if (map.contains("TIT1")) TStringToStdString(map["TIT1"].front()->toString(), song->mutable_grouping());
// original artist/performer
if (map.contains("TOPE")) TStringToStdString(map["TOPE"].front()->toString(), song->mutable_performer());
// Skip TPE1 (which is the artist) here because we already fetched it
// non-standard: Apple, Microsoft
if (map.contains("TPE2")) TStringToStdString(map["TPE2"].front()->toString(), song->mutable_albumartist());
if (map.contains("TCMP")) compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
if (map.contains("TDOR")) {
song->set_originalyear(map["TDOR"].front()->toString().substr(0, 4).toInt());
}
else if (map.contains("TORY")) {
song->set_originalyear(map["TORY"].front()->toString().substr(0, 4).toInt());
}
if (map.contains("USLT")) {
TStringToStdString(map["USLT"].front()->toString(), song->mutable_lyrics());
}
else if (map.contains("SYLT")) {
TStringToStdString(map["SYLT"].front()->toString(), song->mutable_lyrics());
}
if (map.contains("APIC")) song->set_art_embedded(true);
// Find a suitable comment tag. For now we ignore iTunNORM comments.
for (uint i = 0; i < map["COMM"].size(); ++i) {
const TagLib::ID3v2::CommentsFrame *frame = dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
if (frame && TStringToQString(frame->description()) != QStringLiteral("iTunNORM")) {
TStringToStdString(frame->text(), song->mutable_comment());
break;
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_playcount = TagLib::ID3v2::UserTextIdentificationFrame::find(file_mpeg->ID3v2Tag(), "FMPS_Playcount")) {
TagLib::StringList frame_field_list = frame_fmps_playcount->fieldList();
if (frame_field_list.size() > 1) {
int playcount = TStringToQString(frame_field_list[1]).toInt();
if (song->playcount() <= 0 && playcount > 0) {
song->set_playcount(playcount);
}
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_rating = TagLib::ID3v2::UserTextIdentificationFrame::find(file_mpeg->ID3v2Tag(), "FMPS_Rating")) {
TagLib::StringList frame_field_list = frame_fmps_rating->fieldList();
if (frame_field_list.size() > 1) {
float rating = TStringToQString(frame_field_list[1]).toFloat();
if (song->rating() <= 0 && rating > 0 && rating <= 1.0) {
song->set_rating(rating);
}
}
}
if (map.contains("POPM")) {
const TagLib::ID3v2::PopularimeterFrame *frame = dynamic_cast<const TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front());
if (frame) {
if (song->playcount() <= 0 && frame->counter() > 0) {
song->set_playcount(frame->counter());
}
if (song->rating() <= 0 && frame->rating() > 0) {
song->set_rating(ConvertPOPMRating(frame->rating()));
}
}
}
if (map.contains("UFID")) {
for (uint i = 0; i < map["UFID"].size(); ++i) {
if (TagLib::ID3v2::UniqueFileIdentifierFrame *frame = dynamic_cast<TagLib::ID3v2::UniqueFileIdentifierFrame*>(map["UFID"][i])) {
const TagLib::PropertyMap property_map = frame->asProperties();
if (property_map.contains(kID3v2_MusicBrainz_RecordingID)) {
TStringToStdString(property_map[kID3v2_MusicBrainz_RecordingID].toString(), song->mutable_musicbrainz_recording_id());
}
}
}
}
if (map.contains("TXXX")) {
for (uint i = 0; i < map["TXXX"].size(); ++i) {
if (TagLib::ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i])) {
const TagLib::StringList frame_field_list = frame->fieldList();
if (frame_field_list.size() != 2) continue;
if (frame->description() == kID3v2_AcoustID_ID) {
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_id());
}
if (frame->description() == kID3v2_AcoustID_Fingerprint) {
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_fingerprint());
}
if (frame->description() == kID3v2_MusicBrainz_AlbumArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_ArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_OriginalArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_AlbumID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_id());
}
if (frame->description() == kID3v2_MusicBrainz_OriginalAlbumID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_album_id());
}
if (frame->description() == kID3v2_MusicBrainz_TrackID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_track_id());
}
if (frame->description() == kID3v2_MusicBrainz_DiscID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_disc_id());
}
if (frame->description() == kID3v2_MusicBrainz_ReleaseGroupID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_release_group_id());
}
if (frame->description() == kID3v2_MusicBrainz_WorkID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_work_id());
}
}
}
}
if (file_mpeg->hasID3v2Tag()) {
ParseID3v2Tag(file_mpeg->ID3v2Tag(), &disc, &compilation, song);
}
}
@ -658,6 +529,12 @@ bool TagReaderTagLib::ReadFile(const QString &filename, spb::tagreader::SongMeta
if (tag) TStringToStdString(tag->comment(), song->mutable_comment());
}
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
if (file_wav->hasID3v2Tag()) {
ParseID3v2Tag(file_wav->ID3v2Tag(), &disc, &compilation, song);
}
}
else if (tag) {
TStringToStdString(tag->comment(), song->mutable_comment());
}
@ -710,6 +587,139 @@ void TagReaderTagLib::TStringToStdString(const TagLib::String &tag, std::string
}
void TagReaderTagLib::ParseID3v2Tag(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const {
TagLib::ID3v2::FrameListMap map = tag->frameListMap();
if (map.contains("TPOS")) *disc = TStringToQString(map["TPOS"].front()->toString()).trimmed();
if (map.contains("TCOM")) TStringToStdString(map["TCOM"].front()->toString(), song->mutable_composer());
// content group
if (map.contains("TIT1")) TStringToStdString(map["TIT1"].front()->toString(), song->mutable_grouping());
// original artist/performer
if (map.contains("TOPE")) TStringToStdString(map["TOPE"].front()->toString(), song->mutable_performer());
// Skip TPE1 (which is the artist) here because we already fetched it
// non-standard: Apple, Microsoft
if (map.contains("TPE2")) TStringToStdString(map["TPE2"].front()->toString(), song->mutable_albumartist());
if (map.contains("TCMP")) *compilation = TStringToQString(map["TCMP"].front()->toString()).trimmed();
if (map.contains("TDOR")) {
song->set_originalyear(map["TDOR"].front()->toString().substr(0, 4).toInt());
}
else if (map.contains("TORY")) {
song->set_originalyear(map["TORY"].front()->toString().substr(0, 4).toInt());
}
if (map.contains("USLT")) {
TStringToStdString(map["USLT"].front()->toString(), song->mutable_lyrics());
}
else if (map.contains("SYLT")) {
TStringToStdString(map["SYLT"].front()->toString(), song->mutable_lyrics());
}
if (map.contains("APIC")) song->set_art_embedded(true);
// Find a suitable comment tag. For now we ignore iTunNORM comments.
for (uint i = 0; i < map["COMM"].size(); ++i) {
const TagLib::ID3v2::CommentsFrame *frame = dynamic_cast<const TagLib::ID3v2::CommentsFrame*>(map["COMM"][i]);
if (frame && TStringToQString(frame->description()) != QStringLiteral("iTunNORM")) {
TStringToStdString(frame->text(), song->mutable_comment());
break;
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_playcount = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, "FMPS_Playcount")) {
TagLib::StringList frame_field_list = frame_fmps_playcount->fieldList();
if (frame_field_list.size() > 1) {
int playcount = TStringToQString(frame_field_list[1]).toInt();
if (song->playcount() <= 0 && playcount > 0) {
song->set_playcount(playcount);
}
}
}
if (TagLib::ID3v2::UserTextIdentificationFrame *frame_fmps_rating = TagLib::ID3v2::UserTextIdentificationFrame::find(tag, "FMPS_Rating")) {
TagLib::StringList frame_field_list = frame_fmps_rating->fieldList();
if (frame_field_list.size() > 1) {
float rating = TStringToQString(frame_field_list[1]).toFloat();
if (song->rating() <= 0 && rating > 0 && rating <= 1.0) {
song->set_rating(rating);
}
}
}
if (map.contains("POPM")) {
const TagLib::ID3v2::PopularimeterFrame *frame = dynamic_cast<const TagLib::ID3v2::PopularimeterFrame*>(map["POPM"].front());
if (frame) {
if (song->playcount() <= 0 && frame->counter() > 0) {
song->set_playcount(frame->counter());
}
if (song->rating() <= 0 && frame->rating() > 0) {
song->set_rating(ConvertPOPMRating(frame->rating()));
}
}
}
if (map.contains("UFID")) {
for (uint i = 0; i < map["UFID"].size(); ++i) {
if (TagLib::ID3v2::UniqueFileIdentifierFrame *frame = dynamic_cast<TagLib::ID3v2::UniqueFileIdentifierFrame*>(map["UFID"][i])) {
const TagLib::PropertyMap property_map = frame->asProperties();
if (property_map.contains(kID3v2_MusicBrainz_RecordingID)) {
TStringToStdString(property_map[kID3v2_MusicBrainz_RecordingID].toString(), song->mutable_musicbrainz_recording_id());
}
}
}
}
if (map.contains("TXXX")) {
for (uint i = 0; i < map["TXXX"].size(); ++i) {
if (TagLib::ID3v2::UserTextIdentificationFrame *frame = dynamic_cast<TagLib::ID3v2::UserTextIdentificationFrame*>(map["TXXX"][i])) {
const TagLib::StringList frame_field_list = frame->fieldList();
if (frame_field_list.size() != 2) continue;
if (frame->description() == kID3v2_AcoustID_ID) {
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_id());
}
if (frame->description() == kID3v2_AcoustID_Fingerprint) {
TStringToStdString(frame_field_list.back(), song->mutable_acoustid_fingerprint());
}
if (frame->description() == kID3v2_MusicBrainz_AlbumArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_ArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_OriginalArtistID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_artist_id());
}
if (frame->description() == kID3v2_MusicBrainz_AlbumID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_album_id());
}
if (frame->description() == kID3v2_MusicBrainz_OriginalAlbumID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_original_album_id());
}
if (frame->description() == kID3v2_MusicBrainz_TrackID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_track_id());
}
if (frame->description() == kID3v2_MusicBrainz_DiscID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_disc_id());
}
if (frame->description() == kID3v2_MusicBrainz_ReleaseGroupID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_release_group_id());
}
if (frame->description() == kID3v2_MusicBrainz_WorkID) {
TStringToStdString(frame_field_list.back(), song->mutable_musicbrainz_work_id());
}
}
}
}
}
void TagReaderTagLib::ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const {
if (map.contains("COMPOSER")) TStringToStdString(map["COMPOSER"].front(), song->mutable_composer());
@ -943,14 +953,7 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
TagLib::ID3v2::Tag *tag = file_mpeg->ID3v2Tag(true);
if (!tag) return false;
if (save_tags) {
SetTextFrame("TPOS", song.disc() <= 0 ? QString() : QString::number(song.disc()), tag);
SetTextFrame("TCOM", song.composer().empty() ? std::string() : song.composer(), tag);
SetTextFrame("TIT1", song.grouping().empty() ? std::string() : song.grouping(), tag);
SetTextFrame("TOPE", song.performer().empty() ? std::string() : song.performer(), tag);
// Skip TPE1 (which is the artist) here because we already set it
SetTextFrame("TPE2", song.albumartist().empty() ? std::string() : song.albumartist(), tag);
SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(), tag);
SetUnsyncLyricsFrame(song.lyrics().empty() ? std::string() : song.lyrics(), tag);
SaveID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song);
@ -959,7 +962,7 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
SetRating(tag, song);
}
if (save_cover) {
SetEmbeddedArt(file_mpeg, tag, cover.data, cover.mime_type);
SetEmbeddedArt(tag, cover.data, cover.mime_type);
}
}
@ -985,6 +988,22 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
}
}
else if (TagLib::RIFF::WAV::File *file_wav = dynamic_cast<TagLib::RIFF::WAV::File*>(fileref->file())) {
TagLib::ID3v2::Tag *tag = file_wav->ID3v2Tag();
if (save_tags) {
SaveID3v2Tag(tag, song);
}
if (save_playcount) {
SetPlaycount(tag, song);
}
if (save_rating) {
SetRating(tag, song);
}
if (save_cover) {
SetEmbeddedArt(tag, cover.data, cover.mime_type);
}
}
// Handle all the files which have VorbisComments (Ogg, OPUS, ...) in the same way;
// apart, so we keep specific behavior for some formats by adding another "else if" block above.
if (!is_flac) {
@ -1016,6 +1035,19 @@ bool TagReaderTagLib::SaveFile(const spb::tagreader::SaveFileRequest &request) c
}
void TagReaderTagLib::SaveID3v2Tag(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const {
SetTextFrame("TPOS", song.disc() <= 0 ? QString() : QString::number(song.disc()), tag);
SetTextFrame("TCOM", song.composer().empty() ? std::string() : song.composer(), tag);
SetTextFrame("TIT1", song.grouping().empty() ? std::string() : song.grouping(), tag);
SetTextFrame("TOPE", song.performer().empty() ? std::string() : song.performer(), tag);
// Skip TPE1 (which is the artist) here because we already set it
SetTextFrame("TPE2", song.albumartist().empty() ? std::string() : song.albumartist(), tag);
SetTextFrame("TCMP", song.compilation() ? QString::number(1) : QString(), tag);
SetUnsyncLyricsFrame(song.lyrics().empty() ? std::string() : song.lyrics(), tag);
}
void TagReaderTagLib::SaveAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const {
tag->setItem("album artist", TagLib::APE::Item("album artist", TagLib::StringList(song.albumartist().c_str())));
@ -1278,9 +1310,7 @@ void TagReaderTagLib::SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, con
}
void TagReaderTagLib::SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const {
(void)file_mp3;
void TagReaderTagLib::SetEmbeddedArt(TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const {
// Remove existing covers
TagLib::ID3v2::FrameList apiclist = tag->frameListMap()["APIC"];
@ -1360,7 +1390,7 @@ bool TagReaderTagLib::SaveEmbeddedArt(const spb::tagreader::SaveEmbeddedArtReque
else if (TagLib::MPEG::File *file_mp3 = dynamic_cast<TagLib::MPEG::File*>(fileref.file())) {
TagLib::ID3v2::Tag *tag = file_mp3->ID3v2Tag();
if (!tag) return false;
SetEmbeddedArt(file_mp3, tag, cover.data, cover.mime_type);
SetEmbeddedArt(tag, cover.data, cover.mime_type);
}
// MP4/AAC

View File

@ -68,10 +68,12 @@ class TagReaderTagLib : public TagReaderBase {
private:
spb::tagreader::SongMetadata_FileType GuessFileType(TagLib::FileRef *fileref) const;
void ParseID3v2Tag(TagLib::ID3v2::Tag *tag, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
void ParseOggTag(const TagLib::Ogg::FieldListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
void ParseAPETag(const TagLib::APE::ItemListMap &map, QString *disc, QString *compilation, spb::tagreader::SongMetadata *song) const;
void SetVorbisComments(TagLib::Ogg::XiphComment *vorbis_comment, const spb::tagreader::SongMetadata &song) const;
void SaveID3v2Tag(TagLib::ID3v2::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SaveAPETag(TagLib::APE::Tag *tag, const spb::tagreader::SongMetadata &song) const;
void SetTextFrame(const char *id, const QString &value, TagLib::ID3v2::Tag *tag) const;
@ -98,7 +100,7 @@ class TagReaderTagLib : public TagReaderBase {
void SetEmbeddedArt(TagLib::FLAC::File *flac_file, TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const;
void SetEmbeddedArt(TagLib::Ogg::XiphComment *xiph_comment, const QByteArray &data, const QString &mime_type) const;
void SetEmbeddedArt(TagLib::MPEG::File *file_mp3, TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const;
void SetEmbeddedArt(TagLib::ID3v2::Tag *tag, const QByteArray &data, const QString &mime_type) const;
void SetEmbeddedArt(TagLib::MP4::File *aac_file, TagLib::MP4::Tag *tag, const QByteArray &data, const QString &mime_type) const;
private:

View File

@ -90,12 +90,14 @@ set(SOURCES
collection/collectiondirectorymodel.cpp
collection/collectionfilteroptions.cpp
collection/collectionfilterwidget.cpp
collection/collectionfilter.cpp
collection/collectionplaylistitem.cpp
collection/collectionquery.cpp
collection/collectionqueryoptions.cpp
collection/savedgroupingmanager.cpp
collection/groupbydialog.cpp
collection/collectiontask.cpp
collection/collectionmodelupdate.cpp
playlist/playlist.cpp
playlist/playlistbackend.cpp
@ -198,6 +200,7 @@ set(SOURCES
settings/settingspage.cpp
settings/behavioursettingspage.cpp
settings/collectionsettingspage.cpp
settings/collectionsettingsdirectorymodel.cpp
settings/backendsettingspage.cpp
settings/playlistsettingspage.cpp
settings/scrobblersettingspage.cpp
@ -345,6 +348,7 @@ set(HEADERS
collection/collectionviewcontainer.h
collection/collectiondirectorymodel.h
collection/collectionfilterwidget.h
collection/collectionfilter.h
collection/savedgroupingmanager.h
collection/groupbydialog.h
@ -441,6 +445,7 @@ set(HEADERS
settings/settingspage.h
settings/behavioursettingspage.h
settings/collectionsettingspage.h
settings/collectionsettingsdirectorymodel.h
settings/backendsettingspage.h
settings/playlistsettingspage.h
settings/scrobblersettingspage.h

View File

@ -49,7 +49,6 @@
using std::make_shared;
const char *SCollection::kSongsTable = "songs";
const char *SCollection::kFtsTable = "songs_fts";
const char *SCollection::kDirsTable = "directories";
const char *SCollection::kSubdirsTable = "subdirectories";
@ -70,7 +69,7 @@ SCollection::SCollection(Application *app, QObject *parent)
backend()->moveToThread(app->database()->thread());
qLog(Debug) << &*backend_ << "moved to thread" << app->database()->thread();
backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, QLatin1String(kSongsTable), QLatin1String(kFtsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable));
backend_->Init(app->database(), app->task_manager(), Song::Source::Collection, QLatin1String(kSongsTable), QLatin1String(kDirsTable), QLatin1String(kSubdirsTable));
model_ = new CollectionModel(backend_, app_, this);
@ -108,7 +107,7 @@ void SCollection::Init() {
watcher_->set_task_manager(app_->task_manager());
QObject::connect(&*backend_, &CollectionBackend::Error, this, &SCollection::Error);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDiscovered, watcher_, &CollectionWatcher::AddDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryAdded, watcher_, &CollectionWatcher::AddDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, watcher_, &CollectionWatcher::RemoveDirectory);
QObject::connect(&*backend_, &CollectionBackend::SongsRatingChanged, this, &SCollection::SongsRatingChanged);
QObject::connect(&*backend_, &CollectionBackend::SongsStatisticsChanged, this, &SCollection::SongsPlaycountChanged);

View File

@ -37,6 +37,7 @@
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QDir>
#include <QFileInfo>
#include <QDateTime>
#include <QRegularExpression>
@ -75,16 +76,13 @@ CollectionBackend::~CollectionBackend() {
}
void CollectionBackend::Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &fts_table, const QString &dirs_table, const QString &subdirs_table) {
void CollectionBackend::Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table, const QString &subdirs_table) {
db_ = db;
task_manager_ = task_manager;
source_ = source;
songs_table_ = songs_table;
dirs_table_ = dirs_table;
subdirs_table_ = subdirs_table;
fts_table_ = fts_table;
}
void CollectionBackend::Close() {
@ -122,6 +120,35 @@ void CollectionBackend::ReportErrors(const CollectionQuery &query) {
}
void CollectionBackend::GetAllSongsAsync(const int id) {
metaObject()->invokeMethod(this, "GetAllSongs", Qt::QueuedConnection, Q_ARG(int, id));
}
void CollectionBackend::GetAllSongs(const int id) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SqlQuery q(db);
q.setForwardOnly(true);
q.prepare(QStringLiteral("SELECT %1 FROM %2").arg(Song::kRowIdColumnSpec, songs_table_));
if (!q.exec()) {
db_->ReportErrors(q);
emit GotSongs(SongList(), id);
return;
}
SongList songs;
while (q.next()) {
Song song(source_);
song.InitFromQuery(q, true);
songs << song;
}
emit GotSongs(songs, id);
}
void CollectionBackend::LoadDirectoriesAsync() {
QMetaObject::invokeMethod(this, &CollectionBackend::LoadDirectories, Qt::QueuedConnection);
}
@ -162,7 +189,7 @@ void CollectionBackend::LoadDirectories() {
QSqlDatabase db(db_->Connect());
for (const CollectionDirectory &dir : dirs) {
emit DirectoryDiscovered(dir, SubdirsInDirectory(dir.id, db));
emit DirectoryAdded(dir, SubdirsInDirectory(dir.id, db));
}
}
@ -334,38 +361,56 @@ void CollectionBackend::UpdateTotalAlbumCount() {
}
void CollectionBackend::AddDirectory(const QString &path) {
void CollectionBackend::AddDirectoryAsync(const QString &path) {
QMetaObject::invokeMethod(this, "AddDirectory", Qt::QueuedConnection, Q_ARG(QString, path));
}
QString canonical_path = QFileInfo(path).canonicalFilePath();
QString db_path = canonical_path;
void CollectionBackend::AddDirectory(const QString &path) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
{
SqlQuery q(db);
q.prepare(QStringLiteral("SELECT ROWID FROM %1 WHERE path = :path").arg(dirs_table_));
q.BindValue(QStringLiteral(":path"), path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
if (q.next()) {
return;
}
}
SqlQuery q(db);
q.prepare(QStringLiteral("INSERT INTO %1 (path, subdirs) VALUES (:path, 1)").arg(dirs_table_));
q.BindValue(QStringLiteral(":path"), db_path);
q.BindValue(QStringLiteral(":path"), path);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
CollectionDirectory dir;
dir.path = canonical_path;
dir.path = path;
dir.id = q.lastInsertId().toInt();
emit DirectoryDiscovered(dir, CollectionSubdirectoryList());
emit DirectoryAdded(dir, CollectionSubdirectoryList());
}
void CollectionBackend::RemoveDirectoryAsync(const CollectionDirectory &dir) {
QMetaObject::invokeMethod(this, "RemoveDirectory", Qt::QueuedConnection, Q_ARG(CollectionDirectory, dir));
}
void CollectionBackend::RemoveDirectory(const CollectionDirectory &dir) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
// Remove songs first
DeleteSongs(FindSongsInDirectory(dir.id));
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction transaction(&db);
// Delete the subdirs that were in this directory
@ -390,10 +435,10 @@ void CollectionBackend::RemoveDirectory(const CollectionDirectory &dir) {
}
}
emit DirectoryDeleted(dir);
transaction.Commit();
emit DirectoryDeleted(dir);
}
SongList CollectionBackend::FindSongsInDirectory(const int id) {
@ -470,7 +515,7 @@ void CollectionBackend::SongPathChanged(const Song &song, const QFileInfo &new_f
// Take a song and update its path
Song updated_song = song;
updated_song.set_source(source_);
updated_song.set_url(QUrl::fromLocalFile(new_file.absoluteFilePath()));
updated_song.set_url(QUrl::fromLocalFile(QDir::cleanPath(new_file.filePath())));
updated_song.set_basefilename(new_file.fileName());
updated_song.InitArtManual();
if (updated_song.is_collection_song() && new_collection_directory_id) {
@ -614,17 +659,6 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
}
}
{
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec));
song.BindToFtsQuery(&q);
q.BindValue(QStringLiteral(":id"), song.id());
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
deleted_songs << old_song;
added_songs << song;
@ -653,17 +687,6 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
}
}
{
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec));
new_song.BindToFtsQuery(&q);
q.BindValue(QStringLiteral(":id"), new_song.id());
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
deleted_songs << old_song;
added_songs << new_song;
@ -688,17 +711,6 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
if (id == -1) return;
{ // Add to the FTS index
SqlQuery q(db);
q.prepare(QStringLiteral("INSERT INTO %1 (ROWID, %2) VALUES (:id, %3)").arg(fts_table_, Song::kFtsColumnSpec, Song::kFtsBindSpec));
q.BindValue(QStringLiteral(":id"), id);
song.BindToFtsQuery(&q);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
Song song_copy(song);
song_copy.set_id(id);
added_songs << song_copy;
@ -708,7 +720,7 @@ void CollectionBackend::AddOrUpdateSongs(const SongList &songs) {
transaction.Commit();
if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs);
if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs);
if (!added_songs.isEmpty()) emit SongsAdded(added_songs);
UpdateTotalSongCountAsync();
UpdateTotalArtistCountAsync();
@ -733,7 +745,7 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
SongMap old_songs;
{
CollectionQuery query(db, songs_table_, fts_table_);
CollectionQuery query(db, songs_table_);
if (!ExecCollectionQuery(&query, old_songs)) {
ReportErrors(query);
return;
@ -759,16 +771,6 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
return;
}
}
{
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET %2 WHERE ROWID = :id").arg(fts_table_, Song::kFtsUpdateSpec));
new_song.BindToFtsQuery(&q);
q.BindValue(QStringLiteral(":id"), old_song.id());
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
deleted_songs << old_song;
Song new_song_copy(new_song);
@ -794,17 +796,6 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
if (id == -1) return;
{ // Add to the FTS index
SqlQuery q(db);
q.prepare(QStringLiteral("INSERT INTO %1 (ROWID, %2) VALUES (:id, %3)").arg(fts_table_, Song::kFtsColumnSpec, Song::kFtsBindSpec));
q.BindValue(QStringLiteral(":id"), id);
new_song.BindToFtsQuery(&q);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
Song new_song_copy(new_song);
new_song_copy.set_id(id);
added_songs << new_song_copy;
@ -824,15 +815,6 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
return;
}
}
{
SqlQuery q(db);
q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_));
q.BindValue(QStringLiteral(":id"), old_song.id());
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
deleted_songs << old_song;
}
}
@ -840,7 +822,7 @@ void CollectionBackend::UpdateSongsBySongID(const SongMap &new_songs) {
transaction.Commit();
if (!deleted_songs.isEmpty()) emit SongsDeleted(deleted_songs);
if (!added_songs.isEmpty()) emit SongsDiscovered(added_songs);
if (!added_songs.isEmpty()) emit SongsAdded(added_songs);
UpdateTotalSongCountAsync();
UpdateTotalArtistCountAsync();
@ -853,11 +835,10 @@ void CollectionBackend::UpdateMTimesOnly(const SongList &songs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id").arg(songs_table_));
ScopedTransaction transaction(&db);
for (const Song &song : songs) {
SqlQuery q(db);
q.prepare(QStringLiteral("UPDATE %1 SET mtime = :mtime WHERE ROWID = :id").arg(songs_table_));
q.BindValue(QStringLiteral(":mtime"), song.mtime());
q.BindValue(QStringLiteral(":id"), song.id());
if (!q.Exec()) {
@ -874,25 +855,17 @@ void CollectionBackend::DeleteSongs(const SongList &songs) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
SqlQuery remove(db);
remove.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
SqlQuery remove_fts(db);
remove_fts.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(fts_table_));
ScopedTransaction transaction(&db);
for (const Song &song : songs) {
remove.BindValue(QStringLiteral(":id"), song.id());
if (!remove.Exec()) {
db_->ReportErrors(remove);
return;
}
remove_fts.BindValue(QStringLiteral(":id"), song.id());
if (!remove_fts.Exec()) {
db_->ReportErrors(remove_fts);
SqlQuery q(db);
q.prepare(QStringLiteral("DELETE FROM %1 WHERE ROWID = :id").arg(songs_table_));
q.BindValue(QStringLiteral(":id"), song.id());
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
transaction.Commit();
emit SongsDeleted(songs);
@ -925,7 +898,7 @@ void CollectionBackend::MarkSongsUnavailable(const SongList &songs, const bool u
emit SongsDeleted(songs);
}
else {
emit SongsDiscovered(songs);
emit SongsAdded(songs);
}
UpdateTotalSongCountAsync();
@ -939,7 +912,7 @@ QStringList CollectionBackend::GetAll(const QString &column, const CollectionFil
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_, filter_options);
CollectionQuery query(db, songs_table_, filter_options);
query.SetColumnSpec(QStringLiteral("DISTINCT ") + column);
query.AddCompilationRequirement(false);
@ -967,13 +940,13 @@ QStringList CollectionBackend::GetAllArtistsWithAlbums(const CollectionFilterOpt
QSqlDatabase db(db_->Connect());
// Albums with 'albumartist' field set:
CollectionQuery query(db, songs_table_, fts_table_, opt);
CollectionQuery query(db, songs_table_, opt);
query.SetColumnSpec(QStringLiteral("DISTINCT albumartist"));
query.AddCompilationRequirement(false);
query.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!="));
// Albums with no 'albumartist' (extract 'artist'):
CollectionQuery query2(db, songs_table_, fts_table_, opt);
CollectionQuery query2(db, songs_table_, opt);
query2.SetColumnSpec(QStringLiteral("DISTINCT artist"));
query2.AddCompilationRequirement(false);
query2.AddWhere(QStringLiteral("album"), QLatin1String(""), QStringLiteral("!="));
@ -1014,7 +987,7 @@ SongList CollectionBackend::GetArtistSongs(const QString &effective_albumartist,
QSqlDatabase db(db_->Connect());
QMutexLocker l(db_->Mutex());
CollectionQuery query(db, songs_table_, fts_table_, opt);
CollectionQuery query(db, songs_table_, opt);
query.AddCompilationRequirement(false);
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
@ -1032,7 +1005,7 @@ SongList CollectionBackend::GetAlbumSongs(const QString &effective_albumartist,
QSqlDatabase db(db_->Connect());
QMutexLocker l(db_->Mutex());
CollectionQuery query(db, songs_table_, fts_table_, opt);
CollectionQuery query(db, songs_table_, opt);
query.AddCompilationRequirement(false);
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
query.AddWhere(QStringLiteral("album"), album);
@ -1051,7 +1024,7 @@ SongList CollectionBackend::GetSongsByAlbum(const QString &album, const Collecti
QSqlDatabase db(db_->Connect());
QMutexLocker l(db_->Mutex());
CollectionQuery query(db, songs_table_, fts_table_, opt);
CollectionQuery query(db, songs_table_, opt);
query.AddCompilationRequirement(false);
query.AddWhere(QStringLiteral("album"), album);
@ -1358,7 +1331,7 @@ SongList CollectionBackend::GetCompilationSongs(const QString &album, const Coll
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_, opt);
CollectionQuery query(db, songs_table_, opt);
query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
query.AddCompilationRequirement(true);
query.AddWhere(QStringLiteral("album"), album);
@ -1444,7 +1417,7 @@ void CollectionBackend::CompilationsNeedUpdating() {
if (!deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
emit SongsAdded(added_songs);
}
}
@ -1495,7 +1468,7 @@ CollectionBackend::AlbumList CollectionBackend::GetAlbums(const QString &artist,
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_, opt);
CollectionQuery query(db, songs_table_, opt);
query.SetColumnSpec(QStringLiteral("url, filetype, cue_path, effective_albumartist, album, compilation_effective, art_embedded, art_automatic, art_manual, art_unset"));
query.SetOrderBy(QStringLiteral("effective_albumartist, album, url"));
@ -1586,7 +1559,7 @@ CollectionBackend::Album CollectionBackend::GetAlbumArt(const QString &effective
ret.album = album;
ret.album_artist = effective_albumartist;
CollectionQuery query(db, songs_table_, fts_table_);
CollectionQuery query(db, songs_table_);
query.SetColumnSpec(QStringLiteral("url, art_embedded, art_automatic, art_manual, art_unset"));
if (!effective_albumartist.isEmpty()) {
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
@ -1622,7 +1595,7 @@ void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumart
QSqlDatabase db(db_->Connect());
// Get the songs before they're updated
CollectionQuery query(db, songs_table_, fts_table_);
CollectionQuery query(db, songs_table_);
query.SetColumnSpec(Song::kRowIdColumnSpec);
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
query.AddWhere(QStringLiteral("album"), album);
@ -1668,7 +1641,7 @@ void CollectionBackend::UpdateEmbeddedAlbumArt(const QString &effective_albumart
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
emit SongsAdded(added_songs);
}
}
@ -1684,7 +1657,7 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_);
CollectionQuery query(db, songs_table_);
query.SetColumnSpec(Song::kRowIdColumnSpec);
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
query.AddWhere(QStringLiteral("album"), album);
@ -1726,7 +1699,7 @@ void CollectionBackend::UpdateManualAlbumArt(const QString &effective_albumartis
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
emit SongsAdded(added_songs);
}
}
@ -1742,7 +1715,7 @@ void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, cons
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_);
CollectionQuery query(db, songs_table_);
query.SetColumnSpec(Song::kRowIdColumnSpec);
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
query.AddWhere(QStringLiteral("album"), album);
@ -1783,7 +1756,7 @@ void CollectionBackend::UnsetAlbumArt(const QString &effective_albumartist, cons
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
emit SongsAdded(added_songs);
}
}
@ -1799,7 +1772,7 @@ void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, cons
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
CollectionQuery query(db, songs_table_, fts_table_);
CollectionQuery query(db, songs_table_);
query.SetColumnSpec(Song::kRowIdColumnSpec);
query.AddWhere(QStringLiteral("effective_albumartist"), effective_albumartist);
query.AddWhere(QStringLiteral("album"), album);
@ -1841,7 +1814,7 @@ void CollectionBackend::ClearAlbumArt(const QString &effective_albumartist, cons
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
emit SongsAdded(added_songs);
}
}
@ -1854,7 +1827,7 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList
for (const QString &artist : artists) {
// Get the songs before they're updated
CollectionQuery query(db, songs_table_, fts_table_);
CollectionQuery query(db, songs_table_);
query.SetColumnSpec(Song::kRowIdColumnSpec);
query.AddWhere(QStringLiteral("album"), album);
if (!artist.isEmpty()) query.AddWhere(QStringLiteral("artist"), artist);
@ -1901,7 +1874,7 @@ void CollectionBackend::ForceCompilation(const QString &album, const QStringList
if (!added_songs.isEmpty() || !deleted_songs.isEmpty()) {
emit SongsDeleted(deleted_songs);
emit SongsDiscovered(added_songs);
emit SongsAdded(added_songs);
}
}
@ -2016,15 +1989,6 @@ void CollectionBackend::DeleteAll() {
}
}
{
SqlQuery q(db);
q.prepare(QStringLiteral("DELETE FROM ") + fts_table_);
if (!q.Exec()) {
db_->ReportErrors(q);
return;
}
}
t.Commit();
}

View File

@ -80,12 +80,13 @@ class CollectionBackendInterface : public QObject {
using AlbumList = QList<Album>;
virtual QString songs_table() const = 0;
virtual QString fts_table() const = 0;
virtual Song::Source source() const = 0;
virtual SharedPtr<Database> db() const = 0;
virtual void GetAllSongsAsync(const int id = 0) = 0;
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
virtual void LoadDirectoriesAsync() = 0;
@ -132,8 +133,8 @@ class CollectionBackendInterface : public QObject {
virtual Song GetSongByUrl(const QUrl &url, const qint64 beginning = 0) = 0;
virtual Song GetSongByUrlAndTrack(const QUrl &url, const int track) = 0;
virtual void AddDirectory(const QString &path) = 0;
virtual void RemoveDirectory(const CollectionDirectory &dir) = 0;
virtual void AddDirectoryAsync(const QString &path) = 0;
virtual void RemoveDirectoryAsync(const CollectionDirectory &dir) = 0;
};
class CollectionBackend : public CollectionBackendInterface {
@ -145,7 +146,8 @@ class CollectionBackend : public CollectionBackendInterface {
~CollectionBackend();
void Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &fts_table, const QString &dirs_table = QString(), const QString &subdirs_table = QString());
void Init(SharedPtr<Database> db, SharedPtr<TaskManager> task_manager, const Song::Source source, const QString &songs_table, const QString &dirs_table = QString(), const QString &subdirs_table = QString());
void Close();
void ExitAsync();
@ -157,10 +159,11 @@ class CollectionBackend : public CollectionBackendInterface {
SharedPtr<Database> db() const override { return db_; }
QString songs_table() const override { return songs_table_; }
QString fts_table() const override { return fts_table_; }
QString dirs_table() const { return dirs_table_; }
QString subdirs_table() const { return subdirs_table_; }
void GetAllSongsAsync(const int id = 0) override;
// Get a list of directories in the collection. Emits DirectoriesDiscovered.
void LoadDirectoriesAsync() override;
@ -206,8 +209,8 @@ class CollectionBackend : public CollectionBackendInterface {
Song GetSongByUrl(const QUrl &url, qint64 beginning = 0) override;
Song GetSongByUrlAndTrack(const QUrl &url, const int track) override;
void AddDirectory(const QString &path) override;
void RemoveDirectory(const CollectionDirectory &dir) override;
void AddDirectoryAsync(const QString &path) override;
void RemoveDirectoryAsync(const CollectionDirectory &dir) override;
bool ExecCollectionQuery(CollectionQuery *query, SongList &songs);
bool ExecCollectionQuery(CollectionQuery *query, SongMap &songs);
@ -235,10 +238,13 @@ class CollectionBackend : public CollectionBackendInterface {
public slots:
void Exit();
void GetAllSongs(const int id);
void LoadDirectories();
void UpdateTotalSongCount();
void UpdateTotalArtistCount();
void UpdateTotalAlbumCount();
void AddDirectory(const QString &path);
void RemoveDirectory(const CollectionDirectory &dir);
void AddOrUpdateSongs(const SongList &songs);
void UpdateSongsBySongID(const SongMap &new_songs);
void UpdateMTimesOnly(const SongList &songs);
@ -270,11 +276,13 @@ class CollectionBackend : public CollectionBackendInterface {
void ExpireSongs(const int directory_id, const int expire_unavailable_songs_days);
signals:
void DirectoryDiscovered(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdir);
void DirectoryAdded(const CollectionDirectory &dir, const CollectionSubdirectoryList &subdir);
void DirectoryDeleted(const CollectionDirectory &dir);
void SongsDiscovered(const SongList &songs);
void GotSongs(const SongList &songs, const int id);
void SongsAdded(const SongList &songs);
void SongsDeleted(const SongList &songs);
void SongsChanged(const SongList &songs);
void SongsStatisticsChanged(const SongList &songs, const bool save_tags = false);
void DatabaseReset();
@ -317,7 +325,6 @@ class CollectionBackend : public CollectionBackendInterface {
QString songs_table_;
QString dirs_table_;
QString subdirs_table_;
QString fts_table_;
QThread *original_thread_;
};

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -44,12 +45,15 @@ CollectionDirectoryModel::CollectionDirectoryModel(SharedPtr<CollectionBackend>
dir_icon_(IconLoader::Load(QStringLiteral("document-open-folder"))),
backend_(backend) {
QObject::connect(&*backend_, &CollectionBackend::DirectoryDiscovered, this, &CollectionDirectoryModel::DirectoryDiscovered);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, this, &CollectionDirectoryModel::DirectoryDeleted);
QObject::connect(&*backend_, &CollectionBackend::DirectoryAdded, this, &CollectionDirectoryModel::AddDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, this, &CollectionDirectoryModel::RemoveDirectory);
}
void CollectionDirectoryModel::DirectoryDiscovered(const CollectionDirectory &dir) {
void CollectionDirectoryModel::AddDirectory(const CollectionDirectory &dir) {
directories_.insert(dir.id, dir);
paths_.append(dir.path);
QStandardItem *item = new QStandardItem(dir.path);
item->setData(dir.id, kIdRole);
@ -59,7 +63,10 @@ void CollectionDirectoryModel::DirectoryDiscovered(const CollectionDirectory &di
}
void CollectionDirectoryModel::DirectoryDeleted(const CollectionDirectory &dir) {
void CollectionDirectoryModel::RemoveDirectory(const CollectionDirectory &dir) {
directories_.remove(dir.id);
paths_.removeAll(dir.path);
for (int i = 0; i < rowCount(); ++i) {
if (item(i, 0)->data(kIdRole).toInt() == dir.id) {
@ -71,26 +78,6 @@ void CollectionDirectoryModel::DirectoryDeleted(const CollectionDirectory &dir)
}
void CollectionDirectoryModel::AddDirectory(const QString &path) {
if (!backend_) return;
backend_->AddDirectory(path);
}
void CollectionDirectoryModel::RemoveDirectory(const QModelIndex &idx) {
if (!backend_ || !idx.isValid()) return;
CollectionDirectory dir;
dir.path = idx.data().toString();
dir.id = idx.data(kIdRole).toInt();
backend_->RemoveDirectory(dir);
}
QVariant CollectionDirectoryModel::data(const QModelIndex &idx, int role) const {
switch (role) {

View File

@ -2,6 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -26,15 +27,17 @@
#include <QObject>
#include <QStandardItemModel>
#include <QList>
#include <QMap>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QIcon>
#include "core/shared_ptr.h"
#include "collectiondirectory.h"
class QModelIndex;
struct CollectionDirectory;
class CollectionBackend;
class MusicStorage;
@ -44,22 +47,24 @@ class CollectionDirectoryModel : public QStandardItemModel {
public:
explicit CollectionDirectoryModel(SharedPtr<CollectionBackend> collection_backend, QObject *parent = nullptr);
// To be called by GUIs
void AddDirectory(const QString &path);
void RemoveDirectory(const QModelIndex &idx);
QVariant data(const QModelIndex &idx, int role) const override;
SharedPtr<CollectionBackend> backend() const { return backend_; }
QMap<int, CollectionDirectory> directories() const { return directories_; }
QStringList paths() const { return paths_; }
private slots:
// To be called by the backend
void DirectoryDiscovered(const CollectionDirectory &directories);
void DirectoryDeleted(const CollectionDirectory &directories);
void AddDirectory(const CollectionDirectory &directory);
void RemoveDirectory(const CollectionDirectory &directory);
private:
static const int kIdRole = Qt::UserRole + 1;
QIcon dir_icon_;
SharedPtr<CollectionBackend> backend_;
QMap<int, CollectionDirectory> directories_;
QStringList paths_;
QList<SharedPtr<MusicStorage>> storage_;
};

View File

@ -0,0 +1,196 @@
/*
* Strawberry Music Player
* Copyright 2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QSortFilterProxyModel>
#include <QVariant>
#include <QString>
#include "collectionfilter.h"
#include "collectionmodel.h"
#include "collectionitem.h"
CollectionFilter::CollectionFilter(QObject *parent) : QSortFilterProxyModel(parent) {
setDynamicSortFilter(true);
setFilterCaseSensitivity(Qt::CaseInsensitive);
}
bool CollectionFilter::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const {
CollectionModel *model = qobject_cast<CollectionModel*>(sourceModel());
if (!model) return false;
QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
if (!idx.isValid()) return false;
CollectionItem *item = model->IndexToItem(idx);
if (!item) return false;
if (item->type == CollectionItem::Type_LoadingIndicator) return true;
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QString filter = filterRegularExpression().pattern().remove(QLatin1Char('\\'));
#else
QString filter = filterRegExp().pattern();
#endif
if (filter.isEmpty()) return true;
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList tokens(filter.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts));
#else
QStringList tokens(filter.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts));
#endif
filter.clear();
QMap<QString, QString> tags;
for (QString token : tokens) {
if (token.contains(QLatin1Char(':'))) {
if (Song::kColumns.contains(token.section(QLatin1Char(':'), 0, 0), Qt::CaseInsensitive)) {
QString tag = token.section(QLatin1Char(':'), 0, 0).remove(QLatin1Char(':')).trimmed();
QString value = token.section(QLatin1Char(':'), 1, -1).remove(QLatin1Char(':')).trimmed();
if (!tag.isEmpty() && !value.isEmpty()) {
tags.insert(tag, value);
}
}
else {
token = token.remove(QLatin1Char(':')).trimmed();
if (!token.isEmpty()) {
if (!filter.isEmpty()) filter.append(QLatin1Char(' '));
filter += token;
}
}
}
else {
if (!filter.isEmpty()) filter.append(QLatin1Char(' '));
filter += token;
}
}
if (ItemMatches(model, item, tags, filter)) return true;
for (CollectionItem *parent = item->parent ; parent ; parent = parent->parent) {
if (ItemMatches(model, parent, tags, filter)) return true;
}
return ChildrenMatches(model, item, tags, filter);
}
bool CollectionFilter::ItemMatches(CollectionModel *model, CollectionItem *item, const QMap<QString, QString> &tags, const QString &filter) const {
if (
(filter.isEmpty() || item->DisplayText().contains(filter, Qt::CaseInsensitive))
&&
(
tags.isEmpty() // If no tags were specified, only the filter needs to match.
||
(item->metadata.is_valid() && TagMatches(item, tags)) // Song node
||
(item->container_level >= 0 && item->container_level <= 2 && TagMatches(item, model->GetGroupBy()[item->container_level], tags)) // Container node
)
) { return true; }
return false;
}
bool CollectionFilter::ChildrenMatches(CollectionModel *model, CollectionItem *item, const QMap<QString, QString> &tags, const QString &filter) const {
if (ItemMatches(model, item, tags, filter)) return true;
for (CollectionItem *child : item->children) {
if (ChildrenMatches(model, child, tags, filter)) return true;
}
return false;
}
bool CollectionFilter::TagMatches(CollectionItem *item, const QMap<QString, QString> &tags) const {
Song &metadata = item->metadata;
for (QMap<QString, QString>::const_iterator it = tags.begin() ; it != tags.end() ; ++it) {
QString tag = it.key().toLower();
QString value = it.value();
if (tag == QStringLiteral("albumartist") && metadata.effective_albumartist().contains(value, Qt::CaseInsensitive)) return true;
if (tag == QStringLiteral("artist") && metadata.artist().contains(value, Qt::CaseInsensitive)) return true;
if (tag == QStringLiteral("album") && metadata.album().contains(value, Qt::CaseInsensitive)) return true;
if (tag == QStringLiteral("title") && metadata.title().contains(value, Qt::CaseInsensitive)) return true;
}
return false;
}
bool CollectionFilter::TagMatches(CollectionItem *item, const CollectionModel::GroupBy group_by, const QMap<QString, QString> &tags) const {
QString tag;
switch (group_by) {
case CollectionModel::GroupBy::AlbumArtist:
tag = QStringLiteral("albumartist");
break;
case CollectionModel::GroupBy::Artist:
tag = QStringLiteral("artist");
break;
case CollectionModel::GroupBy::Album:
case CollectionModel::GroupBy::AlbumDisc:
case CollectionModel::GroupBy::YearAlbum:
case CollectionModel::GroupBy::YearAlbumDisc:
case CollectionModel::GroupBy::OriginalYearAlbum:
case CollectionModel::GroupBy::OriginalYearAlbumDisc:
tag = QStringLiteral("album");
break;
case CollectionModel::GroupBy::Disc:
case CollectionModel::GroupBy::Year:
case CollectionModel::GroupBy::OriginalYear:
break;
case CollectionModel::GroupBy::Genre:
tag = QStringLiteral("genre");
break;
case CollectionModel::GroupBy::Composer:
tag = QStringLiteral("composer");
break;
case CollectionModel::GroupBy::Performer:
tag = QStringLiteral("performer");
break;
case CollectionModel::GroupBy::Grouping:
tag = QStringLiteral("grouping");
break;
case CollectionModel::GroupBy::FileType:
tag = QStringLiteral("filetype");
break;
case CollectionModel::GroupBy::Format:
case CollectionModel::GroupBy::Bitdepth:
case CollectionModel::GroupBy::Samplerate:
case CollectionModel::GroupBy::Bitrate:
case CollectionModel::GroupBy::None:
case CollectionModel::GroupBy::GroupByCount:
break;
}
QString value;
if (!tag.isEmpty() && tags.contains(tag)) {
value = tags[tag];
}
return !value.isEmpty() && item->DisplayText().contains(value, Qt::CaseInsensitive);
}

View File

@ -0,0 +1,50 @@
/*
* Strawberry Music Player
* Copyright 2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONFILTER_H
#define COLLECTIONFILTER_H
#include "config.h"
#include <QtGlobal>
#include <QObject>
#include <QSortFilterProxyModel>
#include <QString>
#include "collectionmodel.h"
class CollectionItem;
class CollectionFilter : public QSortFilterProxyModel {
Q_OBJECT
public:
explicit CollectionFilter(QObject *parent = nullptr);
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
private:
bool TagMatches(CollectionItem *item, const QMap<QString, QString> &tags) const;
bool TagMatches(CollectionItem *item, const CollectionModel::GroupBy group_by, const QMap<QString, QString> &tags) const;
bool ItemMatches(CollectionModel *model, CollectionItem *item, const QMap<QString, QString> &tags, const QString &filter) const;
bool ChildrenMatches(CollectionModel *model, CollectionItem *item, const QMap<QString, QString> &tags, const QString &filter) const;
};
#endif // COLLECTIONFILTER_H

View File

@ -50,6 +50,8 @@
#include "core/settings.h"
#include "collectionfilteroptions.h"
#include "collectionmodel.h"
#include "collectionfilter.h"
#include "collectionquery.h"
#include "savedgroupingmanager.h"
#include "collectionfilterwidget.h"
#include "groupbydialog.h"
@ -62,6 +64,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
: QWidget(parent),
ui_(new Ui_CollectionFilterWidget),
model_(nullptr),
filter_(nullptr),
group_by_dialog_(new GroupByDialog(this)),
groupings_manager_(nullptr),
filter_age_menu_(nullptr),
@ -74,8 +77,8 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
ui_->setupUi(this);
QString available_fields = Song::kFtsColumns.join(QStringLiteral(", ")).replace(QRegularExpression(QStringLiteral("\\bfts")), QLatin1String(""));
available_fields += QStringLiteral(", ") + Song::kNumericalColumns.join(QStringLiteral(", "));
QString available_fields = Song::kTextSearchColumns.join(QStringLiteral(", "));
available_fields += QStringLiteral(", ") + Song::kNumericalSearchColumns.join(QStringLiteral(", "));
ui_->search_field->setToolTip(
QStringLiteral("<html><head/><body><p>") +
@ -156,7 +159,7 @@ CollectionFilterWidget::CollectionFilterWidget(QWidget *parent)
CollectionFilterWidget::~CollectionFilterWidget() { delete ui_; }
void CollectionFilterWidget::Init(CollectionModel *model) {
void CollectionFilterWidget::Init(CollectionModel *model, CollectionFilter *filter) {
if (model_) {
QObject::disconnect(model_, nullptr, this, nullptr);
@ -169,6 +172,7 @@ void CollectionFilterWidget::Init(CollectionModel *model) {
}
model_ = model;
filter_ = filter;
// Connect signals
QObject::connect(model_, &CollectionModel::GroupingChanged, group_by_dialog_, &GroupByDialog::CollectionGroupingChanged);
@ -217,6 +221,10 @@ void CollectionFilterWidget::SetSettingsPrefix(const QString &prefix) {
}
void CollectionFilterWidget::setFilter(CollectionFilter *filter) {
filter_ = filter;
}
void CollectionFilterWidget::ReloadSettings() {
Settings s;
@ -518,9 +526,6 @@ void CollectionFilterWidget::keyReleaseEvent(QKeyEvent *e) {
void CollectionFilterWidget::FilterTextChanged(const QString &text) {
// Searching with one or two characters can be very expensive on the database even with FTS,
// so if there are a large number of songs in the database introduce a small delay before actually filtering the model,
// so if the user is typing the first few characters of something it will be quicker.
const bool delay = (delay_behaviour_ == DelayBehaviour::AlwaysDelayed) || (delay_behaviour_ == DelayBehaviour::DelayedOnLargeLibraries && !text.isEmpty() && text.length() < 3 && model_->total_song_count() >= 100000);
if (delay) {
@ -535,9 +540,8 @@ void CollectionFilterWidget::FilterTextChanged(const QString &text) {
void CollectionFilterWidget::FilterDelayTimeout() {
emit Filter(ui_->search_field->text());
if (filter_applies_to_model_) {
model_->SetFilterText(ui_->search_field->text());
filter_->setFilterFixedString(ui_->search_field->text());
}
}

View File

@ -41,6 +41,7 @@ class QKeyEvent;
class GroupByDialog;
class SavedGroupingManager;
class CollectionFilter;
class Ui_CollectionFilterWidget;
class CollectionFilterWidget : public QWidget {
@ -58,7 +59,9 @@ class CollectionFilterWidget : public QWidget {
AlwaysDelayed,
};
void Init(CollectionModel *model);
void Init(CollectionModel *model, CollectionFilter *filter);
void setFilter(CollectionFilter *filter);
static QActionGroup *CreateGroupByActions(const QString &saved_groupings_settings_group, QObject *parent);
@ -94,7 +97,6 @@ class CollectionFilterWidget : public QWidget {
void UpPressed();
void DownPressed();
void ReturnPressed();
void Filter(const QString &text);
protected:
void keyReleaseEvent(QKeyEvent *e) override;
@ -115,6 +117,7 @@ class CollectionFilterWidget : public QWidget {
private:
Ui_CollectionFilterWidget *ui_;
CollectionModel *model_;
CollectionFilter *filter_;
GroupByDialog *group_by_dialog_;
SavedGroupingManager *groupings_manager_;

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,5 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
@ -44,6 +42,7 @@
#include <QIcon>
#include <QPixmap>
#include <QNetworkDiskCache>
#include <QQueue>
#include "core/shared_ptr.h"
#include "core/simpletreemodel.h"
@ -51,10 +50,11 @@
#include "core/sqlrow.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "collectionmodelupdate.h"
#include "collectionfilteroptions.h"
#include "collectionqueryoptions.h"
#include "collectionitem.h"
class QTimer;
class Settings;
class Application;
@ -69,7 +69,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
~CollectionModel() override;
static const int kPrettyCoverSize;
static const char *kPixmapDiskCacheDir;
enum Role {
Role_Type = Qt::UserRole + 1,
@ -126,10 +125,8 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
};
struct QueryResult {
QueryResult() : create_va(false) {}
SqlRowList rows;
bool create_va;
QueryResult() {}
SongList songs;
};
SharedPtr<CollectionBackend> backend() const { return backend_; }
@ -153,7 +150,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
Qt::ItemFlags flags(const QModelIndex &idx) const override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
bool canFetchMore(const QModelIndex &parent) const override;
// Whether or not to use album cover art, if it exists, in the collection view
void set_pretty_covers(const bool use_pretty_covers);
@ -165,6 +161,9 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
// Whether to skip articles such as “The” when sorting artist names
void set_sort_skips_articles(const bool sort_skips_articles);
void Init();
void Reset();
// Reload settings.
void ReloadSettings();
@ -188,8 +187,6 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
}
static bool IsAlbumGroupBy(const GroupBy group_by) { return group_by == GroupBy::Album || group_by == GroupBy::YearAlbum || group_by == GroupBy::AlbumDisc || group_by == GroupBy::YearAlbumDisc || group_by == GroupBy::OriginalYearAlbum || group_by == GroupBy::OriginalYearAlbumDisc; }
void set_use_lazy_loading(const bool value) { use_lazy_loading_ = value; }
QMap<QString, CollectionItem*> container_nodes(const int i) { return container_nodes_[i]; }
QList<CollectionItem*> song_nodes() const { return song_nodes_.values(); }
int divider_nodes_count() const { return divider_nodes_.count(); }
@ -208,63 +205,45 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
void GroupingChanged(const CollectionModel::Grouping g, const bool separate_albums_by_grouping);
public slots:
void SetFilterMode(CollectionFilterOptions::FilterMode filter_mode);
void SetFilterMode(const CollectionFilterOptions::FilterMode filter_mode);
void SetFilterAge(const int filter_age);
void SetFilterText(const QString &filter_text);
void Init(const bool async = true);
void Reset();
void ResetAsync();
void SongsDiscovered(const SongList &songs);
protected:
void LazyPopulate(CollectionItem *item) override { LazyPopulate(item, true); }
void LazyPopulate(CollectionItem *parent, const bool signal);
void SongsAdded(const SongList &songs);
void SongsRemoved(const SongList &songs);
void SongsChanged(const SongList &songs);
void SongsUpdated(const SongList &songs);
private slots:
void ScheduleReset();
void Reload();
// From CollectionBackend
void SongsDeleted(const SongList &songs);
void SongsSlightlyChanged(const SongList &songs);
void TotalSongCountUpdatedSlot(const int count);
void TotalArtistCountUpdatedSlot(const int count);
void TotalAlbumCountUpdatedSlot(const int count);
static void ClearDiskCache();
// Called after ResetAsync
void ResetAsyncQueryFinished();
void LoadSongsFromSqlFinished();
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
private:
// Provides some optimizations for loading the list of items in the root.
// This gets called a lot when filtering the playlist, so it's nice to be able to do it in a background thread.
CollectionQueryOptions PrepareQuery(CollectionItem *parent);
QueryResult RunQuery(const CollectionFilterOptions &filter_options = CollectionFilterOptions(), const CollectionQueryOptions &query_options = CollectionQueryOptions());
void PostQuery(CollectionItem *parent, const QueryResult &result, const bool signal);
void ProcessUpdate();
bool HasCompilations(const QSqlDatabase &db, const CollectionFilterOptions &filter_options, const CollectionQueryOptions &query_options);
private:
void StartLoadSongsFromSql();
SongList LoadSongsFromSql(const CollectionFilterOptions &filter_options = CollectionFilterOptions());
void Clear();
void BeginReset();
void EndReset();
// Functions for working with queries and creating items.
// When the model is reset or when a node is lazy-loaded the Collection constructs a database query to populate the items.
// Filters are added for each parent item, restricting the songs returned to a particular album or artist for example.
static void SetQueryColumnSpec(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionQueryOptions *query_options);
static void AddQueryWhere(const GroupBy group_by, const bool separate_albums_by_grouping, CollectionItem *item, CollectionQueryOptions *query_options);
// Items can be created either from a query that's been run to populate a node, or by a spontaneous SongsDiscovered emission from the backend.
CollectionItem *ItemFromQuery(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const SqlRow &row, const int container_level);
CollectionItem *InitItem(const GroupBy group_by, const bool signal, CollectionItem *parent, const int container_level);
CollectionItem *ItemFromSong(const GroupBy group_by, const bool separate_albums_by_grouping, const bool signal, const bool create_divider, CollectionItem *parent, const Song &s, const int container_level);
void FinishItem(const GroupBy group_by, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item);
// The "Various Artists" node is an annoying special case.
CollectionItem *CreateCompilationArtistNode(const bool signal, CollectionItem *parent);
// Helpers for ItemFromQuery and ItemFromSong
CollectionItem *InitItem(const GroupBy group_by, const bool signal, CollectionItem *parent, const int container_level);
void FinishItem(const GroupBy group_by, const bool signal, const bool create_divider, CollectionItem *parent, CollectionItem *item);
static QString DividerKey(const GroupBy group_by, CollectionItem *item);
static QString DividerDisplayText(const GroupBy group_by, const QString &key);
@ -277,6 +256,15 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
bool CompareItems(const CollectionItem *a, const CollectionItem *b) const;
static qint64 MaximumCacheSize(Settings *s, const char *size_id, const char *size_unit_id, const qint64 cache_size_default);
void ScheduleUpdate(const CollectionModelUpdate::Type type, const SongList &songs);
void AddSongs(const SongList &songs);
void RemoveSongs(const SongList &songs);
void UpdateSongs(const SongList &songs);
void ReAddOrUpdate(const SongList &songs);
static bool IsCollectionMetadataEqual(const Song &song1, const Song &song2);
private:
SharedPtr<CollectionBackend> backend_;
Application *app_;
@ -293,6 +281,7 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
bool separate_albums_by_grouping_;
// Keyed on database ID
QMap<int, Song> songs_;
QMap<int, CollectionItem*> song_nodes_;
// Keyed on whatever the key is for that level - artist, album, year, etc.
@ -313,13 +302,17 @@ class CollectionModel : public SimpleTreeModel<CollectionItem> {
bool use_pretty_covers_;
bool show_dividers_;
bool use_disk_cache_;
bool use_lazy_loading_;
AlbumCoverLoaderOptions::Types cover_types_;
using ItemAndCacheKey = QPair<CollectionItem*, QString>;
QMap<quint64, ItemAndCacheKey> pending_art_;
QSet<QString> pending_cache_keys_;
QTimer *timer_reset_;
QTimer *timer_update_;
QQueue<CollectionModelUpdate> updates_;
};
Q_DECLARE_METATYPE(CollectionModel::Grouping)

View File

@ -0,0 +1,23 @@
/*
* Strawberry Music Player
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "collectionmodelupdate.h"
CollectionModelUpdate::CollectionModelUpdate(const Type &_type, const SongList &_songs)
: type(_type), songs(_songs) {}

View File

@ -0,0 +1,38 @@
/*
* Strawberry Music Player
* Copyright 2023, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONMODELUPDATE_H
#define COLLECTIONMODELUPDATE_H
#include "core/song.h"
class CollectionModelUpdate {
public:
enum class Type {
Add,
Remove,
ReAddOrUpdate,
Update,
};
explicit CollectionModelUpdate(const Type &_type, const SongList &_songs);
Type type;
SongList songs;
};
#endif // COLLECTIONMODELUPDATE_H

View File

@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -30,6 +30,7 @@
#include <QStringBuilder>
#include <QRegularExpression>
#include <QSqlDatabase>
#include <QSqlQuery>
#include "core/sqlquery.h"
#include "core/song.h"
@ -38,78 +39,13 @@
#include "collectionfilteroptions.h"
#include "utilities/searchparserutils.h"
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options)
CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options)
: SqlQuery(db),
songs_table_(songs_table),
fts_table_(fts_table),
include_unavailable_(false),
join_with_fts_(false),
duplicates_only_(false),
limit_(-1) {
if (!filter_options.filter_text().isEmpty()) {
// We need to munge the filter text a little bit to get it to work as expected with sqlite's FTS5:
// 1) Append * to all tokens.
// 2) Prefix "fts" to column names.
// 3) Remove colons which don't correspond to column names.
// Split on whitespace
QString filter_text = filter_options.filter_text().replace(QRegularExpression(QStringLiteral(":\\s+")), QStringLiteral(":"));
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QStringList tokens(filter_text.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts));
#else
QStringList tokens(filter_text.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts));
#endif
QString query;
for (QString token : tokens) {
token.remove(QLatin1Char('('))
.remove(QLatin1Char(')'))
.remove(QLatin1Char('"'))
.replace(QLatin1Char('-'), QLatin1Char(' '));
if (token.contains(QLatin1Char(':'))) {
const QString columntoken = token.section(QLatin1Char(':'), 0, 0);
QString subtoken = token.section(QLatin1Char(':'), 1, -1).replace(QLatin1String(":"), QLatin1String(" ")).trimmed();
if (subtoken.isEmpty()) continue;
if (Song::kFtsColumns.contains(QLatin1String("fts") + columntoken, Qt::CaseInsensitive)) {
if (!query.isEmpty()) query.append(QLatin1String(" "));
query += QStringLiteral("fts") + columntoken + QStringLiteral(":\"") + subtoken + QStringLiteral("\"*");
}
else if (Song::kNumericalColumns.contains(columntoken, Qt::CaseInsensitive)) {
QString comparator = RemoveSqlOperator(subtoken);
if (columntoken.compare(QLatin1String("rating"), Qt::CaseInsensitive) == 0) {
AddWhereRating(subtoken, comparator);
}
else if (columntoken.compare(QLatin1String("length"), Qt::CaseInsensitive) == 0) {
// Time is saved in nanoseconds, so add 9 0's
QString parsedTime = QString::number(Utilities::ParseSearchTime(subtoken)) + QStringLiteral("000000000");
AddWhere(columntoken, parsedTime, comparator);
}
else {
AddWhere(columntoken, subtoken, comparator);
}
}
// Not a valid filter, remove
else {
token = token.replace(QLatin1String(":"), QLatin1String(" ")).trimmed();
if (!token.isEmpty()) {
if (!query.isEmpty()) query.append(QLatin1Char(' '));
query += QLatin1Char('\"') + token + QStringLiteral("\"*");
}
}
}
else {
if (!query.isEmpty()) query.append(QLatin1Char(' '));
query += QLatin1Char('\"') + token + QStringLiteral("\"*");
}
}
if (!query.isEmpty()) {
where_clauses_ << QStringLiteral("fts.%fts_table_noprefix MATCH ?");
bound_values_ << query;
join_with_fts_ = true;
}
}
if (filter_options.max_age() != -1) {
qint64 cutoff = QDateTime::currentDateTime().toSecsSinceEpoch() - filter_options.max_age();
@ -117,12 +53,6 @@ CollectionQuery::CollectionQuery(const QSqlDatabase &db, const QString &songs_ta
bound_values_ << cutoff;
}
// TODO: Currently you cannot use any FilterMode other than All and FTS at the same time.
// Joining songs, duplicated_songs and songs_fts all together takes a huge amount of time.
// The query takes about 20 seconds on my machine then. Why?
// Untagged mode could work with additional filtering but I'm disabling it just to be consistent
// this way filtering is available only in the All mode.
// Remember though that when you fix the Duplicates + FTS cooperation, enable the filtering in both Duplicates and Untagged modes.
duplicates_only_ = filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Duplicates;
if (filter_options.filter_mode() == CollectionFilterOptions::FilterMode::Untagged) {
@ -152,7 +82,7 @@ QString CollectionQuery::RemoveSqlOperator(QString &token) {
void CollectionQuery::AddWhere(const QString &column, const QVariant &value, const QString &op) {
// Ignore 'literal' for IN
if (op.compare(QLatin1String("IN"), Qt::CaseInsensitive) == 0) {
if (op.compare(QStringLiteral("IN"), Qt::CaseInsensitive) == 0) {
QStringList values = value.toStringList();
QStringList final_values;
final_values.reserve(values.count());
@ -161,7 +91,7 @@ void CollectionQuery::AddWhere(const QString &column, const QVariant &value, con
bound_values_ << single_value;
}
where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(QStringLiteral(",")));
where_clauses_ << QStringLiteral("%1 IN (%2)").arg(column, final_values.join(QLatin1Char(',')));
}
else {
// Do integers inline - sqlite seems to get confused when you pass integers to bound parameters
@ -231,8 +161,6 @@ void CollectionQuery::AddWhereRating(const QVariant &value, const QString &op) {
void CollectionQuery::AddCompilationRequirement(const bool compilation) {
// The unary + is added to prevent sqlite from using the index idx_comp_artist.
// When joining with fts, sqlite 3.8 has a tendency to use this index and thereby nesting the tables in an order which gives very poor performance
where_clauses_ << QStringLiteral("+compilation_effective = %1").arg(compilation ? 1 : 0);
}
@ -248,14 +176,7 @@ QString CollectionQuery::GetInnerQuery() const {
bool CollectionQuery::Exec() {
QString sql;
if (join_with_fts_) {
sql = QStringLiteral("SELECT %1 FROM %2 INNER JOIN %3 AS fts ON %2.ROWID = fts.ROWID").arg(column_spec_, songs_table_, fts_table_);
}
else {
sql = QStringLiteral("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery());
}
QString sql = QStringLiteral("SELECT %1 FROM %2 %3").arg(column_spec_, songs_table_, GetInnerQuery());
QStringList where_clauses(where_clauses_);
if (!include_unavailable_) {
@ -268,9 +189,7 @@ bool CollectionQuery::Exec() {
if (limit_ != -1) sql += QStringLiteral(" LIMIT ") + QString::number(limit_);
sql.replace(QLatin1String("%songs_table"), songs_table_);
sql.replace(QLatin1String("%fts_table_noprefix"), fts_table_.section(QLatin1Char('.'), -1, -1));
sql.replace(QLatin1String("%fts_table"), fts_table_);
sql.replace(QStringLiteral("%songs_table"), songs_table_);
if (!QSqlQuery::prepare(sql)) return false;

View File

@ -36,7 +36,7 @@
class CollectionQuery : public SqlQuery {
public:
explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const QString &fts_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
explicit CollectionQuery(const QSqlDatabase &db, const QString &songs_table, const CollectionFilterOptions &filter_options = CollectionFilterOptions());
QVariant Value(const int column) const;
QVariant value(const int column) const { return Value(column); }
@ -51,7 +51,6 @@ class CollectionQuery : public SqlQuery {
QStringList where_clauses() const { return where_clauses_; }
QVariantList bound_values() const { return bound_values_; }
bool include_unavailable() const { return include_unavailable_; }
bool join_with_fts() const { return join_with_fts_; }
bool duplicates_only() const { return duplicates_only_; }
int limit() const { return limit_; }
@ -83,7 +82,6 @@ class CollectionQuery : public SqlQuery {
QSqlDatabase db_;
QString songs_table_;
QString fts_table_;
QString column_spec_;
QString order_by_;
@ -91,7 +89,6 @@ class CollectionQuery : public SqlQuery {
QVariantList bound_values_;
bool include_unavailable_;
bool join_with_fts_;
bool duplicates_only_;
int limit_;
};

View File

@ -34,6 +34,7 @@
#include <QIODevice>
#include <QDataStream>
#include <QBuffer>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QByteArray>
@ -364,7 +365,7 @@ bool CommandlineOptions::Parse() {
const QString value = DecodeName(argv_[i]);
QFileInfo fileinfo(value);
if (fileinfo.exists()) {
urls_ << QUrl::fromLocalFile(fileinfo.canonicalFilePath());
urls_ << QUrl::fromLocalFile(QDir::cleanPath(fileinfo.filePath()));
}
else {
urls_ << QUrl::fromUserInput(value);

View File

@ -131,10 +131,11 @@
#include "collection/collection.h"
#include "collection/collectionbackend.h"
#include "collection/collectiondirectorymodel.h"
#include "collection/collectionviewcontainer.h"
#include "collection/collectionfilterwidget.h"
#include "collection/collectionfilter.h"
#include "collection/collectionmodel.h"
#include "collection/collectionview.h"
#include "collection/collectionviewcontainer.h"
#include "playlist/playlist.h"
#include "playlist/playlistbackend.h"
#include "playlist/playlistcontainer.h"
@ -335,7 +336,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
playlist_add_to_another_(nullptr),
playlistitem_actions_separator_(nullptr),
playlist_rescan_songs_(nullptr),
collection_sort_model_(new QSortFilterProxyModel(this)),
collection_filter_(new CollectionFilter(this)),
track_position_timer_(new QTimer(this)),
track_slider_timer_(new QTimer(this)),
keep_running_(false),
@ -418,11 +419,11 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
// Models
qLog(Debug) << "Creating models";
collection_sort_model_->setSourceModel(app_->collection()->model());
collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
collection_sort_model_->setDynamicSortFilter(true);
collection_sort_model_->setSortLocaleAware(true);
collection_sort_model_->sort(0);
collection_filter_->setSourceModel(app_->collection()->model());
collection_filter_->setSortRole(CollectionModel::Role_SortText);
collection_filter_->setDynamicSortFilter(true);
collection_filter_->setSortLocaleAware(true);
collection_filter_->sort(0);
qLog(Debug) << "Creating models finished";
@ -432,7 +433,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
ui_->playlist->view()->Init(app_);
collection_view_->view()->setModel(collection_sort_model_);
collection_view_->view()->setModel(collection_filter_);
collection_view_->view()->SetApplication(app_);
#ifndef Q_OS_WIN
device_view_->view()->SetApplication(app_);
@ -692,7 +693,7 @@ MainWindow::MainWindow(Application *app, SharedPtr<SystemTrayIcon> tray_icon, OS
QAction *collection_config_action = new QAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure collection..."), this);
QObject::connect(collection_config_action, &QAction::triggered, this, &MainWindow::ShowCollectionConfig);
collection_view_->filter_widget()->SetSettingsGroup(QLatin1String(CollectionSettingsPage::kSettingsGroup));
collection_view_->filter_widget()->Init(app_->collection()->model());
collection_view_->filter_widget()->Init(app_->collection()->model(), collection_filter_);
QAction *separator = new QAction(this);
separator->setSeparator(true);
@ -2298,18 +2299,18 @@ void MainWindow::AddFile() {
PlaylistParser parser(app_->collection_backend());
// Show dialog
QStringList file_names = QFileDialog::getOpenFileNames(this, tr("Add file"), directory, QStringLiteral("%1 (%2);;%3;;%4").arg(tr("Music"), QLatin1String(FileView::kFileFilter), parser.filters(PlaylistParser::Type::Load), tr(kAllFilesFilterSpec)));
QStringList filenames = QFileDialog::getOpenFileNames(this, tr("Add file"), directory, QStringLiteral("%1 (%2);;%3;;%4").arg(tr("Music"), QLatin1String(FileView::kFileFilter), parser.filters(PlaylistParser::Type::Load), tr(kAllFilesFilterSpec)));
if (file_names.isEmpty()) return;
if (filenames.isEmpty()) return;
// Save last used directory
settings_.setValue("add_media_path", file_names[0]);
settings_.setValue("add_media_path", filenames[0]);
// Convert to URLs
QList<QUrl> urls;
urls.reserve(file_names.count());
for (const QString &path : file_names) {
urls << QUrl::fromLocalFile(QFileInfo(path).canonicalFilePath());
urls.reserve(filenames.count());
for (const QString &path : filenames) {
urls << QUrl::fromLocalFile(QDir::cleanPath(path));
}
MimeData *mimedata = new MimeData;
@ -2332,7 +2333,7 @@ void MainWindow::AddFolder() {
// Add media
MimeData *mimedata = new MimeData;
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(QFileInfo(directory).canonicalFilePath()));
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(QDir::cleanPath(directory)));
AddToPlaylist(mimedata);
}

View File

@ -68,6 +68,7 @@ class AlbumCoverManager;
class Application;
class ContextView;
class CollectionViewContainer;
class CollectionFilter;
class AlbumCoverChoiceController;
class CommandlineOptions;
#ifndef Q_OS_WIN
@ -376,7 +377,7 @@ class MainWindow : public QMainWindow, public PlatformInterface {
QModelIndex playlist_menu_index_;
QSortFilterProxyModel *collection_sort_model_;
CollectionFilter *collection_filter_;
QTimer *track_position_timer_;
QTimer *track_slider_timer_;

View File

@ -55,7 +55,6 @@ class SimpleTreeItem {
QString display_text;
int row;
bool lazy_loaded;
T *parent;
QList<T*> children;
@ -68,7 +67,6 @@ template<typename T>
SimpleTreeItem<T>::SimpleTreeItem(int _type, SimpleTreeModel<T> *_model)
: type(_type),
row(0),
lazy_loaded(true),
parent(nullptr),
child_model(nullptr),
model(_model) {}
@ -77,7 +75,6 @@ template<typename T>
SimpleTreeItem<T>::SimpleTreeItem(int _type, const QString &_key, T *_parent)
: type(_type),
key(_key),
lazy_loaded(false),
parent(_parent),
child_model(nullptr),
model(_parent ? _parent->model : nullptr) {
@ -90,7 +87,6 @@ SimpleTreeItem<T>::SimpleTreeItem(int _type, const QString &_key, T *_parent)
template<typename T>
SimpleTreeItem<T>::SimpleTreeItem(int _type, T *_parent)
: type(_type),
lazy_loaded(false),
parent(_parent),
child_model(nullptr),
model(_parent ? _parent->model : nullptr) {

View File

@ -39,8 +39,6 @@ class SimpleTreeModel : public QAbstractItemModel {
QModelIndex parent(const QModelIndex &idx) const override;
int rowCount(const QModelIndex &parent) const override;
bool hasChildren(const QModelIndex &parent) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
T *IndexToItem(const QModelIndex &idx) const;
QModelIndex ItemToIndex(T *item) const;
@ -52,9 +50,6 @@ class SimpleTreeModel : public QAbstractItemModel {
void EndDelete();
void EmitDataChanged(T *item);
protected:
virtual void LazyPopulate(T *item) { item->lazy_loaded = true; }
protected:
T *root_;
};
@ -99,32 +94,13 @@ QModelIndex SimpleTreeModel<T>::parent(const QModelIndex &idx) const {
template<typename T>
int SimpleTreeModel<T>::rowCount(const QModelIndex &parent) const {
T *item = IndexToItem(parent);
if (!item) return 0;
return item->children.count();
}
template<typename T>
bool SimpleTreeModel<T>::hasChildren(const QModelIndex &parent) const {
T *item = IndexToItem(parent);
if (!item) return false;
if (item->lazy_loaded)
return !item->children.isEmpty();
else
return true;
}
template<typename T>
bool SimpleTreeModel<T>::canFetchMore(const QModelIndex &parent) const {
T *item = IndexToItem(parent);
return item && !item->lazy_loaded;
}
template<typename T>
void SimpleTreeModel<T>::fetchMore(const QModelIndex &parent) {
T *item = IndexToItem(parent);
if (item && !item->lazy_loaded) {
LazyPopulate(item);
}
return !item->children.isEmpty();
}
template<typename T>

View File

@ -149,30 +149,24 @@ const QString Song::kRowIdColumnSpec = Song::kRowIdColumns.join(QStringLiteral("
const QString Song::kBindSpec = Utilities::Prepend(QStringLiteral(":"), Song::kColumns).join(QStringLiteral(", "));
const QString Song::kUpdateSpec = Utilities::Updateify(Song::kColumns).join(QStringLiteral(", "));
// used to indicate, what columns can be filtered numerically. Used by the CollectionQuery.
const QStringList Song::kNumericalColumns = QStringList() << QStringLiteral("year")
<< QStringLiteral("length")
<< QStringLiteral("samplerate")
<< QStringLiteral("bitdepth")
<< QStringLiteral("bitrate")
<< QStringLiteral("rating")
<< QStringLiteral("playcount")
<< QStringLiteral("skipcount");
const QStringList Song::kTextSearchColumns = QStringList() << QStringLiteral("title")
<< QStringLiteral("album")
<< QStringLiteral("artist")
<< QStringLiteral("albumartist")
<< QStringLiteral("composer")
<< QStringLiteral("performer")
<< QStringLiteral("grouping")
<< QStringLiteral("genre")
<< QStringLiteral("comment");
const QStringList Song::kFtsColumns = QStringList() << QStringLiteral("ftstitle")
<< QStringLiteral("ftsalbum")
<< QStringLiteral("ftsartist")
<< QStringLiteral("ftsalbumartist")
<< QStringLiteral("ftscomposer")
<< QStringLiteral("ftsperformer")
<< QStringLiteral("ftsgrouping")
<< QStringLiteral("ftsgenre")
<< QStringLiteral("ftscomment");
const QString Song::kFtsColumnSpec = Song::kFtsColumns.join(QStringLiteral(", "));
const QString Song::kFtsBindSpec = Utilities::Prepend(QStringLiteral(":"), Song::kFtsColumns).join(QStringLiteral(", "));
const QString Song::kFtsUpdateSpec = Utilities::Updateify(Song::kFtsColumns).join(QStringLiteral(", "));
const QStringList Song::kNumericalSearchColumns = QStringList() << QStringLiteral("year")
<< QStringLiteral("length")
<< QStringLiteral("samplerate")
<< QStringLiteral("bitdepth")
<< QStringLiteral("bitrate")
<< QStringLiteral("rating")
<< QStringLiteral("playcount")
<< QStringLiteral("skipcount");
const Song::RegularExpressionList Song::kAlbumDisc = Song::RegularExpressionList()
<< QRegularExpression(QStringLiteral("\\s+-*\\s*(Disc|CD)\\s*([0-9]{1,2})$"), QRegularExpression::CaseInsensitiveOption)
@ -199,10 +193,33 @@ const Song::RegularExpressionList Song::kTitleMisc = Song::RegularExpressionList
const QStringList Song::kArticles = QStringList() << QStringLiteral("the ") << QStringLiteral("a ") << QStringLiteral("an ");
const QStringList Song::kAcceptedExtensions = QStringList() << QStringLiteral("wav") << QStringLiteral("flac") << QStringLiteral("wv") << QStringLiteral("ogg") << QStringLiteral("oga") << QStringLiteral("opus") << QStringLiteral("spx") << QStringLiteral("ape") << QStringLiteral("mpc")
<< QStringLiteral("mp2") << QStringLiteral("mp3") << QStringLiteral("m4a") << QStringLiteral("mp4") << QStringLiteral("aac") << QStringLiteral("asf") << QStringLiteral("asx") << QStringLiteral("wma")
<< QStringLiteral("aif << aiff") << QStringLiteral("mka") << QStringLiteral("tta") << QStringLiteral("dsf") << QStringLiteral("dsd")
<< QStringLiteral("ac3") << QStringLiteral("dts") << QStringLiteral("spc") << QStringLiteral("vgm");
const QStringList Song::kAcceptedExtensions = QStringList() << QStringLiteral("wav")
<< QStringLiteral("flac")
<< QStringLiteral("wv")
<< QStringLiteral("ogg")
<< QStringLiteral("oga")
<< QStringLiteral("opus")
<< QStringLiteral("spx")
<< QStringLiteral("ape")
<< QStringLiteral("mpc")
<< QStringLiteral("mp2")
<< QStringLiteral("mp3")
<< QStringLiteral("m4a")
<< QStringLiteral("mp4")
<< QStringLiteral("aac")
<< QStringLiteral("asf")
<< QStringLiteral("asx")
<< QStringLiteral("wma")
<< QStringLiteral("aif")
<< QStringLiteral("aiff")
<< QStringLiteral("mka")
<< QStringLiteral("tta")
<< QStringLiteral("dsf")
<< QStringLiteral("dsd")
<< QStringLiteral("ac3")
<< QStringLiteral("dts")
<< QStringLiteral("spc")
<< QStringLiteral("vgm");
struct Song::Private : public QSharedData {
@ -570,7 +587,8 @@ bool Song::write_tags_supported() const {
d->filetype_ == FileType::TrueAudio ||
d->filetype_ == FileType::APE ||
d->filetype_ == FileType::DSF ||
d->filetype_ == FileType::DSDIFF;
d->filetype_ == FileType::DSDIFF ||
d->filetype_ == FileType::WAV;
}
@ -585,7 +603,8 @@ bool Song::additional_tags_supported() const {
d->filetype_ == FileType::MPEG ||
d->filetype_ == FileType::MP4 ||
d->filetype_ == FileType::MPC ||
d->filetype_ == FileType::APE;
d->filetype_ == FileType::APE ||
d->filetype_ == FileType::WAV;
}
@ -607,7 +626,8 @@ bool Song::performer_supported() const {
d->filetype_ == FileType::OggSpeex ||
d->filetype_ == FileType::MPEG ||
d->filetype_ == FileType::MPC ||
d->filetype_ == FileType::APE;
d->filetype_ == FileType::APE ||
d->filetype_ == FileType::WAV;
}
@ -1778,20 +1798,6 @@ void Song::BindToQuery(SqlQuery *query) const {
}
void Song::BindToFtsQuery(SqlQuery *query) const {
query->BindValue(QStringLiteral(":ftstitle"), d->title_);
query->BindValue(QStringLiteral(":ftsalbum"), d->album_);
query->BindValue(QStringLiteral(":ftsartist"), d->artist_);
query->BindValue(QStringLiteral(":ftsalbumartist"), d->albumartist_);
query->BindValue(QStringLiteral(":ftscomposer"), d->composer_);
query->BindValue(QStringLiteral(":ftsperformer"), d->performer_);
query->BindValue(QStringLiteral(":ftsgrouping"), d->grouping_);
query->BindValue(QStringLiteral(":ftsgenre"), d->genre_);
query->BindValue(QStringLiteral(":ftscomment"), d->comment_);
}
#ifdef HAVE_DBUS
void Song::ToXesam(QVariantMap *map) const {

View File

@ -119,12 +119,8 @@ class Song {
static const QString kBindSpec;
static const QString kUpdateSpec;
static const QStringList kNumericalColumns;
static const QStringList kFtsColumns;
static const QString kFtsColumnSpec;
static const QString kFtsBindSpec;
static const QString kFtsUpdateSpec;
static const QStringList kTextSearchColumns;
static const QStringList kNumericalSearchColumns;
using RegularExpressionList = QList<QRegularExpression>;
static const RegularExpressionList kAlbumDisc;
@ -439,7 +435,6 @@ class Song {
// Save
void BindToQuery(SqlQuery *query) const;
void BindToFtsQuery(SqlQuery *query) const;
#ifdef HAVE_DBUS
void ToXesam(QVariantMap *map) const;
#endif

View File

@ -236,7 +236,7 @@ SongLoader::Result SongLoader::LoadLocal(const QString &filename) {
QMutexLocker l(collection_backend_->db()->Mutex());
QSqlDatabase db(collection_backend_->db()->Connect());
CollectionQuery query(db, collection_backend_->songs_table(), collection_backend_->fts_table());
CollectionQuery query(db, collection_backend_->songs_table());
query.SetColumnSpec(QStringLiteral("%songs_table.ROWID, ") + Song::kColumnSpec);
query.AddWhere(QStringLiteral("url"), url.toEncoded());

View File

@ -876,7 +876,7 @@ SongList AlbumCoverManager::GetSongsInAlbum(const QModelIndex &idx) const {
QMutexLocker l(collection_backend_->db()->Mutex());
QSqlDatabase db(collection_backend_->db()->Connect());
CollectionQuery q(db, collection_backend_->songs_table(), collection_backend_->fts_table());
CollectionQuery q(db, collection_backend_->songs_table());
q.SetColumnSpec(Song::kRowIdColumnSpec);
q.AddWhere(QStringLiteral("album"), idx.data(Role_Album).toString());
q.SetOrderBy(QStringLiteral("disc, track, title"));

View File

@ -41,7 +41,7 @@ CddaDevice::CddaDevice(const QUrl &url, DeviceLister *lister, const QString &uni
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsLoaded, this, &CddaDevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsDurationLoaded, this, &CddaDevice::SongsLoaded);
QObject::connect(&cdda_song_loader_, &CddaSongLoader::SongsMetadataLoaded, this, &CddaDevice::SongsLoaded);
QObject::connect(this, &CddaDevice::SongsDiscovered, model_, &CollectionModel::SongsDiscovered);
QObject::connect(this, &CddaDevice::SongsDiscovered, model_, &CollectionModel::SongsAdded);
}

View File

@ -68,7 +68,6 @@ ConnectedDevice::ConnectedDevice(const QUrl &url, DeviceLister *lister, const QS
app_->task_manager(),
Song::Source::Device,
QStringLiteral("device_%1_songs").arg(database_id),
QStringLiteral("device_%1_fts").arg(database_id),
QStringLiteral("device_%1_directories").arg(database_id),
QStringLiteral("device_%1_subdirectories").arg(database_id));

View File

@ -186,7 +186,7 @@ void DeviceDatabaseBackend::RemoveDevice(const int id) {
{
SqlQuery q(db);
q.prepare(QStringLiteral("DROP TABLE device_%1_fts").arg(id));
q.prepare(QStringLiteral("DROP TABLE IF EXISTS device_%1_fts").arg(id));
if (!q.Exec()) {
db_->ReportErrors(q);
return;

View File

@ -53,7 +53,7 @@ FilesystemDevice::FilesystemDevice(const QUrl &url, DeviceLister *lister, const
watcher_->set_backend(backend_);
watcher_->set_task_manager(app_->task_manager());
QObject::connect(&*backend_, &CollectionBackend::DirectoryDiscovered, watcher_, &CollectionWatcher::AddDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryAdded, watcher_, &CollectionWatcher::AddDirectory);
QObject::connect(&*backend_, &CollectionBackend::DirectoryDeleted, watcher_, &CollectionWatcher::RemoveDirectory);
QObject::connect(watcher_, &CollectionWatcher::NewOrUpdatedSongs, &*backend_, &CollectionBackend::AddOrUpdateSongs);
QObject::connect(watcher_, &CollectionWatcher::SongsMTimeUpdated, &*backend_, &CollectionBackend::UpdateMTimesOnly);

View File

@ -33,10 +33,10 @@
#include "settings/settingsdialog.h"
#include "internetsearchview.h"
class QSortFilterProxyModel;
class Application;
class CollectionBackend;
class CollectionModel;
class CollectionFilter;
class InternetService : public QObject {
Q_OBJECT
@ -69,9 +69,9 @@ class InternetService : public QObject {
virtual CollectionModel *albums_collection_model() { return nullptr; }
virtual CollectionModel *songs_collection_model() { return nullptr; }
virtual QSortFilterProxyModel *artists_collection_sort_model() { return nullptr; }
virtual QSortFilterProxyModel *albums_collection_sort_model() { return nullptr; }
virtual QSortFilterProxyModel *songs_collection_sort_model() { return nullptr; }
virtual CollectionFilter *artists_collection_filter_model() { return nullptr; }
virtual CollectionFilter *albums_collection_filter_model() { return nullptr; }
virtual CollectionFilter *songs_collection_filter_model() { return nullptr; }
public slots:
virtual void ShowConfig() {}

View File

@ -34,6 +34,7 @@
#include "core/iconloader.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectionfilter.h"
#include "internetservice.h"
#include "internetsongsview.h"
#include "internetcollectionview.h"
@ -51,10 +52,10 @@ InternetSongsView::InternetSongsView(Application *app, InternetServicePtr servic
ui_->stacked->setCurrentWidget(ui_->internetcollection_page);
ui_->view->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model(), false);
ui_->view->setModel(service_->songs_collection_sort_model());
ui_->view->setModel(service_->songs_collection_filter_model());
ui_->view->SetFilter(ui_->filter_widget);
ui_->filter_widget->SetSettingsGroup(settings_group);
ui_->filter_widget->Init(service_->songs_collection_model());
ui_->filter_widget->Init(service_->songs_collection_model(), service_->songs_collection_filter_model());
QAction *action_configure = new QAction(IconLoader::Load(QStringLiteral("configure")), tr("Configure %1...").arg(Song::DescriptionForSource(service_->source())), this);
QObject::connect(action_configure, &QAction::triggered, this, &InternetSongsView::OpenSettingsDialog);

View File

@ -38,6 +38,7 @@
#include "core/settings.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectionfilter.h"
#include "collection/collectionfilterwidget.h"
#include "internetservice.h"
#include "internettabsview.h"
@ -66,11 +67,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service,
if (service_->artists_collection_model()) {
ui_->artists_collection->stacked()->setCurrentWidget(ui_->artists_collection->internetcollection_page());
ui_->artists_collection->view()->Init(app_, service_->artists_collection_backend(), service_->artists_collection_model(), true);
ui_->artists_collection->view()->setModel(service_->artists_collection_sort_model());
ui_->artists_collection->view()->setModel(service_->artists_collection_filter_model());
ui_->artists_collection->view()->SetFilter(ui_->artists_collection->filter_widget());
ui_->artists_collection->filter_widget()->SetSettingsGroup(settings_group);
ui_->artists_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("artists"));
ui_->artists_collection->filter_widget()->Init(service_->artists_collection_model());
ui_->artists_collection->filter_widget()->Init(service_->artists_collection_model(), service_->artists_collection_filter_model());
ui_->artists_collection->filter_widget()->AddMenuAction(action_configure);
QObject::connect(ui_->artists_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetArtists);
@ -98,11 +99,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service,
if (service_->albums_collection_model()) {
ui_->albums_collection->stacked()->setCurrentWidget(ui_->albums_collection->internetcollection_page());
ui_->albums_collection->view()->Init(app_, service_->albums_collection_backend(), service_->albums_collection_model(), true);
ui_->albums_collection->view()->setModel(service_->albums_collection_sort_model());
ui_->albums_collection->view()->setModel(service_->albums_collection_filter_model());
ui_->albums_collection->view()->SetFilter(ui_->albums_collection->filter_widget());
ui_->albums_collection->filter_widget()->SetSettingsGroup(settings_group);
ui_->albums_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("albums"));
ui_->albums_collection->filter_widget()->Init(service_->albums_collection_model());
ui_->albums_collection->filter_widget()->Init(service_->albums_collection_model(), service_->albums_collection_filter_model());
ui_->albums_collection->filter_widget()->AddMenuAction(action_configure);
QObject::connect(ui_->albums_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetAlbums);
@ -130,11 +131,11 @@ InternetTabsView::InternetTabsView(Application *app, InternetServicePtr service,
if (service_->songs_collection_model()) {
ui_->songs_collection->stacked()->setCurrentWidget(ui_->songs_collection->internetcollection_page());
ui_->songs_collection->view()->Init(app_, service_->songs_collection_backend(), service_->songs_collection_model(), true);
ui_->songs_collection->view()->setModel(service_->songs_collection_sort_model());
ui_->songs_collection->view()->setModel(service_->songs_collection_filter_model());
ui_->songs_collection->view()->SetFilter(ui_->songs_collection->filter_widget());
ui_->songs_collection->filter_widget()->SetSettingsGroup(settings_group);
ui_->songs_collection->filter_widget()->SetSettingsPrefix(QStringLiteral("songs"));
ui_->songs_collection->filter_widget()->Init(service_->songs_collection_model());
ui_->songs_collection->filter_widget()->Init(service_->songs_collection_model(), service_->songs_collection_filter_model());
ui_->songs_collection->filter_widget()->AddMenuAction(action_configure);
QObject::connect(ui_->songs_collection->view(), &InternetCollectionView::GetSongs, this, &InternetTabsView::GetSongs);

View File

@ -96,7 +96,7 @@ void PlaylistManager::Init(SharedPtr<CollectionBackend> collection_backend, Shar
parser_ = new PlaylistParser(collection_backend, this);
playlist_container_ = playlist_container;
QObject::connect(&*collection_backend_, &CollectionBackend::SongsDiscovered, this, &PlaylistManager::SongsDiscovered);
QObject::connect(&*collection_backend_, &CollectionBackend::SongsAdded, this, &PlaylistManager::SongsDiscovered);
QObject::connect(&*collection_backend_, &CollectionBackend::SongsStatisticsChanged, this, &PlaylistManager::SongsDiscovered);
QObject::connect(&*collection_backend_, &CollectionBackend::SongsRatingChanged, this, &PlaylistManager::SongsDiscovered);

View File

@ -63,23 +63,10 @@ void ParserBase::LoadSong(const QString &filename_or_url, const qint64 beginning
}
}
// Strawberry always wants / separators internally.
// Using QDir::fromNativeSeparators() only works on the same platform the playlist was created on/for, using replace() lets playlists work on any platform.
filename = filename.replace(QLatin1Char('\\'), QLatin1Char('/'));
// Make the path absolute
if (!QDir::isAbsolutePath(filename)) {
filename = dir.absoluteFilePath(filename);
}
// Use the canonical path
if (QFile::exists(filename)) {
filename = QFileInfo(filename).canonicalFilePath();
}
filename = QDir::cleanPath(filename);
const QUrl url = QUrl::fromLocalFile(filename);
// Search in the collection
// Search the collection
if (collection_backend_ && collection_search) {
Song collection_song;
if (track > 0) {
@ -88,6 +75,19 @@ void ParserBase::LoadSong(const QString &filename_or_url, const qint64 beginning
if (!collection_song.is_valid()) {
collection_song = collection_backend_->GetSongByUrl(url, beginning);
}
// Try absolute path
if (!collection_song.is_valid() && !QDir::isAbsolutePath(filename)) {
QString absolute_filename = dir.absoluteFilePath(filename);
if (absolute_filename != filename) {
const QUrl absolute_url = QUrl::fromLocalFile(absolute_filename);
if (track > 0) {
collection_song = collection_backend_->GetSongByUrlAndTrack(absolute_url, track);
}
if (!collection_song.is_valid()) {
collection_song = collection_backend_->GetSongByUrl(absolute_url, beginning);
}
}
}
// If it was found in the collection then use it, otherwise load metadata from disk.
if (collection_song.is_valid()) {
*song = collection_song;

View File

@ -63,7 +63,6 @@ class ParserBase : public QObject {
protected:
// Loads a song. If filename_or_url is a URL (with a scheme other than "file") then it is set on the song and the song marked as a stream.
// If it is a filename or a file:// URL then it is made absolute and canonical and set as a file:// url on the song.
// Also sets the song's metadata by searching in the Collection, or loading from the file as a fallback.
// This function should always be used when loading a playlist.
Song LoadSong(const QString &filename_or_url, const qint64 beginning, const int track, const QDir &dir, const bool collection_search) const;

View File

@ -35,7 +35,6 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QSortFilterProxyModel>
#include <QSslError>
#include "core/shared_ptr.h"
@ -50,6 +49,7 @@
#include "internet/internetsearchview.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectionfilter.h"
#include "qobuzservice.h"
#include "qobuzurlhandler.h"
#include "qobuzbaserequest.h"
@ -75,10 +75,6 @@ constexpr char kArtistsSongsTable[] = "qobuz_artists_songs";
constexpr char kAlbumsSongsTable[] = "qobuz_albums_songs";
constexpr char kSongsTable[] = "qobuz_songs";
constexpr char kArtistsSongsFtsTable[] = "qobuz_artists_songs_fts";
constexpr char kAlbumsSongsFtsTable[] = "qobuz_albums_songs_fts";
constexpr char kSongsFtsTable[] = "qobuz_songs_fts";
} // namespace
QobuzService::QobuzService(Application *app, QObject *parent)
@ -92,9 +88,9 @@ QobuzService::QobuzService(Application *app, QObject *parent)
artists_collection_model_(nullptr),
albums_collection_model_(nullptr),
songs_collection_model_(nullptr),
artists_collection_sort_model_(new QSortFilterProxyModel(this)),
albums_collection_sort_model_(new QSortFilterProxyModel(this)),
songs_collection_sort_model_(new QSortFilterProxyModel(this)),
artists_collection_filter_model_(new CollectionFilter(this)),
albums_collection_filter_model_(new CollectionFilter(this)),
songs_collection_filter_model_(new CollectionFilter(this)),
timer_search_delay_(new QTimer(this)),
timer_login_attempt_(new QTimer(this)),
favorite_request_(new QobuzFavoriteRequest(this, network_, this)),
@ -120,37 +116,37 @@ QobuzService::QobuzService(Application *app, QObject *parent)
artists_collection_backend_ = make_shared<CollectionBackend>();
artists_collection_backend_->moveToThread(app_->database()->thread());
artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kArtistsSongsTable), QLatin1String(kArtistsSongsFtsTable));
artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kArtistsSongsTable));
albums_collection_backend_ = make_shared<CollectionBackend>();
albums_collection_backend_->moveToThread(app_->database()->thread());
albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kAlbumsSongsTable), QLatin1String(kAlbumsSongsFtsTable));
albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kAlbumsSongsTable));
songs_collection_backend_ = make_shared<CollectionBackend>();
songs_collection_backend_->moveToThread(app_->database()->thread());
songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable));
songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Qobuz, QLatin1String(kSongsTable));
artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this);
albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this);
songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this);
artists_collection_sort_model_->setSourceModel(artists_collection_model_);
artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
artists_collection_sort_model_->setDynamicSortFilter(true);
artists_collection_sort_model_->setSortLocaleAware(true);
artists_collection_sort_model_->sort(0);
artists_collection_filter_model_->setSourceModel(artists_collection_model_);
artists_collection_filter_model_->setSortRole(CollectionModel::Role_SortText);
artists_collection_filter_model_->setDynamicSortFilter(true);
artists_collection_filter_model_->setSortLocaleAware(true);
artists_collection_filter_model_->sort(0);
albums_collection_sort_model_->setSourceModel(albums_collection_model_);
albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
albums_collection_sort_model_->setDynamicSortFilter(true);
albums_collection_sort_model_->setSortLocaleAware(true);
albums_collection_sort_model_->sort(0);
albums_collection_filter_model_->setSourceModel(albums_collection_model_);
albums_collection_filter_model_->setSortRole(CollectionModel::Role_SortText);
albums_collection_filter_model_->setDynamicSortFilter(true);
albums_collection_filter_model_->setSortLocaleAware(true);
albums_collection_filter_model_->sort(0);
songs_collection_sort_model_->setSourceModel(songs_collection_model_);
songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
songs_collection_sort_model_->setDynamicSortFilter(true);
songs_collection_sort_model_->setSortLocaleAware(true);
songs_collection_sort_model_->sort(0);
songs_collection_filter_model_->setSourceModel(songs_collection_model_);
songs_collection_filter_model_->setSortRole(CollectionModel::Role_SortText);
songs_collection_filter_model_->setDynamicSortFilter(true);
songs_collection_filter_model_->setSortLocaleAware(true);
songs_collection_filter_model_->sort(0);
// Search

View File

@ -44,7 +44,6 @@
class QTimer;
class QNetworkReply;
class QSortFilterProxyModel;
class Application;
class NetworkAccessManager;
class QobuzUrlHandler;
@ -53,6 +52,7 @@ class QobuzFavoriteRequest;
class QobuzStreamURLRequest;
class CollectionBackend;
class CollectionModel;
class CollectionFilter;
class QobuzService : public InternetService {
Q_OBJECT
@ -105,9 +105,9 @@ class QobuzService : public InternetService {
CollectionModel *albums_collection_model() override { return albums_collection_model_; }
CollectionModel *songs_collection_model() override { return songs_collection_model_; }
QSortFilterProxyModel *artists_collection_sort_model() override { return artists_collection_sort_model_; }
QSortFilterProxyModel *albums_collection_sort_model() override { return albums_collection_sort_model_; }
QSortFilterProxyModel *songs_collection_sort_model() override { return songs_collection_sort_model_; }
CollectionFilter *artists_collection_filter_model() override { return artists_collection_filter_model_; }
CollectionFilter *albums_collection_filter_model() override { return albums_collection_filter_model_; }
CollectionFilter *songs_collection_filter_model() override { return songs_collection_filter_model_; }
public slots:
void ShowConfig() override;
@ -160,9 +160,9 @@ class QobuzService : public InternetService {
CollectionModel *albums_collection_model_;
CollectionModel *songs_collection_model_;
QSortFilterProxyModel *artists_collection_sort_model_;
QSortFilterProxyModel *albums_collection_sort_model_;
QSortFilterProxyModel *songs_collection_sort_model_;
CollectionFilter *artists_collection_filter_model_;
CollectionFilter *albums_collection_filter_model_;
CollectionFilter *songs_collection_filter_model_;
QTimer *timer_search_delay_;
QTimer *timer_login_attempt_;

View File

@ -45,8 +45,6 @@ RadioModel::RadioModel(Application *app, QObject *parent)
: SimpleTreeModel<RadioItem>(new RadioItem(this), parent),
app_(app) {
root_->lazy_loaded = true;
if (app_) {
QObject::connect(&*app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &RadioModel::AlbumCoverLoaded);
}
@ -154,7 +152,6 @@ void RadioModel::Reset() {
pending_cache_keys_.clear();
delete root_;
root_ = new RadioItem(this);
root_->lazy_loaded = true;
endResetModel();
}
@ -172,7 +169,6 @@ void RadioModel::AddChannels(const RadioChannelList &channels) {
item->source = channel.source;
item->display_text = Song::DescriptionForSource(channel.source);
item->sort_text = SortText(Song::TextForSource(channel.source));
item->lazy_loaded = true;
container_nodes_.insert(channel.source, item);
endInsertRows();
container = item;
@ -183,7 +179,6 @@ void RadioModel::AddChannels(const RadioChannelList &channels) {
item->display_text = channel.name;
item->sort_text = SortText(Song::TextForSource(channel.source) + QStringLiteral(" - ") + channel.name);
item->channel = channel;
item->lazy_loaded = true;
items_ << item;
endInsertRows();
}

View File

@ -0,0 +1,63 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QStandardItemModel>
#include <QVariant>
#include <QString>
#include "core/iconloader.h"
#include "collectionsettingsdirectorymodel.h"
CollectionSettingsDirectoryModel::CollectionSettingsDirectoryModel(QObject *parent)
: QStandardItemModel(parent),
dir_icon_(IconLoader::Load(QStringLiteral("document-open-folder"))) {}
void CollectionSettingsDirectoryModel::AddDirectory(const QString &path) {
QStandardItem *item = new QStandardItem(path);
item->setIcon(dir_icon_);
appendRow(item);
paths_ << path;
}
void CollectionSettingsDirectoryModel::AddDirectories(const QStringList &paths) {
for (const QString &path : paths) {
AddDirectory(path);
}
}
void CollectionSettingsDirectoryModel::RemoveDirectory(const QModelIndex &idx) {
if (!idx.isValid()) return;
const QString path = data(idx).toString();
removeRow(idx.row());
if (paths_.contains(path)) {
paths_.removeAll(path);
}
}

View File

@ -0,0 +1,48 @@
/*
* Strawberry Music Player
* Copyright 2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Strawberry is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef COLLECTIONSETTINGSDIRECTORYMODEL_H
#define COLLECTIONSETTINGSDIRECTORYMODEL_H
#include "config.h"
#include <QStandardItemModel>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QIcon>
class CollectionSettingsDirectoryModel : public QStandardItemModel {
Q_OBJECT
public:
explicit CollectionSettingsDirectoryModel(QObject *parent = nullptr);
void AddDirectory(const QString &path);
void AddDirectories(const QStringList &paths);
void RemoveDirectory(const QModelIndex &idx);
QStringList paths() const { return paths_; }
private:
QIcon dir_icon_;
QStringList paths_;
};
#endif // COLLECTIONSETTINGSDIRECTORYMODEL_H

View File

@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -28,6 +28,7 @@
#include <QItemSelectionModel>
#include <QString>
#include <QStringList>
#include <QDir>
#include <QFileDialog>
#include <QCheckBox>
#include <QLineEdit>
@ -47,9 +48,12 @@
#include "utilities/strutils.h"
#include "utilities/timeutils.h"
#include "collection/collection.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectiondirectory.h"
#include "collection/collectiondirectorymodel.h"
#include "collectionsettingspage.h"
#include "collectionsettingsdirectorymodel.h"
#include "playlist/playlistdelegates.h"
#include "settings/settingsdialog.h"
#include "settings/settingspage.h"
@ -67,6 +71,9 @@ const int CollectionSettingsPage::kSettingsDiskCacheSizeDefault = 360;
CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog, QWidget *parent)
: SettingsPage(dialog, parent),
ui_(new Ui_CollectionSettingsPage),
collection_backend_(dialog->app()->collection_backend()),
collectionsettings_directory_model_(new CollectionSettingsDirectoryModel(this)),
collection_directory_model_(dialog->collection_directory_model()),
initialized_model_(false) {
ui_->setupUi(this);
@ -74,7 +81,7 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog, QWidget *
// Icons
setWindowIcon(IconLoader::Load(QStringLiteral("library-music"), true, 0, 32));
ui_->add->setIcon(IconLoader::Load(QStringLiteral("document-open-folder")));
ui_->add_directory->setIcon(IconLoader::Load(QStringLiteral("document-open-folder")));
ui_->combobox_cache_size->addItem(QStringLiteral("KB"), static_cast<int>(CacheSizeUnit::KB));
ui_->combobox_cache_size->addItem(QStringLiteral("MB"), static_cast<int>(CacheSizeUnit::MB));
@ -83,8 +90,8 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog, QWidget *
ui_->combobox_disk_cache_size->addItem(QStringLiteral("MB"), static_cast<int>(CacheSizeUnit::MB));
ui_->combobox_disk_cache_size->addItem(QStringLiteral("GB"), static_cast<int>(CacheSizeUnit::GB));
QObject::connect(ui_->add, &QPushButton::clicked, this, &CollectionSettingsPage::Add);
QObject::connect(ui_->remove, &QPushButton::clicked, this, &CollectionSettingsPage::Remove);
QObject::connect(ui_->add_directory, &QPushButton::clicked, this, &CollectionSettingsPage::AddDirectory);
QObject::connect(ui_->remove_directory, &QPushButton::clicked, this, &CollectionSettingsPage::RemoveDirectory);
#ifdef HAVE_SONGFINGERPRINTING
QObject::connect(ui_->song_tracking, &QCheckBox::toggled, this, &CollectionSettingsPage::SongTrackingToggled);
@ -111,56 +118,6 @@ CollectionSettingsPage::CollectionSettingsPage(SettingsDialog *dialog, QWidget *
CollectionSettingsPage::~CollectionSettingsPage() { delete ui_; }
void CollectionSettingsPage::Add() {
Settings s;
s.beginGroup(kSettingsGroup);
QString path(s.value("last_path", QStandardPaths::writableLocation(QStandardPaths::MusicLocation)).toString());
path = QFileDialog::getExistingDirectory(this, tr("Add directory..."), path);
if (!path.isEmpty()) {
dialog()->collection_directory_model()->AddDirectory(path);
}
s.setValue("last_path", path);
set_changed();
}
void CollectionSettingsPage::Remove() {
dialog()->collection_directory_model()->RemoveDirectory(ui_->list->currentIndex());
set_changed();
}
void CollectionSettingsPage::CurrentRowChanged(const QModelIndex &idx) {
ui_->remove->setEnabled(idx.isValid());
}
void CollectionSettingsPage::SongTrackingToggled() {
ui_->mark_songs_unavailable->setEnabled(!ui_->song_tracking->isChecked());
if (ui_->song_tracking->isChecked()) {
ui_->mark_songs_unavailable->setChecked(true);
}
}
void CollectionSettingsPage::DiskCacheEnable(const int state) {
bool checked = state == Qt::Checked;
ui_->label_disk_cache_size->setEnabled(checked);
ui_->spinbox_disk_cache_size->setEnabled(checked);
ui_->combobox_disk_cache_size->setEnabled(checked);
ui_->label_disk_cache_in_use->setEnabled(checked);
ui_->disk_cache_in_use->setEnabled(checked);
ui_->button_clear_disk_cache->setEnabled(checked);
}
void CollectionSettingsPage::Load() {
if (!initialized_model_) {
@ -168,12 +125,17 @@ void CollectionSettingsPage::Load() {
QObject::disconnect(ui_->list->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &CollectionSettingsPage::CurrentRowChanged);
}
ui_->list->setModel(dialog()->collection_directory_model());
ui_->list->setModel(collectionsettings_directory_model_);
initialized_model_ = true;
QObject::connect(ui_->list->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &CollectionSettingsPage::CurrentRowChanged);
}
ui_->list->model()->removeRows(0, ui_->list->model()->rowCount());
for (const QString &path : collection_directory_model_->paths()) {
collectionsettings_directory_model_->AddDirectory(path);
}
Settings s;
s.beginGroup(kSettingsGroup);
@ -261,6 +223,69 @@ void CollectionSettingsPage::Save() {
s.endGroup();
for (const CollectionDirectory &dir : collection_directory_model_->directories()) {
if (!collectionsettings_directory_model_->paths().contains(dir.path)) {
collection_backend_->RemoveDirectoryAsync(dir);
}
}
for (const QString &path : collectionsettings_directory_model_->paths()) {
if (!collection_directory_model_->paths().contains(path)) {
collection_backend_->AddDirectoryAsync(path);
}
}
}
void CollectionSettingsPage::AddDirectory() {
Settings s;
s.beginGroup(kSettingsGroup);
QString path = s.value("last_path", QStandardPaths::writableLocation(QStandardPaths::MusicLocation)).toString();
path = QDir::cleanPath(QFileDialog::getExistingDirectory(this, tr("Add directory..."), path));
if (!path.isEmpty()) {
collectionsettings_directory_model_->AddDirectory(path);
}
s.setValue("last_path", path);
set_changed();
}
void CollectionSettingsPage::RemoveDirectory() {
collectionsettings_directory_model_->RemoveDirectory(ui_->list->currentIndex());
set_changed();
}
void CollectionSettingsPage::CurrentRowChanged(const QModelIndex &idx) {
ui_->remove_directory->setEnabled(idx.isValid());
}
void CollectionSettingsPage::SongTrackingToggled() {
ui_->mark_songs_unavailable->setEnabled(!ui_->song_tracking->isChecked());
if (ui_->song_tracking->isChecked()) {
ui_->mark_songs_unavailable->setChecked(true);
}
}
void CollectionSettingsPage::DiskCacheEnable(const int state) {
bool checked = state == Qt::Checked;
ui_->label_disk_cache_size->setEnabled(checked);
ui_->spinbox_disk_cache_size->setEnabled(checked);
ui_->combobox_disk_cache_size->setEnabled(checked);
ui_->label_disk_cache_in_use->setEnabled(checked);
ui_->disk_cache_in_use->setEnabled(checked);
ui_->button_clear_disk_cache->setEnabled(checked);
}
void CollectionSettingsPage::ClearPixmapDiskCache() {

View File

@ -2,7 +2,7 @@
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
* Copyright 2018-2021, Jonas Kvinge <jonas@jkvinge.net>
* Copyright 2018-2024, Jonas Kvinge <jonas@jkvinge.net>
*
* Strawberry is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -30,8 +30,13 @@
#include "settingspage.h"
#include "core/shared_ptr.h"
class QModelIndex;
class SettingsDialog;
class CollectionBackend;
class CollectionDirectoryModel;
class CollectionSettingsDirectoryModel;
class Ui_CollectionSettingsPage;
class CollectionSettingsPage : public SettingsPage {
@ -61,8 +66,8 @@ class CollectionSettingsPage : public SettingsPage {
void Save() override;
private slots:
void Add();
void Remove();
void AddDirectory();
void RemoveDirectory();
void CurrentRowChanged(const QModelIndex &idx);
void SongTrackingToggled();
@ -74,6 +79,9 @@ class CollectionSettingsPage : public SettingsPage {
private:
Ui_CollectionSettingsPage *ui_;
SharedPtr<CollectionBackend> collection_backend_;
CollectionSettingsDirectoryModel *collectionsettings_directory_model_;
CollectionDirectoryModel *collection_directory_model_;
bool initialized_model_;
};

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>519</width>
<height>920</height>
<width>565</width>
<height>973</height>
</rect>
</property>
<property name="windowTitle">
@ -39,7 +39,7 @@
<item>
<layout class="QVBoxLayout" name="layout_collection_folder_buttons">
<item>
<widget class="QPushButton" name="add">
<widget class="QPushButton" name="add_directory">
<property name="text">
<string>Add new folder...</string>
</property>
@ -49,7 +49,7 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="remove">
<widget class="QPushButton" name="remove_directory">
<property name="text">
<string>Remove folder</string>
</property>
@ -58,7 +58,7 @@
<item>
<spacer name="spacer_collection_buttons">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -167,7 +167,7 @@
<item>
<spacer name="spacer_expire_unavailable_songs">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -288,7 +288,7 @@ If there are no matches then it will use the largest image in the directory.</st
<item row="0" column="4">
<spacer name="spacer_cache_size">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -312,7 +312,7 @@ If there are no matches then it will use the largest image in the directory.</st
<item>
<spacer name="spacer_disk_cache">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -371,7 +371,7 @@ If there are no matches then it will use the largest image in the directory.</st
<item row="0" column="4">
<spacer name="spacer_disk_cache_size">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -415,7 +415,7 @@ If there are no matches then it will use the largest image in the directory.</st
<item>
<spacer name="spacer_disk_cache_in_use">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -476,7 +476,7 @@ If there are no matches then it will use the largest image in the directory.</st
<item>
<spacer name="spacer_save_stats">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -502,8 +502,8 @@ If there are no matches then it will use the largest image in the directory.</st
</widget>
<tabstops>
<tabstop>list</tabstop>
<tabstop>add</tabstop>
<tabstop>remove</tabstop>
<tabstop>add_directory</tabstop>
<tabstop>remove_directory</tabstop>
<tabstop>startup_scan</tabstop>
<tabstop>monitor</tabstop>
<tabstop>song_tracking</tabstop>

View File

@ -51,11 +51,7 @@ const int SmartPlaylistsModel::kSmartPlaylistsVersion = 1;
SmartPlaylistsModel::SmartPlaylistsModel(SharedPtr<CollectionBackend> collection_backend, QObject *parent)
: SimpleTreeModel<SmartPlaylistsItem>(new SmartPlaylistsItem(this), parent),
collection_backend_(collection_backend),
icon_(IconLoader::Load(QStringLiteral("view-media-playlist"))) {
root_->lazy_loaded = true;
}
icon_(IconLoader::Load(QStringLiteral("view-media-playlist"))) {}
SmartPlaylistsModel::~SmartPlaylistsModel() { delete root_; }
@ -164,7 +160,6 @@ void SmartPlaylistsModel::ItemFromSmartPlaylist(const Settings &s, const bool no
item->sort_text = item->display_text;
item->smart_playlist_type = PlaylistGenerator::Type(s.value("type").toInt());
item->smart_playlist_data = s.value("data").toByteArray();
item->lazy_loaded = true;
if (notify) item->InsertNotify(root_);

View File

@ -40,7 +40,6 @@
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QSortFilterProxyModel>
#include "core/logging.h"
#include "core/shared_ptr.h"
@ -52,6 +51,7 @@
#include "utilities/randutils.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectionfilter.h"
#include "subsonicservice.h"
#include "subsonicurlhandler.h"
#include "subsonicrequest.h"
@ -68,7 +68,6 @@ const char *SubsonicService::kApiVersion = "1.11.0";
namespace {
constexpr char kSongsTable[] = "subsonic_songs";
constexpr char kSongsFtsTable[] = "subsonic_songs_fts";
constexpr int kMaxRedirects = 3;
} // namespace
@ -78,7 +77,7 @@ SubsonicService::SubsonicService(Application *app, QObject *parent)
url_handler_(new SubsonicUrlHandler(app, this)),
collection_backend_(nullptr),
collection_model_(nullptr),
collection_sort_model_(new QSortFilterProxyModel(this)),
collection_filter_model_(new CollectionFilter(this)),
http2_(false),
verify_certificate_(false),
download_album_covers_(true),
@ -92,16 +91,16 @@ SubsonicService::SubsonicService(Application *app, QObject *parent)
collection_backend_ = make_shared<CollectionBackend>();
collection_backend_->moveToThread(app_->database()->thread());
collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Subsonic, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable));
collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Subsonic, QLatin1String(kSongsTable));
// Model
collection_model_ = new CollectionModel(collection_backend_, app_, this);
collection_sort_model_->setSourceModel(collection_model_);
collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
collection_sort_model_->setDynamicSortFilter(true);
collection_sort_model_->setSortLocaleAware(true);
collection_sort_model_->sort(0);
collection_filter_model_->setSourceModel(collection_model_);
collection_filter_model_->setSortRole(CollectionModel::Role_SortText);
collection_filter_model_->setDynamicSortFilter(true);
collection_filter_model_->setSortLocaleAware(true);
collection_filter_model_->sort(0);
SubsonicService::ReloadSettings();

View File

@ -42,7 +42,6 @@
#include "internet/internetservice.h"
#include "settings/subsonicsettingspage.h"
class QSortFilterProxyModel;
class QNetworkReply;
class Application;
@ -51,6 +50,7 @@ class SubsonicRequest;
class SubsonicScrobbleRequest;
class CollectionBackend;
class CollectionModel;
class CollectionFilter;
class SubsonicService : public InternetService {
Q_OBJECT
@ -78,11 +78,11 @@ class SubsonicService : public InternetService {
SharedPtr<CollectionBackend> collection_backend() const { return collection_backend_; }
CollectionModel *collection_model() const { return collection_model_; }
QSortFilterProxyModel *collection_sort_model() const { return collection_sort_model_; }
CollectionFilter *collection_filter_model() const { return collection_filter_model_; }
SharedPtr<CollectionBackend> songs_collection_backend() override { return collection_backend_; }
CollectionModel *songs_collection_model() override { return collection_model_; }
QSortFilterProxyModel *songs_collection_sort_model() override { return collection_sort_model_; }
CollectionFilter *songs_collection_filter_model() override { return collection_filter_model_; }
void CheckConfiguration();
void Scrobble(const QString &song_id, const bool submission, const QDateTime &time);
@ -109,7 +109,7 @@ class SubsonicService : public InternetService {
SharedPtr<CollectionBackend> collection_backend_;
CollectionModel *collection_model_;
QSortFilterProxyModel *collection_sort_model_;
CollectionFilter *collection_filter_model_;
SharedPtr<SubsonicRequest> songs_request_;
SharedPtr<SubsonicScrobbleRequest> scrobble_request_;

View File

@ -54,6 +54,7 @@
#include "internet/internetsearchview.h"
#include "collection/collectionbackend.h"
#include "collection/collectionmodel.h"
#include "collection/collectionfilter.h"
#include "tidalservice.h"
#include "tidalurlhandler.h"
#include "tidalbaserequest.h"
@ -85,10 +86,6 @@ constexpr char kArtistsSongsTable[] = "tidal_artists_songs";
constexpr char kAlbumsSongsTable[] = "tidal_albums_songs";
constexpr char kSongsTable[] = "tidal_songs";
constexpr char kArtistsSongsFtsTable[] = "tidal_artists_songs_fts";
constexpr char kAlbumsSongsFtsTable[] = "tidal_albums_songs_fts";
constexpr char kSongsFtsTable[] = "tidal_songs_fts";
} // namespace
TidalService::TidalService(Application *app, QObject *parent)
@ -102,9 +99,9 @@ TidalService::TidalService(Application *app, QObject *parent)
artists_collection_model_(nullptr),
albums_collection_model_(nullptr),
songs_collection_model_(nullptr),
artists_collection_sort_model_(new QSortFilterProxyModel(this)),
albums_collection_sort_model_(new QSortFilterProxyModel(this)),
songs_collection_sort_model_(new QSortFilterProxyModel(this)),
artists_collection_filter_model_(new CollectionFilter(this)),
albums_collection_filter_model_(new CollectionFilter(this)),
songs_collection_filter_model_(new CollectionFilter(this)),
timer_search_delay_(new QTimer(this)),
timer_login_attempt_(new QTimer(this)),
timer_refresh_login_(new QTimer(this)),
@ -135,37 +132,37 @@ TidalService::TidalService(Application *app, QObject *parent)
artists_collection_backend_ = make_shared<CollectionBackend>();
artists_collection_backend_->moveToThread(app_->database()->thread());
artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kArtistsSongsTable), QLatin1String(kArtistsSongsFtsTable));
artists_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kArtistsSongsTable));
albums_collection_backend_ = make_shared<CollectionBackend>();
albums_collection_backend_->moveToThread(app_->database()->thread());
albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kAlbumsSongsTable), QLatin1String(kAlbumsSongsFtsTable));
albums_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kAlbumsSongsTable));
songs_collection_backend_ = make_shared<CollectionBackend>();
songs_collection_backend_->moveToThread(app_->database()->thread());
songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kSongsTable), QLatin1String(kSongsFtsTable));
songs_collection_backend_->Init(app_->database(), app->task_manager(), Song::Source::Tidal, QLatin1String(kSongsTable));
artists_collection_model_ = new CollectionModel(artists_collection_backend_, app_, this);
albums_collection_model_ = new CollectionModel(albums_collection_backend_, app_, this);
songs_collection_model_ = new CollectionModel(songs_collection_backend_, app_, this);
artists_collection_sort_model_->setSourceModel(artists_collection_model_);
artists_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
artists_collection_sort_model_->setDynamicSortFilter(true);
artists_collection_sort_model_->setSortLocaleAware(true);
artists_collection_sort_model_->sort(0);
artists_collection_filter_model_->setSourceModel(artists_collection_model_);
artists_collection_filter_model_->setSortRole(CollectionModel::Role_SortText);
artists_collection_filter_model_->setDynamicSortFilter(true);
artists_collection_filter_model_->setSortLocaleAware(true);
artists_collection_filter_model_->sort(0);
albums_collection_sort_model_->setSourceModel(albums_collection_model_);
albums_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
albums_collection_sort_model_->setDynamicSortFilter(true);
albums_collection_sort_model_->setSortLocaleAware(true);
albums_collection_sort_model_->sort(0);
albums_collection_filter_model_->setSourceModel(albums_collection_model_);
albums_collection_filter_model_->setSortRole(CollectionModel::Role_SortText);
albums_collection_filter_model_->setDynamicSortFilter(true);
albums_collection_filter_model_->setSortLocaleAware(true);
albums_collection_filter_model_->sort(0);
songs_collection_sort_model_->setSourceModel(songs_collection_model_);
songs_collection_sort_model_->setSortRole(CollectionModel::Role_SortText);
songs_collection_sort_model_->setDynamicSortFilter(true);
songs_collection_sort_model_->setSortLocaleAware(true);
songs_collection_sort_model_->sort(0);
songs_collection_filter_model_->setSourceModel(songs_collection_model_);
songs_collection_filter_model_->setSortRole(CollectionModel::Role_SortText);
songs_collection_filter_model_->setDynamicSortFilter(true);
songs_collection_filter_model_->setSortLocaleAware(true);
songs_collection_filter_model_->sort(0);
// Search

View File

@ -42,7 +42,6 @@
#include "internet/internetsearchview.h"
#include "settings/tidalsettingspage.h"
class QSortFilterProxyModel;
class QNetworkReply;
class QTimer;
@ -54,6 +53,7 @@ class TidalFavoriteRequest;
class TidalStreamURLRequest;
class CollectionBackend;
class CollectionModel;
class CollectionFilter;
class TidalService : public InternetService {
Q_OBJECT
@ -112,9 +112,9 @@ class TidalService : public InternetService {
CollectionModel *albums_collection_model() override { return albums_collection_model_; }
CollectionModel *songs_collection_model() override { return songs_collection_model_; }
QSortFilterProxyModel *artists_collection_sort_model() override { return artists_collection_sort_model_; }
QSortFilterProxyModel *albums_collection_sort_model() override { return albums_collection_sort_model_; }
QSortFilterProxyModel *songs_collection_sort_model() override { return songs_collection_sort_model_; }
CollectionFilter *artists_collection_filter_model() override { return artists_collection_filter_model_; }
CollectionFilter *albums_collection_filter_model() override { return albums_collection_filter_model_; }
CollectionFilter *songs_collection_filter_model() override { return songs_collection_filter_model_; }
public slots:
void ShowConfig() override;
@ -172,9 +172,9 @@ class TidalService : public InternetService {
CollectionModel *albums_collection_model_;
CollectionModel *songs_collection_model_;
QSortFilterProxyModel *artists_collection_sort_model_;
QSortFilterProxyModel *albums_collection_sort_model_;
QSortFilterProxyModel *songs_collection_sort_model_;
CollectionFilter *artists_collection_filter_model_;
CollectionFilter *albums_collection_filter_model_;
CollectionFilter *songs_collection_filter_model_;
QTimer *timer_search_delay_;
QTimer *timer_login_attempt_;

View File

@ -43,13 +43,14 @@
# Papoteur <yves.brungard_mageia@gadz.org>, 2022. #zanata
# David Geiger <geiger.david68210@gmail.com>, 2023. #zanata
# Peter Blackman <peter@pblackman.plus.com>, 2023. #zanata
# Wedone <wedoneofficiel@outlook.fr>, 2024.
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"MIME-Version: 1.0\n"
"PO-Revision-Date: 2023-12-11 11:29-0500\n"
"Last-Translator: hatstand <john.maguire@gmail.com>\n"
"Last-Translator: Wedone <wedoneofficiel@outlook.fr>\n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Language-Team: French\n"
@ -1417,7 +1418,7 @@ msgstr "Compilation"
#: ../build/src/ui_edittagdialog.h:935
msgid "Complete lyrics automatically"
msgstr ""
msgstr "Compléter les paroles automatiquement"
#: ../build/src/ui_edittagdialog.h:926
msgid "Complete tags automatically"
@ -1527,12 +1528,12 @@ msgstr ""
#: device/gpoddevice.cpp:241
#, qt-format
msgid "Could not copy %1 to %2: %3"
msgstr ""
msgstr "Impossible de copier %1 dans %2 : %3"
#: core/filesystemmusicstorage.cpp:98
#, qt-format
msgid "Could not copy file %1 to %2."
msgstr ""
msgstr "Impossible de copier le fichier %1 vers %2."
#: transcoder/transcoder.cpp:72
#, qt-format
@ -1550,7 +1551,7 @@ msgstr "Impossible d'ouvrir le fichier CUE %1 pour la lecture : %2"
#: device/mtpconnection.cpp:75 device/mtpconnection.cpp:109
msgid "Could not open MTP device."
msgstr ""
msgstr "Impossible d'ouvrir le périphérique MTP."
#: scrobbler/scrobblingapi20.cpp:220
msgid "Could not open URL. Please open this URL in your browser"
@ -1885,12 +1886,12 @@ msgstr "Destination"
#: core/filesystemmusicstorage.cpp:92
#, qt-format
msgid "Destination file %1 exists, but not allowed to overwrite"
msgstr ""
msgstr "Le fichier de destination %1 existe, mais il n'est pas autorisé à l'écraser."
#: core/filesystemmusicstorage.cpp:73
#, qt-format
msgid "Destination file %1 exists, but not allowed to overwrite."
msgstr ""
msgstr "Le fichier de destination %1 existe, mais il n'est pas autorisé à l'écraser."
#: ../build/src/ui_transcodedialog.h:235
msgid "Details..."
@ -2256,7 +2257,7 @@ msgstr "Erreur lors de la connexion au périphérique MTP %1"
#: device/mtploader.cpp:77
#, qt-format
msgid "Error connecting MTP device %1: %2"
msgstr ""
msgstr "Erreur de connexion du dispositif MTP %1 : %2"
#: organize/organizeerrordialog.cpp:71
msgid "Error copying songs"
@ -2297,7 +2298,7 @@ msgstr "Excepté entre les pistes d'un même album ou d'une même CUE sheet"
#: ../build/src/ui_backendsettingspage.h:677
msgid "Exclusive mode (Experimental)"
msgstr ""
msgstr "Mode exclusif (Expérimental)"
#: ../build/src/ui_albumcoverexport.h:205
msgid "Existing covers"
@ -2391,7 +2392,7 @@ msgstr "Échec de la requête SQL : %1"
#: core/filesystemmusicstorage.cpp:57
#, qt-format
msgid "Failed to create directory %1."
msgstr ""
msgstr "Échec de la création du répertoire %1."
#: covermanager/albumcoverchoicecontroller.cpp:379
#: covermanager/albumcoverchoicecontroller.cpp:396
@ -2951,7 +2952,7 @@ msgstr "Installer strawberry via PPA :"
#: playlist/playlist.cpp:1409
msgid "Integrated Loudness"
msgstr ""
msgstr "Intensité sonore intégrée"
#: core/database.cpp:486
msgid "Integrity check"
@ -2972,7 +2973,7 @@ msgstr "Introduction des pistes"
#: device/mtpconnection.cpp:57
#, qt-format
msgid "Invalid MTP device: %1"
msgstr ""
msgstr "Dispositif MTP non valide : %1"
#: scrobbler/scrobblingapi20.cpp:254
msgid "Invalid reply from web browser. Missing token."
@ -3176,7 +3177,7 @@ msgstr "Profil de prédiction à long terme (PLT)"
#: playlist/playlist.cpp:1410
msgid "Loudness Range"
msgstr ""
msgstr "Plage d'intensité sonore"
#: globalshortcuts/globalshortcutsmanager.cpp:79
#: ../build/src/ui_mainwindow.h:634
@ -3611,7 +3612,7 @@ msgstr "Déconnecté"
#: dialogs/edittagdialog.cpp:1438
msgid "Not found."
msgstr ""
msgstr "Non trouvé"
#: device/deviceview.cpp:135
msgid "Not mounted - double click to mount"
@ -5961,12 +5962,12 @@ msgstr "Écrire des métadonnées lors de la sauvegarde des listes de lecture"
#: device/gpoddevice.cpp:288
msgid "Writing database failed."
msgstr ""
msgstr "L'écriture de la base de données a échoué"
#: device/gpoddevice.cpp:284
#, qt-format
msgid "Writing database failed: %1"
msgstr ""
msgstr "L'écriture de la base de données a échoué : %1"
#: collection/savedgroupingmanager.cpp:119 playlist/playlist.cpp:1380
#: organize/organizedialog.cpp:114 ../build/src/ui_groupbydialog.h:199
@ -6203,7 +6204,7 @@ msgstr "moins que"
#: dialogs/edittagdialog.cpp:1426
msgid "loading..."
msgstr ""
msgstr "chargement..."
#: smartplaylists/smartplaylistsearchterm.cpp:433
msgid "longest first"

View File

@ -72,7 +72,7 @@ QList<QUrl> FileViewList::UrlListFromSelection() const {
const QModelIndexList indexes = menu_selection_.indexes();
for (const QModelIndex &index : indexes) {
if (index.column() == 0) {
filenames << qobject_cast<QFileSystemModel*>(model())->fileInfo(index).canonicalFilePath();
filenames << QDir::cleanPath(qobject_cast<QFileSystemModel*>(model())->fileInfo(index).filePath());
}
}

View File

@ -25,7 +25,7 @@
class QResizeEvent;
class ResizableTextEdit : public QTextEdit {
Q_OBJECT
Q_OBJECT
public:
explicit ResizableTextEdit(QWidget *parent = nullptr);

View File

@ -47,7 +47,7 @@ class CollectionBackendTest : public ::testing::Test {
void SetUp() override {
database_.reset(new MemoryDatabase(nullptr));
backend_ = make_unique<CollectionBackend>();
backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
}
static Song MakeDummySong(int directory_id) {
@ -78,14 +78,14 @@ TEST_F(CollectionBackendTest, EmptyDatabase) {
TEST_F(CollectionBackendTest, AddDirectory) {
QSignalSpy spy(&*backend_, &CollectionBackend::DirectoryDiscovered);
QSignalSpy spy(&*backend_, &CollectionBackend::DirectoryAdded);
backend_->AddDirectory(QStringLiteral("/tmp"));
// Check the signal was emitted correctly
ASSERT_EQ(1, spy.count());
CollectionDirectory dir = spy[0][0].value<CollectionDirectory>();
EXPECT_EQ(QFileInfo(QStringLiteral("/tmp")).canonicalFilePath(), dir.path);
EXPECT_EQ(QStringLiteral("/tmp"), dir.path);
EXPECT_EQ(1, dir.id);
EXPECT_EQ(0, spy[0][1].value<CollectionSubdirectoryList>().size());
@ -132,7 +132,7 @@ class SingleSong : public CollectionBackendTest {
}
void AddDummySong() {
QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsDiscovered);
QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsAdded);
QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted);
// Add the song
@ -266,7 +266,7 @@ TEST_F(SingleSong, UpdateSong) {
new_song.set_title(QStringLiteral("A different title"));
QSignalSpy deleted_spy(&*backend_, &CollectionBackend::SongsDeleted);
QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsDiscovered);
QSignalSpy added_spy(&*backend_, &CollectionBackend::SongsAdded);
backend_->AddOrUpdateSongs(SongList() << new_song);
@ -389,7 +389,7 @@ TEST_F(TestUrls, TestUrls) {
}
QSignalSpy spy(&*backend_, &CollectionBackend::SongsDiscovered);
QSignalSpy spy(&*backend_, &CollectionBackend::SongsAdded);
backend_->AddOrUpdateSongs(songs);
if (HasFatalFailure()) return;
@ -474,7 +474,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
}
QSignalSpy spy(&*backend_, &CollectionBackend::SongsDiscovered);
QSignalSpy spy(&*backend_, &CollectionBackend::SongsAdded);
backend_->UpdateSongsBySongID(songs);
@ -495,7 +495,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
SongMap songs;
{
QSqlDatabase db(database_->Connect());
CollectionQuery query(db, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable));
CollectionQuery query(db, QLatin1String(SCollection::kSongsTable));
EXPECT_TRUE(backend_->ExecCollectionQuery(&query, songs));
}
@ -512,7 +512,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
}
{ // Remove some songs
QSignalSpy spy1(&*backend_, &CollectionBackend::SongsDiscovered);
QSignalSpy spy1(&*backend_, &CollectionBackend::SongsAdded);
QSignalSpy spy2(&*backend_, &CollectionBackend::SongsDeleted);
SongMap songs;
@ -558,7 +558,7 @@ TEST_F(UpdateSongsBySongID, UpdateSongsBySongID) {
{ // Update some songs
QSignalSpy spy1(&*backend_, &CollectionBackend::SongsDeleted);
QSignalSpy spy2(&*backend_, &CollectionBackend::SongsDiscovered);
QSignalSpy spy2(&*backend_, &CollectionBackend::SongsAdded);
SongMap songs;

View File

@ -54,7 +54,7 @@ class CollectionModelTest : public ::testing::Test {
void SetUp() override {
database_ = make_shared<MemoryDatabase>(nullptr);
backend_ = make_shared<CollectionBackend>();
backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kFtsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
backend_->Init(database_, nullptr, Song::Source::Collection, QLatin1String(SCollection::kSongsTable), QLatin1String(SCollection::kDirsTable), QLatin1String(SCollection::kSubdirsTable));
model_ = make_unique<CollectionModel>(backend_, nullptr);
added_dir_ = false;
@ -108,7 +108,7 @@ TEST_F(CollectionModelTest, WithInitialArtists) {
AddSong(QStringLiteral("Title"), QStringLiteral("Artist 1"), QStringLiteral("Album"), 123);
AddSong(QStringLiteral("Title"), QStringLiteral("Artist 2"), QStringLiteral("Album"), 123);
AddSong(QStringLiteral("Title"), QStringLiteral("Foo"), QStringLiteral("Album"), 123);
model_->Init(false);
model_->Init();
ASSERT_EQ(5, model_sorted_->rowCount(QModelIndex()));
EXPECT_EQ(QStringLiteral("A"), model_sorted_->index(0, 0, QModelIndex()).data().toString());
@ -128,7 +128,7 @@ TEST_F(CollectionModelTest, CompilationAlbums) {
song.set_ctime(0);
AddSong(song);
model_->Init(false);
model_->Init();
model_->fetchMore(model_->index(0, 0));
ASSERT_EQ(1, model_->rowCount(QModelIndex()));
@ -150,7 +150,7 @@ TEST_F(CollectionModelTest, NumericHeaders) {
AddSong(QStringLiteral("Title"), QStringLiteral("2artist"), QStringLiteral("Album"), 123);
AddSong(QStringLiteral("Title"), QStringLiteral("0artist"), QStringLiteral("Album"), 123);
AddSong(QStringLiteral("Title"), QStringLiteral("zartist"), QStringLiteral("Album"), 123);
model_->Init(false);
model_->Init();
ASSERT_EQ(6, model_sorted_->rowCount(QModelIndex()));
EXPECT_EQ(QStringLiteral("0-9"), model_sorted_->index(0, 0, QModelIndex()).data().toString());
@ -166,7 +166,7 @@ TEST_F(CollectionModelTest, MixedCaseHeaders) {
AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123);
AddSong(QStringLiteral("Title"), QStringLiteral("artist"), QStringLiteral("Album"), 123);
model_->Init(false);
model_->Init();
ASSERT_EQ(3, model_sorted_->rowCount(QModelIndex()));
EXPECT_EQ(QStringLiteral("A"), model_sorted_->index(0, 0, QModelIndex()).data().toString());
@ -178,7 +178,7 @@ TEST_F(CollectionModelTest, MixedCaseHeaders) {
TEST_F(CollectionModelTest, UnknownArtists) {
AddSong(QStringLiteral("Title"), QLatin1String(""), QStringLiteral("Album"), 123);
model_->Init(false);
model_->Init();
model_->fetchMore(model_->index(0, 0));
ASSERT_EQ(1, model_->rowCount(QModelIndex()));
@ -194,7 +194,7 @@ TEST_F(CollectionModelTest, UnknownAlbums) {
AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QLatin1String(""), 123);
AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123);
model_->Init(false);
model_->Init();
model_->fetchMore(model_->index(0, 0));
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
@ -228,7 +228,7 @@ TEST_F(CollectionModelTest, VariousArtistSongs) {
for (int i=0 ; i < 4 ; ++i)
AddSong(songs[i]);
model_->Init(false);
model_->Init();
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
model_->fetchMore(artist_index);
@ -250,7 +250,7 @@ TEST_F(CollectionModelTest, RemoveSongsLazyLoaded) {
Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1);
Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); two.set_id(2);
AddSong(QStringLiteral("Title 3"), QStringLiteral("Artist"), QStringLiteral("Album"), 123);
model_->Init(false);
model_->Init();
// Lazy load the items
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
@ -283,7 +283,7 @@ TEST_F(CollectionModelTest, RemoveSongsNotLazyLoaded) {
Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1);
Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); two.set_id(2);
model_->Init(false);
model_->Init();
// Remove the first two songs
QSignalSpy spy_preremove(&*model_, &CollectionModel::rowsAboutToBeRemoved);
@ -303,7 +303,7 @@ TEST_F(CollectionModelTest, RemoveEmptyAlbums) {
Song one = AddSong(QStringLiteral("Title 1"), QStringLiteral("Artist"), QStringLiteral("Album 1"), 123); one.set_id(1);
Song two = AddSong(QStringLiteral("Title 2"), QStringLiteral("Artist"), QStringLiteral("Album 2"), 123); two.set_id(2);
Song three = AddSong(QStringLiteral("Title 3"), QStringLiteral("Artist"), QStringLiteral("Album 2"), 123); three.set_id(3);
model_->Init(false);
model_->Init();
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
model_->fetchMore(artist_index);
@ -328,7 +328,7 @@ TEST_F(CollectionModelTest, RemoveEmptyAlbums) {
TEST_F(CollectionModelTest, RemoveEmptyArtists) {
Song one = AddSong(QStringLiteral("Title"), QStringLiteral("Artist"), QStringLiteral("Album"), 123); one.set_id(1);
model_->Init(false);
model_->Init();
// Lazy load the items
QModelIndex artist_index = model_->index(0, 0, QModelIndex());
@ -351,8 +351,8 @@ TEST_F(CollectionModelTest, RemoveEmptyArtists) {
// Test to check that the container nodes are created identical and unique all through the model with all possible collection groupings.
// model1 - Nodes are created from a complete reset done through lazy-loading.
// model2 - Initial container nodes are created in SongsDiscovered.
// model3 - All container nodes are created in SongsDiscovered.
// model2 - Initial container nodes are created in SongsAdded.
// model3 - All container nodes are created in SongsAdded.
// WARNING: This test can take up to 30 minutes to complete.
#if 0
@ -567,9 +567,9 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
backend1 = make_unique<CollectionBackend>();
backend2= make_unique<CollectionBackend>();
backend3 = make_unique<CollectionBackend>();
backend1->Init(database1.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
backend2->Init(database2.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
backend3->Init(database3.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kFtsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
backend1->Init(database1.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
backend2->Init(database2.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
backend3->Init(database3.get(), Song::Source::Collection, SCollection::kSongsTable, SCollection::kDirsTable, SCollection::kSubdirsTable);
model1 = make_unique<CollectionModel>(backend1.get(), nullptr);
model2 = make_unique<CollectionModel>(backend2.get(), nullptr);
model3 = make_unique<CollectionModel>(backend3.get(), nullptr);
@ -596,7 +596,7 @@ TEST_F(CollectionModelTest, TestContainerNodes) {
ASSERT_EQ(model2->song_nodes().count(), 0);
ASSERT_EQ(model3->song_nodes().count(), songs.count());
model1->Init(false);
model1->Init();
model1->ExpandAll();
model2->ExpandAll();