diff --git a/src/Media.cpp b/src/Media.cpp index 1e8011ac23408d8d95d3bf5265591240e7e5621b..5ca08cc2294be11bd09baaef298e25dbd757ea45 100644 --- a/src/Media.cpp +++ b/src/Media.cpp @@ -886,6 +886,7 @@ std::string Media::sortRequest( const QueryParameters* params ) auto sort = params != nullptr ? params->sort : SortingCriteria::Default; auto desc = params != nullptr ? params->desc : false; + auto descAdded = false; switch ( sort ) { case SortingCriteria::Duration: @@ -902,7 +903,8 @@ std::string Media::sortRequest( const QueryParameters* params ) desc = !desc; // Make decreasing order default for play count sorting break; case SortingCriteria::Filename: - req += "m.filename"; + req += sqlite::Tools::sortedStringField("m.filename",desc,true); + descAdded = true; break; case SortingCriteria::LastModificationDate: req += "f.last_modification_date"; @@ -911,30 +913,36 @@ std::string Media::sortRequest( const QueryParameters* params ) req += "f.size"; break; case SortingCriteria::Album: - if ( desc == true ) - req += "alb.title DESC, att.track_number"; + + req += sqlite::Tools::sortedStringField("alb.title",desc,false); + if ( desc == true ) + req += ", att.track_number"; else - req += "alb.title, att.track_number"; + req += ", att.track_number"; + descAdded = true; break; case SortingCriteria::Artist: - req += "art.name"; + req += sqlite::Tools::sortedStringField("art.name",desc,false); + descAdded = true; break; case SortingCriteria::TrackId: - if ( desc == true ) - req += "alb.title, att.track_number DESC, att.disc_number"; + req += sqlite::Tools::sortedStringField("alb.title",false,false); + if ( desc == true ) + req += ", att.track_number DESC, att.disc_number"; else - req += "alb.title, att.track_number, att.disc_number"; + req += ", att.track_number, att.disc_number"; + descAdded = true; break; default: LOG_WARN( "Unsupported sorting criteria, falling back to SortingCriteria::Default (Alpha)" ); /* fall-through */ case SortingCriteria::Default: case SortingCriteria::Alpha: - req += "m.title"; + req += sqlite::Tools::sortedStringField("m.title",desc,false); + descAdded = true; break; } - if ( desc == true && sort != SortingCriteria::Album && - sort != SortingCriteria::TrackId ) + if ( desc == true && !descAdded) req += " DESC"; return req; } diff --git a/src/database/SqliteTools.h b/src/database/SqliteTools.h index 95e539c92e5f25cc3b1dba42eb50ddc6321cae86..e04b21e0fd61fc421d361782fcb6b2f7a46567b1 100644 --- a/src/database/SqliteTools.h +++ b/src/database/SqliteTools.h @@ -459,7 +459,25 @@ class Tools * sqlite, and a '*' appended */ static std::string sanitizePattern( const std::string& pattern ); + static std::string sortedStringField( const std::string& field, bool desc, bool hasExt) + { + auto d = desc ? " DESC":" ASC"; + + auto removeExt = "rtrim(rtrim(" + field + ",replace(" + field + ",'.','')),'.')"; + auto lengthAfterRemoveExtNumber = "length(rtrim(" + removeExt + ",'0123456789'))"; + + auto removeLastNumber = "rtrim(" + field + ",'0123456789')"; + auto removeNumberAndExt = "trim(" + removeExt + ",'0123456789')"; + auto lastNumberNoExt = "cast(ltrim(" + field + ",rtrim(" + field + ",'0123456789')) as INTEGER)"; + auto lastNumberAfterRemoveExt = "cast(substr(" + removeExt + "," + lengthAfterRemoveExtNumber + " + 1) as INTEGER) "; + + auto titleNoExtNumber = (hasExt ? removeNumberAndExt : removeLastNumber) + " COLLATE NOCASE " + d; + auto lastNumber = (hasExt ? lastNumberAfterRemoveExt : lastNumberNoExt) + d; + auto firstNumber = " cast(" + field + " as INTEGER) " + d; + + return titleNoExtNumber + "," + firstNumber + " , " + lastNumber; + } private: template static void executeRequestLocked( sqlite::Connection* dbConnection, diff --git a/test/unittest/MediaTests.cpp b/test/unittest/MediaTests.cpp index 01bddb92e528a57a5739b9454d7810173244f158..530e44560a1ea013bfe85ce5390b5d4211229688 100644 --- a/test/unittest/MediaTests.cpp +++ b/test/unittest/MediaTests.cpp @@ -867,7 +867,46 @@ TEST_F( Medias, Pagination ) media = paginator->items( 1, i ); } } +TEST_F( Medias, SortFilenameStartsWithNumber ) +{ + auto m1 = std::static_pointer_cast( ml->addMedia( "9 song.mp3", IMedia::Type::Audio ) ); + auto m2 = std::static_pointer_cast( ml->addMedia( "1 song.mp3", IMedia::Type::Audio ) ); + auto m3 = std::static_pointer_cast( ml->addMedia( "10 song.mp3", IMedia::Type::Audio ) ); + + QueryParameters params { SortingCriteria::Filename, false }; + auto media = ml->audioFiles( ¶ms )->all(); + ASSERT_EQ( 3u, media.size() ); + ASSERT_EQ( m2->id(), media[0]->id() ); + ASSERT_EQ( m1->id(), media[1]->id() ); + ASSERT_EQ( m3->id(), media[2]->id() ); + params.desc = true; + media = ml->audioFiles( ¶ms )->all(); + ASSERT_EQ( 3u, media.size() ); + ASSERT_EQ( m2->id(), media[2]->id() ); + ASSERT_EQ( m1->id(), media[1]->id() ); + ASSERT_EQ( m3->id(), media[0]->id() ); +} +TEST_F( Medias, SortFilenameEndsWithNumber ) +{ + auto m1 = std::static_pointer_cast( ml->addMedia( "song9.mp3", IMedia::Type::Audio ) ); + auto m2 = std::static_pointer_cast( ml->addMedia( "song1.mp3", IMedia::Type::Audio ) ); + auto m3 = std::static_pointer_cast( ml->addMedia( "song10.mp3", IMedia::Type::Audio ) ); + + QueryParameters params { SortingCriteria::Filename, false }; + auto media = ml->audioFiles( ¶ms )->all(); + ASSERT_EQ( 3u, media.size() ); + ASSERT_EQ( m2->id(), media[0]->id() ); + ASSERT_EQ( m1->id(), media[1]->id() ); + ASSERT_EQ( m3->id(), media[2]->id() ); + + params.desc = true; + media = ml->audioFiles( ¶ms )->all(); + ASSERT_EQ( 3u, media.size() ); + ASSERT_EQ( m2->id(), media[2]->id() ); + ASSERT_EQ( m1->id(), media[1]->id() ); + ASSERT_EQ( m3->id(), media[0]->id() ); +} TEST_F( Medias, SortFilename ) { auto m1 = std::static_pointer_cast( ml->addMedia( "AAAAB.mp3", IMedia::Type::Audio ) );