Files

254 lines
12 KiB
Swift

import Domain
import Foundation
struct SeededGenerator: RandomNumberGenerator {
private var state: UInt64
init(seed: UInt64) {
state = seed &+ 0x9E37_79B9_7F4A_7C15
}
mutating func next() -> UInt64 {
state &+= 0x9E37_79B9_7F4A_7C15
var mixed = state
mixed = (mixed ^ (mixed >> 30)) &* 0xBF58_476D_1CE4_E5B9
mixed = (mixed ^ (mixed >> 27)) &* 0x94D0_49BB_1331_11EB
return mixed ^ (mixed >> 31)
}
}
struct FakeCatalog {
struct AlbumSeed {
let title: String
let year: Int
let tracks: Int
}
struct ArtistSeed {
let name: String
let genre: String
let albums: [AlbumSeed]
}
static let artists: [ArtistSeed] = [
ArtistSeed(name: "Velvet Harbor", genre: "Indie Pop", albums: [
AlbumSeed(title: "Salt in the Signal", year: 2024, tracks: 11),
AlbumSeed(title: "Low Tide Radio", year: 2021, tracks: 10),
AlbumSeed(title: "Postcards From Nowhere", year: 2018, tracks: 12),
]),
ArtistSeed(name: "Нина Орлова", genre: "Alternative", albums: [
AlbumSeed(title: "Тихий час", year: 2025, tracks: 9),
AlbumSeed(title: "Север внутри", year: 2022, tracks: 11),
]),
ArtistSeed(name: "Kaito Mori", genre: "Electronic", albums: [
AlbumSeed(title: "Neon Monsoon", year: 2023, tracks: 8),
AlbumSeed(title: "東京 After Hours", year: 2020, tracks: 10),
AlbumSeed(title: "Glass Circuits", year: 2017, tracks: 9),
AlbumSeed(title: "Soft Machines (Remixes)", year: 2024, tracks: 6),
]),
ArtistSeed(name: "The Paper Lanterns", genre: "Rock", albums: [
AlbumSeed(title: "Everything Burns Brighter at Night and Other Small Disasters", year: 2019, tracks: 13),
AlbumSeed(title: "Static Bloom", year: 2016, tracks: 11),
AlbumSeed(title: "Live at the Old Observatory", year: 2022, tracks: 16),
]),
ArtistSeed(name: "Marisol Vega", genre: "Latin", albums: [
AlbumSeed(title: "Corazón de Verano", year: 2024, tracks: 12),
AlbumSeed(title: "Luna Roja", year: 2021, tracks: 10),
]),
ArtistSeed(name: "Oskar Lindqvist Trio", genre: "Jazz", albums: [
AlbumSeed(title: "Blue Hours", year: 2015, tracks: 7),
AlbumSeed(title: "Winter Standards", year: 2018, tracks: 9),
AlbumSeed(title: "Stockholm Sessions", year: 2023, tracks: 8),
]),
ArtistSeed(name: "DJ Quartz", genre: "Hip-Hop", albums: [
AlbumSeed(title: "Concrete Gardens", year: 2022, tracks: 14),
AlbumSeed(title: "No Signal", year: 2025, tracks: 12),
]),
ArtistSeed(name: "Aurora Fields", genre: "Ambient", albums: [
AlbumSeed(title: "Weightless", year: 2020, tracks: 6),
AlbumSeed(title: "Slow Light", year: 2023, tracks: 7),
AlbumSeed(title: "Music for Empty Airports", year: 2017, tracks: 5),
]),
ArtistSeed(name: "Holloway & Finch", genre: "Folk", albums: [
AlbumSeed(title: "The Orchard Tapes", year: 2019, tracks: 10),
AlbumSeed(title: "Rivers We Crossed", year: 2024, tracks: 11),
]),
ArtistSeed(name: "Сёстры Грома", genre: "Rock", albums: [
AlbumSeed(title: "Электричество", year: 2023, tracks: 10),
AlbumSeed(title: "Город без сна", year: 2020, tracks: 12),
]),
ArtistSeed(name: "Camille Durand", genre: "Classical", albums: [
AlbumSeed(title: "Nocturnes & Preludes", year: 2021, tracks: 14),
AlbumSeed(title: "Satie: Works for Piano", year: 2016, tracks: 12),
]),
ArtistSeed(name: "Midnight Arcade", genre: "Electronic", albums: [
AlbumSeed(title: "Insert Coin", year: 2018, tracks: 10),
AlbumSeed(title: "High Score", year: 2021, tracks: 11),
AlbumSeed(title: "Continue?", year: 2025, tracks: 9),
]),
ArtistSeed(name: "Juno Okafor", genre: "R&B/Soul", albums: [
AlbumSeed(title: "Honey & Smoke", year: 2024, tracks: 12),
AlbumSeed(title: "Afterglow", year: 2022, tracks: 10),
]),
ArtistSeed(name: "Iron Meridian", genre: "Metal", albums: [
AlbumSeed(title: "Cathedral of Ash", year: 2019, tracks: 9),
AlbumSeed(title: "Tidal Lock", year: 2023, tracks: 8),
]),
]
static let openers = [
"Midnight", "Paper", "Golden", "Silent", "Electric", "Hollow", "Crimson", "Distant", "Velvet", "Broken",
"Summer", "Neon", "Quiet", "Wild", "Frozen", "Burning", "Little", "Endless", "Lonely", "Silver",
]
static let closers = [
"Hearts", "Rivers", "Machines", "Letters", "Highways", "Gardens", "Satellites", "Windows", "Echoes", "Tides",
"Dreams", "Wires", "Shadows", "Mornings", "Skylines", "Ghosts", "Oceans", "Rooms", "Signals", "Fires",
]
static let singles = [
"Interlude", "Overture", "Home", "Gravity", "Again", "Stay", "Run", "Waves", "Closer", "Tomorrow",
"Если бы", "Навсегда", "Après la pluie", "Sin Ti", "さよなら",
]
static let suffixes = ["", "", "", "", "", "", " (Reprise)", " (feat. Juno Okafor)", " (Acoustic)", ", Pt. 2"]
static let formats: [AudioFormat] = [
AudioFormat(codec: .flac, bitrateKbps: 1011, sampleRate: 44100, bitDepth: 16),
AudioFormat(codec: .flac, bitrateKbps: 2890, sampleRate: 96000, bitDepth: 24),
AudioFormat(codec: .flac, bitrateKbps: 1411, sampleRate: 48000, bitDepth: 24),
AudioFormat(codec: .mp3, bitrateKbps: 320, sampleRate: 44100),
AudioFormat(codec: .aac, bitrateKbps: 256, sampleRate: 44100),
]
let source: SourceID
private(set) var changes: [Change] = []
init(source: SourceID, now: Date = Date()) {
self.source = source
var random = SeededGenerator(seed: 42)
var allTracks: [Track] = []
var albumIndex = 0
for (artistIndex, seed) in Self.artists.enumerated() {
let artistID = id("artist-\(artistIndex)")
let artist = Artist(
id: artistID,
name: seed.name,
sortName: seed.name.hasPrefix("The ") ? String(seed.name.dropFirst(4)) : seed.name,
artwork: id("art-artist-\(artistIndex)"),
albumCount: seed.albums.count
)
changes.append(.upsert(.artist(artist)))
for albumSeed in seed.albums {
let tracks = makeTracks(albumSeed, albumIndex: albumIndex, artist: artist, genre: seed.genre, random: &random)
let album = Album(
id: id("album-\(albumIndex)"),
title: albumSeed.title,
artistID: artistID,
artistName: seed.name,
year: albumSeed.year,
genre: seed.genre,
artwork: id("art-album-\(albumIndex)"),
trackCount: tracks.count,
duration: tracks.reduce(0) { $0 + $1.duration },
addedAt: now.addingTimeInterval(-Double(Int.random(in: 1 ... 400, using: &random)) * 86400),
bestFormat: tracks.first?.formats.first
)
changes.append(.upsert(.album(album)))
changes.append(contentsOf: tracks.map { .upsert(.track($0)) })
allTracks.append(contentsOf: tracks)
albumIndex += 1
}
}
makePlaylists(from: allTracks, now: now, random: &random)
makeAnnotations(for: allTracks, now: now, random: &random)
makeStations()
}
private func id(_ remote: String) -> EntityID {
EntityID(source: source, remote: remote)
}
private func makeTracks(
_ seed: AlbumSeed,
albumIndex: Int,
artist: Artist,
genre: String,
random: inout SeededGenerator
) -> [Track] {
let format = Self.formats[Int.random(in: 0 ..< Self.formats.count, using: &random)]
return (1 ... seed.tracks).map { number in
let base = Int.random(in: 0 ..< 5, using: &random) == 0
? Self.singles.randomElement(using: &random) ?? ""
: "\(Self.openers.randomElement(using: &random) ?? "") \(Self.closers.randomElement(using: &random) ?? "")"
return Track(
id: id("track-\(albumIndex)-\(number)"),
title: base + (Self.suffixes.randomElement(using: &random) ?? ""),
albumID: id("album-\(albumIndex)"),
albumTitle: seed.title,
artistID: artist.id,
artistName: artist.name,
trackNumber: number,
duration: genre == "Ambient" || genre == "Jazz"
? Double(Int.random(in: 280 ... 620, using: &random))
: Double(Int.random(in: 118 ... 325, using: &random)),
year: seed.year,
genre: genre,
artwork: id("art-album-\(albumIndex)"),
isExplicit: genre == "Hip-Hop" && Int.random(in: 0 ..< 2, using: &random) == 0,
formats: [format]
)
}
}
private mutating func makePlaylists(from tracks: [Track], now: Date, random: inout SeededGenerator) {
let seeds: [(name: String, summary: String)] = [
("Late Night Drive", "Windows down, city lights."),
("Focus", "Instrumental and quiet."),
("Утро", ""),
("Gym", "Loud."),
("Sunday Kitchen", "Cooking with the radio on."),
]
for (index, seed) in seeds.enumerated() {
let picked = Array(tracks.shuffled(using: &random).prefix(12 + index * 5))
let playlist = Playlist(
id: id("playlist-\(index)"),
name: seed.name,
summary: seed.summary,
artwork: id("art-playlist-\(index)"),
trackCount: picked.count,
duration: picked.reduce(0) { $0 + $1.duration },
updatedAt: now.addingTimeInterval(-Double(index) * 86400 * 9)
)
let entries = picked.enumerated().map { PlaylistEntry(id: "entry-\(index)-\($0.offset)", track: $0.element.id) }
changes.append(.upsert(.playlist(playlist, entries: entries)))
}
}
private mutating func makeAnnotations(for tracks: [Track], now: Date, random: inout SeededGenerator) {
for track in tracks.shuffled(using: &random).prefix(140) {
let plays = Int.random(in: 1 ... 60, using: &random)
changes.append(.annotate(track.id, Annotation(
favoritedAt: plays > 42 ? now.addingTimeInterval(-Double(plays) * 3600) : nil,
playCount: plays,
lastPlayedAt: now.addingTimeInterval(-Double(Int.random(in: 1 ... 900, using: &random)) * 3600)
)))
}
}
private mutating func makeStations() {
let seeds = [
("Radio Paradise", "Eclectic, listener supported", "https://stream.radioparadise.com/flacm"),
("SomaFM Groove Salad", "Ambient and downtempo beats", "https://ice1.somafm.com/groovesalad-256-mp3"),
("KEXP 90.3", "Where the music matters", "https://kexp-mp3-128.streamguys1.com/kexp128.mp3"),
("FIP", "Radio France, sans frontières", "https://icecast.radiofrance.fr/fip-hifi.aac"),
]
for (index, seed) in seeds.enumerated() {
guard let url = URL(string: seed.2) else { continue }
changes.append(.upsert(.station(RadioStation(
id: id("station-\(index)"),
name: seed.0,
tagline: seed.1,
streamURL: url,
artwork: id("art-station-\(index)")
))))
}
}
}