| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- import Foundation
- import SwiftData
- /// Imports playlist data from the iOS MixBoard app's sync JSON file.
- /// Matches tracks by filename across devices.
- struct SyncImporter {
- // MARK: - Sync Data Models (must match iOS export format)
- struct SyncPayload: Codable {
- let version: Int
- let exportedAt: Date
- let exportedFrom: String
- let playlists: [SyncPlaylist]
- }
- struct SyncPlaylist: Codable {
- let id: UUID
- let name: String
- let notes: String
- let color: String
- let dateCreated: Date
- let dateModified: Date
- let targetBPM: Double?
- let entries: [SyncEntry]
- }
- struct SyncEntry: Codable {
- let id: UUID
- let position: Int
- let filename: String
- let title: String
- let artist: String
- let album: String
- let duration: TimeInterval
- let bpm: Double?
- let musicalKey: String?
- let crossfadeDuration: TimeInterval
- let startOffset: TimeInterval
- let endOffset: TimeInterval
- let gainAdjustment: Double
- let notes: String
- }
- // MARK: - Import
- /// Import playlists from a sync JSON file. Returns a summary.
- @MainActor
- static func importFromFile(
- _ url: URL,
- context: ModelContext
- ) throws -> ImportResult {
- let data = try Data(contentsOf: url)
- let decoder = JSONDecoder()
- decoder.dateDecodingStrategy = .iso8601
- let payload = try decoder.decode(SyncPayload.self, from: data)
- // Fetch all existing tracks for matching
- let trackDescriptor = FetchDescriptor<Track>()
- let allTracks = (try? context.fetch(trackDescriptor)) ?? []
- // Build a filename → Track lookup
- var tracksByFilename: [String: Track] = [:]
- for track in allTracks {
- let filename = (track.filePath as NSString).lastPathComponent
- tracksByFilename[filename] = track
- }
- var created = 0
- var matched = 0
- var unmatched = 0
- var unmatchedFiles: [String] = []
- for sp in payload.playlists {
- // Create the playlist with "[iOS]" prefix to distinguish
- let playlist = Playlist(name: "📱 \(sp.name)", notes: sp.notes, color: sp.color)
- playlist.targetBPM = sp.targetBPM
- context.insert(playlist)
- created += 1
- for entry in sp.entries.sorted(by: { $0.position < $1.position }) {
- if let track = tracksByFilename[entry.filename] {
- // Found matching track
- let pe = PlaylistEntry(
- position: entry.position,
- track: track,
- crossfadeDuration: entry.crossfadeDuration,
- startOffset: entry.startOffset,
- endOffset: entry.endOffset,
- gainAdjustment: entry.gainAdjustment,
- notes: entry.notes
- )
- playlist.entries.append(pe)
- matched += 1
- } else {
- // No matching track — create placeholder entry with info in notes
- let info = "\(entry.artist) — \(entry.title) [\(entry.filename)]"
- let pe = PlaylistEntry(
- position: entry.position,
- track: nil,
- crossfadeDuration: entry.crossfadeDuration,
- notes: "⚠ Not found: \(info)\n\(entry.notes)"
- )
- playlist.entries.append(pe)
- unmatched += 1
- unmatchedFiles.append(entry.filename)
- }
- }
- }
- try context.save()
- return ImportResult(
- playlistsCreated: created,
- tracksMatched: matched,
- tracksUnmatched: unmatched,
- unmatchedFiles: unmatchedFiles,
- exportedFrom: payload.exportedFrom,
- exportedAt: payload.exportedAt
- )
- }
- struct ImportResult {
- let playlistsCreated: Int
- let tracksMatched: Int
- let tracksUnmatched: Int
- let unmatchedFiles: [String]
- let exportedFrom: String
- let exportedAt: Date
- var summary: String {
- var lines = [
- "Imported \(playlistsCreated) playlist(s) from \(exportedFrom)",
- " \(tracksMatched) tracks matched",
- ]
- if tracksUnmatched > 0 {
- lines.append(" \(tracksUnmatched) tracks not found locally:")
- for file in unmatchedFiles.prefix(10) {
- lines.append(" • \(file)")
- }
- if unmatchedFiles.count > 10 {
- lines.append(" ... and \(unmatchedFiles.count - 10) more")
- }
- }
- return lines.joined(separator: "\n")
- }
- }
- }
|