From 050ca79d0ac8bc48ad4d1482ee743edb1d6153c5 Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Mon, 13 Jul 2026 04:10:21 +0000 Subject: [PATCH] jukewizard: Winamp-style redesign — playback on top, dressed-up track list - vertical layout: now-playing header (large art / streaming video, gold title, artist, platform links, waveform, transport, green LED time) over a full-width track list - colorful TrackRowView rows tinted per lane: art thumb + title + artist + Spotify/Apple/YouTube/DistroKid badges; star ratings dropped from the list - published tracks stream their video in the header (local clip, else the released single's CDN cut) via NowPlayingMedia, replacing the wizard backdrop - notes/comments/rating moved into a collapsible drawer (♪ notes) - classic translucent shiny CD icon (less rainbow); artist in pop-library index --- juke-wizard/Sources/JukeWizard/Assets/jukewizard-cd.png | 0 juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-dark.png | 0 juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-light.png | 0 juke-wizard/Sources/JukeWizard/Assets/jukewizard-mascot.png | 0 juke-wizard/Sources/JukeWizard/JukeController.swift | 412 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ juke-wizard/Sources/JukeWizard/Model.swift | 5 +++-- juke-wizard/Sources/JukeWizard/NowPlayingMedia.swift | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ juke-wizard/Sources/JukeWizard/TrackRowView.swift | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ juke-wizard/bin/gen-cd-icon.sh | 28 ++++++++++++++++------------ pop/bin/pop-library.mjs | 1 + pop/out/pop-library.json | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 11 file(s) changed, 534 insertion(s)(+), 225 deletion(s)(-) diff --git a/juke-wizard/Sources/JukeWizard/Assets/jukewizard-cd.png b/juke-wizard/Sources/JukeWizard/Assets/jukewizard-cd.png --- a/juke-wizard/Sources/JukeWizard/Assets/jukewizard-cd.png +++ b/juke-wizard/Sources/JukeWizard/Assets/jukewizard-cd.png diff --git a/juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-dark.png b/juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-dark.png --- a/juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-dark.png +++ b/juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-dark.png diff --git a/juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-light.png b/juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-light.png --- a/juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-light.png +++ b/juke-wizard/Sources/JukeWizard/Assets/jukewizard-icon-light.png diff --git a/juke-wizard/Sources/JukeWizard/Assets/jukewizard-mascot.png b/juke-wizard/Sources/JukeWizard/Assets/jukewizard-mascot.png --- a/juke-wizard/Sources/JukeWizard/Assets/jukewizard-mascot.png +++ b/juke-wizard/Sources/JukeWizard/Assets/jukewizard-mascot.png diff --git a/juke-wizard/Sources/JukeWizard/JukeController.swift b/juke-wizard/Sources/JukeWizard/JukeController.swift --- a/juke-wizard/Sources/JukeWizard/JukeController.swift +++ b/juke-wizard/Sources/JukeWizard/JukeController.swift @@ -85,17 +85,20 @@ } } } - // views - var backdrop: BackdropView! + // views — Winamp-style: now-playing header on top, track list underneath. + var nowPlaying: NowPlayingMedia! var listTable: NSTableView! var titleLabel: NSTextField! + var artistLabel: NSTextField! var laneLabel: NSTextField! - var artView: NSImageView! var linkButtons: [NSButton] = [] - var mediaButtons: [NSButton] = [] var wave: WaveformView! var playButton: NSButton! - var timeLabel: NSTextField! + var ledLabel: NSTextField! + var notesToggle: NSButton! + var drawerPanel: NSView! + var drawerOpen = false + var currentArt: NSImage? var commentNowButton: NSButton! var starButtons: [NSButton] = [] var notesView: NSTextView! @@ -172,64 +175,19 @@ private func setupUI() { guard let content = window?.contentView else { return } content.wantsLayer = true applyThemeBackground() - DistributedNotificationCenter.default().addObserver( - forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"), - object: nil, queue: .main) { [weak self] _ in self?.applyThemeBackground() } - // living video backdrop behind everything (never intercepts clicks; - // falls back to the faded static illy until the loop is generated) - backdrop = BackdropView(frame: content.bounds) - backdrop.autoresizingMask = [.width, .height] - content.addSubview(backdrop) + // ── now-playing header: big art / streaming video on the left ───────── + nowPlaying = NowPlayingMedia(frame: .zero) + content.addSubview(nowPlaying) - sortPopup = NSPopUpButton(frame: .zero, pullsDown: false) - sortPopup.addItems(withTitles: SortMode.allCases.map { "sort: \($0.label)" }) - sortPopup.selectItem(at: sortMode.rawValue) - sortPopup.target = self - sortPopup.action = #selector(sortChanged(_:)) - sortPopup.bezelStyle = .rounded - sortPopup.controlSize = .small - sortPopup.font = NSFont.systemFont(ofSize: 11) - content.addSubview(sortPopup) - - listTable = NSTableView() - let col = NSTableColumn(identifier: .init("track")) - col.title = "queue" - listTable.addTableColumn(col) - listTable.headerView = nil - listTable.rowHeight = 34 - listTable.dataSource = self - listTable.delegate = self - listTable.target = self - listTable.action = #selector(listClicked) - listScroll = NSScrollView() - listScroll.documentView = listTable - listScroll.hasVerticalScroller = true - listScroll.borderType = .bezelBorder - content.addSubview(listScroll) - - titleLabel = label("", size: 20, bold: true) + titleLabel = label("", size: 24, bold: true) + titleLabel.lineBreakMode = .byTruncatingTail + titleLabel.textColor = Palette.gold + artistLabel = label("", size: 13, color: NSColor(white: 0.85, alpha: 1)) laneLabel = label("", size: 11, color: .secondaryLabelColor) - laneLabel.usesSingleLineMode = false - laneLabel.maximumNumberOfLines = 2 - laneLabel.lineBreakMode = .byWordWrapping - content.addSubview(titleLabel) - content.addSubview(laneLabel) - - // cover art — square thumbnail top-right; click to open full size. - artView = NSImageView() - artView.imageScaling = .scaleProportionallyUpOrDown - artView.wantsLayer = true - artView.layer?.cornerRadius = 6 - artView.layer?.masksToBounds = true - artView.layer?.borderWidth = 1 - artView.layer?.borderColor = Palette.inkDim.withAlphaComponent(0.4).cgColor - artView.isHidden = true - let artClick = NSClickGestureRecognizer(target: self, action: #selector(openArt)) - artView.addGestureRecognizer(artClick) - content.addSubview(artView) + laneLabel.lineBreakMode = .byTruncatingTail + content.addSubview(titleLabel); content.addSubview(artistLabel); content.addSubview(laneLabel) - // per-service release links (only the ones this track has are shown). for svc in LinkService.allCases { let b = NSButton(title: svc.title, target: self, action: #selector(linkClicked(_:))) b.bezelStyle = .inline @@ -254,25 +212,74 @@ let next = NSButton(title: "⏭", target: self, action: #selector(nextTrack)) next.bezelStyle = .rounded; next.tag = 2 content.addSubview(prev); content.addSubview(next) transportExtra = [prev, next] - commentNowButton = NSButton(title: "+ comment @ now", target: self, action: #selector(addCommentNow)) - commentNowButton.bezelStyle = .rounded - content.addSubview(commentNowButton) - timeLabel = label("0:00 / 0:00", size: 12, color: .secondaryLabelColor) - timeLabel.alignment = .right - content.addSubview(timeLabel) + + // green LED time readout (Winamp signature) — monospace, glowing green. + ledLabel = label("0:00 / 0:00", size: 15, color: NSColor(srgbRed: 0.30, green: 1.0, blue: 0.45, alpha: 1)) + ledLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 15, weight: .bold) + ledLabel.alignment = .right + content.addSubview(ledLabel) + + notesToggle = NSButton(title: "♪ notes", target: self, action: #selector(toggleDrawer)) + notesToggle.bezelStyle = .rounded + notesToggle.setButtonType(.pushOnPushOff) + content.addSubview(notesToggle) + + // ── the track list (underneath) ─────────────────────────────────────── + sortPopup = NSPopUpButton(frame: .zero, pullsDown: false) + sortPopup.addItems(withTitles: SortMode.allCases.map { "sort: \($0.label)" }) + sortPopup.selectItem(at: sortMode.rawValue) + sortPopup.target = self + sortPopup.action = #selector(sortChanged(_:)) + sortPopup.bezelStyle = .rounded + sortPopup.controlSize = .small + sortPopup.font = NSFont.systemFont(ofSize: 11) + content.addSubview(sortPopup) + + listTable = NSTableView() + let col = NSTableColumn(identifier: .init("track")) + listTable.addTableColumn(col) + listTable.headerView = nil + listTable.rowHeight = TrackRowView.height + listTable.backgroundColor = .clear + listTable.selectionHighlightStyle = .none + listTable.dataSource = self + listTable.delegate = self + listTable.target = self + listTable.action = #selector(listClicked) + listScroll = NSScrollView() + listScroll.documentView = listTable + listScroll.hasVerticalScroller = true + listScroll.drawsBackground = false + listScroll.borderType = .noBorder + content.addSubview(listScroll) + + buildDrawer(in: content) + + playButton.contentTintColor = Palette.teal + transportExtra.forEach { $0.contentTintColor = Palette.teal } + } + + // The collapsible notes + comments + rating drawer (hidden by default). + private func buildDrawer(in content: NSView) { + drawerPanel = NSView() + drawerPanel.wantsLayer = true + drawerPanel.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.82).cgColor + drawerPanel.layer?.cornerRadius = 10 + drawerPanel.layer?.borderWidth = 1 + drawerPanel.layer?.borderColor = Palette.teal.withAlphaComponent(0.5).cgColor + drawerPanel.isHidden = true + content.addSubview(drawerPanel) for i in 1...5 { let b = NSButton(title: "☆", target: self, action: #selector(starClicked(_:))) - b.tag = i - b.isBordered = false + b.tag = i; b.isBordered = false b.font = NSFont.systemFont(ofSize: 22) b.contentTintColor = .systemYellow - starButtons.append(b) - content.addSubview(b) + starButtons.append(b); drawerPanel.addSubview(b) } clearStars = NSButton(title: "clear", target: self, action: #selector(clearStarsClicked)) - clearStars.bezelStyle = .inline - content.addSubview(clearStars) + clearStars.bezelStyle = .inline; clearStars.contentTintColor = Palette.inkDim + drawerPanel.addSubview(clearStars) notesView = NSTextView() notesView.isRichText = false @@ -283,16 +290,20 @@ notesScroll = NSScrollView() notesScroll.documentView = notesView notesScroll.hasVerticalScroller = true notesScroll.borderType = .bezelBorder - content.addSubview(notesScroll) + drawerPanel.addSubview(notesScroll) notesPlaceholder = label("notes…", size: 12, color: .tertiaryLabelColor) - content.addSubview(notesPlaceholder) + drawerPanel.addSubview(notesPlaceholder) + + commentNowButton = NSButton(title: "+ comment @ now", target: self, action: #selector(addCommentNow)) + commentNowButton.bezelStyle = .rounded; commentNowButton.contentTintColor = Palette.coral + drawerPanel.addSubview(commentNowButton) commentsTable = NSTableView() let cc = NSTableColumn(identifier: .init("comment")) - cc.title = "comments" commentsTable.addTableColumn(cc) commentsTable.headerView = nil commentsTable.rowHeight = 24 + commentsTable.backgroundColor = .clear commentsTable.dataSource = self commentsTable.delegate = self commentsTable.target = self @@ -301,35 +312,32 @@ commentsTable.action = #selector(commentClicked) commentsScroll = NSScrollView() commentsScroll.documentView = commentsTable commentsScroll.hasVerticalScroller = true + commentsScroll.drawsBackground = false commentsScroll.borderType = .bezelBorder - content.addSubview(commentsScroll) - commentsHeader = label("comments · space play · ←→ seek · ↑↓ track · 1–5 stars · C comment", size: 11, color: .secondaryLabelColor) - content.addSubview(commentsHeader) + drawerPanel.addSubview(commentsScroll) + commentsHeader = label("comments · click to jump", size: 11, color: .secondaryLabelColor) + drawerPanel.addSubview(commentsHeader) delCommentButton = NSButton(title: "– delete", target: self, action: #selector(deleteComment)) - delCommentButton.bezelStyle = .inline - delCommentButton.contentTintColor = Palette.coral - content.addSubview(delCommentButton) + delCommentButton.bezelStyle = .inline; delCommentButton.contentTintColor = Palette.coral + drawerPanel.addSubview(delCommentButton) - // colored controls — teal transport, gold rating, coral accents. - playButton.contentTintColor = Palette.teal - transportExtra.forEach { $0.contentTintColor = Palette.teal } - commentNowButton.contentTintColor = Palette.coral - clearStars.contentTintColor = Palette.inkDim - titleLabel.textColor = Palette.teal - laneLabel.textColor = Palette.coral commentsHeader.textColor = Palette.inkDim - listTable.backgroundColor = .clear - commentsTable.backgroundColor = .clear - listTable.enclosingScrollView?.drawsBackground = false - commentsTable.enclosingScrollView?.drawsBackground = false notesScroll.drawsBackground = false notesView.drawsBackground = false + } + + @objc private func toggleDrawer() { + drawerOpen.toggle() + drawerPanel.isHidden = !drawerOpen + notesToggle.state = drawerOpen ? .on : .off + relayout() } private func applyThemeBackground() { - let dark = window?.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua - window?.backgroundColor = Palette.bg(dark) - window?.contentView?.layer?.backgroundColor = Palette.bg(dark).withAlphaComponent(0.6).cgColor + // Winamp chassis is always a dark deck, regardless of system theme. + let chassis = NSColor(srgbRed: 0.07, green: 0.08, blue: 0.10, alpha: 1) + window?.backgroundColor = chassis + window?.contentView?.layer?.backgroundColor = chassis.cgColor } var transportExtra: [NSButton] = [] @@ -352,71 +360,71 @@ private func relayout() { guard let content = window?.contentView else { return } let W = content.bounds.width, H = content.bounds.height let pad: CGFloat = 12 - // proportional, clamped — looks right tiny or huge. - let sidebarW = max(200, min(340, W * 0.26)) - let sortH: CGFloat = 22 - sortPopup.frame = NSRect(x: pad, y: H - pad - sortH, width: sidebarW, height: sortH) - listScroll.frame = NSRect(x: pad, y: pad, width: sidebarW, height: H - pad * 2 - sortH - 6) - let rx = sidebarW + pad * 2 - let rw = W - rx - pad - // cover art — square thumbnail pinned top-right of the right panel. - let artSize: CGFloat = artView.isHidden ? 0 : min(84, max(56, rw * 0.14)) - let textW = rw - (artSize > 0 ? artSize + 10 : 0) - if artSize > 0 { - artView.frame = NSRect(x: rx + rw - artSize, y: H - pad - artSize, width: artSize, height: artSize) - } - var y = H - pad - 28 - titleLabel.frame = NSRect(x: rx, y: y, width: textW, height: 28) + // ── header (now-playing) across the top ─────────────────────────────── + let headerH = max(240, min(380, H * 0.46)) + let headerBottom = H - headerH + let mediaSide = min(headerH - pad * 2, W * 0.44) + nowPlaying.frame = NSRect(x: pad, y: headerBottom + pad, width: mediaSide, height: headerH - pad * 2) + let rx = pad + mediaSide + 14 + let rw = max(120, W - rx - pad) + + var y = H - pad - 32 + titleLabel.frame = NSRect(x: rx, y: y, width: rw, height: 32) + y -= 24 + artistLabel.frame = NSRect(x: rx, y: y, width: rw, height: 20) + y -= 18 + laneLabel.frame = NSRect(x: rx, y: y, width: rw, height: 16) y -= 26 - // per-service link buttons flow left→right on their own row. - var lx = rx + var lx = rx // per-service link buttons row for b in linkButtons where !b.isHidden { let bw = b.attributedTitle.size().width + 16 b.frame = NSRect(x: lx, y: y, width: bw, height: 20) lx += bw + 6 } - let hasLinks = linkButtons.contains { !$0.isHidden } - if hasLinks { y -= 24 } - laneLabel.frame = NSRect(x: rx, y: y, width: textW, height: 28) - y -= 8 - // media buttons (reels/stories) flow on their own row when present. - if !mediaButtons.isEmpty { - var mx = rx - y -= 22 - for b in mediaButtons { - let bw = min(160, b.attributedTitle.size().width + 16) - if mx + bw > rx + rw { mx = rx; y -= 22 } // wrap - b.frame = NSRect(x: mx, y: y, width: bw, height: 20) - mx += bw + 6 - } - y -= 6 + let linksBottom = y + + // transport row pinned to the header's bottom edge + let transY = headerBottom + pad + transportExtra[0].frame = NSRect(x: rx, y: transY, width: 40, height: 30) // prev + playButton.frame = NSRect(x: rx + 44, y: transY, width: 54, height: 30) + transportExtra[1].frame = NSRect(x: rx + 102, y: transY, width: 40, height: 30) // next + notesToggle.frame = NSRect(x: rx + 150, y: transY, width: 92, height: 30) + ledLabel.frame = NSRect(x: rx + rw - 160, y: transY + 5, width: 160, height: 22) + + // waveform fills the space between the links row and the transport + let waveTop = linksBottom - 6 + let waveBottom = transY + 38 + wave.frame = NSRect(x: rx, y: waveBottom, width: rw, height: max(44, waveTop - waveBottom)) + + // ── track list underneath ───────────────────────────────────────────── + let sortY = headerBottom - 4 - 22 + sortPopup.frame = NSRect(x: pad, y: sortY, width: 230, height: 22) + listScroll.frame = NSRect(x: pad, y: pad, width: W - pad * 2, height: sortY - pad - 6) + + // ── drawer overlays the list when open ──────────────────────────────── + if drawerOpen { + drawerPanel.frame = listScroll.frame.insetBy(dx: 0, dy: 0) + layoutDrawer() } - let waveH: CGFloat = max(110, min(195, H * 0.22)) - y -= waveH - wave.frame = NSRect(x: rx, y: y, width: rw, height: waveH) - y -= 8 + 30 - playButton.frame = NSRect(x: rx, y: y, width: 46, height: 30) - transportExtra[0].frame = NSRect(x: rx + 52, y: y, width: 46, height: 30) - transportExtra[1].frame = NSRect(x: rx + 104, y: y, width: 46, height: 30) - commentNowButton.frame = NSRect(x: rx + 160, y: y, width: 170, height: 30) - timeLabel.frame = NSRect(x: rx + rw - 150, y: y + 6, width: 150, height: 18) - y -= 8 + 30 + } + + private func layoutDrawer() { + let dw = drawerPanel.bounds.width, dh = drawerPanel.bounds.height, dp: CGFloat = 12 + let starsY = dh - dp - 30 for (i, b) in starButtons.enumerated() { - b.frame = NSRect(x: rx + CGFloat(i) * 30, y: y, width: 30, height: 30) + b.frame = NSRect(x: dp + CGFloat(i) * 30, y: starsY, width: 30, height: 30) } - clearStars.frame = NSRect(x: rx + 5 * 30 + 8, y: y + 4, width: 50, height: 22) - y -= 8 - // comments block pinned to the bottom; notes fill the middle. - let commentsH: CGFloat = max(120, (y - pad) * 0.42) - let commentsTop = pad + commentsH - delCommentButton.frame = NSRect(x: rx + rw - 70, y: pad + commentsH + 2, width: 70, height: 18) - commentsHeader.frame = NSRect(x: rx, y: pad + commentsH + 2, width: rw - 75, height: 16) - commentsScroll.frame = NSRect(x: rx, y: pad, width: rw, height: commentsH) - let notesTop = y - let notesBottom = commentsTop + 22 - let notesH = max(60, notesTop - notesBottom) - notesScroll.frame = NSRect(x: rx, y: notesBottom, width: rw, height: notesH) - notesPlaceholder.frame = NSRect(x: rx + 6, y: notesBottom + notesH - 20, width: 100, height: 16) + clearStars.frame = NSRect(x: dp + 5 * 30 + 8, y: starsY + 4, width: 50, height: 22) + commentNowButton.frame = NSRect(x: dw - 180 - dp, y: starsY, width: 180, height: 30) + let commentsH = max(90, (dh - dp * 2) * 0.4) + commentsHeader.frame = NSRect(x: dp, y: dp + commentsH + 2, width: dw - 90, height: 16) + delCommentButton.frame = NSRect(x: dw - 70 - dp, y: dp + commentsH + 2, width: 70, height: 18) + commentsScroll.frame = NSRect(x: dp, y: dp, width: dw - dp * 2, height: commentsH) + let notesTop = starsY - 8 + let notesBottom = dp + commentsH + 22 + let notesH = max(50, notesTop - notesBottom) + notesScroll.frame = NSRect(x: dp, y: notesBottom, width: dw - dp * 2, height: notesH) + notesPlaceholder.frame = NSRect(x: dp + 6, y: notesBottom + notesH - 20, width: 100, height: 16) } // ── menu-bar CD ──────────────────────────────────────────────────────── @@ -441,25 +449,31 @@ func select(_ i: Int, autoplay: Bool) { guard i >= 0, i < library.tracks.count else { return } commitNotes() + let old = current current = i let t = library.tracks[i] titleLabel.stringValue = t.title + titleLabel.textColor = Self.statusColor(t.meta?.status) + artistLabel.stringValue = t.meta?.artist ?? "Aesthetic Dot Computer" laneLabel.stringValue = Self.metaLine(t) - laneLabel.textColor = Self.statusColor(t.meta?.status) - loadArt(t) + laneLabel.textColor = .secondaryLabelColor + updateNowPlaying(t) loadLinks(t) - buildMediaButtons(t) - relayout() // links/media/art change the header height + relayout() // link count changes the header row width wave.load(url: t.url) wave.comments = t.data.comments notesView.string = t.data.notes notesPlaceholder.isHidden = !t.data.notes.isEmpty renderStars(t.data.stars) commentsTable.reloadData() - listTable.selectRowIndexes(IndexSet(integer: i), byExtendingSelection: false) + // refresh only the two affected rows (custom .selected chip) + var rows = IndexSet(integer: i) + if old >= 0, old < library.tracks.count { rows.insert(old) } + listTable.reloadData(forRowIndexes: rows, columnIndexes: IndexSet(integer: 0)) listTable.scrollRowToVisible(i) updateTime() - if autoplay { wave.play(); playButton.title = "❚❚" } else { playButton.title = "▶" } + if autoplay { wave.play(); playButton.title = "❚❚"; nowPlaying.setPaused(false) } + else { playButton.title = "▶"; nowPlaying.setPaused(true) } refreshMenuBar() // new tempo + play state → spin the bar CD } @@ -522,6 +536,7 @@ } @objc private func togglePlay() { wave.togglePlay() playButton.title = wave.isPlaying ? "❚❚" : "▶" + nowPlaying.setPaused(!wave.isPlaying) refreshMenuBar() } @objc private func prevTrack() { if current > 0 { select(current - 1, autoplay: true) } } @@ -580,7 +595,7 @@ commentsTable.reloadData() listTable.reloadData(forRowIndexes: IndexSet(integer: current), columnIndexes: IndexSet(integer: 0)) } } - if wasPlaying { wave.play(); playButton.title = "❚❚" } + if wasPlaying { wave.play(); playButton.title = "❚❚"; nowPlaying.setPaused(false) } refreshMenuBar() } @objc private func commentClicked() { @@ -604,7 +619,7 @@ // ── waveform delegate ─────────────────────────────────────────────────── func waveformDidFinish() { playButton.title = "▶"; nextTrack(); refreshMenuBar() } func waveformTick() { updateTime() } private func updateTime() { - timeLabel.stringValue = "\(JukeController.mmss(wave.currentTime)) / \(JukeController.mmss(wave.duration))" + ledLabel.stringValue = "\(JukeController.mmss(wave.currentTime)) / \(JukeController.mmss(wave.duration))" } // ── tables ─────────────────────────────────────────────────────────────── @@ -612,19 +627,19 @@ func numberOfRows(in tableView: NSTableView) -> Int { if tableView == listTable { return library.tracks.count } return track?.data.comments.count ?? 0 } + // list = dressed-up view rows; comments = plain cell strings. + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + guard tableView == listTable, row < library.tracks.count else { return nil } + let cell = (listTable.makeView(withIdentifier: TrackRowView.id, owner: self) as? TrackRowView) + ?? { let v = TrackRowView(); v.identifier = TrackRowView.id; return v }() + cell.configure(library.tracks[row]) + cell.selected = (row == current) + return cell + } func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { - if tableView == listTable { - let t = library.tracks[row] - let stars = String(repeating: "★", count: t.data.stars) + String(repeating: "☆", count: 5 - t.data.stars) - let cc = t.data.comments.count - let badge = cc > 0 ? " 💬\(cc)" : "" - let st = t.meta?.status.map { " · \($0)" } ?? "" - return "\(stars) \(t.title)\n \(t.lane)\(st)\(badge)" - } else { - guard let t = track, row < t.data.comments.count else { return "" } - let c = t.data.comments[row] - return "\(JukeController.mmss(c.t)) \(c.text)" - } + guard tableView == commentsTable, let t = track, row < t.data.comments.count else { return "" } + let c = t.data.comments[row] + return "\(JukeController.mmss(c.t)) \(c.text)" } // ── auto-pop watcher ───────────────────────────────────────────────────── @@ -720,16 +735,24 @@ if let k = m.key { parts.append(k) } if let sz = size(m.bytes) { parts.append(sz) } return parts.joined(separator: " · ") } - // ── art + links + media ────────────────────────────────────────────────── - private func loadArt(_ t: Track) { - artView.image = nil - artView.isHidden = true - guard let p = t.meta?.art else { return } - let url = URL(fileURLWithPath: (p as NSString).expandingTildeInPath) - if let img = NSImage(contentsOf: url) { - artView.image = img - artView.isHidden = false + // ── now-playing art + video + links ────────────────────────────────────── + // Big cover in the header; if the track has a video (a local reel/story, or + // the CDN cut for a released single) it streams there instead — muted. + private func updateNowPlaying(_ t: Track) { + currentArt = t.meta?.art.flatMap { + NSImage(contentsOf: URL(fileURLWithPath: ($0 as NSString).expandingTildeInPath)) } + nowPlaying.present(art: currentArt, videoURL: Self.videoURL(for: t)) + } + // Prefer a local clip; else stream the released single's CDN cut. + private static func videoURL(for t: Track) -> URL? { + if let v = t.meta?.media?.first(where: { $0.kind == "video" }) { + return URL(fileURLWithPath: (v.path as NSString).expandingTildeInPath) + } + if t.meta?.status == "RELEASED" { + return URL(string: "https://assets.aesthetic.computer/pop/\(t.title).mp4") + } + return nil } private func loadLinks(_ t: Track) { for b in linkButtons { @@ -737,40 +760,9 @@ let svc = LinkService(rawValue: b.tag)! b.isHidden = (svc.url(t.meta?.links) == nil) } } - private func buildMediaButtons(_ t: Track) { - mediaButtons.forEach { $0.removeFromSuperview() } - mediaButtons.removeAll() - guard let media = t.meta?.media, !media.isEmpty else { return } - for (i, m) in media.enumerated() { - let name = URL(fileURLWithPath: m.path).deletingPathExtension().lastPathComponent - // trim the track title prefix so the label reads "reel", "story"… - var short = name - if short.lowercased().hasPrefix(t.title.lowercased()) { - short = String(short.dropFirst(t.title.count)).trimmingCharacters(in: CharacterSet(charactersIn: "-_ ")) - } - if short.isEmpty { short = m.kind } - let b = NSButton(title: "▶ \(short)", target: self, action: #selector(mediaClicked(_:))) - b.bezelStyle = .inline - b.tag = i - b.contentTintColor = Palette.teal - b.font = NSFont.systemFont(ofSize: 11) - mediaButtons.append(b) - window?.contentView?.addSubview(b) - } - } @objc private func linkClicked(_ sender: NSButton) { guard let t = track, let svc = LinkService(rawValue: sender.tag), let s = svc.url(t.meta?.links), let u = URL(string: s) else { return } NSWorkspace.shared.open(u) - } - @objc private func mediaClicked(_ sender: NSButton) { - guard let t = track, let media = t.meta?.media, - sender.tag < media.count else { return } - let url = URL(fileURLWithPath: (media[sender.tag].path as NSString).expandingTildeInPath) - NSWorkspace.shared.open(url) - } - @objc private func openArt() { - guard let t = track, let p = t.meta?.art else { return } - NSWorkspace.shared.open(URL(fileURLWithPath: (p as NSString).expandingTildeInPath)) } } diff --git a/juke-wizard/Sources/JukeWizard/Model.swift b/juke-wizard/Sources/JukeWizard/Model.swift --- a/juke-wizard/Sources/JukeWizard/Model.swift +++ b/juke-wizard/Sources/JukeWizard/Model.swift @@ -25,6 +25,7 @@ // 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? @@ -114,7 +115,7 @@ } // a pop-library.json (from pop/bin/pop-library.mjs): tracks + status/meta. private struct LibEntry: Codable { - var path: String; var title: String?; var lane: String? + 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? @@ -130,7 +131,7 @@ 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(backend: e.backend, status: e.status, updated: e.updated, + 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) diff --git a/juke-wizard/Sources/JukeWizard/NowPlayingMedia.swift b/juke-wizard/Sources/JukeWizard/NowPlayingMedia.swift new file mode 100644 --- /dev/null +++ b/juke-wizard/Sources/JukeWizard/NowPlayingMedia.swift @@ -0,0 +1,88 @@ +// NowPlayingMedia.swift — the big square in the Winamp-style header. It shows +// the track's cover art large, and when a published track has a video (a local +// reel or the CDN cut) it STREAMS that video here instead — muted, looping, +// aspect-filled — the moving accompaniment while the mp3 plays underneath. +// Replaces the old wizard-mp4 backdrop. +import AppKit +import AVFoundation + +final class NowPlayingMedia: NSView { + private var player: AVQueuePlayer? + private var looper: AVPlayerLooper? + private let playerLayer = AVPlayerLayer() + private var art: NSImage? + private var currentVideoKey: String? + + override init(frame: NSRect) { + super.init(frame: frame) + wantsLayer = true + layer?.backgroundColor = NSColor.black.withAlphaComponent(0.28).cgColor + layer?.cornerRadius = 10 + layer?.masksToBounds = true + layer?.borderWidth = 1 + layer?.borderColor = NSColor.white.withAlphaComponent(0.10).cgColor + playerLayer.videoGravity = .resizeAspectFill + playerLayer.opacity = 0 + layer?.addSublayer(playerLayer) + } + required init?(coder: NSCoder) { fatalError() } + override var isFlipped: Bool { false } + + // Show the cover; stream the video if a published track has one. + func present(art: NSImage?, videoURL: URL?) { + self.art = art + needsDisplay = true + let key = videoURL?.absoluteString + if key == currentVideoKey { return } // already showing this clip + currentVideoKey = key + teardownVideo() + guard let url = videoURL else { return } + let item = AVPlayerItem(url: url) + let q = AVQueuePlayer() + q.isMuted = true // audio comes from the mp3 + q.actionAtItemEnd = .advance + looper = AVPlayerLooper(player: q, templateItem: item) + player = q + playerLayer.player = q + playerLayer.opacity = 1 + q.play() + } + + func setPaused(_ paused: Bool) { + guard player != nil else { return } + paused ? player?.pause() : player?.play() + } + + private func teardownVideo() { + player?.pause() + looper = nil + player = nil + playerLayer.player = nil + playerLayer.opacity = 0 + } + + override func layout() { + super.layout() + CATransaction.begin(); CATransaction.setDisableActions(true) + playerLayer.frame = bounds + CATransaction.commit() + } + + override func draw(_ dirtyRect: NSRect) { + NSColor.black.withAlphaComponent(0.28).setFill() + dirtyRect.fill() + guard playerLayer.opacity == 0, let img = art else { return } // art shows only when no video + // aspect-fit the cover, centered + let iar = img.size.width / max(1, img.size.height) + let var_ = bounds.width / max(1, bounds.height) + var r = bounds.insetBy(dx: 6, dy: 6) + if iar > var_ { + let h = r.width / iar + r = NSRect(x: r.minX, y: r.midY - h / 2, width: r.width, height: h) + } else { + let w = r.height * iar + r = NSRect(x: r.midX - w / 2, y: r.minY, width: w, height: r.height) + } + img.draw(in: r, from: .zero, operation: .sourceOver, fraction: 1) + } +} diff --git a/juke-wizard/Sources/JukeWizard/TrackRowView.swift b/juke-wizard/Sources/JukeWizard/TrackRowView.swift new file mode 100644 --- /dev/null +++ b/juke-wizard/Sources/JukeWizard/TrackRowView.swift @@ -0,0 +1,119 @@ +// TrackRowView.swift — one dressed-up row in the Winamp track list: a colored +// chip tinted by the track's lane, a rounded album-art thumbnail, the title + +// artist, and little platform badges (Spotify / Apple / YouTube / DistroKid) +// for released tracks. No star ratings. Reused by the table via makeView. +import AppKit + +final class TrackRowView: NSView { + static let id = NSUserInterfaceItemIdentifier("TrackRow") + static let height: CGFloat = 46 + + private let thumb = NSImageView() + private let titleField = NSTextField(labelWithString: "") + private let artistField = NSTextField(labelWithString: "") + private let badges = NSTextField(labelWithString: "") + private var tint = NSColor.clear + var selected = false { didSet { needsDisplay = true } } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + wantsLayer = true + thumb.imageScaling = .scaleProportionallyUpOrDown + thumb.wantsLayer = true + thumb.layer?.cornerRadius = 5 + thumb.layer?.masksToBounds = true + thumb.layer?.borderWidth = 1 + thumb.layer?.borderColor = NSColor.white.withAlphaComponent(0.15).cgColor + titleField.font = .systemFont(ofSize: 13, weight: .semibold) + titleField.lineBreakMode = .byTruncatingTail + artistField.font = .systemFont(ofSize: 10.5) + artistField.textColor = .secondaryLabelColor + artistField.lineBreakMode = .byTruncatingTail + badges.font = .systemFont(ofSize: 12, weight: .bold) + badges.alignment = .right + for v in [thumb, titleField, artistField, badges] { addSubview(v) } + } + required init?(coder: NSCoder) { fatalError() } + override var isFlipped: Bool { true } + + func configure(_ t: Track) { + // fallback cover: a colored disc placeholder if no art + if let p = t.meta?.art, let img = NSImage(contentsOf: URL(fileURLWithPath: (p as NSString).expandingTildeInPath)) { + thumb.image = img + } else { + thumb.image = TrackRowView.placeholder(for: t.lane) + } + let cc = t.data.comments.count + titleField.stringValue = t.title + titleField.textColor = TrackRowView.titleColor(t) + artistField.stringValue = (t.meta?.artist ?? "Aesthetic Dot Computer") + + (cc > 0 ? " 💬\(cc)" : "") + badges.attributedStringValue = TrackRowView.badgeString(t) + tint = TrackRowView.laneTint(t.lane) + needsDisplay = true + needsLayout = true + } + + override func layout() { + super.layout() + let h = bounds.height, pad: CGFloat = 8 + let side = h - 10 + thumb.frame = NSRect(x: pad, y: 5, width: side, height: side) + let tx = pad + side + 9 + let bw: CGFloat = 96 + let textW = bounds.width - tx - bw - pad + titleField.frame = NSRect(x: tx, y: 6, width: textW, height: 17) + artistField.frame = NSRect(x: tx, y: 24, width: textW, height: 14) + badges.frame = NSRect(x: bounds.width - bw - pad, y: 14, width: bw, height: 18) + } + + override func draw(_ dirtyRect: NSRect) { + // colored chip tinted by lane; brighter when selected + let r = bounds.insetBy(dx: 3, dy: 2) + let path = NSBezierPath(roundedRect: r, xRadius: 7, yRadius: 7) + (selected ? tint.blended(withFraction: 0.45, of: .white) ?? tint + : tint).withAlphaComponent(selected ? 0.55 : 0.22).setFill() + path.fill() + if selected { + tint.withAlphaComponent(0.9).setStroke() + path.lineWidth = 1.5 + path.stroke() + } + } + + // ── styling helpers ────────────────────────────────────────────────────── + static func laneTint(_ lane: String) -> NSColor { + // stable hue from the lane name → each lane reads as its own color + var h: UInt64 = 1469598103934665603 + for b in lane.utf8 { h = (h ^ UInt64(b)) &* 1099511628211 } + let hue = CGFloat(h % 360) / 360.0 + return NSColor(calibratedHue: hue, saturation: 0.62, brightness: 0.95, alpha: 1) + } + static func titleColor(_ t: Track) -> NSColor { + JukeController.statusColor(t.meta?.status) + } + static func badgeString(_ t: Track) -> NSAttributedString { + let out = NSMutableAttributedString() + func add(_ s: String, _ c: NSColor, on: Bool) { + guard on else { return } + out.append(NSAttributedString(string: s + " ", attributes: [.foregroundColor: c])) + } + let l = t.meta?.links + add("♫", NSColor(srgbRed: 0.11, green: 0.73, blue: 0.33, alpha: 1), on: l?.spotify != nil) + add("", NSColor(srgbRed: 0.98, green: 0.30, blue: 0.40, alpha: 1), on: l?.apple != nil) + add("▶", NSColor(srgbRed: 0.95, green: 0.20, blue: 0.20, alpha: 1), on: l?.youtube != nil) + add("◆", NSColor(srgbRed: 0.55, green: 0.75, blue: 0.95, alpha: 1), on: l?.distrokid != nil) + return out + } + static func placeholder(for lane: String) -> NSImage { + let side: CGFloat = 40 + let img = NSImage(size: NSSize(width: side, height: side)) + img.lockFocus() + laneTint(lane).withAlphaComponent(0.9).setFill() + NSBezierPath(ovalIn: NSRect(x: 0, y: 0, width: side, height: side)).fill() + NSColor.black.withAlphaComponent(0.55).setFill() + NSBezierPath(ovalIn: NSRect(x: side/2-5, y: side/2-5, width: 10, height: 10)).fill() + img.unlockFocus() + return img + } +} diff --git a/juke-wizard/bin/gen-cd-icon.sh b/juke-wizard/bin/gen-cd-icon.sh --- a/juke-wizard/bin/gen-cd-icon.sh +++ b/juke-wizard/bin/gen-cd-icon.sh @@ -29,24 +29,28 @@ \( -size 1x240 gradient:magenta-red -rotate 90 \) \ +append -resize ${S}x${S}\! -distort Polar 0 -resize ${S}x${S}\! \ -colorspace sRGB -type TrueColor "$TMP/wheel.png" -# ── 2. brushed-silver base, rainbow dissolved over → iridescent metal ────── -"$MAGICK" -size ${S}x${S} radial-gradient:'gray90-gray62' -colorspace sRGB -type TrueColor "$TMP/silver.png" -"$MAGICK" "$TMP/silver.png" "$TMP/wheel.png" \ - -compose dissolve -define compose:args=56 -composite \ - -modulate 108,124,100 "$TMP/sheen.png" +# ── 2. bright glassy silver base; rainbow dissolved VERY lightly and +# desaturated → a translucent shiny disc with just a whisper of the +# diffraction sheen (classic pressed-CD look, not a rainbow wheel) ──── +"$MAGICK" -size ${S}x${S} radial-gradient:'gray98-gray70' -colorspace sRGB -type TrueColor "$TMP/silver.png" +"$MAGICK" "$TMP/silver.png" \ + \( "$TMP/wheel.png" -modulate 100,45,100 \) -compose dissolve -define compose:args=26 -composite \ + \( "$TMP/wheel.png" -rotate 90 -modulate 100,40,100 \) -compose dissolve -define compose:args=14 -composite \ + -modulate 104,100,100 "$TMP/sheen.png" -# ── 3. hub clamp rings + soft specular streak ────────────────────────────── -"$MAGICK" -size ${S}x${S} xc:none -stroke 'rgba(255,255,255,0.30)' -strokewidth 6 -fill none \ +# ── 3. hub clamp rings + soft glossy sweep + hot sparkle ─────────────────── +"$MAGICK" -size ${S}x${S} xc:none -stroke 'rgba(255,255,255,0.35)' -strokewidth 6 -fill none \ -draw "circle $C,$C $C,$((C-RCLAMP))" -draw "circle $C,$C $C,$((C-RCLAMPO))" "$TMP/clamp.png" -"$MAGICK" -size ${S}x${S} xc:none -fill 'rgba(255,255,255,0.26)' \ - -draw "translate $C,$C rotate -30 ellipse 0,0 $((S/2-60)),64 210,330" \ - -blur 0x40 "$TMP/spec.png" +"$MAGICK" -size ${S}x${S} xc:none \ + -fill 'rgba(255,255,255,0.32)' -draw "translate $C,$C rotate -28 ellipse 0,0 $((S/2-60)),148 205,335" -blur 0x80 \ + -fill 'rgba(255,255,255,0.55)' -draw "translate $((C-156)),$((C-192)) ellipse 0,0 96,40 0,360" -blur 0x44 "$TMP/gloss.png" -# ── 4. alpha mask: disc with center hole; assemble the disc ──────────────── +# ── 4. alpha mask (disc + hole), rim shade, assemble ─────────────────────── "$MAGICK" -size ${S}x${S} xc:black -fill white -draw "circle $C,$C $C,$((C-ROUT))" \ -fill black -draw "circle $C,$C $C,$((C-RHOLE))" -blur 0x1 "$TMP/amask.png" "$MAGICK" "$TMP/sheen.png" "$TMP/clamp.png" -compose over -composite \ - "$TMP/spec.png" -compose screen -composite \ + "$TMP/gloss.png" -compose screen -composite \ + \( -size ${S}x${S} radial-gradient:'none-rgba(0,0,0,0.18)' \) -compose over -composite \ "$TMP/amask.png" -alpha off -compose CopyOpacity -composite "$ASSETS/$NAME-cd.png" # roster mascot = the bare disc (back up the original wizard art once) diff --git a/pop/bin/pop-library.mjs b/pop/bin/pop-library.mjs --- a/pop/bin/pop-library.mjs +++ b/pop/bin/pop-library.mjs @@ -153,6 +153,7 @@ const { art, media } = findMedia(p, title); tracks.push({ path: p, title, + artist: "Aesthetic Dot Computer", lane: laneDirRel, backend: backendFor(laneTop, laneDirRel), status: relInfo?.status || "UNRELEASED", diff --git a/pop/out/pop-library.json b/pop/out/pop-library.json --- a/pop/out/pop-library.json +++ b/pop/out/pop-library.json @@ -1,10 +1,11 @@ { - "generated": "2026-07-12T23:13:16.736Z", + "generated": "2026-07-13T03:46:31.009Z", "count": 104, "tracks": [ { "path": "/Users/jas/aesthetic-computer/pop/americomputadora/out/americomputadora.mp3", "title": "americomputadora", + "artist": "Aesthetic Dot Computer", "lane": "americomputadora", "backend": "C engine", "status": "RELEASED", @@ -27,6 +28,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/fluttabap360.mp3", "title": "fluttabap360", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "RELEASED", @@ -78,6 +80,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/momboba/out/momabobasheep.mp3", "title": "momabobasheep", + "artist": "Aesthetic Dot Computer", "lane": "momboba", "backend": "C engine", "status": "RELEASED", @@ -121,6 +124,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/big-pictures/out/amaythingra.mp3", "title": "amaythingra", + "artist": "Aesthetic Dot Computer", "lane": "big-pictures", "backend": "C engine", "status": "RELEASED", @@ -143,6 +147,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/marimbaba.mp3", "title": "marimbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "RELEASED", @@ -194,6 +199,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/chillwave/out/helpabeach.mp3", "title": "helpabeach", + "artist": "Aesthetic Dot Computer", "lane": "chillwave", "backend": "JS DSP", "status": "RELEASED", @@ -216,6 +222,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/dance/out/trancenwaltz.mp3", "title": "trancenwaltz", + "artist": "Aesthetic Dot Computer", "lane": "dance", "backend": "C engine", "status": "RELEASED", @@ -238,6 +245,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/jungle/out/solafiya.mp3", "title": "solafiya", + "artist": "Aesthetic Dot Computer", "lane": "jungle", "backend": "JS DSP", "status": "MASTERING", @@ -260,6 +268,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/dance/out/cutezenwaltzi.mp3", "title": "cutezenwaltzi", + "artist": "Aesthetic Dot Computer", "lane": "dance", "backend": "C engine", "status": "WIP", @@ -282,6 +291,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/nullabye/out/nullabata.mp3", "title": "nullabata", + "artist": "Aesthetic Dot Computer", "lane": "nullabye", "backend": "C engine", "status": "WIP", @@ -304,6 +314,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/teknull/out/teknull.mp3", "title": "teknull", + "artist": "Aesthetic Dot Computer", "lane": "teknull", "backend": "JS DSP", "status": "WIP", @@ -326,6 +337,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/nullabye/out/nuellaby.mp3", "title": "nuellaby", + "artist": "Aesthetic Dot Computer", "lane": "nullabye", "backend": "C engine", "status": "WIP", @@ -348,6 +360,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/nullabye/out/nullabye.mp3", "title": "nullabye", + "artist": "Aesthetic Dot Computer", "lane": "nullabye", "backend": "C engine", "status": "WIP", @@ -370,6 +383,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/flatterbop180.mp3", "title": "flatterbop180", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -387,6 +401,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/menuband/out/vo.mp3", "title": "vo", + "artist": "Aesthetic Dot Computer", "lane": "menuband", "backend": "JS DSP", "status": "UNRELEASED", @@ -404,6 +419,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/wattajetta/out/wattajetta.mp3", "title": "wattajetta", + "artist": "Aesthetic Dot Computer", "lane": "wattajetta", "backend": "C engine", "status": "UNRELEASED", @@ -421,6 +437,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezenith.mp3", "title": "cutezenith", + "artist": "Aesthetic Dot Computer", "lane": "cutezen", "backend": "JS DSP", "status": "UNRELEASED", @@ -438,6 +455,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezip.mp3", "title": "cutezip", + "artist": "Aesthetic Dot Computer", "lane": "cutezen", "backend": "JS DSP", "status": "UNRELEASED", @@ -455,6 +473,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/cutezen/out/cutezen.mp3", "title": "cutezen", + "artist": "Aesthetic Dot Computer", "lane": "cutezen", "backend": "JS DSP", "status": "UNRELEASED", @@ -472,6 +491,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/_cmp.mp3", "title": "_cmp", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -489,6 +509,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/_solo_scr.mp3", "title": "_solo_scr", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -506,6 +527,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/_solo_bell.mp3", "title": "_solo_bell", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -523,6 +545,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/flutterbap.mp3", "title": "flutterbap", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -540,6 +563,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/pianotrax.mp3", "title": "pianotrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -557,6 +581,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/cornerfive/out/cornerfive-footwork.mp3", "title": "cornerfive-footwork", + "artist": "Aesthetic Dot Computer", "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", @@ -574,6 +599,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/cornerfive/out/cornerfive-dub.mp3", "title": "cornerfive-dub", + "artist": "Aesthetic Dot Computer", "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", @@ -591,6 +617,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/cornerfive/out/cornerfive-musicbox.mp3", "title": "cornerfive-musicbox", + "artist": "Aesthetic Dot Computer", "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", @@ -608,6 +635,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/cornerfive/out/cornerfive.mp3", "title": "cornerfive", + "artist": "Aesthetic Dot Computer", "lane": "cornerfive", "backend": "JS DSP", "status": "UNRELEASED", @@ -625,6 +653,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/hushabye.mp3", "title": "hushabye", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -642,6 +671,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/driftwoodbaba.mp3", "title": "driftwoodbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -659,6 +689,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/spiralbaba.mp3", "title": "spiralbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -676,6 +707,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/meadowbaba.mp3", "title": "meadowbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -693,6 +725,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/velvetbaba.mp3", "title": "velvetbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -710,6 +743,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/snowbaba.mp3", "title": "snowbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -727,6 +761,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/cometbaba.mp3", "title": "cometbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -744,6 +779,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/lanternbaba.mp3", "title": "lanternbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -761,6 +797,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/prismbaba.mp3", "title": "prismbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -778,6 +815,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/tidebaba.mp3", "title": "tidebaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -795,6 +833,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/waltzbaba.mp3", "title": "waltzbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -812,6 +851,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/tuckbaba.mp3", "title": "tuckbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -829,6 +869,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/toybaba.mp3", "title": "toybaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -846,6 +887,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/starbaba.mp3", "title": "starbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -863,6 +905,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/rockingbaba.mp3", "title": "rockingbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -880,6 +923,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/pentababa.mp3", "title": "pentababa", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -897,6 +941,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/musicboxbaba.mp3", "title": "musicboxbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -914,6 +959,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/moonbaba.mp3", "title": "moonbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -931,6 +977,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/mistbaba.mp3", "title": "mistbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -948,6 +995,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/minorbaba.mp3", "title": "minorbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -965,6 +1013,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/lydianbaba.mp3", "title": "lydianbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -982,6 +1031,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/gamelanbaba.mp3", "title": "gamelanbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -999,6 +1049,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/flybaba.mp3", "title": "flybaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -1016,6 +1067,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/dorianbaba.mp3", "title": "dorianbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -1033,6 +1085,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/deepbaba.mp3", "title": "deepbaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -1050,6 +1103,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/lullabies/out/_template.mp3", "title": "_template", + "artist": "Aesthetic Dot Computer", "lane": "marimba/lullabies", "backend": "C engine", "status": "UNRELEASED", @@ -1067,6 +1121,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/rumbletek.mp3", "title": "rumbletek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1084,6 +1139,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/glitchtek.mp3", "title": "glitchtek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1101,6 +1157,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/brokentek.mp3", "title": "brokentek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1118,6 +1175,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/deeptek.mp3", "title": "deeptek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1135,6 +1193,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/peaktek.mp3", "title": "peaktek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1152,6 +1211,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/dubtek.mp3", "title": "dubtek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1169,6 +1229,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/acidtek.mp3", "title": "acidtek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1186,6 +1247,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/hypnotek.mp3", "title": "hypnotek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1203,6 +1265,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/minitek/out/minitek.mp3", "title": "minitek", + "artist": "Aesthetic Dot Computer", "lane": "minitek", "backend": "C engine", "status": "UNRELEASED", @@ -1220,6 +1283,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/lumitrax.mp3", "title": "lumitrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1237,6 +1301,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/cavetrax.mp3", "title": "cavetrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1254,6 +1319,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/dusktrax.mp3", "title": "dusktrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1271,6 +1337,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/frosttrax.mp3", "title": "frosttrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1288,6 +1355,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/glasstrax.mp3", "title": "glasstrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1305,6 +1373,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/mosstrax.mp3", "title": "mosstrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1322,6 +1391,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/embertrax.mp3", "title": "embertrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1339,6 +1409,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/tidetrax.mp3", "title": "tidetrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1356,6 +1427,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/dewtrax.mp3", "title": "dewtrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1373,6 +1445,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/sinetrax.mp3", "title": "sinetrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1390,6 +1463,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/maytrax-c.mp3", "title": "maytrax-c", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1407,6 +1481,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/maytrax/out/maytrax.mp3", "title": "maytrax", + "artist": "Aesthetic Dot Computer", "lane": "maytrax", "backend": "C engine", "status": "UNRELEASED", @@ -1424,6 +1499,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/hopehop/out/hopehop.mp3", "title": "hopehop", + "artist": "Aesthetic Dot Computer", "lane": "hopehop", "backend": "C engine", "status": "UNRELEASED", @@ -1441,6 +1517,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/kikbouba.mp3", "title": "kikbouba", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1458,6 +1535,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/bonggabap.mp3", "title": "bonggabap", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1475,6 +1553,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/mooncalf.mp3", "title": "mooncalf", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1492,6 +1571,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/gloomboo.mp3", "title": "gloomboo", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1509,6 +1589,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/trumpaba.mp3", "title": "trumpaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1526,6 +1607,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/goosabap.mp3", "title": "goosabap", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1543,6 +1625,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/piggabap.mp3", "title": "piggabap", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1560,6 +1643,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/slinkybap.mp3", "title": "slinkybap", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1577,6 +1661,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/mommyboo.mp3", "title": "mommyboo", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1594,6 +1679,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/alleyhop.mp3", "title": "alleyhop", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1611,6 +1697,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/wibblybop.mp3", "title": "wibblybop", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1628,6 +1715,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/buzaboo.mp3", "title": "buzaboo", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1645,6 +1733,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/lollybap.mp3", "title": "lollybap", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1662,6 +1751,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/dewabap.mp3", "title": "dewabap", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1679,6 +1769,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/dingaba.mp3", "title": "dingaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1696,6 +1787,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/kookabap.mp3", "title": "kookabap", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1713,6 +1805,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/marimbabapp.mp3", "title": "marimbabapp", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1730,6 +1823,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/hum/out/hum.mp3", "title": "hum", + "artist": "Aesthetic Dot Computer", "lane": "hum", "backend": "JS DSP", "status": "UNRELEASED", @@ -1747,6 +1841,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/tinkaboo.mp3", "title": "tinkaboo", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1764,6 +1859,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/bronzaba.mp3", "title": "bronzaba", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1781,6 +1877,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/marimba/out/doziba.mp3", "title": "doziba", + "artist": "Aesthetic Dot Computer", "lane": "marimba", "backend": "C engine", "status": "UNRELEASED", @@ -1798,6 +1895,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/gecs/out/gecs.mp3", "title": "gecs", + "artist": "Aesthetic Dot Computer", "lane": "gecs", "backend": "JS DSP", "status": "UNRELEASED", @@ -1815,6 +1913,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/hippyhayzard/out/hippyhayzard.mp3", "title": "hippyhayzard", + "artist": "Aesthetic Dot Computer", "lane": "hippyhayzard", "backend": "JS DSP", "status": "UNRELEASED", @@ -1832,6 +1931,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/jungle/out/solfia.mp3", "title": "solfia", + "artist": "Aesthetic Dot Computer", "lane": "jungle", "backend": "JS DSP", "status": "UNRELEASED", @@ -1849,6 +1949,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/jungle/out/rodando.mp3", "title": "rodando", + "artist": "Aesthetic Dot Computer", "lane": "jungle", "backend": "JS DSP", "status": "UNRELEASED", @@ -1866,6 +1967,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/jungle/out/raggasol.mp3", "title": "raggasol", + "artist": "Aesthetic Dot Computer", "lane": "jungle", "backend": "JS DSP", "status": "UNRELEASED", @@ -1883,6 +1985,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/jungle/out/jungleton.mp3", "title": "jungleton", + "artist": "Aesthetic Dot Computer", "lane": "jungle", "backend": "JS DSP", "status": "UNRELEASED", @@ -1900,6 +2003,7 @@ }, { "path": "/Users/jas/aesthetic-computer/pop/dance/out/.booty.mp3", "title": ".booty", + "artist": "Aesthetic Dot Computer", "lane": "dance", "backend": "C engine", "status": "UNRELEASED", -- tangled.sh