Fix SwiftData persistence concurrency stability

This commit is contained in:
diyaa
2026-05-29 00:06:57 +02:00
parent e5027152ab
commit ebc187f4a1
19 changed files with 554 additions and 132 deletions
@@ -278,8 +278,9 @@ public final class PlaybackController {
}
do {
let fileURL = try localFileURL(for: currentTrack)
try engine.loadTrack(
at: URL(fileURLWithPath: currentTrack.localFilePath),
at: fileURL,
startTime: startTime
)
loadedTrackID = currentTrack.id
@@ -299,15 +300,16 @@ public final class PlaybackController {
nowPlayingState.isPlaying = false
nowPlayingState.currentTrack = currentTrack
nowPlayingState.currentTime = startTime
nowPlayingState.duration = effectiveDuration(for: currentTrack)
nowPlayingState.duration = currentTrack.durationSeconds ?? 0
applyPlaybackError(error)
}
}
private func restoreTrack(_ track: LibraryTrack, position: Double) {
do {
let fileURL = try localFileURL(for: track)
try engine.loadTrack(
at: URL(fileURLWithPath: track.localFilePath),
at: fileURL,
startTime: position
)
loadedTrackID = track.id
@@ -320,7 +322,7 @@ public final class PlaybackController {
nowPlayingState.currentTrack = track
nowPlayingState.isPlaying = false
nowPlayingState.currentTime = position
nowPlayingState.duration = effectiveDuration(for: track)
nowPlayingState.duration = track.durationSeconds ?? 0
applyPlaybackError(error)
}
@@ -378,6 +380,14 @@ public final class PlaybackController {
return track?.durationSeconds ?? 0
}
private func localFileURL(for track: LibraryTrack) throws -> URL {
guard !track.localFilePath.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw PlaybackError.missingLocalFile(path: track.localFilePath)
}
return URL(fileURLWithPath: track.localFilePath)
}
private func startProgressTimer() {
progressTimer?.invalidate()
progressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) {
@@ -44,19 +44,19 @@ public final class AVFoundationPlaybackEngine: NSObject, PlaybackEngine, AVAudio
}
public func loadTrack(at fileURL: URL, startTime: Double) throws {
guard fileManager.fileExists(atPath: fileURL.path) else {
throw PlaybackError.missingLocalFile(path: fileURL.path)
}
do {
unloadCurrentTrack()
try validatePlayableFile(at: fileURL)
let audioPlayer = try AVAudioPlayer(contentsOf: fileURL)
audioPlayer.delegate = self
audioPlayer.prepareToPlay()
audioPlayer.currentTime = min(max(startTime, 0), audioPlayer.duration)
self.audioPlayer = audioPlayer
} catch let error as PlaybackError {
unloadCurrentTrack()
throw error
} catch {
unloadCurrentTrack()
throw PlaybackError.failedToLoadTrack(path: fileURL.path)
}
}
@@ -88,6 +88,38 @@ public final class AVFoundationPlaybackEngine: NSObject, PlaybackEngine, AVAudio
audioPlayer.currentTime = min(max(time, 0), audioPlayer.duration)
}
private func validatePlayableFile(at fileURL: URL) throws {
let filePath = Self.displayPath(for: fileURL)
guard fileURL.isFileURL else {
throw PlaybackError.missingLocalFile(path: filePath)
}
guard !fileURL.path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
throw PlaybackError.missingLocalFile(path: filePath)
}
guard fileManager.fileExists(atPath: fileURL.path) else {
throw PlaybackError.missingLocalFile(path: filePath)
}
guard (try? fileURL.checkResourceIsReachable()) == true else {
throw PlaybackError.missingLocalFile(path: filePath)
}
}
private func unloadCurrentTrack() {
audioPlayer?.stop()
audioPlayer = nil
}
private static func displayPath(for fileURL: URL) -> String {
if fileURL.isFileURL {
return fileURL.path
}
return fileURL.absoluteString
}
nonisolated public func audioPlayerDidFinishPlaying(
_ player: AVAudioPlayer,
successfully flag: Bool
@@ -18,9 +18,19 @@ public enum PlaybackError: Error, LocalizedError, Equatable, Hashable, Sendable
case .noTrackLoaded:
return "No audio track is currently loaded."
case .missingLocalFile(let path):
return "The local file could not be found: \(path)"
let safePath = Self.safeDisplayPath(from: path)
if safePath.isEmpty {
return "The local file could not be found."
}
return "The local file could not be found: \(safePath)"
case .failedToLoadTrack(let path):
return "The audio file could not be opened: \(path)"
let safePath = Self.safeDisplayPath(from: path)
if safePath.isEmpty {
return "The audio file could not be opened."
}
return "The audio file could not be opened: \(safePath)"
case .failedToStartPlayback:
return "Playback could not be started."
case .seekUnavailable:
@@ -28,3 +38,14 @@ public enum PlaybackError: Error, LocalizedError, Equatable, Hashable, Sendable
}
}
}
private extension PlaybackError {
static func safeDisplayPath(from path: String) -> String {
let trimmedPath = path.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedPath.isEmpty else {
return ""
}
return String(trimmedPath)
}
}