feat(mac,ios,playback,store): initial macos implementation
This commit is contained in:
1 parent
e5bb559d7f
commit
d2d7166cf0
72 files changed
+4452
-318
No files matched your search
@@ -5,7 +5,24 @@ public enum AlbumSort: String, CaseIterable, Sendable {
|
||||
case recentlyAdded, title, artist, year
|
||||
}
|
||||
|
||||
public enum SearchHit: Identifiable, Hashable, Codable, Sendable {
|
||||
case artist(Artist)
|
||||
case album(Album)
|
||||
case track(Track)
|
||||
case playlist(Playlist)
|
||||
|
||||
public var id: EntityID {
|
||||
switch self {
|
||||
case let .artist(artist): artist.id
|
||||
case let .album(album): album.id
|
||||
case let .track(track): track.id
|
||||
case let .playlist(playlist): playlist.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SearchResults: Sendable {
|
||||
public var top: [SearchHit] = []
|
||||
public var artists: [Artist] = []
|
||||
public var albums: [Album] = []
|
||||
public var tracks: [Track] = []
|
||||
|
||||
@@ -115,7 +115,7 @@ public actor MemoryLibrary: Library {
|
||||
|
||||
public func tracks(byArtist id: EntityID) -> [Track] {
|
||||
tracksByID.values.filter { $0.artistID == id }
|
||||
.sorted { (annotations[$0.id]?.playCount ?? 0, $1.title) > (annotations[$1.id]?.playCount ?? 0, $0.title) }
|
||||
.sorted { (plays($0), $1.title) > (plays($1), $0.title) }
|
||||
}
|
||||
|
||||
public func tracks(inGenre genre: String) -> [Track] {
|
||||
@@ -181,14 +181,59 @@ public actor MemoryLibrary: Library {
|
||||
public func search(_ query: String) -> SearchResults {
|
||||
let needle = query.trimmingCharacters(in: .whitespaces)
|
||||
guard !needle.isEmpty else { return SearchResults() }
|
||||
func matches(_ text: String) -> Bool {
|
||||
text.range(of: needle, options: [.caseInsensitive, .diacriticInsensitive]) != nil
|
||||
func named(_ name: String) -> Int? {
|
||||
relevance(of: needle, in: name)
|
||||
}
|
||||
func credited(_ title: String, _ artist: String) -> Int? {
|
||||
[named(title), named(artist).map { $0 + 1 }].compactMap(\.self).min()
|
||||
}
|
||||
let popular = tracks().sorted { (plays($0), $1.title) > (plays($1), $0.title) }
|
||||
let artists = ranked(artists()) { named($0.name) }
|
||||
let tracks = Array(ranked(popular) { credited($0.title, $0.artistName) }.prefix(50))
|
||||
let albums = ranked(albums(sortedBy: .title)) { credited($0.title, $0.artistName) }
|
||||
let playlists = ranked(playlists()) { named($0.name) }
|
||||
let candidates = artists.prefix(3).map { $0.map(SearchHit.artist) }
|
||||
+ tracks.prefix(12).map { $0.map(SearchHit.track) }
|
||||
+ albums.prefix(4).map { $0.map(SearchHit.album) }
|
||||
+ playlists.prefix(3).map { $0.map(SearchHit.playlist) }
|
||||
var results = SearchResults()
|
||||
results.artists = artists().filter { matches($0.name) }
|
||||
results.albums = albums(sortedBy: .title).filter { matches($0.title) }
|
||||
results.tracks = Array(tracks().filter { matches($0.title) }.prefix(50))
|
||||
results.playlists = playlists().filter { matches($0.name) }
|
||||
results.top = ranked(candidates) { $0.score }.map(\.item.item)
|
||||
results.artists = artists.map(\.item)
|
||||
results.albums = albums.map(\.item)
|
||||
results.tracks = tracks.map(\.item)
|
||||
results.playlists = playlists.map(\.item)
|
||||
return results
|
||||
}
|
||||
|
||||
private func plays(_ track: Track) -> Int {
|
||||
annotations[track.id]?.playCount ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
private struct Ranked<Item> {
|
||||
let item: Item
|
||||
let score: Int
|
||||
|
||||
func map<Other>(_ transform: (Item) -> Other) -> Ranked<Other> {
|
||||
Ranked<Other>(item: transform(item), score: score)
|
||||
}
|
||||
}
|
||||
|
||||
private func ranked<Item>(_ items: [Item], by score: (Item) -> Int?) -> [Ranked<Item>] {
|
||||
items.enumerated()
|
||||
.compactMap { offset, item in score(item).map { (Ranked(item: item, score: $0), offset) } }
|
||||
.sorted { ($0.0.score, $0.1) < ($1.0.score, $1.1) }
|
||||
.map(\.0)
|
||||
}
|
||||
|
||||
private func relevance(of needle: String, in text: String) -> Int? {
|
||||
let options: String.CompareOptions = [.caseInsensitive, .diacriticInsensitive]
|
||||
guard let range = text.range(of: needle, options: options) else { return nil }
|
||||
if text.compare(needle, options: options) == .orderedSame {
|
||||
return 0
|
||||
}
|
||||
if range.lowerBound == text.startIndex {
|
||||
return 1
|
||||
}
|
||||
return text[text.index(before: range.lowerBound)].isLetter ? 3 : 2
|
||||
}
|
||||
Reference in new issue
Block a user