mirror of
https://github.com/MioMioOS/mio-plugin-music.git
synced 2026-06-11 03:44:31 +00:00
137 lines
4.4 KiB
Swift
137 lines
4.4 KiB
Swift
|
|
//
|
||
|
|
// SpotifyAppleScript.swift
|
||
|
|
// MioIsland Music Plugin
|
||
|
|
//
|
||
|
|
// AppleScript bridge for the Spotify desktop app. Spotify exposes a rich
|
||
|
|
// scripting dictionary so we can pull title / artist / album / duration /
|
||
|
|
// position and drive transport controls. Artwork URL is also scriptable
|
||
|
|
// which makes this cheaper than any other source.
|
||
|
|
//
|
||
|
|
// Threading: all scripts run on a background queue via runAppleScript.
|
||
|
|
// NSAppleScript is NOT safe to share across threads; each call creates a
|
||
|
|
// fresh instance. Duration from Spotify is in milliseconds (we divide by
|
||
|
|
// 1000 inside the script) and player position is in seconds.
|
||
|
|
//
|
||
|
|
|
||
|
|
import AppKit
|
||
|
|
|
||
|
|
struct AppleScriptTrackInfo {
|
||
|
|
var title: String
|
||
|
|
var artist: String
|
||
|
|
var album: String
|
||
|
|
var duration: TimeInterval
|
||
|
|
var elapsedTime: TimeInterval
|
||
|
|
var isPlaying: Bool
|
||
|
|
var artworkURL: String?
|
||
|
|
var source: String // "Spotify" / "Apple Music"
|
||
|
|
var bundleId: String
|
||
|
|
}
|
||
|
|
|
||
|
|
enum SpotifyAppleScript {
|
||
|
|
private static let bundleId = "com.spotify.client"
|
||
|
|
private static let sourceName = "Spotify"
|
||
|
|
|
||
|
|
// MARK: - Fetch
|
||
|
|
|
||
|
|
static func fetch() async -> AppleScriptTrackInfo? {
|
||
|
|
let script = """
|
||
|
|
tell application "System Events"
|
||
|
|
if not (exists process "Spotify") then return "NOT_RUNNING"
|
||
|
|
end tell
|
||
|
|
tell application "Spotify"
|
||
|
|
if player state is playing or player state is paused then
|
||
|
|
set trackName to name of current track
|
||
|
|
set trackArtist to artist of current track
|
||
|
|
set trackAlbum to album of current track
|
||
|
|
set trackDuration to duration of current track
|
||
|
|
set trackPosition to player position
|
||
|
|
set stateString to "PAUSED"
|
||
|
|
if player state is playing then set stateString to "PLAYING"
|
||
|
|
set artURL to ""
|
||
|
|
try
|
||
|
|
set artURL to artwork url of current track
|
||
|
|
end try
|
||
|
|
return stateString & "||" & trackName & "||" & trackArtist & "||" & trackAlbum & "||" & (trackDuration / 1000) & "||" & trackPosition & "||" & artURL
|
||
|
|
else
|
||
|
|
return "NOT_PLAYING"
|
||
|
|
end if
|
||
|
|
end tell
|
||
|
|
"""
|
||
|
|
|
||
|
|
guard let raw = await runAppleScript(script, tag: "spotify") else { return nil }
|
||
|
|
if raw == "NOT_RUNNING" || raw == "NOT_PLAYING" { return nil }
|
||
|
|
|
||
|
|
let parts = raw.components(separatedBy: "||")
|
||
|
|
guard parts.count >= 6 else { return nil }
|
||
|
|
|
||
|
|
let isPlaying = parts[0] == "PLAYING"
|
||
|
|
let artURL = parts.count >= 7 ? parts[6] : ""
|
||
|
|
|
||
|
|
return AppleScriptTrackInfo(
|
||
|
|
title: parts[1],
|
||
|
|
artist: parts[2],
|
||
|
|
album: parts[3],
|
||
|
|
duration: TimeInterval(parts[4]) ?? 0,
|
||
|
|
elapsedTime: TimeInterval(parts[5]) ?? 0,
|
||
|
|
isPlaying: isPlaying,
|
||
|
|
artworkURL: artURL.isEmpty ? nil : artURL,
|
||
|
|
source: sourceName,
|
||
|
|
bundleId: bundleId
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Artwork
|
||
|
|
|
||
|
|
static func fetchArtwork() async -> NSImage? {
|
||
|
|
let script = """
|
||
|
|
tell application "System Events"
|
||
|
|
if not (exists process "Spotify") then return ""
|
||
|
|
end tell
|
||
|
|
tell application "Spotify"
|
||
|
|
try
|
||
|
|
return artwork url of current track
|
||
|
|
on error
|
||
|
|
return ""
|
||
|
|
end try
|
||
|
|
end tell
|
||
|
|
"""
|
||
|
|
guard let urlString = await runAppleScript(script, tag: "spotify-art"),
|
||
|
|
!urlString.isEmpty,
|
||
|
|
let url = URL(string: urlString) else {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return await downloadImage(from: url)
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - Controls
|
||
|
|
|
||
|
|
static func togglePlay() {
|
||
|
|
runAppleScriptFireAndForget(
|
||
|
|
"tell application \"Spotify\" to playpause",
|
||
|
|
tag: "spotify-toggle"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
static func next() {
|
||
|
|
runAppleScriptFireAndForget(
|
||
|
|
"tell application \"Spotify\" to next track",
|
||
|
|
tag: "spotify-next"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
static func previous() {
|
||
|
|
runAppleScriptFireAndForget(
|
||
|
|
"tell application \"Spotify\" to previous track",
|
||
|
|
tag: "spotify-prev"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
static func seek(to time: TimeInterval) {
|
||
|
|
let clamped = max(0, time)
|
||
|
|
runAppleScriptFireAndForget(
|
||
|
|
"tell application \"Spotify\" to set player position to \(clamped)",
|
||
|
|
tag: "spotify-seek"
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|