ChadMusic.swift 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import Foundation
  2. // MARK: - Chad Music API Response Models
  3. /// A category entry from GET /api/cat/:category (e.g., album, artist, genre).
  4. struct ChadCategory: Codable, Identifiable, Hashable {
  5. let item: String
  6. let count: Int?
  7. var id: String { item }
  8. var name: String { item }
  9. enum CodingKeys: String, CodingKey {
  10. case item, count
  11. }
  12. init(from decoder: Decoder) throws {
  13. let container = try decoder.container(keyedBy: CodingKeys.self)
  14. // item may be a string ("Rock") or a number (2012) depending on category type
  15. if let stringValue = try? container.decode(String.self, forKey: .item) {
  16. item = stringValue
  17. } else if let intValue = try? container.decode(Int.self, forKey: .item) {
  18. item = String(intValue)
  19. } else if let doubleValue = try? container.decode(Double.self, forKey: .item) {
  20. // 2012.0 → "2012", but preserve fractional values
  21. if doubleValue == doubleValue.rounded() {
  22. item = String(Int(doubleValue))
  23. } else {
  24. item = String(doubleValue)
  25. }
  26. } else {
  27. throw DecodingError.typeMismatch(
  28. String.self,
  29. DecodingError.Context(
  30. codingPath: container.codingPath + [CodingKeys.item],
  31. debugDescription: "Expected String, Int, or Double for 'item'"
  32. )
  33. )
  34. }
  35. count = try container.decodeIfPresent(Int.self, forKey: .count)
  36. }
  37. init(item: String, count: Int?) {
  38. self.item = item
  39. self.count = count
  40. }
  41. }
  42. /// An album from the Chad Music API.
  43. struct ChadAlbum: Codable, Identifiable, Hashable {
  44. let id: String
  45. let album: String?
  46. let artist: String?
  47. let year: Int?
  48. let genre: String?
  49. let trackCount: Int?
  50. let cover: String?
  51. let publisher: String?
  52. let country: String?
  53. let type: String?
  54. let status: String?
  55. let totalDuration: Double?
  56. let originalDate: String?
  57. let mbId: String?
  58. var title: String { album ?? "Untitled" }
  59. enum CodingKeys: String, CodingKey {
  60. case id, album, artist, year, genre, cover, publisher, country
  61. case trackCount = "track_count"
  62. case totalDuration = "total_duration"
  63. case originalDate = "original_date"
  64. case mbId = "mb_id"
  65. case type, status
  66. }
  67. }
  68. /// A track from GET /api/album/:id/tracks.
  69. struct ChadTrack: Codable, Identifiable, Hashable {
  70. let id: String
  71. let title: String
  72. let artist: String?
  73. let albumArtist: String?
  74. let album: String?
  75. let duration: Double?
  76. let no: Int?
  77. let url: String
  78. let bitRate: Int?
  79. let year: Int?
  80. let cover: String?
  81. var trackNumber: Int? { no }
  82. enum CodingKeys: String, CodingKey {
  83. case id, title, artist, album, duration, no, url, year, cover
  84. case albumArtist = "album_artist"
  85. case bitRate = "bit_rate"
  86. }
  87. var formattedDuration: String {
  88. guard let duration else { return "—" }
  89. let total = Int(duration)
  90. let minutes = total / 60
  91. let seconds = total % 60
  92. return String(format: "%d:%02d", minutes, seconds)
  93. }
  94. }
  95. /// Library statistics from GET /api/stats.
  96. struct ChadStats: Codable {
  97. let tracks: Int?
  98. let albums: Int?
  99. let artists: Int?
  100. let duration: String?
  101. }
  102. /// The browsable category types exposed by the API.
  103. enum ChadCategoryType: String, CaseIterable, Identifiable {
  104. case album
  105. case artist
  106. case genre
  107. case year
  108. case publisher
  109. case country
  110. case type
  111. case status
  112. var id: String { rawValue }
  113. var displayName: String {
  114. switch self {
  115. case .album: "Albums"
  116. case .artist: "Artists"
  117. case .genre: "Genres"
  118. case .year: "Years"
  119. case .publisher: "Publishers"
  120. case .country: "Countries"
  121. case .type: "Types"
  122. case .status: "Status"
  123. }
  124. }
  125. var icon: String {
  126. switch self {
  127. case .album: "square.stack"
  128. case .artist: "person.2"
  129. case .genre: "guitars"
  130. case .year: "calendar"
  131. case .publisher: "building.2"
  132. case .country: "globe"
  133. case .type: "tag"
  134. case .status: "checkmark.circle"
  135. }
  136. }
  137. }