mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Merge pull request #357 from nightah/fix/applemusic-streaming-artwork
fix(mediacontrollers): apple music artwork for streamed content
This commit is contained in:
commit
c31757e5ff
1 changed files with 114 additions and 81 deletions
|
|
@ -20,9 +20,19 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
private struct ITunesSearchResponse: Decodable {
|
||||
let results: [ITunesTrack]
|
||||
}
|
||||
|
||||
private struct ITunesTrack: Decodable {
|
||||
let trackName: String?
|
||||
let collectionName: String?
|
||||
let artworkUrl100: String?
|
||||
}
|
||||
|
||||
class AppleMusicController: MediaControllerProtocol {
|
||||
// MARK: - Properties
|
||||
|
|
@ -39,7 +49,13 @@ class AppleMusicController: MediaControllerProtocol {
|
|||
return true // AppleMusic controller always works
|
||||
}
|
||||
|
||||
/// Minimum byte count for artwork data to be considered valid. Anything
|
||||
/// smaller is likely an empty descriptor or error string, not image data.
|
||||
private static let minimumArtworkSize = 16
|
||||
|
||||
private var notificationTask: Task<Void, Never>?
|
||||
private var lastCatalogArtworkKey: String?
|
||||
private var cachedCatalogArtwork: Data?
|
||||
|
||||
// MARK: - Initialization
|
||||
init() {
|
||||
|
|
@ -132,20 +148,78 @@ class AppleMusicController: MediaControllerProtocol {
|
|||
updatedState.isShuffled = descriptor.atIndex(7)?.booleanValue ?? false
|
||||
let repeatModeValue = descriptor.atIndex(8)?.int32Value ?? 0
|
||||
updatedState.repeatMode = RepeatMode(rawValue: Int(repeatModeValue)) ?? .off
|
||||
updatedState.artwork = descriptor.atIndex(9)?.data as Data?
|
||||
|
||||
// AppleScript returns artwork data for library tracks. For streamed
|
||||
// content not in the library it returns an empty descriptor, so we
|
||||
// fall back to the iTunes Search API to fetch artwork by metadata.
|
||||
if let artworkData = descriptor.atIndex(9)?.data as Data?,
|
||||
artworkData.count > Self.minimumArtworkSize {
|
||||
updatedState.artwork = artworkData
|
||||
} else {
|
||||
updatedState.artwork = await fetchArtworkFromCatalog(
|
||||
title: updatedState.title, artist: updatedState.artist, album: updatedState.album
|
||||
)
|
||||
}
|
||||
|
||||
updatedState.lastUpdated = Date()
|
||||
self.playbackState = updatedState
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func fetchArtworkFromCatalog(title: String, artist: String, album: String) async -> Data? {
|
||||
let key = "\(title)|\(artist)|\(album)"
|
||||
|
||||
if key == lastCatalogArtworkKey, let cached = cachedCatalogArtwork {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Search with title + artist only; including album in the query can
|
||||
// confuse the free-text search when names overlap. We validate the
|
||||
// album from the structured response fields instead.
|
||||
let query = "\(title) \(artist)"
|
||||
guard !query.trimmingCharacters(in: .whitespaces).isEmpty,
|
||||
let encoded = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
|
||||
let url = URL(string: "https://itunes.apple.com/search?term=\(encoded)&media=music&entity=song&limit=10")
|
||||
else { return nil }
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let response = try JSONDecoder().decode(ITunesSearchResponse.self, from: data)
|
||||
guard !response.results.isEmpty else { return nil }
|
||||
|
||||
let normalizedAlbum = album.lowercased()
|
||||
let normalizedTitle = title.lowercased()
|
||||
|
||||
let match = response.results.first(where: {
|
||||
$0.trackName?.lowercased() == normalizedTitle &&
|
||||
$0.collectionName?.lowercased() == normalizedAlbum
|
||||
}) ?? response.results.first(where: {
|
||||
$0.collectionName?.lowercased() == normalizedAlbum
|
||||
}) ?? response.results.first(where: {
|
||||
$0.trackName?.lowercased() == normalizedTitle
|
||||
}) ?? response.results.first
|
||||
|
||||
guard let artworkURLString = match?.artworkUrl100 else { return nil }
|
||||
|
||||
let highResURL = artworkURLString.replacingOccurrences(of: "100x100", with: "600x600")
|
||||
guard let imageURL = URL(string: highResURL) else { return nil }
|
||||
|
||||
let (imageData, _) = try await URLSession.shared.data(from: imageURL)
|
||||
lastCatalogArtworkKey = key
|
||||
cachedCatalogArtwork = imageData
|
||||
return imageData
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func executeCommand(_ command: String) async {
|
||||
let script = "tell application \"Music\" to \(command)"
|
||||
try? await AppleScriptHelper.executeVoid(script)
|
||||
}
|
||||
|
||||
private func fetchPlaybackInfoAsync() async throws -> NSAppleEventDescriptor? {
|
||||
if #available(macOS 26.0, *) {
|
||||
let script = """
|
||||
tell application "Music"
|
||||
try
|
||||
|
|
@ -167,14 +241,7 @@ class AppleMusicController: MediaControllerProtocol {
|
|||
|
||||
set artData to ""
|
||||
try
|
||||
set libraryTrack to first track of playlist "Library" whose name is currentTrackName and artist is currentTrackArtist
|
||||
set artData to data of artwork 1 of libraryTrack
|
||||
on error
|
||||
try
|
||||
set artData to data of artwork 1 of current track
|
||||
on error
|
||||
set artData to ""
|
||||
end try
|
||||
set artData to raw data of artwork 1 of current track
|
||||
end try
|
||||
|
||||
return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatValue, artData}
|
||||
|
|
@ -184,39 +251,5 @@ class AppleMusicController: MediaControllerProtocol {
|
|||
end tell
|
||||
"""
|
||||
return try await AppleScriptHelper.execute(script)
|
||||
} else {
|
||||
let script = """
|
||||
tell application "Music"
|
||||
set isRunning to true
|
||||
try
|
||||
set playerState to player state is playing
|
||||
set currentTrackName to name of current track
|
||||
set currentTrackArtist to artist of current track
|
||||
set currentTrackAlbum to album of current track
|
||||
set trackPosition to player position
|
||||
set trackDuration to duration of current track
|
||||
set shuffleState to shuffle enabled
|
||||
set repeatState to song repeat
|
||||
if repeatState is off then
|
||||
set repeatValue to 1
|
||||
else if repeatState is one then
|
||||
set repeatValue to 2
|
||||
else if repeatState is all then
|
||||
set repeatValue to 3
|
||||
end if
|
||||
|
||||
try
|
||||
set artData to data of artwork 1 of current track
|
||||
on error
|
||||
set artData to ""
|
||||
end try
|
||||
return {playerState, currentTrackName, currentTrackArtist, currentTrackAlbum, trackPosition, trackDuration, shuffleState, repeatValue, artData}
|
||||
on error
|
||||
return {false, "Not Playing", "Unknown", "Unknown", 0, 0, false, 0, ""}
|
||||
end try
|
||||
end tell
|
||||
"""
|
||||
return try await AppleScriptHelper.execute(script)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue