I think we're done
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
22
Sources/MusicBrainz/Models/Shared/Media.swift
Normal file
22
Sources/MusicBrainz/Models/Shared/Media.swift
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
20
Sources/MusicBrainz/Models/Shared/Wikimedia.swift
Normal file
20
Sources/MusicBrainz/Models/Shared/Wikimedia.swift
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T>, ...) instead"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
public protocol MusicBrainzSearchable: Codable, Sendable {
|
||||
public protocol MusicBrainzSearchable: Codable, Sendable, Hashable, Equatable {
|
||||
static var entityType: MusicBrainzEntity { get }
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
public struct SearchResponse<T: MusicBrainzSearchable>: Codable, Sendable {
|
||||
public struct SearchResponse<T: MusicBrainzSearchable>: 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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user