import Foundation import AVFoundation struct LocalTrack: Identifiable, Hashable { let id: UUID let url: URL let displayName: String let duration: TimeInterval static func libraryDirectory() -> URL { let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let dir = docs.appendingPathComponent("RunPlus", isDirectory: true) if !FileManager.default.fileExists(atPath: dir.path) { try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) } return dir } static func loadLibrary() async -> [LocalTrack] { let dir = libraryDirectory() let urls = (try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil)) ?? [] var tracks: [LocalTrack] = [] for url in urls { let asset = AVURLAsset(url: url) let duration = (try? await asset.load(.duration).seconds) ?? 0 let track = LocalTrack( id: UUID(uuidString: url.deletingPathExtension().lastPathComponent) ?? UUID(), url: url, displayName: url.deletingPathExtension().lastPathComponent, duration: duration ) tracks.append(track) } return tracks.sorted { $0.displayName.lowercased() < $1.displayName.lowercased() } } static func importTrack(from url: URL) async throws -> LocalTrack { let dir = libraryDirectory() let id = UUID() let dest = dir.appendingPathComponent("\(id.uuidString).mp3") let didStart = url.startAccessingSecurityScopedResource() defer { if didStart { url.stopAccessingSecurityScopedResource() } } try FileManager.default.copyItem(at: url, to: dest) let duration = (try? await AVURLAsset(url: dest).load(.duration).seconds) ?? 0 return LocalTrack( id: id, url: dest, displayName: url.deletingPathExtension().lastPathComponent, duration: duration ) } static func importReceivedTrack(from url: URL, trackID: String, displayName: String) async throws -> LocalTrack { let dir = libraryDirectory() let dest = dir.appendingPathComponent("\(trackID).mp3") if FileManager.default.fileExists(atPath: dest.path) { try FileManager.default.removeItem(at: dest) } try FileManager.default.moveItem(at: url, to: dest) let duration = (try? await AVURLAsset(url: dest).load(.duration).seconds) ?? 0 return LocalTrack( id: UUID(uuidString: trackID) ?? UUID(), url: dest, displayName: displayName, duration: duration ) } }