Features: - Dual-AVPlayer Smart DJ crossfade with LUFS normalization - Mitsuha-style FFT visualizer (real-time + offline pre-computed) - Companion API integration (Smart DJ, tag editing, vis frames) - Offline-first SyncEngine with delta sync and album detail pre-caching - Audio pre-fetcher for gapless queue playback - Optimistic action queue (star/unstar with background retry) - ShazamKit recognition with MusicKit preview playback - Radio streaming with HLS/PLS/M3U support and buffer seek - Watch app with Crown Sequencer and Ultra speaker support - Batch metadata editing with album_artist fix for split albums - Cache-first UI pattern across all views - NWPathMonitor offline detection with reactive song greying
319 lines
6.9 KiB
Swift
319 lines
6.9 KiB
Swift
import Foundation
|
|
|
|
// MARK: - Server Configuration
|
|
struct ServerConfig: Codable, Identifiable, Hashable {
|
|
var id: UUID = UUID()
|
|
var name: String
|
|
var url: String
|
|
var username: String
|
|
var password: String // stored in Keychain in production
|
|
var isActive: Bool = true
|
|
|
|
var baseURL: URL? { URL(string: url) }
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(id)
|
|
}
|
|
}
|
|
|
|
// MARK: - Subsonic API Response Wrappers
|
|
struct SubsonicResponse: Codable {
|
|
let subsonicResponse: SubsonicResponseBody
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case subsonicResponse = "subsonic-response"
|
|
}
|
|
}
|
|
|
|
struct SubsonicResponseBody: Codable {
|
|
let status: String
|
|
let version: String?
|
|
let type: String?
|
|
let serverVersion: String?
|
|
let openSubsonic: Bool?
|
|
let error: SubsonicError?
|
|
|
|
// Various response types
|
|
let musicFolders: MusicFoldersContainer?
|
|
let indexes: IndexesContainer?
|
|
let artists: ArtistsContainer?
|
|
let artist: ArtistWithAlbums?
|
|
let album: AlbumWithSongs?
|
|
let song: Song?
|
|
let directory: DirectoryContainer?
|
|
let searchResult3: SearchResult3?
|
|
let albumList2: AlbumList2Container?
|
|
let playlists: PlaylistsContainer?
|
|
let playlist: PlaylistWithSongs?
|
|
let genres: GenresContainer?
|
|
let starred2: Starred2Container?
|
|
let nowPlaying: NowPlayingContainer?
|
|
let randomSongs: RandomSongsContainer?
|
|
let scanStatus: ScanStatus?
|
|
let lyrics: LyricsResult?
|
|
let internetRadioStations: InternetRadioContainer?
|
|
let similarSongs2: SimilarSongsContainer?
|
|
}
|
|
|
|
struct SubsonicError: Codable {
|
|
let code: Int
|
|
let message: String
|
|
}
|
|
|
|
// MARK: - Music Folders
|
|
struct MusicFoldersContainer: Codable {
|
|
let musicFolder: [MusicFolder]?
|
|
}
|
|
|
|
struct MusicFolder: Codable, Identifiable {
|
|
let id: String
|
|
let name: String?
|
|
}
|
|
|
|
// MARK: - Indexes / Artists
|
|
struct IndexesContainer: Codable {
|
|
let index: [ArtistIndex]?
|
|
let lastModified: Int64?
|
|
let ignoredArticles: String?
|
|
}
|
|
|
|
struct ArtistsContainer: Codable {
|
|
let index: [ArtistIndex]?
|
|
let ignoredArticles: String?
|
|
}
|
|
|
|
struct ArtistIndex: Codable, Identifiable {
|
|
let name: String
|
|
let artist: [Artist]?
|
|
|
|
var id: String { name }
|
|
}
|
|
|
|
struct Artist: Codable, Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let coverArt: String?
|
|
let artistImageUrl: String?
|
|
let albumCount: Int?
|
|
let starred: String?
|
|
let musicBrainzId: String?
|
|
let sortName: String?
|
|
}
|
|
|
|
// MARK: - Albums
|
|
struct ArtistWithAlbums: Codable, Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let coverArt: String?
|
|
let albumCount: Int?
|
|
let album: [Album]?
|
|
}
|
|
|
|
struct Album: Codable, Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let artist: String?
|
|
let artistId: String?
|
|
let coverArt: String?
|
|
let songCount: Int?
|
|
let duration: Int?
|
|
let playCount: Int?
|
|
let created: String?
|
|
let starred: String?
|
|
let year: Int?
|
|
let genre: String?
|
|
}
|
|
|
|
struct AlbumWithSongs: Codable, Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let artist: String?
|
|
let artistId: String?
|
|
let coverArt: String?
|
|
let songCount: Int?
|
|
let duration: Int?
|
|
let playCount: Int?
|
|
let created: String?
|
|
let starred: String?
|
|
let year: Int?
|
|
let genre: String?
|
|
let song: [Song]?
|
|
}
|
|
|
|
struct AlbumList2Container: Codable {
|
|
let album: [Album]?
|
|
}
|
|
|
|
// MARK: - Songs
|
|
struct Song: Codable, Identifiable {
|
|
let id: String
|
|
let parent: String?
|
|
let isDir: Bool?
|
|
let title: String
|
|
let album: String?
|
|
let artist: String?
|
|
let track: Int?
|
|
let year: Int?
|
|
let genre: String?
|
|
let coverArt: String?
|
|
let size: Int64?
|
|
let contentType: String?
|
|
let suffix: String?
|
|
let transcodedContentType: String?
|
|
let transcodedSuffix: String?
|
|
let duration: Int?
|
|
let bitRate: Int?
|
|
let path: String?
|
|
let playCount: Int?
|
|
let discNumber: Int?
|
|
let created: String?
|
|
let albumId: String?
|
|
let artistId: String?
|
|
let type: String?
|
|
let starred: String?
|
|
let bpm: Int?
|
|
let musicBrainzId: String?
|
|
|
|
var durationFormatted: String {
|
|
guard let dur = duration else { return "--:--" }
|
|
let min = dur / 60
|
|
let sec = dur % 60
|
|
return String(format: "%d:%02d", min, sec)
|
|
}
|
|
}
|
|
|
|
// MARK: - Directory
|
|
struct DirectoryContainer: Codable {
|
|
let id: String
|
|
let name: String?
|
|
let parent: String?
|
|
let child: [Song]?
|
|
}
|
|
|
|
// MARK: - Search
|
|
struct SearchResult3: Codable {
|
|
let artist: [Artist]?
|
|
let album: [Album]?
|
|
let song: [Song]?
|
|
}
|
|
|
|
// MARK: - Playlists
|
|
struct PlaylistsContainer: Codable {
|
|
let playlist: [Playlist]?
|
|
}
|
|
|
|
struct Playlist: Codable, Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let comment: String?
|
|
let songCount: Int?
|
|
let duration: Int?
|
|
let coverArt: String?
|
|
let owner: String?
|
|
let `public`: Bool?
|
|
let created: String?
|
|
let changed: String?
|
|
}
|
|
|
|
struct PlaylistWithSongs: Codable, Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let comment: String?
|
|
let songCount: Int?
|
|
let duration: Int?
|
|
let coverArt: String?
|
|
let owner: String?
|
|
let `public`: Bool?
|
|
let created: String?
|
|
let changed: String?
|
|
let entry: [Song]?
|
|
}
|
|
|
|
// MARK: - Genres
|
|
struct GenresContainer: Codable {
|
|
let genre: [Genre]?
|
|
}
|
|
|
|
struct Genre: Codable, Identifiable {
|
|
let value: String
|
|
let songCount: Int?
|
|
let albumCount: Int?
|
|
|
|
var id: String { value }
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case value, songCount, albumCount
|
|
}
|
|
}
|
|
|
|
// MARK: - Starred
|
|
struct Starred2Container: Codable {
|
|
let artist: [Artist]?
|
|
let album: [Album]?
|
|
let song: [Song]?
|
|
}
|
|
|
|
// MARK: - Now Playing
|
|
struct NowPlayingContainer: Codable {
|
|
let entry: [NowPlayingEntry]?
|
|
}
|
|
|
|
struct NowPlayingEntry: Codable, Identifiable {
|
|
let id: String
|
|
let title: String
|
|
let artist: String?
|
|
let album: String?
|
|
let username: String?
|
|
let minutesAgo: Int?
|
|
let playerId: Int?
|
|
let playerName: String?
|
|
}
|
|
|
|
// MARK: - Random Songs
|
|
struct RandomSongsContainer: Codable {
|
|
let song: [Song]?
|
|
}
|
|
|
|
// MARK: - Scan Status
|
|
struct ScanStatus: Codable {
|
|
let scanning: Bool
|
|
let count: Int?
|
|
}
|
|
|
|
// MARK: - Lyrics
|
|
struct LyricsResult: Codable {
|
|
let artist: String?
|
|
let title: String?
|
|
let value: String?
|
|
}
|
|
|
|
// MARK: - Offline Download
|
|
struct DownloadedSong: Codable, Identifiable {
|
|
let id: String
|
|
let serverId: UUID
|
|
let song: Song
|
|
let localPath: String
|
|
let downloadDate: Date
|
|
let fileSize: Int64
|
|
}
|
|
|
|
// MARK: - Internet Radio
|
|
struct InternetRadioContainer: Codable {
|
|
let internetRadioStation: [RadioStation]?
|
|
}
|
|
|
|
struct RadioStation: Codable, Identifiable {
|
|
let id: String
|
|
let name: String
|
|
let streamUrl: String
|
|
let homePageUrl: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, name, streamUrl, homePageUrl
|
|
}
|
|
}
|
|
|
|
// MARK: - Similar Songs
|
|
struct SimilarSongsContainer: Codable {
|
|
let song: [Song]?
|
|
}
|