145 lines
5.2 KiB
Swift
145 lines
5.2 KiB
Swift
//
|
|
// ProviderBadge.swift
|
|
// Mobile Music Assistant
|
|
//
|
|
// Created by Sven Hanold on 06.04.26.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
/// Monochrome badges indicating which music provider(s) an item comes from.
|
|
/// For provider-specific URIs (e.g. spotify://) a single badge is shown.
|
|
/// For library:// items all distinct source providers found in the metadata
|
|
/// images are shown side by side.
|
|
struct ProviderBadge: View {
|
|
let uri: String
|
|
var metadata: MediaItemMetadata? = nil
|
|
|
|
private var providers: [MusicProvider] {
|
|
// Use string-based parsing — URL(string:) returns nil for schemes with
|
|
// underscores like "apple_music" which violate RFC 2396.
|
|
let scheme = uri.components(separatedBy: "://").first?.lowercased()
|
|
|
|
// Non-library URI → show only that provider
|
|
if let fromScheme = MusicProvider.from(scheme: scheme), fromScheme != .library {
|
|
return [fromScheme]
|
|
}
|
|
|
|
// library:// URI → collect all distinct providers from metadata images
|
|
var seen = Set<MusicProvider>()
|
|
var result = [MusicProvider]()
|
|
|
|
let keys = metadata?.images?.compactMap { $0.provider } ?? []
|
|
for key in keys {
|
|
if let p = MusicProvider.from(providerKey: key), !seen.contains(p) {
|
|
seen.insert(p)
|
|
result.append(p)
|
|
}
|
|
}
|
|
|
|
// Nothing found for a library item → fall back to the library badge
|
|
if result.isEmpty && scheme == "library" {
|
|
return [.library]
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: 4) {
|
|
ForEach(providers, id: \.self) { provider in
|
|
Image(systemName: provider.icon)
|
|
.font(.system(size: 9, weight: .bold))
|
|
.foregroundStyle(.white)
|
|
.frame(width: 20, height: 20)
|
|
.background(.black.opacity(0.55))
|
|
.clipShape(Circle())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Provider Mapping
|
|
|
|
enum MusicProvider: Hashable {
|
|
case library
|
|
case subsonic
|
|
case spotify
|
|
case tidal
|
|
case qobuz
|
|
case plex
|
|
case ytmusic
|
|
case appleMusic
|
|
case deezer
|
|
case soundcloud
|
|
case tunein
|
|
case filesystem
|
|
case jellyfin
|
|
case dlna
|
|
|
|
/// SF Symbol name for this provider.
|
|
var icon: String {
|
|
switch self {
|
|
case .library: return "building.columns.fill"
|
|
case .subsonic: return "ferry.fill"
|
|
case .spotify: return "antenna.radiowaves.left.and.right.circle.fill"
|
|
case .tidal: return "water.waves"
|
|
case .qobuz: return "hifispeaker.fill"
|
|
case .plex: return "play.square.stack.fill"
|
|
case .ytmusic: return "play.rectangle.fill"
|
|
case .appleMusic: return "applelogo"
|
|
case .deezer: return "waveform"
|
|
case .soundcloud: return "cloud.fill"
|
|
case .tunein: return "radio.fill"
|
|
case .filesystem: return "folder.fill"
|
|
case .jellyfin: return "server.rack"
|
|
case .dlna: return "wifi"
|
|
}
|
|
}
|
|
|
|
/// Match a URI scheme to a known provider.
|
|
static func from(scheme: String?) -> MusicProvider? {
|
|
guard let scheme = scheme?.lowercased() else { return nil }
|
|
|
|
if scheme == "library" { return .library }
|
|
if scheme.hasPrefix("subsonic") { return .subsonic }
|
|
if scheme.hasPrefix("spotify") { return .spotify }
|
|
if scheme.hasPrefix("tidal") { return .tidal }
|
|
if scheme.hasPrefix("qobuz") { return .qobuz }
|
|
if scheme.hasPrefix("plex") { return .plex }
|
|
if scheme.hasPrefix("ytmusic") { return .ytmusic }
|
|
if scheme.hasPrefix("apple") { return .appleMusic }
|
|
if scheme.hasPrefix("deezer") { return .deezer }
|
|
if scheme.hasPrefix("soundcloud") { return .soundcloud }
|
|
if scheme.hasPrefix("tunein") { return .tunein }
|
|
if scheme.hasPrefix("filesystem") { return .filesystem }
|
|
if scheme.hasPrefix("jellyfin") { return .jellyfin }
|
|
if scheme.hasPrefix("dlna") { return .dlna }
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Match a provider key from image metadata (e.g. "subsonic", "spotify", "filesystem_local").
|
|
static func from(providerKey key: String) -> MusicProvider? {
|
|
let k = key.lowercased()
|
|
|
|
if k.hasPrefix("subsonic") { return .subsonic }
|
|
if k.hasPrefix("spotify") { return .spotify }
|
|
if k.hasPrefix("tidal") { return .tidal }
|
|
if k.hasPrefix("qobuz") { return .qobuz }
|
|
if k.hasPrefix("plex") { return .plex }
|
|
if k.hasPrefix("ytmusic") { return .ytmusic }
|
|
if k.hasPrefix("apple") { return .appleMusic }
|
|
if k.hasPrefix("deezer") { return .deezer }
|
|
if k.hasPrefix("soundcloud") { return .soundcloud }
|
|
if k.hasPrefix("tunein") { return .tunein }
|
|
if k.hasPrefix("filesystem") { return .filesystem }
|
|
if k.hasPrefix("jellyfin") { return .jellyfin }
|
|
if k.hasPrefix("dlna") { return .dlna }
|
|
// Image-only metadata providers — not a music source
|
|
if k == "lastfm" || k == "musicbrainz" || k == "fanarttv" { return nil }
|
|
|
|
return nil
|
|
}
|
|
}
|