import Foundation public struct RadioStation: Codable, Hashable, Identifiable, Sendable { public var uuid: String public var name: String public var url: String public var resolvedURL: String public var homepage: String public var favicon: String public var country: String public var tags: String public var codec: String public var bitrate: Int public var votes: Int public var isOnline: Bool public var isFavorite: Bool public var isManual: Bool public var id: String { if !uuid.isEmpty { return uuid } if !resolvedURL.isEmpty { return resolvedURL } if !url.isEmpty { return url } return name } public var playbackURLString: String { resolvedURL.isEmpty ? url : resolvedURL } public var searchableText: String { [name, tags, country, codec].joined(separator: " ").lowercased() } public init( uuid: String = "", name: String, url: String = "", resolvedURL: String = "", homepage: String = "", favicon: String = "", country: String = "", tags: String = "", codec: String = "", bitrate: Int = 0, votes: Int = 0, isOnline: Bool = true, isFavorite: Bool = false, isManual: Bool = false ) { self.uuid = uuid self.name = name self.url = url self.resolvedURL = resolvedURL self.homepage = homepage self.favicon = favicon self.country = country self.tags = tags self.codec = codec self.bitrate = bitrate self.votes = votes self.isOnline = isOnline self.isFavorite = isFavorite self.isManual = isManual } enum CodingKeys: String, CodingKey { case uuid = "stationuuid" case legacyUUID = "uuid" case name case url case resolvedURL = "url_resolved" case legacyResolvedURL = "urlResolved" case homepage case favicon case country case tags case codec case bitrate case votes case lastCheckOK = "lastcheckok" case isFavorite case isManual } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) uuid = try container.decodeIfPresent(String.self, forKey: .uuid) ?? container.decodeIfPresent(String.self, forKey: .legacyUUID) ?? "" name = try container.decodeIfPresent(String.self, forKey: .name) ?? "" url = try container.decodeIfPresent(String.self, forKey: .url) ?? "" resolvedURL = try container.decodeIfPresent(String.self, forKey: .resolvedURL) ?? container.decodeIfPresent(String.self, forKey: .legacyResolvedURL) ?? "" homepage = try container.decodeIfPresent(String.self, forKey: .homepage) ?? "" favicon = try container.decodeIfPresent(String.self, forKey: .favicon) ?? "" country = try container.decodeIfPresent(String.self, forKey: .country) ?? "" tags = try container.decodeIfPresent(String.self, forKey: .tags) ?? "" codec = try container.decodeIfPresent(String.self, forKey: .codec) ?? "" bitrate = try container.decodeFlexibleInt(forKey: .bitrate) ?? 0 votes = try container.decodeFlexibleInt(forKey: .votes) ?? 0 let lastCheckOK = try container.decodeFlexibleInt(forKey: .lastCheckOK) isOnline = lastCheckOK.map { $0 > 0 } ?? true isFavorite = try container.decodeIfPresent(Bool.self, forKey: .isFavorite) ?? false isManual = try container.decodeIfPresent(Bool.self, forKey: .isManual) ?? false } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(uuid, forKey: .legacyUUID) try container.encode(name, forKey: .name) try container.encode(url, forKey: .url) try container.encode(resolvedURL, forKey: .legacyResolvedURL) try container.encode(homepage, forKey: .homepage) try container.encode(favicon, forKey: .favicon) try container.encode(country, forKey: .country) try container.encode(tags, forKey: .tags) try container.encode(codec, forKey: .codec) try container.encode(bitrate, forKey: .bitrate) try container.encode(votes, forKey: .votes) try container.encode(isFavorite, forKey: .isFavorite) try container.encode(isManual, forKey: .isManual) } } extension KeyedDecodingContainer { func decodeFlexibleInt(forKey key: Key) throws -> Int? { if let intValue = try? decodeIfPresent(Int.self, forKey: key) { return intValue } if let stringValue = try? decodeIfPresent(String.self, forKey: key) { return Int(stringValue) } return nil } }