mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-26 22:11:03 +02:00
9b7f1631f0
- Verification challenge no longer abandons the batch: batch mates the worker cancel would mark skipped are requeued and fenced from later snapshot applications, and the adoption loop restarts the queue for requeued items when the run ends - Pause vs cancel race: a cancelled result with no pause flag set waits up to 1.5s for the pause intent to land on the main looper before being classified as a permanent skip - Stale Go cancel flag: new ResetDownloadCancel export (wired through Android and iOS bridges) drops a pre-registered cancel with no active download; retryItem/retryAllFailed call it so the first retry of a cancelled-before-start item no longer aborts instantly - Reconcile-loop errors now cancel the native worker and clear the run id before marking items failed, instead of leaving the service downloading a batch the UI already wrote off
199 lines
4.1 KiB
Go
199 lines
4.1 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
)
|
|
|
|
// ErrDownloadCancelled is returned when a download is cancelled by the user.
|
|
var ErrDownloadCancelled = errors.New("download cancelled")
|
|
|
|
// ErrExtensionRequestCancelled is returned when a UI-driven extension request
|
|
// is superseded by a newer home/search request.
|
|
var ErrExtensionRequestCancelled = errors.New("extension request cancelled")
|
|
|
|
type cancelEntry struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
canceled bool
|
|
refs int
|
|
}
|
|
|
|
var (
|
|
cancelMu sync.Mutex
|
|
cancelMap = make(map[string]*cancelEntry)
|
|
|
|
extensionRequestCancelMu sync.Mutex
|
|
extensionRequestCancelMap = make(map[string]*cancelEntry)
|
|
)
|
|
|
|
func initDownloadCancel(itemID string) context.Context {
|
|
if itemID == "" {
|
|
return context.Background()
|
|
}
|
|
|
|
cancelMu.Lock()
|
|
defer cancelMu.Unlock()
|
|
|
|
if entry, ok := cancelMap[itemID]; ok {
|
|
if entry.ctx == nil {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
entry.ctx = ctx
|
|
entry.cancel = cancel
|
|
if entry.canceled && entry.cancel != nil {
|
|
entry.cancel()
|
|
}
|
|
}
|
|
entry.refs++
|
|
return entry.ctx
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancelMap[itemID] = &cancelEntry{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
canceled: false,
|
|
refs: 1,
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func cancelDownload(itemID string) {
|
|
if itemID == "" {
|
|
return
|
|
}
|
|
|
|
cancelMu.Lock()
|
|
entry, ok := cancelMap[itemID]
|
|
if ok {
|
|
entry.canceled = true
|
|
if entry.cancel != nil {
|
|
entry.cancel()
|
|
}
|
|
} else {
|
|
cancelMap[itemID] = &cancelEntry{canceled: true}
|
|
}
|
|
cancelMu.Unlock()
|
|
|
|
RemoveItemProgress(itemID)
|
|
}
|
|
|
|
func isDownloadCancelled(itemID string) bool {
|
|
if itemID == "" {
|
|
return false
|
|
}
|
|
|
|
cancelMu.Lock()
|
|
entry, ok := cancelMap[itemID]
|
|
canceled := ok && entry.canceled
|
|
cancelMu.Unlock()
|
|
return canceled
|
|
}
|
|
|
|
// resetDownloadCancel removes a cancellation entry that has no active
|
|
// download attached (refs <= 0). Such entries exist to catch an item that is
|
|
// just about to start, but if the item never starts the flag lingers and the
|
|
// next explicit retry would consume it and abort immediately.
|
|
func resetDownloadCancel(itemID string) {
|
|
if itemID == "" {
|
|
return
|
|
}
|
|
|
|
cancelMu.Lock()
|
|
if entry, ok := cancelMap[itemID]; ok && entry.refs <= 0 {
|
|
delete(cancelMap, itemID)
|
|
}
|
|
cancelMu.Unlock()
|
|
}
|
|
|
|
func clearDownloadCancel(itemID string) {
|
|
if itemID == "" {
|
|
return
|
|
}
|
|
|
|
cancelMu.Lock()
|
|
if entry, ok := cancelMap[itemID]; ok {
|
|
entry.refs--
|
|
if entry.refs <= 0 {
|
|
delete(cancelMap, itemID)
|
|
}
|
|
}
|
|
cancelMu.Unlock()
|
|
}
|
|
|
|
func initExtensionRequestCancel(requestID string) context.Context {
|
|
if requestID == "" {
|
|
return context.Background()
|
|
}
|
|
|
|
extensionRequestCancelMu.Lock()
|
|
defer extensionRequestCancelMu.Unlock()
|
|
|
|
if entry, ok := extensionRequestCancelMap[requestID]; ok {
|
|
if entry.ctx == nil {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
entry.ctx = ctx
|
|
entry.cancel = cancel
|
|
if entry.canceled && entry.cancel != nil {
|
|
entry.cancel()
|
|
}
|
|
}
|
|
entry.refs++
|
|
return entry.ctx
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
extensionRequestCancelMap[requestID] = &cancelEntry{
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
canceled: false,
|
|
refs: 1,
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func cancelExtensionRequest(requestID string) {
|
|
if requestID == "" {
|
|
return
|
|
}
|
|
|
|
extensionRequestCancelMu.Lock()
|
|
if entry, ok := extensionRequestCancelMap[requestID]; ok {
|
|
entry.canceled = true
|
|
if entry.cancel != nil {
|
|
entry.cancel()
|
|
}
|
|
} else {
|
|
extensionRequestCancelMap[requestID] = &cancelEntry{canceled: true}
|
|
}
|
|
extensionRequestCancelMu.Unlock()
|
|
}
|
|
|
|
func isExtensionRequestCancelled(requestID string) bool {
|
|
if requestID == "" {
|
|
return false
|
|
}
|
|
|
|
extensionRequestCancelMu.Lock()
|
|
entry, ok := extensionRequestCancelMap[requestID]
|
|
canceled := ok && entry.canceled
|
|
extensionRequestCancelMu.Unlock()
|
|
return canceled
|
|
}
|
|
|
|
func clearExtensionRequestCancel(requestID string) {
|
|
if requestID == "" {
|
|
return
|
|
}
|
|
|
|
extensionRequestCancelMu.Lock()
|
|
if entry, ok := extensionRequestCancelMap[requestID]; ok {
|
|
entry.refs--
|
|
if entry.refs <= 0 {
|
|
delete(extensionRequestCancelMap, requestID)
|
|
}
|
|
}
|
|
extensionRequestCancelMu.Unlock()
|
|
}
|