diff --git a/Music/Services/DatabaseService.swift b/Music/Services/DatabaseService.swift index b5361dc..59c74d5 100644 --- a/Music/Services/DatabaseService.swift +++ b/Music/Services/DatabaseService.swift @@ -95,6 +95,15 @@ nonisolated final class DatabaseService: Sendable { ) } + migrator.registerMigration("v3-create-smart-playlists") { db in + try db.create(table: "smart_playlists") { t in + t.autoIncrementedPrimaryKey("id") + t.column("name", .text).notNull().unique() + t.column("searchQuery", .text).notNull() + t.column("createdAt", .datetime).notNull() + } + } + try migrator.migrate(db) } @@ -340,4 +349,54 @@ nonisolated final class DatabaseService: Sendable { ) } } + + // MARK: - Smart Playlists + + func createSmartPlaylist(name: String, searchQuery: String) throws -> SmartPlaylist { + try dbPool.write { db in + var smartPlaylist = SmartPlaylist( + id: nil, name: name, searchQuery: searchQuery, createdAt: Date() + ) + try smartPlaylist.insert(db) + return smartPlaylist + } + } + + func renameSmartPlaylist(id: Int64, name: String) throws { + try dbPool.write { db in + try db.execute( + sql: "UPDATE smart_playlists SET name = ? WHERE id = ?", + arguments: [name, id] + ) + } + } + + func updateSmartPlaylistQuery(id: Int64, searchQuery: String) throws { + try dbPool.write { db in + try db.execute( + sql: "UPDATE smart_playlists SET searchQuery = ? WHERE id = ?", + arguments: [searchQuery, id] + ) + } + } + + func deleteSmartPlaylist(id: Int64) throws { + try dbPool.write { db in + try db.execute(sql: "DELETE FROM smart_playlists WHERE id = ?", arguments: [id]) + } + } + + func fetchSmartPlaylists() throws -> [SmartPlaylist] { + try dbPool.read { db in + try SmartPlaylist.fetchAll( + db, sql: "SELECT * FROM smart_playlists ORDER BY name COLLATE NOCASE ASC" + ) + } + } + + func fetchSmartPlaylists(db: Database) throws -> [SmartPlaylist] { + try SmartPlaylist.fetchAll( + db, sql: "SELECT * FROM smart_playlists ORDER BY name COLLATE NOCASE ASC" + ) + } } diff --git a/MusicTests/SmartPlaylistTests.swift b/MusicTests/SmartPlaylistTests.swift index 5d1ce8c..b099238 100644 --- a/MusicTests/SmartPlaylistTests.swift +++ b/MusicTests/SmartPlaylistTests.swift @@ -15,4 +15,65 @@ struct SmartPlaylistTests { #expect(sp.searchQuery == "miles davis") #expect(sp.isSmartPlaylist == true) } + + // Creates a smart playlist in the database and fetches it back. + @Test func createAndFetchSmartPlaylist() throws { + let db = try DatabaseService(inMemory: true) + let sp = try db.createSmartPlaylist(name: "Jazz Vibes", searchQuery: "jazz") + + #expect(sp.id != nil) + #expect(sp.name == "Jazz Vibes") + #expect(sp.searchQuery == "jazz") + + let all = try db.fetchSmartPlaylists() + #expect(all.count == 1) + #expect(all[0].name == "Jazz Vibes") + } + + // Renames a smart playlist and verifies the new name persists. + @Test func renameSmartPlaylist() throws { + let db = try DatabaseService(inMemory: true) + let sp = try db.createSmartPlaylist(name: "Old Name", searchQuery: "old") + try db.renameSmartPlaylist(id: sp.id!, name: "New Name") + + let all = try db.fetchSmartPlaylists() + #expect(all[0].name == "New Name") + } + + // Updates the search query of a smart playlist. + @Test func updateSmartPlaylistQuery() throws { + let db = try DatabaseService(inMemory: true) + let sp = try db.createSmartPlaylist(name: "Jazz", searchQuery: "jazz") + try db.updateSmartPlaylistQuery(id: sp.id!, searchQuery: "jazz fusion") + + let all = try db.fetchSmartPlaylists() + #expect(all[0].searchQuery == "jazz fusion") + } + + // Deletes a smart playlist and verifies it's gone. + @Test func deleteSmartPlaylist() throws { + let db = try DatabaseService(inMemory: true) + let sp = try db.createSmartPlaylist(name: "To Delete", searchQuery: "delete") + try db.deleteSmartPlaylist(id: sp.id!) + + let all = try db.fetchSmartPlaylists() + #expect(all.isEmpty) + } + + // Verifies smart playlist tracks are computed via FTS5 search (not stored). + @Test func smartPlaylistReturnsDynamicResults() throws { + let db = try DatabaseService(inMemory: true) + var t1 = Track.fixture(fileURL: "/a.mp3", title: "Kind of Blue", artist: "Miles Davis") + var t2 = Track.fixture(fileURL: "/b.mp3", title: "Hotel California", artist: "Eagles") + var t3 = Track.fixture(fileURL: "/c.mp3", title: "Bitches Brew", artist: "Miles Davis") + try db.insert(&t1) + try db.insert(&t2) + try db.insert(&t3) + + // Smart playlist uses the existing fetchTracks with its searchQuery + let results = try db.fetchTracks(search: "miles davis", sortColumn: "title", ascending: true) + #expect(results.count == 2) + #expect(results[0].title == "Bitches Brew") + #expect(results[1].title == "Kind of Blue") + } }