import Foundation public struct MusicBrainzSearchAction: Sendable { public let entity: MusicBrainzEntity public let query: String fileprivate static func buildQuery(_ fields: [String: Any?], raw: String?) -> String { var parts: [String] = [] for (key, value) in fields { if let value { let stringValue: String if let val = value as? String { stringValue = val } else if let val = value as? CustomStringConvertible { stringValue = val.description } else if let val = value as? (any RawRepresentable), let rawVal = val.rawValue as? String { stringValue = rawVal } else { continue } if !stringValue.isEmpty { let escaped = stringValue.replacingOccurrences(of: "\"", with: "\\\"") parts.append("\(key):\"\(escaped)\"") } } } if let raw, !raw.isEmpty { parts.append(raw) } return parts.joined(separator: " AND ") } } extension MusicBrainzSearchAction where T == Area { public static func area( name: String? = nil, type: String? = nil, iso: String? = nil, aid: String? = nil, alias: String? = nil, begin: String? = nil, comment: String? = nil, end: String? = nil, ended: Bool? = nil, sortname: String? = nil, tag: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "area": name, "type": type, "iso": iso, "aid": aid, "alias": alias, "begin": begin, "comment": comment, "end": end, "ended": ended?.description, "sortname": sortname, "tag": tag, ], raw: raw) return .init(entity: .area, query: q) } } extension MusicBrainzSearchAction where T == Artist { public static func artist( name: String? = nil, gender: Gender? = nil, country: Country? = nil, type: ArtistType? = nil, alias: String? = nil, area: String? = nil, arid: String? = nil, begin: String? = nil, beginarea: String? = nil, comment: String? = nil, end: String? = nil, endarea: String? = nil, ended: Bool? = nil, ipi: String? = nil, isni: String? = nil, sortname: String? = nil, tag: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "artist": name, "gender": gender?.rawValue, "country": country?.rawValue, "type": type?.rawValue, "alias": alias, "area": area, "arid": arid, "begin": begin, "beginarea": beginarea, "comment": comment, "end": end, "endarea": endarea, "ended": ended?.description, "ipi": ipi, "isni": isni, "sortname": sortname, "tag": tag, ], raw: raw) return .init(entity: .artist, query: q) } } extension MusicBrainzSearchAction where T == Event { public static func event( name: String? = nil, type: String? = nil, artist: String? = nil, place: String? = nil, aid: String? = nil, area: String? = nil, arid: String? = nil, begin: String? = nil, comment: String? = nil, end: String? = nil, ended: Bool? = nil, eid: String? = nil, pid: String? = nil, tag: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "event": name, "type": type, "artist": artist, "place": place, "aid": aid, "area": area, "arid": arid, "begin": begin, "comment": comment, "end": end, "ended": ended?.description, "eid": eid, "pid": pid, "tag": tag, ], raw: raw) return .init(entity: .event, query: q) } } extension MusicBrainzSearchAction where T == Instrument { public static func instrument( name: String? = nil, type: String? = nil, description: String? = nil, alias: String? = nil, comment: String? = nil, iid: String? = nil, tag: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "instrument": name, "type": type, "description": description, "alias": alias, "comment": comment, "iid": iid, "tag": tag, ], raw: raw) return .init(entity: .instrument, query: q) } } extension MusicBrainzSearchAction where T == Label { public static func label( name: String? = nil, type: String? = nil, code: String? = nil, country: Country? = nil, alias: String? = nil, area: String? = nil, begin: String? = nil, comment: String? = nil, end: String? = nil, ended: Bool? = nil, ipi: String? = nil, isni: String? = nil, laid: String? = nil, sortname: String? = nil, tag: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "label": name, "type": type, "code": code, "country": country?.rawValue, "alias": alias, "area": area, "begin": begin, "comment": comment, "end": end, "ended": ended?.description, "ipi": ipi, "isni": isni, "laid": laid, "sortname": sortname, "tag": tag, ], raw: raw) return .init(entity: .label, query: q) } } extension MusicBrainzSearchAction where T == Place { public static func place( name: String? = nil, type: String? = nil, address: String? = nil, area: String? = nil, alias: String? = nil, begin: String? = nil, comment: String? = nil, end: String? = nil, ended: Bool? = nil, lat: String? = nil, long: String? = nil, pid: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "place": name, "type": type, "address": address, "area": area, "alias": alias, "begin": begin, "comment": comment, "end": end, "ended": ended?.description, "lat": lat, "long": long, "pid": pid, ], raw: raw) return .init(entity: .place, query: q) } } extension MusicBrainzSearchAction where T == Recording { public static func recording( title: String? = nil, artist: String? = nil, release: String? = nil, isrc: String? = nil, arid: String? = nil, comment: String? = nil, country: Country? = nil, date: String? = nil, dur: String? = nil, firstreleasedate: String? = nil, format: String? = nil, number: String? = nil, position: String? = nil, primarytype: String? = nil, reid: String? = nil, rgid: String? = nil, rid: String? = nil, secondarytype: String? = nil, status: String? = nil, tag: String? = nil, tid: String? = nil, tnum: String? = nil, video: Bool? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "recording": title, "artist": artist, "release": release, "isrc": isrc, "arid": arid, "comment": comment, "country": country?.rawValue, "date": date, "dur": dur, "firstreleasedate": firstreleasedate, "format": format, "number": number, "position": position, "primarytype": primarytype, "reid": reid, "rgid": rgid, "rid": rid, "secondarytype": secondarytype, "status": status, "tag": tag, "tid": tid, "tnum": tnum, "video": video?.description, ], raw: raw) return .init(entity: .recording, query: q) } } extension MusicBrainzSearchAction where T == Release { public static func release( title: String? = nil, artist: String? = nil, label: String? = nil, barcode: String? = nil, status: String? = nil, arid: String? = nil, asin: String? = nil, catno: String? = nil, comment: String? = nil, country: Country? = nil, date: String? = nil, discids: String? = nil, format: String? = nil, laid: String? = nil, mediums: String? = nil, primarytype: String? = nil, puid: String? = nil, reid: String? = nil, rgid: String? = nil, script: String? = nil, secondarytype: String? = nil, tag: String? = nil, tracks: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "release": title, "artist": artist, "label": label, "barcode": barcode, "status": status, "arid": arid, "asin": asin, "catno": catno, "comment": comment, "country": country?.rawValue, "date": date, "discids": discids, "format": format, "laid": laid, "mediums": mediums, "primarytype": primarytype, "puid": puid, "reid": reid, "rgid": rgid, "script": script, "secondarytype": secondarytype, "tag": tag, "tracks": tracks, ], raw: raw) return .init(entity: .release, query: q) } } extension MusicBrainzSearchAction where T == ReleaseGroup { public static func releaseGroup( title: String? = nil, artist: String? = nil, type: String? = nil, arid: String? = nil, comment: String? = nil, primarytype: String? = nil, rgid: String? = nil, releases: String? = nil, secondarytype: String? = nil, status: String? = nil, tag: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "releasegroup": title, "artist": artist, "type": type, "arid": arid, "comment": comment, "primarytype": primarytype, "rgid": rgid, "releases": releases, "secondarytype": secondarytype, "status": status, "tag": tag, ], raw: raw) return .init(entity: .releaseGroup, query: q) } } extension MusicBrainzSearchAction where T == Series { public static func series( name: String? = nil, type: String? = nil, alias: String? = nil, comment: String? = nil, sid: String? = nil, tag: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "series": name, "type": type, "alias": alias, "comment": comment, "sid": sid, "tag": tag, ], raw: raw) return .init(entity: .series, query: q) } } extension MusicBrainzSearchAction where T == Work { public static func work( title: String? = nil, artist: String? = nil, type: String? = nil, iswc: String? = nil, alias: String? = nil, arid: String? = nil, comment: String? = nil, lang: String? = nil, tag: String? = nil, wid: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "work": title, "artist": artist, "type": type, "iswc": iswc, "alias": alias, "arid": arid, "comment": comment, "lang": lang, "tag": tag, "wid": wid, ], raw: raw) return .init(entity: .work, query: q) } } extension MusicBrainzSearchAction where T == URLReference { public static func url( resource: String? = nil, targettype: String? = nil, targetid: String? = nil, uid: String? = nil, raw: String? = nil ) -> Self { let q = buildQuery( [ "url": resource, "targettype": targettype, "targetid": targetid, "uid": uid, ], raw: raw) return .init(entity: .url, query: q) } }