Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Packages/ClementineKit/Sources/ClementineKit/Browse/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ final class Database {
handle = nil
throw DatabaseError(message: message)
}
// Text in order for the phone's language, whatever its case or accents: SQLite's own
// collation orders text by its bytes, putting "abba" after "ZZ Top", and "脡mile" after
// both. Named as the one Android's SQLite has, so both clients' SQL is the same. Names
// differing only in case are equal here: the query then orders them by their bytes, so
// "Apple" comes before "apple" on both clients.
sqlite3_create_collation_v2(handle, "LOCALIZED", SQLITE_UTF8, nil, { _, length1, text1, length2, text2 in
let first = String(decoding: UnsafeRawBufferPointer(start: text1, count: Int(length1)), as: UTF8.self)
let second = String(decoding: UnsafeRawBufferPointer(start: text2, count: Int(length2)), as: UTF8.self)
switch first.compare(second, options: .caseInsensitive, locale: .current) {
case .orderedAscending: return -1
case .orderedSame: return 0
case .orderedDescending: return 1
}
}, nil)
}

deinit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,13 @@ struct SongQuery {
}
sql += ", CAST(filename AS TEXT), artist, album FROM \(table)"
sql += whereClause(selection)
// Names in order for the phone's language, whatever their case or accents (Database), then
// by their bytes, so names differing only in case are always in the same order.
let field = fields[level]
if isSongLevel {
sql += " ORDER BY album, disc, track \(sorting.rawValue)"
sql += " ORDER BY album COLLATE LOCALIZED, album, disc, track \(sorting.rawValue)"
} else {
sql += " GROUP BY \(fields[level]) ORDER BY \(fields[level]) \(sorting.rawValue)"
sql += " GROUP BY \(field) ORDER BY \(field) COLLATE LOCALIZED \(sorting.rawValue), \(field) \(sorting.rawValue)"
}

return try database.query(sql, selection).map { row in
Expand Down
38 changes: 38 additions & 0 deletions Packages/ClementineKit/Tests/ClementineKitTests/LibraryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,44 @@ struct LibraryTests {
#expect(albums.items.map(\.value) == ["Suite bergamasque", "Gymnop茅dies", ""])
}

/// Names whose case and accents differ, and an album's songs whose names aren't in track order.
private func unsortedLibrary() throws -> Database {
let database = try Database(path: nil)
try database.execute("CREATE TABLE songs (artist TEXT, album TEXT, title TEXT, filename TEXT, disc INTEGER, track INTEGER)")
let songs: [(String, String, String, Int64)] = [
("ZZ Top", "Eliminator", "Song", 1), ("abba", "arrival", "Song", 1),
("脡rik Satie", "Gymnop茅dies", "Song", 1), ("Blondie", "Parallel Lines", "Song", 1),
// Names differing only in case, the lower-case one first, so their order doesn't come
// from the rows'.
("Various", "zebra", "Song", 1), ("Various", "apple", "Song", 1), ("Various", "Apple", "Song", 1),
("Various", "脡t茅", "Song", 3), ("Various", "脡t茅", "b second", 2), ("Various", "脡t茅", "A first", 1),
]
for song in songs {
try database.run("INSERT INTO songs VALUES (?, ?, ?, 'file:///song', 1, ?)",
[.text(song.0), .text(song.1), .text(song.2), .integer(song.3)])
}
return database
}

@Test func sortsIgnoringCaseAndAccents() throws {
let database = try unsortedLibrary()
let query = SongQuery(fields: ["artist", "album", "title"], sorting: .ascending, table: "songs")
#expect(try query.items(in: database, level: 0, selection: []).map(\.value)
== ["abba", "Blondie", "脡rik Satie", "Various", "ZZ Top"])
// Names differing only in case in a fixed order, the same as on Android.
#expect(try query.items(in: database, level: 1, selection: ["Various"]).map(\.value)
== ["Apple", "apple", "脡t茅", "zebra"])
// An album's songs stay in track order.
#expect(try query.items(in: database, level: 2, selection: ["Various", "脡t茅"]).map(\.value)
== ["A first", "b second", "Song"])

let descending = SongQuery(fields: ["artist", "album", "title"], sorting: .descending, table: "songs")
#expect(try descending.items(in: database, level: 0, selection: []).map(\.value)
== ["ZZ Top", "Various", "脡rik Satie", "Blondie", "abba"])
#expect(try descending.items(in: database, level: 1, selection: ["Various"]).map(\.value)
== ["zebra", "脡t茅", "apple", "Apple"])
}

@Test func groupsByYear() async throws {
let library = try await SampleLibrary().store()
let debussy = try await library.level(below: nil, grouping: .artistYear, sorting: .ascending).items[1]
Expand Down
Loading