diff --git a/GEMINI.md b/GEMINI.md index 1038081..4bb04d9 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -4,3 +4,38 @@ Documentation and reference for the MusicBrainz API implementation. ## Search API Reference - [MusicBrainz Search API Documentation](https://musicbrainz.org/doc/MusicBrainz_API/Search) + +## SwiftUI and SwiftData Readiness + +The library is designed to be cross-platform and modern-Swift ready. + +### SwiftUI +All models conform to `Identifiable`, `Hashable`, and `Equatable`. This makes them perfect for: +- Efficient list rendering using `ForEach`. +- State management with `@State` or `@Observable`. +- Value-based equality checks. + +### SwiftData Bridging +Since this library is cross-platform, it does not import `SwiftData` directly. To persist MusicBrainz entities in SwiftData, you should create separate `@Model` classes and bridge them. + +**Example:** + +```swift +import SwiftData +import MusicBrainz + +@Model +class PersistentArtist { + @Attribute(.unique) var id: String + var name: String + var country: String? + + init(from artist: MusicBrainz.Artist) { + self.id = artist.id + self.name = artist.name + self.country = artist.country + } +} +``` + +Bridging DTOs to local storage models ensures that your persistence layer remains decoupled from the network layer, which is a best practice. diff --git a/Sources/MusicBrainz/Enums.swift b/Sources/MusicBrainz/Enums.swift index 05811bf..afc48a2 100644 --- a/Sources/MusicBrainz/Enums.swift +++ b/Sources/MusicBrainz/Enums.swift @@ -1,15 +1,15 @@ import Foundation -public enum Gender: String, Sendable { +public enum Gender: String, Sendable, Hashable, Equatable { case male, female, other case notApplicable = "not applicable" } -public enum ArtistType: String, Sendable { +public enum ArtistType: String, Sendable, Hashable, Equatable { case person, group, orchestra, choir, character, other } -public enum Country: String, Sendable { +public enum Country: String, Sendable, Hashable, Equatable { case af = "AF" // Afghanistan case al = "AL" // Albania case dz = "DZ" // Algeria @@ -49,3 +49,31 @@ public enum Country: String, Sendable { case gb = "GB" // United Kingdom case us = "US" // United States } + +public enum ReleaseFormat: String, Codable, Sendable, Hashable, Equatable { + case cd = "CD" + case cdR = "CD-R" + case enhancedCD = "Enhanced CD" + case vinyl = "Vinyl" + case vinyl12 = "12\" Vinyl" + case vinyl10 = "10\" Vinyl" + case vinyl7 = "7\" Vinyl" + case cassette = "Cassette" + case digitalMedia = "Digital Media" + case dvd = "DVD" + case dvdVideo = "DVD-Video" + case dvdAudio = "DVD-Audio" + case bluRay = "Blu-ray" + case miniDisc = "MiniDisc" + case sacd = "SACD" + case ld = "LD" + case vhs = "VHS" + case other = "Other" + + // Fallback decoding for unknown formats + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + self = ReleaseFormat(rawValue: rawValue) ?? .other + } +} diff --git a/Sources/MusicBrainz/Models/Area.swift b/Sources/MusicBrainz/Models/Area.swift index ddecadc..aea439d 100644 --- a/Sources/MusicBrainz/Models/Area.swift +++ b/Sources/MusicBrainz/Models/Area.swift @@ -1,6 +1,6 @@ import Foundation -public struct Area: MusicBrainzSearchable { +public struct Area: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .area public let id: String @@ -12,7 +12,7 @@ public struct Area: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id, type, name, disambiguation, score, relations case sortName = "sort-name" case lifeSpan = "life-span" diff --git a/Sources/MusicBrainz/Models/Artist.swift b/Sources/MusicBrainz/Models/Artist.swift index 43ea402..ddd300d 100644 --- a/Sources/MusicBrainz/Models/Artist.swift +++ b/Sources/MusicBrainz/Models/Artist.swift @@ -1,6 +1,6 @@ import Foundation -public struct Artist: MusicBrainzSearchable { +public struct Artist: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .artist public let id: String @@ -11,9 +11,11 @@ public struct Artist: MusicBrainzSearchable { public let disambiguation: String? public let lifeSpan: LifeSpan? public let relations: [Relation]? + public let releases: [Release]? + public let releaseGroups: [ReleaseGroup]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id case name case sortName = "sort-name" @@ -22,6 +24,8 @@ public struct Artist: MusicBrainzSearchable { case disambiguation case lifeSpan = "life-span" case relations + case releases + case releaseGroups = "release-groups" case score } @@ -31,7 +35,15 @@ public struct Artist: MusicBrainzSearchable { for rel in relations { if rel.type == "image" || rel.type == "wikimedia commons" { if let resource = rel.url?.resource { - return URL(string: resource) + var urlString = resource + // Handle Wikimedia Commons page URLs by converting to Special:FilePath + if urlString.contains("commons.wikimedia.org/wiki/File:") { + let filename = urlString.components(separatedBy: "/wiki/File:").last ?? "" + if !filename.isEmpty { + urlString = "https://commons.wikimedia.org/wiki/Special:FilePath/\(filename)" + } + } + return URL(string: urlString) } } } diff --git a/Sources/MusicBrainz/Models/Event.swift b/Sources/MusicBrainz/Models/Event.swift index c50e580..0695068 100644 --- a/Sources/MusicBrainz/Models/Event.swift +++ b/Sources/MusicBrainz/Models/Event.swift @@ -1,6 +1,6 @@ import Foundation -public struct Event: MusicBrainzSearchable { +public struct Event: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .event public let id: String @@ -11,7 +11,7 @@ public struct Event: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id, name, time, disambiguation, score, relations case lifeSpan = "life-span" } diff --git a/Sources/MusicBrainz/Models/Instrument.swift b/Sources/MusicBrainz/Models/Instrument.swift index b28c8f2..ac0853f 100644 --- a/Sources/MusicBrainz/Models/Instrument.swift +++ b/Sources/MusicBrainz/Models/Instrument.swift @@ -1,6 +1,6 @@ import Foundation -public struct Instrument: MusicBrainzSearchable { +public struct Instrument: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .instrument public let id: String @@ -11,7 +11,7 @@ public struct Instrument: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id, name, type, description, disambiguation, score, relations } } diff --git a/Sources/MusicBrainz/Models/Label.swift b/Sources/MusicBrainz/Models/Label.swift index fb4084e..e2ca951 100644 --- a/Sources/MusicBrainz/Models/Label.swift +++ b/Sources/MusicBrainz/Models/Label.swift @@ -1,6 +1,6 @@ import Foundation -public struct Label: MusicBrainzSearchable { +public struct Label: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .label public let id: String @@ -13,7 +13,7 @@ public struct Label: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id, name, type, country, disambiguation, score, relations case labelCode = "label-code" case lifeSpan = "life-span" diff --git a/Sources/MusicBrainz/Models/Place.swift b/Sources/MusicBrainz/Models/Place.swift index 33b21e1..4306698 100644 --- a/Sources/MusicBrainz/Models/Place.swift +++ b/Sources/MusicBrainz/Models/Place.swift @@ -1,6 +1,6 @@ import Foundation -public struct Place: MusicBrainzSearchable { +public struct Place: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .place public let id: String @@ -12,7 +12,7 @@ public struct Place: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id, name, type, address, area, disambiguation, score, relations } } diff --git a/Sources/MusicBrainz/Models/Recording.swift b/Sources/MusicBrainz/Models/Recording.swift index a9f0082..6fc1680 100644 --- a/Sources/MusicBrainz/Models/Recording.swift +++ b/Sources/MusicBrainz/Models/Recording.swift @@ -1,6 +1,6 @@ import Foundation -public struct Recording: MusicBrainzSearchable { +public struct Recording: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .recording public let id: String @@ -12,7 +12,7 @@ public struct Recording: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id case title case length @@ -22,4 +22,16 @@ public struct Recording: MusicBrainzSearchable { case relations case score } + + public var youtubeURL: URL? { + guard let relations else { return nil } + for rel in relations { + if let resource = rel.url?.resource { + if rel.type == "youtube" || resource.contains("youtube.com") || resource.contains("youtu.be") { + return URL(string: resource) + } + } + } + return nil + } } diff --git a/Sources/MusicBrainz/Models/Release.swift b/Sources/MusicBrainz/Models/Release.swift index befb188..fa27f2b 100644 --- a/Sources/MusicBrainz/Models/Release.swift +++ b/Sources/MusicBrainz/Models/Release.swift @@ -1,6 +1,6 @@ import Foundation -public struct Release: MusicBrainzSearchable { +public struct Release: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .release public let id: String @@ -11,9 +11,10 @@ public struct Release: MusicBrainzSearchable { public let barcode: String? public let disambiguation: String? public let relations: [Relation]? + public let media: [Media]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id case title case status @@ -22,6 +23,7 @@ public struct Release: MusicBrainzSearchable { case barcode case disambiguation case relations + case media case score } } diff --git a/Sources/MusicBrainz/Models/ReleaseGroup.swift b/Sources/MusicBrainz/Models/ReleaseGroup.swift index f939839..10214bc 100644 --- a/Sources/MusicBrainz/Models/ReleaseGroup.swift +++ b/Sources/MusicBrainz/Models/ReleaseGroup.swift @@ -1,6 +1,6 @@ import Foundation -public struct ReleaseGroup: MusicBrainzSearchable { +public struct ReleaseGroup: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .releaseGroup public let id: String @@ -11,7 +11,7 @@ public struct ReleaseGroup: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id, title, disambiguation, score, relations case primaryType = "primary-type" case artistCredit = "artist-credit" diff --git a/Sources/MusicBrainz/Models/Series.swift b/Sources/MusicBrainz/Models/Series.swift index 928700b..e3de67c 100644 --- a/Sources/MusicBrainz/Models/Series.swift +++ b/Sources/MusicBrainz/Models/Series.swift @@ -1,6 +1,6 @@ import Foundation -public struct Series: MusicBrainzSearchable { +public struct Series: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .series public let id: String @@ -10,7 +10,7 @@ public struct Series: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id, name, type, disambiguation, score, relations } } diff --git a/Sources/MusicBrainz/Models/Shared/ArtistCredit.swift b/Sources/MusicBrainz/Models/Shared/ArtistCredit.swift index 2c278eb..6f1fb15 100644 --- a/Sources/MusicBrainz/Models/Shared/ArtistCredit.swift +++ b/Sources/MusicBrainz/Models/Shared/ArtistCredit.swift @@ -1,6 +1,6 @@ import Foundation -public struct ArtistCredit: Codable, Sendable { +public struct ArtistCredit: Codable, Sendable, Hashable, Equatable { public let name: String public let artist: Artist? public let joinphrase: String? diff --git a/Sources/MusicBrainz/Models/Shared/CoverArt.swift b/Sources/MusicBrainz/Models/Shared/CoverArt.swift index d21d84f..4721cc4 100644 --- a/Sources/MusicBrainz/Models/Shared/CoverArt.swift +++ b/Sources/MusicBrainz/Models/Shared/CoverArt.swift @@ -1,6 +1,6 @@ import Foundation -public struct CoverArtImage: Codable, Sendable { +public struct CoverArtImage: Codable, Sendable, Identifiable, Hashable, Equatable { public let image: String public let thumbnails: [String: String] public let types: [String] @@ -11,7 +11,7 @@ public struct CoverArtImage: Codable, Sendable { public let id: String } -public struct CoverArtResponse: Codable, Sendable { +public struct CoverArtResponse: Codable, Sendable, Hashable, Equatable { public let images: [CoverArtImage] public let release: String diff --git a/Sources/MusicBrainz/Models/Shared/LifeSpan.swift b/Sources/MusicBrainz/Models/Shared/LifeSpan.swift index bd4a5d5..ac11d86 100644 --- a/Sources/MusicBrainz/Models/Shared/LifeSpan.swift +++ b/Sources/MusicBrainz/Models/Shared/LifeSpan.swift @@ -1,6 +1,6 @@ import Foundation -public struct LifeSpan: Codable, Sendable { +public struct LifeSpan: Codable, Sendable, Hashable, Equatable { public let begin: String? public let end: String? public let ended: Bool? diff --git a/Sources/MusicBrainz/Models/Shared/Media.swift b/Sources/MusicBrainz/Models/Shared/Media.swift new file mode 100644 index 0000000..3bd0b6d --- /dev/null +++ b/Sources/MusicBrainz/Models/Shared/Media.swift @@ -0,0 +1,22 @@ +import Foundation + +public struct Track: Codable, Sendable, Identifiable, Hashable, Equatable { + public let id: String + public let number: String + public let position: Int + public let title: String + public let length: Int? + public let recording: Recording? +} + +public struct Media: Codable, Sendable, Hashable, Equatable { + public let position: Int + public let format: ReleaseFormat? + public let trackCount: Int + public let tracks: [Track]? + + enum CodingKeys: String, CodingKey { + case position, format, tracks + case trackCount = "track-count" + } +} diff --git a/Sources/MusicBrainz/Models/Shared/Relation.swift b/Sources/MusicBrainz/Models/Shared/Relation.swift index 40b1443..6a39c52 100644 --- a/Sources/MusicBrainz/Models/Shared/Relation.swift +++ b/Sources/MusicBrainz/Models/Shared/Relation.swift @@ -1,6 +1,6 @@ import Foundation -public struct Relation: Codable, Sendable { +public struct Relation: Codable, Sendable, Hashable, Equatable { public let type: String public let direction: String public let url: URLResource? diff --git a/Sources/MusicBrainz/Models/Shared/URLResource.swift b/Sources/MusicBrainz/Models/Shared/URLResource.swift index b6eea92..da23468 100644 --- a/Sources/MusicBrainz/Models/Shared/URLResource.swift +++ b/Sources/MusicBrainz/Models/Shared/URLResource.swift @@ -1,6 +1,6 @@ import Foundation -public struct URLResource: Codable, Sendable { +public struct URLResource: Codable, Sendable, Identifiable, Hashable, Equatable { public let id: String public let resource: String } diff --git a/Sources/MusicBrainz/Models/Shared/Wikimedia.swift b/Sources/MusicBrainz/Models/Shared/Wikimedia.swift new file mode 100644 index 0000000..33c7218 --- /dev/null +++ b/Sources/MusicBrainz/Models/Shared/Wikimedia.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct WikimediaResponse: Codable, Sendable { + public let query: WikimediaQuery +} + +public struct WikimediaQuery: Codable, Sendable { + public let pages: [String: WikimediaPage] +} + +public struct WikimediaPage: Codable, Sendable { + public let pageid: Int? + public let title: String + public let imageinfo: [WikimediaImageInfo]? +} + +public struct WikimediaImageInfo: Codable, Sendable { + public let url: String + public let descriptionurl: String +} diff --git a/Sources/MusicBrainz/Models/URLReference.swift b/Sources/MusicBrainz/Models/URLReference.swift index 16a8040..a35df13 100644 --- a/Sources/MusicBrainz/Models/URLReference.swift +++ b/Sources/MusicBrainz/Models/URLReference.swift @@ -1,6 +1,6 @@ import Foundation -public struct URLReference: MusicBrainzSearchable { +public struct URLReference: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .url public let id: String diff --git a/Sources/MusicBrainz/Models/Work.swift b/Sources/MusicBrainz/Models/Work.swift index d4f115e..01cd8b9 100644 --- a/Sources/MusicBrainz/Models/Work.swift +++ b/Sources/MusicBrainz/Models/Work.swift @@ -1,6 +1,6 @@ import Foundation -public struct Work: MusicBrainzSearchable { +public struct Work: MusicBrainzSearchable, Identifiable, Hashable, Equatable { public static let entityType: MusicBrainzEntity = .work public let id: String @@ -11,7 +11,7 @@ public struct Work: MusicBrainzSearchable { public let relations: [Relation]? public let score: Int? - enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey, Hashable, Equatable { case id, title, type, language, disambiguation, score, relations } } diff --git a/Sources/MusicBrainz/MusicBrainzClient.swift b/Sources/MusicBrainz/MusicBrainzClient.swift index 9fbb50b..c88fafd 100644 --- a/Sources/MusicBrainz/MusicBrainzClient.swift +++ b/Sources/MusicBrainz/MusicBrainzClient.swift @@ -106,10 +106,8 @@ public struct MusicBrainzClient: Sendable { } } - public func fetchCoverArt(releaseId: String) async throws(MusicBrainzError) -> CoverArtResponse - { - let data = try await fetchRaw( - baseURL: MusicBrainzClient.caaBaseURL, "/release/\(releaseId)") + public func fetchCoverArt(releaseId: String) async throws(MusicBrainzError) -> CoverArtResponse { + let data = try await fetchRaw(baseURL: MusicBrainzClient.caaBaseURL, "/release/\(releaseId)") do { return try JSONDecoder().decode(CoverArtResponse.self, from: data) } catch { @@ -117,6 +115,47 @@ public struct MusicBrainzClient: Sendable { } } + public func resolveWikimediaImageURL(_ url: URL) async throws(MusicBrainzError) -> URL { + let urlString = url.absoluteString + guard urlString.contains("commons.wikimedia.org/wiki/File:") || urlString.contains("Special:FilePath") else { + return url + } + + let filename: String + if urlString.contains("/wiki/File:") { + filename = urlString.components(separatedBy: "/wiki/File:").last ?? "" + } else if urlString.contains("Special:FilePath/") { + filename = urlString.components(separatedBy: "Special:FilePath/").last ?? "" + } else { + return url + } + + guard !filename.isEmpty else { return url } + + let wikimediaBaseURL = URL(string: "https://commons.wikimedia.org/w/api.php")! + let queryItems = [ + URLQueryItem(name: "action", value: "query"), + URLQueryItem(name: "titles", value: "File:\(filename)"), + URLQueryItem(name: "prop", value: "imageinfo"), + URLQueryItem(name: "iiprop", value: "url"), + URLQueryItem(name: "format", value: "json"), + ] + + let data = try await fetchRaw(baseURL: wikimediaBaseURL, "", queryItems: queryItems) + do { + let response = try JSONDecoder().decode(WikimediaResponse.self, from: data) + if let page = response.query.pages.values.first, + let directURLString = page.imageinfo?.first?.url, + let directURL = URL(string: directURLString) + { + return directURL + } + return url + } catch { + throw .decodingError(error) + } + } + @available( *, deprecated, message: "Use search(_ action: MusicBrainzSearchAction, ...) instead" ) diff --git a/Sources/MusicBrainz/MusicBrainzSearchable.swift b/Sources/MusicBrainz/MusicBrainzSearchable.swift index b78d041..3443d88 100644 --- a/Sources/MusicBrainz/MusicBrainzSearchable.swift +++ b/Sources/MusicBrainz/MusicBrainzSearchable.swift @@ -1,5 +1,5 @@ import Foundation -public protocol MusicBrainzSearchable: Codable, Sendable { +public protocol MusicBrainzSearchable: Codable, Sendable, Hashable, Equatable { static var entityType: MusicBrainzEntity { get } } diff --git a/Sources/MusicBrainz/SearchResponse.swift b/Sources/MusicBrainz/SearchResponse.swift index 30352fe..6d1ad63 100644 --- a/Sources/MusicBrainz/SearchResponse.swift +++ b/Sources/MusicBrainz/SearchResponse.swift @@ -1,11 +1,11 @@ import Foundation -public struct SearchResponse: Codable, Sendable { +public struct SearchResponse: Codable, Sendable, Hashable, Equatable { public let count: Int public let offset: Int public let entities: [T] - private enum CodingKeys: String, CodingKey { + private enum CodingKeys: String, CodingKey, Hashable, Equatable { case count, offset } diff --git a/Sources/MusicBrainzExe/main.swift b/Sources/MusicBrainzExe/main.swift index ce4ee42..f9d093e 100644 --- a/Sources/MusicBrainzExe/main.swift +++ b/Sources/MusicBrainzExe/main.swift @@ -12,32 +12,42 @@ do { if let artist = artistSearch.entities.first { print("- Found Artist: \(artist.name) (\(artist.id))") - // Fetch artist with URL relationships to find an image - print(" Fetching artist details (with url-rels)...") - let detailedArtist = try await client.fetch(.artist, id: artist.id, includes: ["url-rels"]) + // Fetch artist with URL relationships and releases + print(" Fetching artist details (with url-rels + releases)...") + let detailedArtist = try await client.fetch( + .artist, id: artist.id, includes: ["url-rels", "releases"]) + if let imageURL = detailedArtist.imageURL { - print(" Artist Image URL: \(imageURL)") - } else { - print(" No image found for artist.") + print(" Artist Image Shortcut: \(imageURL)") } - } - print("\n--- Release Search: 'Thriller' ---") - let releaseSearch = try await client.search(.release(title: "Thriller"), limit: 1) - - if let release = releaseSearch.entities.first { - print("- Found Release: \(release.title) (\(release.id))") - - // Fetch cover art from Cover Art Archive - print(" Fetching cover art...") - do { - let coverArt = try await client.fetchCoverArt(releaseId: release.id) - if let front = coverArt.frontImage { - print(" Front Cover Image: \(front.image)") - print(" Thumbnails: \(front.thumbnails)") + if let releases = detailedArtist.releases { + print(" Releases count: \(releases.count)") + for release in releases.prefix(5) { + print(" - \(release.title) (\(release.id))") + } + } + + // Fetch tracks for the first release + if let release = detailedArtist.releases?.first { + print("\n--- Fetching tracks for release: '\(release.title)' ---") + let detailedRelease = try await client.fetch( + .release, id: release.id, includes: ["recordings"]) + if let media = detailedRelease.media { + for medium in media { + print( + " Medium \(medium.position) (\(medium.format?.rawValue ?? "Unknown format")):" + ) + if let tracks = medium.tracks { + for track in tracks { + print(" \(track.number). \(track.title)") + if let youtubeURL = track.recording?.youtubeURL { + print(" YouTube: \(youtubeURL)") + } + } + } + } } - } catch { - print(" Could not fetch cover art: \(error)") } } diff --git a/Tests/MusicBrainzTests/ExtendedSearchTests.swift b/Tests/MusicBrainzTests/ExtendedSearchTests.swift index 2aa67f0..fe80bbd 100644 --- a/Tests/MusicBrainzTests/ExtendedSearchTests.swift +++ b/Tests/MusicBrainzTests/ExtendedSearchTests.swift @@ -157,5 +157,43 @@ import Testing """.data(using: .utf8)! let artist = try JSONDecoder().decode(Artist.self, from: json) - #expect(artist.imageURL?.absoluteString == "https://commons.wikimedia.org/wiki/File:Michael_Jackson_1984.jpg") + #expect(artist.imageURL?.absoluteString == "https://commons.wikimedia.org/wiki/Special:FilePath/Michael_Jackson_1984.jpg") +} + +@Test func testRecordingYouTubeURL() throws { + let json = """ + { + "id": "abc", + "title": "Billie Jean", + "relations": [ + { + "type": "video", + "direction": "forward", + "url": { + "id": "456", + "resource": "https://www.youtube.com/watch?v=Zi_XLOBDo_Y" + } + } + ] + } + """.data(using: .utf8)! + + let recording = try JSONDecoder().decode(Recording.self, from: json) + #expect(recording.youtubeURL?.absoluteString == "https://www.youtube.com/watch?v=Zi_XLOBDo_Y") +} + +@Test func testReleaseFormatDecoding() throws { + let json = """ + [ + "CD", + "Digital Media", + "Unknown Format XY" + ] + """.data(using: .utf8)! + + let formats = try JSONDecoder().decode([ReleaseFormat].self, from: json) + #expect(formats.count == 3) + #expect(formats[0] == .cd) + #expect(formats[1] == .digitalMedia) + #expect(formats[2] == .other) }