MainasuK / Song-Rating

macOS song rating app for Music.app and iTunes.app
8 stars 0 forks source link

Delay until song rating is transferred to Apple Music #18

Open thekryz opened 4 hours ago

thekryz commented 4 hours ago

Hi there! Thank you for your app, I use it a lot! I have a huge unrated library of music, so I rate a lot. Often, I want to rate songs quickly and jump to the next one. But there is a delay - and checking your code, it's there deliberately to "debounce" (?) for 2s in the setRating function. Is this really (still) necessary? I would appreciate an immediate (or only slightly) delayed rating transferral. Would removing the debouncing be possible or what issues does it lead to?

thekryz commented 4 hours ago

How about something along these lines @ iTunesRadioStation.swift:

class LeadingEdgeDebouncer {
    private let delay: TimeInterval
    private var workItem: DispatchWorkItem?
    private var lastExecutionTime: Date?

    init(delay: TimeInterval) {
        self.delay = delay
    }

    func debounce(_ block: @escaping () -> Void) {
        // Cancel any pending execution
        workItem?.cancel()

        let now = Date()
        if lastExecutionTime == nil || now.timeIntervalSince(lastExecutionTime!) > delay {
            // Execute immediately if it's the first call or if enough time has passed since the last execution
            lastExecutionTime = now
            block()
        } else {
            // Debounce subsequent calls
            let newWorkItem = DispatchWorkItem {
                self.lastExecutionTime = Date()
                block()
            }
            workItem = newWorkItem
            // Schedule the work item to be executed after the specified delay
            DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: newWorkItem)
        }
    }
}

extension iTunesRadioStation {
    // Create a debouncer with a 0.3 second delay
    private let ratingDebouncer = LeadingEdgeDebouncer(delay: 0.3)

    func setRating(_ rating: Int) {
        // Invalidate any existing timer
        debounceSetRatingTimer?.invalidate()

        // Ensure we have a current track to rate
        guard latestPlayInfo != nil || !(iTunes?.currentTrack?.name ?? "").isEmpty else {
            os_log("%{public}s[%{public}ld], %{public}s: try to set rating but no current track info", ((#file as NSString).lastPathComponent), #line, #function)
            return
        }

        let name = latestPlayInfo?.name ?? iTunes?.currentTrack?.name ?? "nil"
        os_log("%{public}s[%{public}ld], %{public}s: debouncing set rating for %{public}s %{public}ld…", ((#file as NSString).lastPathComponent), #line, #function, name, rating)

        // Use the debouncer to handle the rating change
        ratingDebouncer.debounce { [weak self] in
            guard let self = self else { return }
            let track = self.iTunes?.currentTrack
            // Set the rating on the track
            track?.setRating?(rating)
            os_log("%{public}s[%{public}ld], %{public}s: … set %{public}s rating %{public}ld", ((#file as NSString).lastPathComponent), #line, #function, track?.name ?? "nil", rating)
        }
    }
}

this should implement a Leading Edge Debouncer which will:

  1. Apply the first rating change immediately
  2. Debounce subsequent rapid changes within a short time window (e.g., 0.3 seconds)

This way the app would instantly transfer the initial rating while protecting against potential issues from rapid successive changes.