Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
8.0 kB · 187 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188// Model.swift — the listening library + per-track rating sidecar.//// JukeWizard is non-destructive: it never touches the audio. Everything// you leave — a star rating, freeform notes, timestamped comments — is// saved next to each track as <track>.juke.json, so ratings travel with// the file and survive re-renders (a fresh render of the same name keeps// its notes). The library can be loaded from a folder, an .m3u/.m3u8// playlist (lane names picked up from "# --- lane ---" comments), or a// list of individual files — that's the cross-lane queue.import Foundation
struct Comment: Codable { var t: Double // seconds into the track var text: String}
struct JukeData: Codable { var stars: Int = 0 var notes: String = "" var comments: [Comment] = [] var updated: String = ""}
// Release/status metadata for a /pop track (from pop/bin/pop-library.mjs).struct TrackLinks: Codable { var spotify: String?; var apple: String?; var youtube: String?; var distrokid: String? }struct MediaItem: Codable { var kind: String; var path: String }struct TrackMeta: Codable { var artist: String? var backend: String? var status: String? var updated: String? var revisions: Int? var bytes: Int? var durationSec: Double? var bpm: Int? var key: String? var releaseDate: String? var art: String? var media: [MediaItem]? var links: TrackLinks?}
final class Track { let url: URL let title: String let lane: String var data: JukeData var meta: TrackMeta? var liveStatus: String? // Cached so row drawing never stats the disk; refreshed by the // newest/oldest sorts so the displayed stamp always matches the order. var renderedAt: Date = .distantPast
init(url: URL, lane: String, title: String? = nil) { self.url = url self.lane = lane self.title = title ?? url.deletingPathExtension().lastPathComponent self.data = Track.loadSidecar(for: url) refreshRenderedAt() }
/// The moment this track's audio was last rendered: the file's own /// modification date, falling back to the library JSON's `updated` /// stamp for files that have vanished from disk. func refreshRenderedAt() { if let date = (try? FileManager.default.attributesOfItem(atPath: url.path))?[.modificationDate] as? Date { renderedAt = date return } if let stamp = meta?.updated { let iso = ISO8601DateFormatter() iso.formatOptions = [.withInternetDateTime, .withFractionalSeconds] if let date = iso.date(from: stamp) { renderedAt = date; return } iso.formatOptions = [.withInternetDateTime] if let date = iso.date(from: stamp) { renderedAt = date; return } } renderedAt = .distantPast }
var sidecarURL: URL { url.deletingPathExtension().appendingPathExtension("juke.json") }
static func loadSidecar(for url: URL) -> JukeData { let s = url.deletingPathExtension().appendingPathExtension("juke.json") if let d = try? Data(contentsOf: s), let j = try? JSONDecoder().decode(JukeData.self, from: d) { return j } return JukeData() }
func save() { data.updated = ISO8601DateFormatter().string(from: Date()) let enc = JSONEncoder() enc.outputFormatting = [.prettyPrinted, .sortedKeys] if let d = try? enc.encode(data) { try? d.write(to: sidecarURL) } }}
final class Library { private(set) var tracks: [Track] = [] private var seen = Set<String>() static let audioExts: Set<String> = ["mp3", "wav", "flac", "m4a", "aac", "aiff", "aif"]
init(inputs: [String]) { for p in inputs { add(path: p) } }
// Reorder the queue in place (JukeWizard's sort control). func reorder(by cmp: (Track, Track) -> Bool) { tracks.sort(by: cmp) }
@discardableResult func addFile(_ url: URL, lane: String) -> Track? { let key = url.standardizedFileURL.path guard !seen.contains(key) else { return nil } guard Library.audioExts.contains(url.pathExtension.lowercased()) else { return nil } guard FileManager.default.fileExists(atPath: url.path) else { return nil } seen.insert(key) let t = Track(url: url, lane: lane) tracks.append(t) return t }
func add(path: String) { let url = URL(fileURLWithPath: (path as NSString).expandingTildeInPath) var isDir: ObjCBool = false guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDir) else { return } if isDir.boolValue { let lane = url.lastPathComponent let items = (try? FileManager.default.contentsOfDirectory( at: url, includingPropertiesForKeys: nil)) ?? [] for f in items.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) { addFile(f, lane: lane) } } else if ["m3u", "m3u8"].contains(url.pathExtension.lowercased()) { loadPlaylist(url) } else if url.pathExtension.lowercased() == "json" { loadLibrary(url) } else { addFile(url, lane: url.deletingLastPathComponent().lastPathComponent) } }
// a pop-library.json (from pop/bin/pop-library.mjs): tracks + status/meta. private struct LibEntry: Codable { var path: String; var title: String?; var artist: String?; var lane: String? var backend: String?; var status: String?; var updated: String? var revisions: Int?; var bytes: Int?; var durationSec: Double? var bpm: Int?; var key: String?; var releaseDate: String? var art: String?; var media: [MediaItem]?; var links: TrackLinks? } private struct LibFile: Codable { var tracks: [LibEntry] } private func loadLibrary(_ url: URL) { guard let d = try? Data(contentsOf: url), let lib = try? JSONDecoder().decode(LibFile.self, from: d) else { return } for e in lib.tracks { let f = URL(fileURLWithPath: (e.path as NSString).expandingTildeInPath) let key = f.standardizedFileURL.path guard !seen.contains(key), FileManager.default.fileExists(atPath: f.path) else { continue } seen.insert(key) let t = Track(url: f, lane: e.lane ?? f.deletingLastPathComponent().lastPathComponent, title: e.title) t.meta = TrackMeta(artist: e.artist, backend: e.backend, status: e.status, updated: e.updated, revisions: e.revisions, bytes: e.bytes, durationSec: e.durationSec, bpm: e.bpm, key: e.key, releaseDate: e.releaseDate, art: e.art, media: e.media, links: e.links) tracks.append(t) } }
private func loadPlaylist(_ url: URL) { guard let text = try? String(contentsOf: url, encoding: .utf8) else { return } let base = url.deletingLastPathComponent() var lane = "playlist" for raw in text.split(separator: "\n", omittingEmptySubsequences: false) { let line = raw.trimmingCharacters(in: .whitespaces) if line.hasPrefix("#") { // "# --- lullabies (…) ---" → lane label for following entries if line.hasPrefix("# ---") { let inner = line.dropFirst(5) .replacingOccurrences(of: "---", with: "") .trimmingCharacters(in: .whitespaces) if let head = inner.split(separator: " ").first { lane = String(head) } } continue } if line.isEmpty { continue } let f = line.hasPrefix("/") ? URL(fileURLWithPath: line) : base.appendingPathComponent(line) addFile(f, lane: lane) } }}