254 lines
12 KiB
Swift
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)")
|
|
))))
|
|
}
|
|
}
|
|
}
|