max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01EEmeiSJnmyL98QzA5Dwsvz
9 files changed,
+1282 insertions,
-500 deletions
| @@ -1,1219 +1,0 @@ | |||
| 1 | - | //! Seed initial forum data for development. Run with `--seed` flag. | |
| 2 | - | ||
| 3 | - | use mt_core::types::CommunityRole; | |
| 4 | - | use sqlx::PgPool; | |
| 5 | - | use uuid::Uuid; | |
| 6 | - | ||
| 7 | - | pub async fn run(pool: &PgPool) { | |
| 8 | - | // Guard: skip if data already exists (threads have no unique constraint) | |
| 9 | - | let thread_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM threads") | |
| 10 | - | .fetch_one(pool) | |
| 11 | - | .await | |
| 12 | - | .unwrap_or(0); | |
| 13 | - | if thread_count > 0 { | |
| 14 | - | tracing::info!("seed data already exists, skipping"); | |
| 15 | - | return; | |
| 16 | - | } | |
| 17 | - | ||
| 18 | - | // --- users | |
| 19 | - | ||
| 20 | - | let users = seed_users(pool).await; | |
| 21 | - | let harness = seed_harness_users(pool).await; | |
| 22 | - | ||
| 23 | - | // --- communities | |
| 24 | - | ||
| 25 | - | let rust_id = seed_community( | |
| 26 | - | pool, | |
| 27 | - | "Rust Programming", | |
| 28 | - | "rust", | |
| 29 | - | Some("All things Rust: language, ecosystem, tooling."), | |
| 30 | - | ) | |
| 31 | - | .await; | |
| 32 | - | ||
| 33 | - | let music_id = seed_community( | |
| 34 | - | pool, | |
| 35 | - | "Music Production", | |
| 36 | - | "music", | |
| 37 | - | Some("DAWs, plugins, mixing, mastering, sound design."), | |
| 38 | - | ) | |
| 39 | - | .await; | |
| 40 | - | ||
| 41 | - | let selfhosted_id = seed_community( | |
| 42 | - | pool, | |
| 43 | - | "Self-Hosted", | |
| 44 | - | "selfhosted", | |
| 45 | - | Some("Running your own infrastructure. No cloud required."), | |
| 46 | - | ) | |
| 47 | - | .await; | |
| 48 | - | ||
| 49 | - | // --- categories | |
| 50 | - | ||
| 51 | - | // Rust | |
| 52 | - | let rust_general = seed_category( | |
| 53 | - | pool, | |
| 54 | - | rust_id, | |
| 55 | - | "General", | |
| 56 | - | "general", | |
| 57 | - | 0, | |
| 58 | - | Some("Anything that doesn't fit elsewhere."), | |
| 59 | - | ) | |
| 60 | - | .await; | |
| 61 | - | let rust_help = seed_category( | |
| 62 | - | pool, | |
| 63 | - | rust_id, | |
| 64 | - | "Help & Questions", | |
| 65 | - | "help", | |
| 66 | - | 1, | |
| 67 | - | Some("Ask for help, get answers."), | |
| 68 | - | ) | |
| 69 | - | .await; | |
| 70 | - | let _rust_show = seed_category( | |
| 71 | - | pool, | |
| 72 | - | rust_id, | |
| 73 | - | "Show & Tell", | |
| 74 | - | "show", | |
| 75 | - | 2, | |
| 76 | - | Some("Share what you've built."), | |
| 77 | - | ) | |
| 78 | - | .await; | |
| 79 | - | let _rust_meta = seed_category( | |
| 80 | - | pool, | |
| 81 | - | rust_id, | |
| 82 | - | "Meta", | |
| 83 | - | "meta", | |
| 84 | - | 3, | |
| 85 | - | Some("Discussion about this community itself."), | |
| 86 | - | ) | |
| 87 | - | .await; | |
| 88 | - | ||
| 89 | - | // Music, the main test community | |
| 90 | - | let music_general = seed_category( | |
| 91 | - | pool, | |
| 92 | - | music_id, | |
| 93 | - | "General", | |
| 94 | - | "general", | |
| 95 | - | 0, | |
| 96 | - | Some("General music production discussion."), | |
| 97 | - | ) | |
| 98 | - | .await; | |
| 99 | - | let music_mixing = seed_category( | |
| 100 | - | pool, | |
| 101 | - | music_id, | |
| 102 | - | "Mixing & Mastering", | |
| 103 | - | "mixing", | |
| 104 | - | 1, | |
| 105 | - | Some("Techniques for getting a polished mix."), | |
| 106 | - | ) | |
| 107 | - | .await; | |
| 108 | - | let music_sound = seed_category( | |
| 109 | - | pool, | |
| 110 | - | music_id, | |
| 111 | - | "Sound Design", | |
| 112 | - | "sound-design", | |
| 113 | - | 2, | |
| 114 | - | Some("Synthesis, sampling, and sound creation."), | |
| 115 | - | ) | |
| 116 | - | .await; | |
| 117 | - | ||
| 118 | - | // Self-Hosted | |
| 119 | - | let sh_general = seed_category( | |
| 120 | - | pool, | |
| 121 | - | selfhosted_id, | |
| 122 | - | "General", | |
| 123 | - | "general", | |
| 124 | - | 0, | |
| 125 | - | Some("General self-hosting discussion."), | |
| 126 | - | ) | |
| 127 | - | .await; | |
| 128 | - | let _sh_homelab = seed_category( | |
| 129 | - | pool, | |
| 130 | - | selfhosted_id, | |
| 131 | - | "Homelab", | |
| 132 | - | "homelab", | |
| 133 | - | 1, | |
| 134 | - | Some("Hardware, networking, and home server setups."), | |
| 135 | - | ) | |
| 136 | - | .await; | |
| 137 | - | ||
| 138 | - | // --- memberships | |
| 139 | - | ||
| 140 | - | for user in &users { | |
| 141 | - | seed_membership(pool, user.id, music_id, CommunityRole::Member).await; | |
| 142 | - | } | |
| 143 | - | // admin owns all, maxmj is moderator of music | |
| 144 | - | seed_membership_upsert(pool, users[0].id, rust_id, CommunityRole::Owner).await; | |
| 145 | - | seed_membership_upsert(pool, users[0].id, music_id, CommunityRole::Owner).await; | |
| 146 | - | seed_membership_upsert(pool, users[0].id, selfhosted_id, CommunityRole::Owner).await; | |
| 147 | - | seed_membership_upsert(pool, users[1].id, rust_id, CommunityRole::Member).await; | |
| 148 | - | seed_membership_upsert(pool, users[1].id, music_id, CommunityRole::Moderator).await; | |
| 149 | - | ||
| 150 | - | // The harness accounts get the roles the browser axis needs to write: | |
| 151 | - | // ordinary membership for thread/reply/flag, and Owner + Moderator on two | |
| 152 | - | // different communities so a moderation action has somewhere to land. The | |
| 153 | - | // roles are seeded rather than granted at login because login only ever | |
| 154 | - | // upserts identity and MNW perks, never membership. | |
| 155 | - | for account in &harness { | |
| 156 | - | seed_membership_upsert(pool, account.id, rust_id, account.rust_role).await; | |
| 157 | - | seed_membership_upsert(pool, account.id, music_id, account.music_role).await; | |
| 158 | - | seed_membership_upsert(pool, account.id, selfhosted_id, CommunityRole::Member).await; | |
| 159 | - | } | |
| 160 | - | ||
| 161 | - | // --- Rust community: a few threads | |
| 162 | - | ||
| 163 | - | let welcome_id = seed_thread( | |
| 164 | - | pool, | |
| 165 | - | rust_general, | |
| 166 | - | users[0].id, | |
| 167 | - | "Welcome, read before posting", | |
| 168 | - | true, | |
| 169 | - | false, | |
| 170 | - | ) | |
| 171 | - | .await; | |
| 172 | - | seed_post(pool, welcome_id, users[0].id, "Welcome to the Rust Programming community. Please be respectful, stay on topic, and use code blocks for code snippets.").await; | |
| 173 | - | ||
| 174 | - | let async_id = seed_thread( | |
| 175 | - | pool, | |
| 176 | - | rust_general, | |
| 177 | - | users[1].id, | |
| 178 | - | "How do I get started with async Rust?", | |
| 179 | - | false, | |
| 180 | - | false, | |
| 181 | - | ) | |
| 182 | - | .await; | |
| 183 | - | seed_post(pool, async_id, users[1].id, "I've been writing synchronous Rust for a few months and want to start using async/await. What runtime should I pick? Is tokio the only option?\n\nAny recommended tutorials or blog posts would be great.").await; | |
| 184 | - | seed_post(pool, async_id, users[0].id, "Tokio is the most popular and what most web frameworks (Axum, Actix) use. There's also `async-std` and `smol`, but the ecosystem gravitates toward tokio.\n\nStart with the tokio tutorial: it covers spawning tasks, channels, and I/O.").await; | |
| 185 | - | ||
| 186 | - | let error_id = seed_thread( | |
| 187 | - | pool, | |
| 188 | - | rust_help, | |
| 189 | - | users[1].id, | |
| 190 | - | "Best practices for error handling in Axum", | |
| 191 | - | false, | |
| 192 | - | false, | |
| 193 | - | ) | |
| 194 | - | .await; | |
| 195 | - | seed_post(pool, error_id, users[1].id, "What's the recommended way to handle errors in Axum handlers? Should I use `anyhow`, `thiserror`, or something else? I keep writing `.map_err(|e| ...)` everywhere.").await; | |
| 196 | - | ||
| 197 | - | // --- Self-Hosted: a few threads | |
| 198 | - | ||
| 199 | - | let caddy_id = seed_thread( | |
| 200 | - | pool, | |
| 201 | - | sh_general, | |
| 202 | - | users[0].id, | |
| 203 | - | "Caddy vs nginx for reverse proxy", | |
| 204 | - | false, | |
| 205 | - | false, | |
| 206 | - | ) | |
| 207 | - | .await; | |
| 208 | - | seed_post(pool, caddy_id, users[0].id, "I have been using nginx for years but Caddy's automatic HTTPS is tempting. Anyone made the switch? What are the tradeoffs?").await; | |
| 209 | - | seed_post(pool, caddy_id, users[3].id, "Switched last year. Caddy's config is so much simpler. Automatic cert renewal is great. Only downside is slightly higher memory usage but it is negligible for small setups.").await; | |
| 210 | - | ||
| 211 | - | // --- Music community: bulk seed | |
| 212 | - | ||
| 213 | - | seed_music_general(pool, music_general, &users).await; | |
| 214 | - | seed_music_mixing(pool, music_mixing, &users).await; | |
| 215 | - | seed_music_sound_design(pool, music_sound, &users).await; | |
| 216 | - | ||
| 217 | - | // reply_count is computed live at read time, nothing to backfill. | |
| 218 | - | ||
| 219 | - | let total_threads: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM threads") | |
| 220 | - | .fetch_one(pool) | |
| 221 | - | .await | |
| 222 | - | .unwrap_or(0); | |
| 223 | - | let total_posts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM posts") | |
| 224 | - | .fetch_one(pool) | |
| 225 | - | .await | |
| 226 | - | .unwrap_or(0); | |
| 227 | - | ||
| 228 | - | tracing::info!( | |
| 229 | - | "seeded 3 communities, {} users, {} threads, {} posts", | |
| 230 | - | users.len(), | |
| 231 | - | total_threads, | |
| 232 | - | total_posts | |
| 233 | - | ); | |
| 234 | - | } | |
| 235 | - | ||
| 236 | - | struct SeedUser { | |
| 237 | - | id: Uuid, | |
| 238 | - | } | |
| 239 | - | ||
| 240 | - | /// A harness account: the local row plus the roles it carries per community. | |
| 241 | - | struct HarnessUser { | |
| 242 | - | id: Uuid, | |
| 243 | - | rust_role: CommunityRole, | |
| 244 | - | music_role: CommunityRole, | |
| 245 | - | } | |
| 246 | - | ||
| 247 | - | /// The three accounts the browser harness logs in as. | |
| 248 | - | /// | |
| 249 | - | /// `mnw_account_id` is the join to MNW and these values are fixed on both | |
| 250 | - | /// sides: the MNW example seed creates the same three ids in | |
| 251 | - | /// `server/src/seed/harness.rs`, which is what lets roles be assigned here | |
| 252 | - | /// before anyone has logged in. The two lists have to be edited together, and | |
| 253 | - | /// the MNW side has a test pinning the literals to make that hard to forget. | |
| 254 | - | /// | |
| 255 | - | /// `is_fan_plus`/`is_creator` mirror the perks MNW will report at login. They | |
| 256 | - | /// are denormalised onto the row for post rendering, and login overwrites them | |
| 257 | - | /// from userinfo, so seeding them wrong is cosmetic rather than a privilege | |
| 258 | - | /// question. Seed them right anyway: a pre-login render of a seeded post is one | |
| 259 | - | /// of the things the harness looks at. | |
| 260 | - | async fn seed_harness_users(pool: &PgPool) -> Vec<HarnessUser> { | |
| 261 | - | let harness_data = [ | |
| 262 | - | ( | |
| 263 | - | "00000000-0000-0000-0000-00000000f001", | |
| 264 | - | "harness_fan", | |
| 265 | - | "Harness Fan", | |
| 266 | - | true, | |
| 267 | - | false, | |
| 268 | - | CommunityRole::Member, | |
| 269 | - | CommunityRole::Member, | |
| 270 | - | ), | |
| 271 | - | ( | |
| 272 | - | "00000000-0000-0000-0000-00000000f002", | |
| 273 | - | "harness_creator", | |
| 274 | - | "Harness Creator", | |
| 275 | - | false, | |
| 276 | - | true, | |
| 277 | - | CommunityRole::Member, | |
| 278 | - | CommunityRole::Member, | |
| 279 | - | ), | |
| 280 | - | ( | |
| 281 | - | "00000000-0000-0000-0000-00000000f003", | |
| 282 | - | "harness_owner", | |
| 283 | - | "Harness Owner", | |
| 284 | - | false, | |
| 285 | - | false, | |
| 286 | - | CommunityRole::Owner, | |
| 287 | - | CommunityRole::Moderator, | |
| 288 | - | ), | |
| 289 | - | ]; | |
| 290 | - | ||
| 291 | - | let mut users = Vec::new(); | |
| 292 | - | for (uuid_str, username, display_name, is_fan_plus, is_creator, rust_role, music_role) in | |
| 293 | - | harness_data | |
| 294 | - | { | |
| 295 | - | let id = Uuid::parse_str(uuid_str).unwrap(); | |
| 296 | - | sqlx::query( | |
| 297 | - | "INSERT INTO users (mnw_account_id, username, display_name, is_fan_plus, is_creator) | |
| 298 | - | VALUES ($1, $2, $3, $4, $5) | |
| 299 | - | ON CONFLICT (mnw_account_id) DO UPDATE | |
| 300 | - | SET username = EXCLUDED.username, | |
| 301 | - | display_name = EXCLUDED.display_name, | |
| 302 | - | is_fan_plus = EXCLUDED.is_fan_plus, | |
| 303 | - | is_creator = EXCLUDED.is_creator", | |
| 304 | - | ) | |
| 305 | - | .bind(id) | |
| 306 | - | .bind(username) | |
| 307 | - | .bind(display_name) | |
| 308 | - | .bind(is_fan_plus) | |
| 309 | - | .bind(is_creator) | |
| 310 | - | .execute(pool) | |
| 311 | - | .await | |
| 312 | - | .expect("failed to seed harness user"); | |
| 313 | - | users.push(HarnessUser { | |
| 314 | - | id, | |
| 315 | - | rust_role, | |
| 316 | - | music_role, | |
| 317 | - | }); | |
| 318 | - | } | |
| 319 | - | users | |
| 320 | - | } | |
| 321 | - | ||
| 322 | - | async fn seed_users(pool: &PgPool) -> Vec<SeedUser> { | |
| 323 | - | let user_data = [ | |
| 324 | - | ("00000000-0000-0000-0000-000000000001", "admin", "Admin"), | |
| 325 | - | ("00000000-0000-0000-0000-000000000002", "maxmj", "Max"), | |
| 326 | - | ( | |
| 327 | - | "00000000-0000-0000-0000-000000000003", | |
| 328 | - | "synthwave99", | |
| 329 | - | "Juno", | |
| 330 | - | ), | |
| 331 | - | ("00000000-0000-0000-0000-000000000004", "basshunter", "Erik"), | |
| 332 | - | ("00000000-0000-0000-0000-000000000005", "tape_hiss", "Rae"), | |
| 333 | - | ("00000000-0000-0000-0000-000000000006", "drumroom", "Cole"), | |
| 334 | - | ("00000000-0000-0000-0000-000000000007", "patchwork", "Lina"), | |
| 335 | - | ("00000000-0000-0000-0000-000000000008", "detuned", "Kai"), | |
| 336 | - | ("00000000-0000-0000-0000-000000000009", "resampled", "Noor"), | |
| 337 | - | ("00000000-0000-0000-0000-00000000000a", "clipgain", "Wren"), | |
| 338 | - | ]; | |
| 339 | - | ||
| 340 | - | let mut users = Vec::new(); | |
| 341 | - | for (uuid_str, username, display_name) in &user_data { | |
| 342 | - | let id = Uuid::parse_str(uuid_str).unwrap(); | |
| 343 | - | sqlx::query( | |
| 344 | - | "INSERT INTO users (mnw_account_id, username, display_name) | |
| 345 | - | VALUES ($1, $2, $3) | |
| 346 | - | ON CONFLICT (mnw_account_id) DO NOTHING", | |
| 347 | - | ) | |
| 348 | - | .bind(id) | |
| 349 | - | .bind(username) | |
| 350 | - | .bind(display_name) | |
| 351 | - | .execute(pool) | |
| 352 | - | .await | |
| 353 | - | .expect("failed to seed user"); | |
| 354 | - | users.push(SeedUser { id }); | |
| 355 | - | } | |
| 356 | - | users | |
| 357 | - | } | |
| 358 | - | ||
| 359 | - | /// 35 threads in Music/General, enough for 2 pages. | |
| 360 | - | async fn seed_music_general(pool: &PgPool, category_id: Uuid, users: &[SeedUser]) { | |
| 361 | - | let threads: &[(&str, &str, &[&str])] = &[ | |
| 362 | - | ( | |
| 363 | - | "Welcome to Music Production", | |
| 364 | - | "Ground rules: be kind, share knowledge, no self-promo spam. Use the right category for your topic.", | |
| 365 | - | &["Glad to be here.", "Thanks for setting this up."], | |
| 366 | - | ), | |
| 367 | - | ( | |
| 368 | - | "What DAW are you using in 2026?", | |
| 369 | - | "Curious what everyone is running these days. I have been on Ableton for years but keep hearing about Bitwig.", | |
| 370 | - | &[ | |
| 371 | - | "Reaper. Lightweight, cheap, endlessly customizable.", | |
| 372 | - | "Bitwig since version 4. The modulation system is unmatched.", | |
| 373 | - | "FL Studio. Started on it when I was 14, never left.", | |
| 374 | - | "Ableton but I keep a Reaper install for editing and batch processing.", | |
| 375 | - | "Logic. It just works and the stock plugins are surprisingly good.", | |
| 376 | - | ], | |
| 377 | - | ), | |
| 378 | - | ( | |
| 379 | - | "Favorite free plugins?", | |
| 380 | - | "What are your go-to free VSTs? Looking for synths, effects, anything.", | |
| 381 | - | &[ | |
| 382 | - | "Vital is the obvious answer. Best free synth by a mile.", | |
| 383 | - | "Valhalla Supermassive for reverb/delay. Sounds incredible for free.", | |
| 384 | - | "TDR Nova for EQ. Clean, surgical, zero CPU.", | |
| 385 | - | "Dexed if you want FM synthesis. Basically a free DX7.", | |
| 386 | - | ], | |
| 387 | - | ), | |
| 388 | - | ( | |
| 389 | - | "How do you organize your sample library?", | |
| 390 | - | "My sample folder is a disaster. Thousands of files with no structure. How do you keep things findable?", | |
| 391 | - | &[ | |
| 392 | - | "Genre > Type > Character. So like Electronic > Kicks > Punchy. Took a weekend to sort but worth it.", | |
| 393 | - | "I use tags instead of folders. AudioFiles has been great for this actually.", | |
| 394 | - | "Honestly I just search. If the filename is descriptive enough, search wins over folders every time.", | |
| 395 | - | ], | |
| 396 | - | ), | |
| 397 | - | ( | |
| 398 | - | "Analog vs digital debate in 2026", | |
| 399 | - | "Is there still a meaningful difference? Modern plugins are so good that I genuinely cannot tell in blind tests anymore.", | |
| 400 | - | &[ | |
| 401 | - | "The difference is workflow, not sound. Hardware forces commitment.", | |
| 402 | - | "I sold all my hardware last year. Zero regrets. Plugins are there.", | |
| 403 | - | "The tactile experience matters. Turning a real knob is different from clicking a virtual one.", | |
| 404 | - | "Both. I track through analog preamps and process in the box.", | |
| 405 | - | ], | |
| 406 | - | ), | |
| 407 | - | ( | |
| 408 | - | "How long did it take you to finish your first track?", | |
| 409 | - | "Been producing for 3 months and I cannot seem to finish anything. Is this normal?", | |
| 410 | - | &[ | |
| 411 | - | "Totally normal. Took me a year to finish something I was not embarrassed by.", | |
| 412 | - | "Force yourself to finish bad tracks. The skill of finishing is separate from the skill of producing.", | |
| 413 | - | "Set a deadline. 48 hours from start to bounce. Quality does not matter, completion does.", | |
| 414 | - | "Still working on my first one honestly.", | |
| 415 | - | "About 6 months. And it was terrible. But I finished it and that mattered.", | |
| 416 | - | ], | |
| 417 | - | ), | |
| 418 | - | ( | |
| 419 | - | "Studio monitors vs headphones for mixing?", | |
| 420 | - | "I live in an apartment and cannot treat the room properly. Should I just mix on headphones?", | |
| 421 | - | &[ | |
| 422 | - | "Headphones are fine if you know their character. Use a reference plugin like Sonarworks.", | |
| 423 | - | "I mix on DT 770s and reference on my car stereo. Not ideal but it works.", | |
| 424 | - | "Even cheap acoustic treatment helps. Hang some moving blankets.", | |
| 425 | - | ], | |
| 426 | - | ), | |
| 427 | - | ( | |
| 428 | - | "What's your creative process?", | |
| 429 | - | "Do you start with drums, melody, a sample? Curious how other people approach a blank session.", | |
| 430 | - | &[ | |
| 431 | - | "Always start with chords. Everything else follows from the harmony.", | |
| 432 | - | "I jam on a synth until something clicks, then build around that loop.", | |
| 433 | - | "Sound design first. Find an interesting texture, then write something that serves it.", | |
| 434 | - | "Drums. Always drums. Get the groove right and the rest writes itself.", | |
| 435 | - | "I hum melodies into my phone all day, then sit down and try to recreate them.", | |
| 436 | - | "Start with a reference track. Analyze the arrangement, then write something inspired by the structure.", | |
| 437 | - | ], | |
| 438 | - | ), | |
| 439 | - | ( | |
| 440 | - | "Best MIDI controllers under $200?", | |
| 441 | - | "Looking for something with keys and some knobs. Not a full workstation, just something to play ideas into the DAW.", | |
| 442 | - | &[ | |
| 443 | - | "Arturia Keystep 37. Great keybed for the price, plus the arpeggiator and sequencer.", | |
| 444 | - | "Novation Launchkey MK3. Deep DAW integration if you use Ableton.", | |
| 445 | - | "Akai MPK Mini. Tiny but surprisingly capable.", | |
| 446 | - | ], | |
| 447 | - | ), | |
| 448 | - | ( | |
| 449 | - | "CPU overload: how do you deal with it?", | |
| 450 | - | "My sessions keep hitting 100% CPU. Running a Ryzen 5 3600 with 16GB RAM. Is it time to upgrade or am I doing something wrong?", | |
| 451 | - | &[ | |
| 452 | - | "Freeze tracks you are not actively working on. Most DAWs support this.", | |
| 453 | - | "Bounce MIDI to audio once you are happy with the sound. Frees up the plugin.", | |
| 454 | - | "Check your buffer size. 256 for recording, 1024+ for mixing.", | |
| 455 | - | "Your CPU is fine. It is probably one plugin eating everything. Check per-plugin CPU.", | |
| 456 | - | ], | |
| 457 | - | ), | |
| 458 | - | ( | |
| 459 | - | "Music theory: how much do you actually use?", | |
| 460 | - | "I know basic chords and scales but never studied theory formally. How much do you all actually apply?", | |
| 461 | - | &[ | |
| 462 | - | "Enough to communicate with other musicians. Circle of fifths, chord functions, basic voice leading.", | |
| 463 | - | "Almost none. I go by ear and fix what sounds wrong.", | |
| 464 | - | "A lot. Understanding why something sounds good helps you recreate it faster.", | |
| 465 | - | ], | |
| 466 | - | ), | |
| 467 | - | ( | |
| 468 | - | "Side-chaining techniques", | |
| 469 | - | "What is your preferred method for side-chaining? Compressor? Volume automation? LFO tool?", | |
| 470 | - | &[ | |
| 471 | - | "Trackspacer. Not technically side-chain compression but solves the same problem better.", | |
| 472 | - | "Volume shaper plugin. More precise than a compressor and you can draw the exact curve.", | |
| 473 | - | "Classic compressor side-chain. Simple, works, everyone knows how to do it.", | |
| 474 | - | "I automate the volume manually. Full control, no artifacts.", | |
| 475 | - | ], | |
| 476 | - | ), | |
| 477 | - | ( | |
| 478 | - | "How do you avoid ear fatigue?", | |
| 479 | - | "I can mix for maybe 2 hours before everything starts sounding the same. Any tips?", | |
| 480 | - | &[ | |
| 481 | - | "Take breaks every 45 minutes. Walk around, drink water, reset.", | |
| 482 | - | "Mix at low volume. If it sounds good quiet, it will sound good loud.", | |
| 483 | - | "Reference constantly. Switch to a commercial track every 15 minutes.", | |
| 484 | - | ], | |
| 485 | - | ), | |
| 486 | - | ( | |
| 487 | - | "Collaboration: how do you share projects?", | |
| 488 | - | "Want to collaborate with a friend who uses a different DAW. What is the best approach?", | |
| 489 | - | &[ | |
| 490 | - | "Stems. Bounce each track as a WAV, share via cloud storage.", | |
| 491 | - | "MIDI + stems for the parts that need to be editable.", | |
| 492 | - | "We just use the same DAW. Easier than any workaround.", | |
| 493 | - | ], | |
| 494 | - | ), | |
| 495 | - | ( | |
| 496 | - | "Loudness standards for streaming platforms", | |
| 497 | - | "Spotify targets -14 LUFS, Apple Music -16. Should I master to a specific target or just make it sound good?", | |
| 498 | - | &[ | |
| 499 | - | "Master to what sounds best, then check loudness after. Most platforms normalize anyway.", | |
| 500 | - | "I aim for -12 to -14. Leaves headroom but still competitive.", |
Lines truncated
| @@ -1,0 +1,59 @@ | |||
| 1 | + | //! The 55 replies that make one thread long enough to page. | |
| 2 | + | ||
| 3 | + | pub(super) const POSTS: [&str; 55] = [ | |
| 4 | + | "Been on Ableton since version 8. Cannot imagine switching at this point.", | |
| 5 | + | "I tried Bitwig for a month. The modulation system is amazing but I missed Ableton's workflow.", | |
| 6 | + | "Reaper deserves more love. The customization is insane once you learn ReaScript.", | |
| 7 | + | "FL Studio's piano roll is still the best. Nothing else comes close for MIDI editing.", | |
| 8 | + | "Logic's Drummer track is legitimately good. Saves me hours on demo tracks.", | |
| 9 | + | "Has anyone tried Ardour? Curious about the open source option.", | |
| 10 | + | "Ardour is great for recording and mixing. Less so for electronic production.", | |
| 11 | + | "Studio One has the best drag-and-drop workflow. Everything just snaps where you want it.", | |
| 12 | + | "I use different DAWs for different tasks. Ableton for production, Reaper for mixing, FL for beats.", | |
| 13 | + | "That sounds exhausting. I would rather master one tool than juggle three.", | |
| 14 | + | "Cubase is still king for film scoring. The expression maps and articulation system are unmatched.", | |
| 15 | + | "Pro Tools is dead for everything except professional studios that need industry compatibility.", | |
| 16 | + | "Hot take: the DAW matters way less than people think. Spend that energy on learning mixing.", | |
| 17 | + | "Agree. I have heard incredible music made in every DAW. And terrible music too.", | |
| 18 | + | "The best DAW is the one you know. Switching costs months of productivity.", | |
| 19 | + | "I switch between FL and Ableton depending on the genre. Hip hop in FL, electronic in Ableton.", | |
| 20 | + | "Reason had so much potential. Shame it never gained traction against the big players.", | |
| 21 | + | "Anyone using hardware-only setups? No DAW at all?", | |
| 22 | + | "I did a hardware-only album last year. It was liberating but the mixdown was painful.", | |
| 23 | + | "Renoise. Tracker workflow is fast once you learn the key commands.", | |
| 24 | + | "I miss trackers. Something about the vertical scrolling interface makes rhythm programming intuitive.", | |
| 25 | + | "What about Maschine or MPC as a DAW? Those have gotten pretty capable.", | |
| 26 | + | "MPC standalone is great for jamming but I always end up exporting to a proper DAW for mixing.", | |
| 27 | + | "Bitwig on Linux is underrated. Full native support, no WINE hacks.", | |
| 28 | + | "I produce on Linux with Bitwig. Works perfectly. Even plugin support is solid with yabridge.", | |
| 29 | + | "The iPad is becoming a legit production platform. GarageBand to Logic hands off cleanly now.", | |
| 30 | + | "Mobile production is fun for sketching ideas but I would not mix on a tablet.", | |
| 31 | + | "Anyone tried the new AI features in some DAWs? Stem separation, auto-mixing, etc.", | |
| 32 | + | "AI stem separation is useful for sampling. The mixing AI is gimmicky.", | |
| 33 | + | "I use LALAL.ai for stem extraction. Better results than the built-in DAW tools.", | |
| 34 | + | "The plugin format wars are annoying. VST3, AU, CLAP. Can we just pick one?", | |
| 35 | + | "CLAP is the future. Open standard, better multithreading, no licensing fees.", | |
| 36 | + | "VST3 is fine. It works everywhere. CLAP is cool but the ecosystem is not there yet.", | |
| 37 | + | "AU is Apple-only, so it is irrelevant for anyone on Windows or Linux.", | |
| 38 | + | "Reaper supports everything. VST2, VST3, AU, CLAP, JS, LV2. No format wars needed.", | |
| 39 | + | "Honestly the DAW you start with tends to be the one you stick with. The muscle memory runs deep.", | |
| 40 | + | "I started on GarageBand, moved to Logic, tried Ableton, came back to Logic. Full circle.", | |
| 41 | + | "FL Studio's lifetime free updates are unbeatable. Paid once in 2015, still getting new versions.", | |
| 42 | + | "That is genuinely impressive. No other DAW offers that deal.", | |
| 43 | + | "The subscription model some companies are pushing is concerning. I want to own my tools.", | |
| 44 | + | "Agree. If the company shuts down, subscriptions die. Perpetual licenses survive.", | |
| 45 | + | "Ableton's standard-to-suite upgrade price is brutal though. Almost the cost of a new DAW.", | |
| 46 | + | "Wait for sales. Ableton does 20-25% off once or twice a year.", | |
| 47 | + | "I use the DAW that came free with my audio interface. Ableton Lite. It is enough for what I do.", | |
| 48 | + | "Lite versions are a great starting point. Upgrade when you hit the track limit.", | |
| 49 | + | "Track limits are artificial but they do force you to commit and bounce stems.", | |
| 50 | + | "Anyone still using Cakewalk? It went free and then got acquired again.", | |
| 51 | + | "Cakewalk is solid but the UI feels dated compared to modern DAWs.", | |
| 52 | + | "UI matters more than people admit. You stare at this thing for hours. It should look good.", | |
| 53 | + | "Function over form. I will take ugly and fast over pretty and slow any day.", | |
| 54 | + | "My dream DAW would have Ableton's session view, FL's piano roll, Logic's stock plugins, and Reaper's customization.", | |
| 55 | + | "So basically everything good from everything. Not unreasonable honestly.", | |
| 56 | + | "The DAW market is mature enough that they are all good. Pick one and make music.", | |
| 57 | + | "This thread has been going for a while. I think the conclusion is: whatever works for you.", | |
| 58 | + | "Final answer: all DAWs are good. Now go make music instead of debating tools.", | |
| 59 | + | ]; |
| @@ -1,0 +1,336 @@ | |||
| 1 | + | //! Music/General: 35 threads, enough for two pages. | |
| 2 | + | ||
| 3 | + | pub(super) const THREADS: &[(&str, &str, &[&str])] = &[ | |
| 4 | + | ( | |
| 5 | + | "Welcome to Music Production", | |
| 6 | + | "Ground rules: be kind, share knowledge, no self-promo spam. Use the right category for your topic.", | |
| 7 | + | &["Glad to be here.", "Thanks for setting this up."], | |
| 8 | + | ), | |
| 9 | + | ( | |
| 10 | + | "What DAW are you using in 2026?", | |
| 11 | + | "Curious what everyone is running these days. I have been on Ableton for years but keep hearing about Bitwig.", | |
| 12 | + | &[ | |
| 13 | + | "Reaper. Lightweight, cheap, endlessly customizable.", | |
| 14 | + | "Bitwig since version 4. The modulation system is unmatched.", | |
| 15 | + | "FL Studio. Started on it when I was 14, never left.", | |
| 16 | + | "Ableton but I keep a Reaper install for editing and batch processing.", | |
| 17 | + | "Logic. It just works and the stock plugins are surprisingly good.", | |
| 18 | + | ], | |
| 19 | + | ), | |
| 20 | + | ( | |
| 21 | + | "Favorite free plugins?", | |
| 22 | + | "What are your go-to free VSTs? Looking for synths, effects, anything.", | |
| 23 | + | &[ | |
| 24 | + | "Vital is the obvious answer. Best free synth by a mile.", | |
| 25 | + | "Valhalla Supermassive for reverb/delay. Sounds incredible for free.", | |
| 26 | + | "TDR Nova for EQ. Clean, surgical, zero CPU.", | |
| 27 | + | "Dexed if you want FM synthesis. Basically a free DX7.", | |
| 28 | + | ], | |
| 29 | + | ), | |
| 30 | + | ( | |
| 31 | + | "How do you organize your sample library?", | |
| 32 | + | "My sample folder is a disaster. Thousands of files with no structure. How do you keep things findable?", | |
| 33 | + | &[ | |
| 34 | + | "Genre > Type > Character. So like Electronic > Kicks > Punchy. Took a weekend to sort but worth it.", | |
| 35 | + | "I use tags instead of folders. AudioFiles has been great for this actually.", | |
| 36 | + | "Honestly I just search. If the filename is descriptive enough, search wins over folders every time.", | |
| 37 | + | ], | |
| 38 | + | ), | |
| 39 | + | ( | |
| 40 | + | "Analog vs digital debate in 2026", | |
| 41 | + | "Is there still a meaningful difference? Modern plugins are so good that I genuinely cannot tell in blind tests anymore.", | |
| 42 | + | &[ | |
| 43 | + | "The difference is workflow, not sound. Hardware forces commitment.", | |
| 44 | + | "I sold all my hardware last year. Zero regrets. Plugins are there.", | |
| 45 | + | "The tactile experience matters. Turning a real knob is different from clicking a virtual one.", | |
| 46 | + | "Both. I track through analog preamps and process in the box.", | |
| 47 | + | ], | |
| 48 | + | ), | |
| 49 | + | ( | |
| 50 | + | "How long did it take you to finish your first track?", | |
| 51 | + | "Been producing for 3 months and I cannot seem to finish anything. Is this normal?", | |
| 52 | + | &[ | |
| 53 | + | "Totally normal. Took me a year to finish something I was not embarrassed by.", | |
| 54 | + | "Force yourself to finish bad tracks. The skill of finishing is separate from the skill of producing.", | |
| 55 | + | "Set a deadline. 48 hours from start to bounce. Quality does not matter, completion does.", | |
| 56 | + | "Still working on my first one honestly.", | |
| 57 | + | "About 6 months. And it was terrible. But I finished it and that mattered.", | |
| 58 | + | ], | |
| 59 | + | ), | |
| 60 | + | ( | |
| 61 | + | "Studio monitors vs headphones for mixing?", | |
| 62 | + | "I live in an apartment and cannot treat the room properly. Should I just mix on headphones?", | |
| 63 | + | &[ | |
| 64 | + | "Headphones are fine if you know their character. Use a reference plugin like Sonarworks.", | |
| 65 | + | "I mix on DT 770s and reference on my car stereo. Not ideal but it works.", | |
| 66 | + | "Even cheap acoustic treatment helps. Hang some moving blankets.", | |
| 67 | + | ], | |
| 68 | + | ), | |
| 69 | + | ( | |
| 70 | + | "What's your creative process?", | |
| 71 | + | "Do you start with drums, melody, a sample? Curious how other people approach a blank session.", | |
| 72 | + | &[ | |
| 73 | + | "Always start with chords. Everything else follows from the harmony.", | |
| 74 | + | "I jam on a synth until something clicks, then build around that loop.", | |
| 75 | + | "Sound design first. Find an interesting texture, then write something that serves it.", | |
| 76 | + | "Drums. Always drums. Get the groove right and the rest writes itself.", | |
| 77 | + | "I hum melodies into my phone all day, then sit down and try to recreate them.", | |
| 78 | + | "Start with a reference track. Analyze the arrangement, then write something inspired by the structure.", | |
| 79 | + | ], | |
| 80 | + | ), | |
| 81 | + | ( | |
| 82 | + | "Best MIDI controllers under $200?", | |
| 83 | + | "Looking for something with keys and some knobs. Not a full workstation, just something to play ideas into the DAW.", | |
| 84 | + | &[ | |
| 85 | + | "Arturia Keystep 37. Great keybed for the price, plus the arpeggiator and sequencer.", | |
| 86 | + | "Novation Launchkey MK3. Deep DAW integration if you use Ableton.", | |
| 87 | + | "Akai MPK Mini. Tiny but surprisingly capable.", | |
| 88 | + | ], | |
| 89 | + | ), | |
| 90 | + | ( | |
| 91 | + | "CPU overload: how do you deal with it?", | |
| 92 | + | "My sessions keep hitting 100% CPU. Running a Ryzen 5 3600 with 16GB RAM. Is it time to upgrade or am I doing something wrong?", | |
| 93 | + | &[ | |
| 94 | + | "Freeze tracks you are not actively working on. Most DAWs support this.", | |
| 95 | + | "Bounce MIDI to audio once you are happy with the sound. Frees up the plugin.", | |
| 96 | + | "Check your buffer size. 256 for recording, 1024+ for mixing.", | |
| 97 | + | "Your CPU is fine. It is probably one plugin eating everything. Check per-plugin CPU.", | |
| 98 | + | ], | |
| 99 | + | ), | |
| 100 | + | ( | |
| 101 | + | "Music theory: how much do you actually use?", | |
| 102 | + | "I know basic chords and scales but never studied theory formally. How much do you all actually apply?", | |
| 103 | + | &[ | |
| 104 | + | "Enough to communicate with other musicians. Circle of fifths, chord functions, basic voice leading.", | |
| 105 | + | "Almost none. I go by ear and fix what sounds wrong.", | |
| 106 | + | "A lot. Understanding why something sounds good helps you recreate it faster.", | |
| 107 | + | ], | |
| 108 | + | ), | |
| 109 | + | ( | |
| 110 | + | "Side-chaining techniques", | |
| 111 | + | "What is your preferred method for side-chaining? Compressor? Volume automation? LFO tool?", | |
| 112 | + | &[ | |
| 113 | + | "Trackspacer. Not technically side-chain compression but solves the same problem better.", | |
| 114 | + | "Volume shaper plugin. More precise than a compressor and you can draw the exact curve.", | |
| 115 | + | "Classic compressor side-chain. Simple, works, everyone knows how to do it.", | |
| 116 | + | "I automate the volume manually. Full control, no artifacts.", | |
| 117 | + | ], | |
| 118 | + | ), | |
| 119 | + | ( | |
| 120 | + | "How do you avoid ear fatigue?", | |
| 121 | + | "I can mix for maybe 2 hours before everything starts sounding the same. Any tips?", | |
| 122 | + | &[ | |
| 123 | + | "Take breaks every 45 minutes. Walk around, drink water, reset.", | |
| 124 | + | "Mix at low volume. If it sounds good quiet, it will sound good loud.", | |
| 125 | + | "Reference constantly. Switch to a commercial track every 15 minutes.", | |
| 126 | + | ], | |
| 127 | + | ), | |
| 128 | + | ( | |
| 129 | + | "Collaboration: how do you share projects?", | |
| 130 | + | "Want to collaborate with a friend who uses a different DAW. What is the best approach?", | |
| 131 | + | &[ | |
| 132 | + | "Stems. Bounce each track as a WAV, share via cloud storage.", | |
| 133 | + | "MIDI + stems for the parts that need to be editable.", | |
| 134 | + | "We just use the same DAW. Easier than any workaround.", | |
| 135 | + | ], | |
| 136 | + | ), | |
| 137 | + | ( | |
| 138 | + | "Loudness standards for streaming platforms", | |
| 139 | + | "Spotify targets -14 LUFS, Apple Music -16. Should I master to a specific target or just make it sound good?", | |
| 140 | + | &[ | |
| 141 | + | "Master to what sounds best, then check loudness after. Most platforms normalize anyway.", | |
| 142 | + | "I aim for -12 to -14. Leaves headroom but still competitive.", | |
| 143 | + | "The number matters less than the dynamic range. A dynamic -14 sounds better than a squashed -8.", | |
| 144 | + | ], | |
| 145 | + | ), | |
| 146 | + | ( | |
| 147 | + | "What headphones do you use?", | |
| 148 | + | "Looking for mixing headphones. Budget around $150-300.", | |
| 149 | + | &[ | |
| 150 | + | "Sennheiser HD 600. Flat, detailed, comfortable for long sessions.", | |
| 151 | + | "Beyerdynamic DT 990 Pro. Wide soundstage. A bit bright but you learn the character.", | |
| 152 | + | "AKG K712. Open-back, great imaging.", | |
| 153 | + | "Audio-Technica ATH-M50x. Closed-back, good isolation, slightly hyped low end.", | |
| 154 | + | ], | |
| 155 | + | ), | |
| 156 | + | ( | |
| 157 | + | "Sampling ethics", | |
| 158 | + | "Where do you draw the line with sampling? Royalty-free packs? Vinyl? Other artists' released music?", | |
| 159 | + | &[ | |
| 160 | + | "Royalty-free is fair game obviously. For anything else, clear it or make it unrecognizable.", | |
| 161 | + | "I only sample stuff I have created myself or bought a license for.", | |
| 162 | + | "Sampling is an art form. The key is transformation.", | |
| 163 | + | ], | |
| 164 | + | ), | |
| 165 | + | ( | |
| 166 | + | "When to call a mix done?", | |
| 167 | + | "I keep tweaking endlessly. How do you know when to stop?", | |
| 168 | + | &[ | |
| 169 | + | "When changes become lateral instead of improvements. If you are just going back and forth, stop.", | |
| 170 | + | "Set a deadline. Ship it. You can always do a v2 later.", | |
| 171 | + | "Compare to your reference. If it holds up, it is done.", | |
| 172 | + | "When you start breaking things that were already working.", | |
| 173 | + | ], | |
| 174 | + | ), | |
| 175 | + | ( | |
| 176 | + | "Learning synthesis from scratch", | |
| 177 | + | "I have been using presets forever. Want to learn to design my own sounds. Where do I start?", | |
| 178 | + | &[ | |
| 179 | + | "Start with subtractive synthesis. One oscillator, one filter, one envelope. Understand what each does.", | |
| 180 | + | "Syntorial. It is a paid app but it teaches synthesis by ear, not from theory.", | |
| 181 | + | "Pick one synth and learn it deeply. Vital or Serum are good because they visualize everything.", | |
| 182 | + | ], | |
| 183 | + | ), | |
| 184 | + | ( | |
| 185 | + | "Vocal processing chain", | |
| 186 | + | "What is your typical vocal chain? EQ, compression, de-essing, etc.", | |
| 187 | + | &[ | |
| 188 | + | "Gain staging, subtractive EQ, compressor (light 2:1), de-esser, additive EQ, reverb send.", | |
| 189 | + | "Similar but I add a saturation plugin before the compressor. Gives warmth.", | |
| 190 | + | "Depends entirely on the vocal and the genre. There is no universal chain.", | |
| 191 | + | ], | |
| 192 | + | ), | |
| 193 | + | ( | |
| 194 | + | "Budget acoustic treatment", | |
| 195 | + | "Can I treat a small room for under $200? What should I prioritize?", | |
| 196 | + | &[ | |
| 197 | + | "First reflections. Put absorption panels at mirror points on side walls.", | |
| 198 | + | "Bass traps in the corners are the highest impact single thing you can do.", | |
| 199 | + | "Rockwool panels in wooden frames. DIY for about $50 per panel.", | |
| 200 | + | "A thick rug on the floor and heavy curtains help more than people think.", | |
| 201 | + | ], | |
| 202 | + | ), | |
| 203 | + | ( | |
| 204 | + | "Favorite reverb plugin?", | |
| 205 | + | "Looking for something versatile. Plates, halls, rooms, all in one.", | |
| 206 | + | &[ | |
| 207 | + | "Valhalla Room. $50, sounds amazing, low CPU.", | |
| 208 | + | "FabFilter Pro-R 2. The decay rate EQ is genius.", | |
| 209 | + | "Stock reverb in your DAW is probably fine for 90% of use cases.", | |
| 210 | + | ], | |
| 211 | + | ), | |
| 212 | + | ( | |
| 213 | + | "How important is mastering?", | |
| 214 | + | "Can I just slap a limiter on the master bus and call it a day?", | |
| 215 | + | &[ | |
| 216 | + | "For demos and personal releases, yes. For commercial releases, get it mastered.", | |
| 217 | + | "Mastering is about perspective. A second set of ears in a treated room catches things you miss.", | |
| 218 | + | "iZotope Ozone is good enough for DIY mastering if you learn what each module does.", | |
| 219 | + | ], | |
| 220 | + | ), | |
| 221 | + | ( | |
| 222 | + | "Managing multiple projects at once", | |
| 223 | + | "I have like 30 unfinished projects. How do you manage the backlog?", | |
| 224 | + | &[ | |
| 225 | + | "Pick three. Finish them. Delete the rest. Harsh but effective.", | |
| 226 | + | "I give each project a status: idea, in progress, mixing, done. Only 2 can be in progress at once.", | |
| 227 | + | "Set a monthly goal. One finished track per month, minimum.", | |
| 228 | + | ], | |
| 229 | + | ), | |
| 230 | + | ( | |
| 231 | + | "Track referencing workflow", | |
| 232 | + | "How do you reference against commercial tracks while mixing?", | |
| 233 | + | &[ | |
| 234 | + | "Import the reference directly into the session. Level match with a LUFS meter, A/B constantly.", | |
| 235 | + | "I use a dedicated plugin that lets me switch between my mix and references with one click.", | |
| 236 | + | "Listen to the reference on the same speakers, back to back. Do not overthink the workflow.", | |
| 237 | + | ], | |
| 238 | + | ), | |
| 239 | + | ( | |
| 240 | + | "Gain staging basics", | |
| 241 | + | "Keep hearing about gain staging but not sure I understand it. ELI5?", | |
| 242 | + | &[ | |
| 243 | + | "Every plugin in your chain should receive signal at a reasonable level. If you push hot into a compressor, it behaves differently than if you feed it -18dBFS.", | |
| 244 | + | "Basically: do not clip anything in the chain before the final limiter. Use trim/gain plugins if needed.", | |
| 245 | + | "Aim for peaks around -12 to -6 on each channel. Leaves headroom on the master.", | |
| 246 | + | ], | |
| 247 | + | ), | |
| 248 | + | ( | |
| 249 | + | "Lo-fi production tips", | |
| 250 | + | "Want to make lo-fi hip hop style beats. What gives that characteristic sound?", | |
| 251 | + | &[ | |
| 252 | + | "Bitcrushing, vinyl noise, tape saturation, de-tuned samples, swing on the drums.", | |
| 253 | + | "Record stuff through a cheap mic or tape deck. Real degradation sounds better than plugins simulating it.", | |
| 254 | + | "Side-chain the whole mix to the kick. Slow attack, slow release. That pumping effect is key.", | |
| 255 | + | "Use jazz chord voicings. Maj7, min9, dom13. That is the harmonic foundation.", | |
| 256 | + | ], | |
| 257 | + | ), | |
| 258 | + | ( | |
| 259 | + | "What's your backup strategy?", | |
| 260 | + | "Lost a project file last week and it hurt. How do you backup your work?", | |
| 261 | + | &[ | |
| 262 | + | "Git for project files, rsync to a NAS for samples and bounces.", | |
| 263 | + | "Time Machine plus a monthly clone to an external drive.", | |
| 264 | + | "Cloud sync. Dropbox for active projects, cold storage archive for finished work.", | |
| 265 | + | "Three copies: local SSD, NAS, off-site. If it does not exist in three places, it does not exist.", | |
| 266 | + | ], | |
| 267 | + | ), | |
| 268 | + | ( | |
| 269 | + | "Distortion as a creative tool", | |
| 270 | + | "Using distortion for more than just guitars. What are your favorite ways to use it?", | |
| 271 | + | &[ | |
| 272 | + | "Parallel saturation on vocals. Blend in just enough to add presence without obvious distortion.", | |
| 273 | + | "Run a sub bass through a wavefolder. Instant aggression.", | |
| 274 | + | "Distort a reverb return. Shoegaze in a box.", | |
| 275 | + | ], | |
| 276 | + | ), | |
| 277 | + | ( | |
| 278 | + | "Hardware synths worth buying in 2026?", | |
| 279 | + | "With plugins being so good, is there any reason to buy hardware synths?", | |
| 280 | + | &[ | |
| 281 | + | "Workflow. Playing a hardware synth is a different experience than clicking a mouse.", | |
| 282 | + | "The Korg Minilogue XD is still hard to beat for the price. Four voices, great effects.", | |
| 283 | + | "I would skip hardware unless you perform live. For studio work, plugins win on convenience.", | |
| 284 | + | "Elektron Digitone. FM synthesis in a box with an incredible sequencer.", | |
| 285 | + | ], | |
| 286 | + | ), | |
| 287 | + | ( | |
| 288 | + | "How to get better at arrangement", | |
| 289 | + | "My loops sound great but full tracks feel flat. Any tips for arrangement?", | |
| 290 | + | &[ | |
| 291 | + | "Study arrangements of songs you like. Map them out on paper. Intro, verse, chorus, bridge. Note what enters and exits.", | |
| 292 | + | "Contrast. If the chorus is dense, strip back the verse. Dynamics come from what you remove.", | |
| 293 | + | "Energy curve. Every section should either build tension or release it. Never stay flat.", | |
| 294 | + | "Reference tracks. Drop one into your session and match the structure.", | |
| 295 | + | "Automate everything. Filter sweeps, volume rides, effect sends. Movement keeps the listener engaged.", | |
| 296 | + | ], | |
| 297 | + | ), | |
| 298 | + | ( | |
| 299 | + | "Mixing in mono", | |
| 300 | + | "People say to check your mix in mono. Why? And how often should I do it?", | |
| 301 | + | &[ | |
| 302 | + | "Phase cancellation. If something disappears in mono, your stereo image has phase issues.", | |
| 303 | + | "I check mono every 30 minutes during a mix. Just hit the mono button, listen for 10 seconds.", | |
| 304 | + | "Some clubs play in mono. If your mix falls apart, half your audience hears a broken version.", | |
| 305 | + | ], | |
| 306 | + | ), | |
| 307 | + | ( | |
| 308 | + | "Best YouTube channels for learning production?", | |
| 309 | + | "What channels do you actually learn from vs just watch for entertainment?", | |
| 310 | + | &[ | |
| 311 | + | "Dan Worrall. No-nonsense, deep technical knowledge, great explanations.", | |
| 312 | + | "You Suck at Producing. Funny but genuinely educational.", | |
| 313 | + | "In The Mix. Good for beginners. Covers basics thoroughly.", | |
| 314 | + | "Pensado's Place for mixing techniques from industry engineers.", | |
| 315 | + | ], | |
| 316 | + | ), | |
| 317 | + | ( | |
| 318 | + | "Dealing with creative blocks", | |
| 319 | + | "Have not made anything in two weeks. Everything I start sounds boring. How do you push through?", | |
| 320 | + | &[ | |
| 321 | + | "Constraints. Give yourself a rule: only use three sounds, or finish in one hour. Limits breed creativity.", | |
| 322 | + | "Listen to music outside your genre. Completely different styles can spark unexpected ideas.", | |
| 323 | + | "Recreate a track you love. You will learn technique and usually branch off into something original.", | |
| 324 | + | "Take a break. Burnout is real. Come back when you actually want to, not when you feel you should.", | |
| 325 | + | ], | |
| 326 | + | ), | |
| 327 | + | ( | |
| 328 | + | "Automation tips", | |
| 329 | + | "What do you automate most in your mixes?", | |
| 330 | + | &[ | |
| 331 | + | "Volume. Subtle 1-2dB rides on vocals and leads make a huge difference.", | |
| 332 | + | "Filter cutoff. Automate a low-pass sweep into the chorus for instant energy lift.", | |
| 333 | + | "Reverb send. More reverb in quiet sections, less in dense sections.", | |
| 334 | + | ], | |
| 335 | + | ), | |
| 336 | + | ]; |
| @@ -1,0 +1,141 @@ | |||
| 1 | + | //! Mixing and Mastering: 15 threads. | |
| 2 | + | ||
| 3 | + | pub(super) const THREADS: &[(&str, &str, &[&str])] = &[ | |
| 4 | + | ( | |
| 5 | + | "EQ before or after compression?", | |
| 6 | + | "Classic debate. What is your default chain order and why?", | |
| 7 | + | &[ | |
| 8 | + | "Subtractive EQ first to clean up, then compress, then additive EQ to shape.", | |
| 9 | + | "Depends on the source. Muddy vocal? EQ first. Clean recording? Compress first.", | |
| 10 | + | "I do both. Light EQ before, heavier shaping after.", | |
| 11 | + | ], | |
| 12 | + | ), | |
| 13 | + | ( | |
| 14 | + | "Parallel compression techniques", | |
| 15 | + | "How are you using parallel compression? Just on drums or on everything?", | |
| 16 | + | &[ | |
| 17 | + | "Drums and vocals mainly. Crush a copy, blend it in at like -12dB.", | |
| 18 | + | "I parallel compress the entire mix bus lightly. Adds glue without squashing dynamics.", | |
| 19 | + | "The NY compression trick on drums is still unbeatable. Heavy compression, blend to taste.", | |
| 20 | + | ], | |
| 21 | + | ), | |
| 22 | + | ( | |
| 23 | + | "Multiband compression: when to use it?", | |
| 24 | + | "I never quite know when multiband is the right tool vs a regular compressor.", | |
| 25 | + | &[ | |
| 26 | + | "When different frequency ranges need different treatment. A boomy vocal with sibilance needs different ratios for lows and highs.", | |
| 27 | + | "On the master bus for gentle tonal balancing. Very light ratios, like 1.5:1.", | |
| 28 | + | "Rarely. Most problems are better solved with EQ. Multiband comp is a precision tool, not a default.", | |
| 29 | + | ], | |
| 30 | + | ), | |
| 31 | + | ( | |
| 32 | + | "Mixing with saturation", | |
| 33 | + | "How much saturation do you add and where in the chain?", | |
| 34 | + | &[ | |
| 35 | + | "A tiny bit on every channel. Tape emulation at the end of each strip. Adds harmonic richness.", | |
| 36 | + | "I saturate the mix bus and nothing else. Keeps it cohesive.", | |
| 37 | + | "Saturation on bass is essential for me. Makes it audible on small speakers.", | |
| 38 | + | "Early in the chain, before compression. Saturated signals compress differently.", | |
| 39 | + | ], | |
| 40 | + | ), | |
| 41 | + | ( | |
| 42 | + | "Taming harsh vocals", | |
| 43 | + | "My vocal recordings always end up harsh around 3-5kHz. De-esser? EQ? Both?", | |
| 44 | + | &[ | |
| 45 | + | "Dynamic EQ on the 2-5kHz range. Only dips when it gets harsh, leaves the presence otherwise.", | |
| 46 | + | "De-esser is specifically for sibilance (6-10kHz). For 3-5kHz harshness, use a dynamic EQ or multiband.", | |
| 47 | + | "Check your microphone and preamp. Some mics just have a harsh presence peak. A different mic might solve it at the source.", | |
| 48 | + | ], | |
| 49 | + | ), | |
| 50 | + | ( | |
| 51 | + | "Reverb mixing tips", | |
| 52 | + | "My reverbs either sound obvious or nonexistent. How do you make them sit right?", | |
| 53 | + | &[ | |
| 54 | + | "Pre-delay. 20-50ms of pre-delay separates the dry signal from the reverb and adds clarity.", | |
| 55 | + | "High-cut the reverb at 6-8kHz. Bright reverb tails make everything washy.", | |
| 56 | + | "Send to reverb, then EQ the return. Treat the reverb as its own instrument.", | |
| 57 | + | ], | |
| 58 | + | ), | |
| 59 | + | ( | |
| 60 | + | "Mixing low end: kick and bass coexistence", | |
| 61 | + | "How do you make the kick and bass work together without one masking the other?", | |
| 62 | + | &[ | |
| 63 | + | "Pick one to own the sub frequencies. If the kick has the sub, high-pass the bass at 60Hz. Or vice versa.", | |
| 64 | + | "Side-chain the bass to the kick. Classic but effective.", | |
| 65 | + | "Complementary EQ. Boost the kick at 60Hz, cut the bass there. Boost the bass at 100Hz, cut the kick.", | |
| 66 | + | "In most genres, the kick owns 40-80Hz and the bass lives at 80-200Hz. Arrange them to not compete.", | |
| 67 | + | ], | |
| 68 | + | ), | |
| 69 | + | ( | |
| 70 | + | "Mix bus processing", | |
| 71 | + | "What do you put on your mix bus? When do you add it?", | |
| 72 | + | &[ | |
| 73 | + | "Gentle glue compressor (SSL style, 2:1, slow attack) from the start. Mix into it.", | |
| 74 | + | "EQ, compression, slight saturation, limiter. But everything is doing very little. 1-2dB each.", | |
| 75 | + | "Nothing until the final mix. I want to hear what I actually have before processing it.", | |
| 76 | + | ], | |
| 77 | + | ), | |
| 78 | + | ( | |
| 79 | + | "Reference level for mixing", | |
| 80 | + | "What volume level do you monitor at while mixing?", | |
| 81 | + | &[ | |
| 82 | + | "Calibrated to 85dB SPL for critical listening. Most of the time I mix around 75dB.", | |
| 83 | + | "Quiet. If I can have a conversation over the mix, the volume is right.", | |
| 84 | + | "I check at multiple levels. Low for balance, medium for detail, loud for short reality checks.", | |
| 85 | + | ], | |
| 86 | + | ), | |
| 87 | + | ( | |
| 88 | + | "Width and stereo imaging", | |
| 89 | + | "How do you create width in a mix without it collapsing in mono?", | |
| 90 | + | &[ | |
| 91 | + | "Keep the center strong (kick, bass, lead vocal, snare). Spread supporting elements.", | |
| 92 | + | "Use different methods: hard panning, stereo delay, chorus, mid-side EQ. Variety avoids phase issues.", | |
| 93 | + | "Always check mono. If it sounds thin in mono, your width is relying on phase tricks that cancel.", | |
| 94 | + | ], | |
| 95 | + | ), | |
| 96 | + | ( | |
| 97 | + | "Handling dynamic range in modern productions", | |
| 98 | + | "Everything is so compressed now. How do you keep dynamics while still being competitive in loudness?", | |
| 99 | + | &[ | |
| 100 | + | "Mix dynamically. Let the mastering engineer handle loudness. That is literally their job.", | |
| 101 | + | "Automate volume instead of compressing. Ride the fader for consistent level without squashing transients.", | |
| 102 | + | "Use upward compression. Raises the quiet parts without touching the peaks.", | |
| 103 | + | ], | |
| 104 | + | ), | |
| 105 | + | ( | |
| 106 | + | "Delay vs reverb for depth", | |
| 107 | + | "When do you reach for a delay instead of a reverb to create depth?", | |
| 108 | + | &[ | |
| 109 | + | "Delay for rhythmic depth (synced to tempo). Reverb for spatial depth (rooms, halls).", | |
| 110 | + | "Short delays (30-80ms) create depth without the wash of reverb. Great for keeping things tight.", | |
| 111 | + | "I use both together. Short delay into a reverb return. Best of both worlds.", | |
| 112 | + | ], | |
| 113 | + | ), | |
| 114 | + | ( | |
| 115 | + | "Panning strategies", | |
| 116 | + | "Do you follow specific panning rules or go by feel?", | |
| 117 | + | &[ | |
| 118 | + | "LCR panning. Hard left, center, hard right. Simple but effective. Forces commitment.", | |
| 119 | + | "I pan where the instruments would be on a stage. Drums from the drummer's perspective.", | |
| 120 | + | "By feel. Whatever serves the song. Rules are starting points, not laws.", | |
| 121 | + | ], | |
| 122 | + | ), | |
| 123 | + | ( | |
| 124 | + | "Mixing with headphones: tips and tricks", | |
| 125 | + | "For those who primarily mix on headphones, what are your strategies?", | |
| 126 | + | &[ | |
| 127 | + | "Crossfeed plugin. Simulates speaker crosstalk so panning sounds more natural.", | |
| 128 | + | "Reference constantly on speakers when you can. Even laptop speakers reveal balance issues.", | |
| 129 | + | "Open-back headphones for mixing, closed-back for tracking. The soundstage difference matters.", | |
| 130 | + | ], | |
| 131 | + | ), | |
| 132 | + | ( | |
| 133 | + | "The loudness war is over, right?", | |
| 134 | + | "With streaming normalization, does anyone still master to -6 LUFS?", | |
| 135 | + | &[ | |
| 136 | + | "EDM and hip hop still push hard. Genre expectations matter more than streaming targets.", | |
| 137 | + | "The war is over for most genres. I master to -10 to -14 depending on the track.", | |
| 138 | + | "Yes. And it sounds better. More dynamics, more punch, more musicality. No reason to smash it.", | |
| 139 | + | ], | |
| 140 | + | ), | |
| 141 | + | ]; |
| @@ -1,0 +1,82 @@ | |||
| 1 | + | //! The forum content itself: which threads exist, who posts in them, and what | |
| 2 | + | //! they say. Each category's table is a sibling const, so the loop that writes | |
| 3 | + | //! it stays readable next to the other three. | |
| 4 | + | ||
| 5 | + | mod discussion; | |
| 6 | + | mod general; | |
| 7 | + | mod mixing; | |
| 8 | + | mod sound_design; | |
| 9 | + | ||
| 10 | + | use super::rows::{seed_post, seed_thread}; | |
| 11 | + | use super::users::SeedUser; | |
| 12 | + | use sqlx::PgPool; | |
| 13 | + | use uuid::Uuid; | |
| 14 | + | ||
| 15 | + | /// 35 threads in Music/General, enough for 2 pages. | |
| 16 | + | pub(super) async fn seed_music_general(pool: &PgPool, category_id: Uuid, users: &[SeedUser]) { | |
| 17 | + | let threads = general::THREADS; | |
| 18 | + | ||
| 19 | + | // Pinned welcome thread | |
| 20 | + | let welcome_id = seed_thread(pool, category_id, users[0].id, threads[0].0, true, false).await; | |
| 21 | + | seed_post(pool, welcome_id, users[0].id, threads[0].1).await; | |
| 22 | + | for (i, reply) in threads[0].2.iter().enumerate() { | |
| 23 | + | seed_post(pool, welcome_id, users[(i + 2) % users.len()].id, reply).await; | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // Remaining threads, one with lots of replies for post pagination | |
| 27 | + | for (idx, (title, body, replies)) in threads.iter().enumerate().skip(1) { | |
| 28 | + | let author = &users[(idx + 1) % users.len()]; | |
| 29 | + | let thread_id = seed_thread(pool, category_id, author.id, title, false, false).await; | |
| 30 | + | seed_post(pool, thread_id, author.id, body).await; | |
| 31 | + | ||
| 32 | + | for (i, reply) in replies.iter().enumerate() { | |
| 33 | + | let replier = &users[(idx + i + 3) % users.len()]; | |
| 34 | + | seed_post(pool, thread_id, replier.id, reply).await; | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // Thread #1 (DAW thread): add 55 extra posts to test post pagination (50/page) | |
| 38 | + | if idx == 1 { | |
| 39 | + | seed_long_discussion(pool, thread_id, users).await; | |
| 40 | + | } | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | /// 15 threads in Mixing & Mastering. | |
| 45 | + | pub(super) async fn seed_music_mixing(pool: &PgPool, category_id: Uuid, users: &[SeedUser]) { | |
| 46 | + | let threads = mixing::THREADS; | |
| 47 | + | ||
| 48 | + | for (idx, (title, body, replies)) in threads.iter().enumerate() { | |
| 49 | + | let author = &users[(idx + 2) % users.len()]; | |
| 50 | + | let thread_id = seed_thread(pool, category_id, author.id, title, false, false).await; | |
| 51 | + | seed_post(pool, thread_id, author.id, body).await; | |
| 52 | + | for (i, reply) in replies.iter().enumerate() { | |
| 53 | + | let replier = &users[(idx + i + 4) % users.len()]; | |
| 54 | + | seed_post(pool, thread_id, replier.id, reply).await; | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | /// 15 threads in Sound Design. | |
| 60 | + | pub(super) async fn seed_music_sound_design(pool: &PgPool, category_id: Uuid, users: &[SeedUser]) { | |
| 61 | + | let threads = sound_design::THREADS; | |
| 62 | + | ||
| 63 | + | for (idx, (title, body, replies)) in threads.iter().enumerate() { | |
| 64 | + | let author = &users[(idx + 5) % users.len()]; | |
| 65 | + | let thread_id = seed_thread(pool, category_id, author.id, title, false, false).await; | |
| 66 | + | seed_post(pool, thread_id, author.id, body).await; | |
| 67 | + | for (i, reply) in replies.iter().enumerate() { | |
| 68 | + | let replier = &users[(idx + i + 6) % users.len()]; | |
| 69 | + | seed_post(pool, thread_id, replier.id, reply).await; | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | /// Add 55 extra posts to a thread to test post pagination (50 posts/page). | |
| 75 | + | pub(super) async fn seed_long_discussion(pool: &PgPool, thread_id: Uuid, users: &[SeedUser]) { | |
| 76 | + | let posts = discussion::POSTS; | |
| 77 | + | ||
| 78 | + | for (i, body) in posts.iter().enumerate() { | |
| 79 | + | let author = &users[(i + 4) % users.len()]; | |
| 80 | + | seed_post(pool, thread_id, author.id, body).await; | |
| 81 | + | } | |
| 82 | + | } |
| @@ -1,0 +1,144 @@ | |||
| 1 | + | //! Sound Design: 15 threads. | |
| 2 | + | ||
| 3 | + | pub(super) const THREADS: &[(&str, &str, &[&str])] = &[ | |
| 4 | + | ( | |
| 5 | + | "FM synthesis for beginners", | |
| 6 | + | "FM synthesis always seemed impenetrable to me. Where do you start?", | |
| 7 | + | &[ | |
| 8 | + | "Two operators. Carrier and modulator. Change the ratio between them and listen. That is literally it to start.", | |
| 9 | + | "Dexed is free and models the DX7. Great for learning because there are tons of tutorials.", | |
| 10 | + | "Start with simple ratios: 1:1 for warmth, 1:2 for brightness, 1:3 for nasality. Then experiment.", | |
| 11 | + | ], | |
| 12 | + | ), | |
| 13 | + | ( | |
| 14 | + | "Wavetable synthesis tips", | |
| 15 | + | "Using Vital/Serum and want to go beyond presets. What are your wavetable tricks?", | |
| 16 | + | &[ | |
| 17 | + | "Import your own audio as a wavetable. Record a word, import it, scrub through it with an LFO.", | |
| 18 | + | "Morph between simple waveforms. A slow morph from saw to square adds movement without being obvious.", | |
| 19 | + | "Stack two oscillators with different wavetable positions and detune slightly. Instant width.", | |
| 20 | + | ], | |
| 21 | + | ), | |
| 22 | + | ( | |
| 23 | + | "How to make a supersaw", | |
| 24 | + | "Every tutorial makes it sound easy but mine never sound as full as professional supersaws.", | |
| 25 | + | &[ | |
| 26 | + | "More unison voices, more detune, slight chorus. But the real secret is layering: a supersaw is usually 2-3 layers, not one patch.", | |
| 27 | + | "High-pass the supersaw and layer a clean sub underneath. The fullness comes from the sub, not the saw.", | |
| 28 | + | "OTT on the supersaw bus. Controversial but it works.", | |
| 29 | + | "Mid-side processing. Keep the center mono, spread the sides. Instant width and power.", | |
| 30 | + | ], | |
| 31 | + | ), | |
| 32 | + | ( | |
| 33 | + | "Granular synthesis: practical uses?", | |
| 34 | + | "Granular synths look cool but I cannot figure out when to actually use them. What do you use granular for?", | |
| 35 | + | &[ | |
| 36 | + | "Pads and textures. Take a field recording, granularize it, and you have a unique atmosphere.", | |
| 37 | + | "Glitch effects. Short grain size + random position = controlled chaos.", | |
| 38 | + | "Stretching vocals to infinity. Load a vocal chop, set huge grain size, and you have a drone.", | |
| 39 | + | ], | |
| 40 | + | ), | |
| 41 | + | ( | |
| 42 | + | "Designing kick drums from scratch", | |
| 43 | + | "How do you synthesize a kick drum? I always just use samples.", | |
| 44 | + | &[ | |
| 45 | + | "Sine wave with a pitch envelope. Start at 200-300Hz, drop to 40-60Hz in about 50ms. Add a click layer for attack.", | |
| 46 | + | "Three layers: sub (sine, pitch envelope), body (short noise burst, filtered), click (very short transient).", | |
| 47 | + | "Operator in Ableton can do it in seconds. One sine oscillator, pitch envelope with fast decay.", | |
| 48 | + | ], | |
| 49 | + | ), | |
| 50 | + | ( | |
| 51 | + | "Making sounds feel organic", | |
| 52 | + | "My synth patches sound too static and digital. How do you add life?", | |
| 53 | + | &[ | |
| 54 | + | "LFOs on everything. Subtle random LFO on pitch, filter, amplitude. Humans are never perfectly steady.", | |
| 55 | + | "Velocity sensitivity. Map velocity to filter cutoff and amplitude. Changes how it responds to playing.", | |
| 56 | + | "Record the performance, do not program it. Even mouse-drawn MIDI lacks human timing.", | |
| 57 | + | "Add noise. A tiny bit of noise mixed in makes digital sounds feel more analog.", | |
| 58 | + | ], | |
| 59 | + | ), | |
| 60 | + | ( | |
| 61 | + | "Resampling workflow", | |
| 62 | + | "What is your resampling process? Bounce to audio and re-process?", | |
| 63 | + | &[ | |
| 64 | + | "Exactly. Design a sound, bounce it, chop it up, throw it in a sampler, add new processing.", | |
| 65 | + | "I resample through effects. Play a pad, record it through a reverb and delay, chop the result.", | |
| 66 | + | "Granular resampling. Take anything, granularize it, it becomes something entirely new.", | |
| 67 | + | ], | |
| 68 | + | ), | |
| 69 | + | ( | |
| 70 | + | "Favorite Vital patches you have designed", | |
| 71 | + | "Share your favorite sound you have made in Vital. What was the approach?", | |
| 72 | + | &[ | |
| 73 | + | "A pluck using two wavetables with very fast envelope decay, short reverb. Simple but cuts through any mix.", | |
| 74 | + | "A pad that morphs between 4 wavetable frames with a slow random LFO. Different every time it plays.", | |
| 75 | + | "A bass using the filter FM feature. Self-oscillating filter modulated by an oscillator. Absolutely nasty.", | |
| 76 | + | ], | |
| 77 | + | ), | |
| 78 | + | ( | |
| 79 | + | "Foley and field recording for music", | |
| 80 | + | "Anyone incorporate real-world recordings into their music?", | |
| 81 | + | &[ | |
| 82 | + | "All the time. City ambience, rain, coffee shop noise. Layer it under pads for realism.", | |
| 83 | + | "I record weird objects and use them as percussion. A spoon on a mug, keys jangling, books dropped.", | |
| 84 | + | "Bird recordings slowed down 10x sound like alien synth pads. Nature is the best sound designer.", | |
| 85 | + | "Contact microphone on everything. Metal fences, bridges, pipes. Gold mine of textures.", | |
| 86 | + | ], | |
| 87 | + | ), | |
| 88 | + | ( | |
| 89 | + | "Modular synthesis without hardware", | |
| 90 | + | "Want to explore modular concepts but cannot afford hardware. Software options?", | |
| 91 | + | &[ | |
| 92 | + | "VCV Rack. Free, open source, massive module library. It is basically Eurorack on your computer.", | |
| 93 | + | "Bitwig's modulation system is semi-modular. Very deep and all built into the DAW.", | |
| 94 | + | "Max/MSP or Pure Data if you want to go deep. Steep learning curve but unlimited.", | |
| 95 | + | "Cherry Audio Voltage Modular. Cheaper than real hardware and great module selection.", | |
| 96 | + | ], | |
| 97 | + | ), | |
| 98 | + | ( | |
| 99 | + | "Designing cinematic impacts and risers", | |
| 100 | + | "How do you create those massive cinematic impacts and tension risers?", | |
| 101 | + | &[ | |
| 102 | + | "Layer, layer, layer. Noise sweep + sub drop + metallic hit + reverb tail = one impact.", | |
| 103 | + | "Reverse cymbal with rising pitch is the classic riser. Add a filter sweep and noise for intensity.", | |
| 104 | + | "Record something large. Slam a door. Drop a heavy book. Process it with convolution reverb in a cathedral IR.", | |
| 105 | + | ], | |
| 106 | + | ), | |
| 107 | + | ( | |
| 108 | + | "Phase distortion synthesis", | |
| 109 | + | "Casio CZ-series used phase distortion. Anyone still using this technique?", | |
| 110 | + | &[ | |
| 111 | + | "Underrated. It is different from FM: smoother harmonics, easier to control.", | |
| 112 | + | "There are a few VST recreations. CZ V from Arturia is faithful to the originals.", | |
| 113 | + | "You can sort of approximate it in any synth by modulating the phase of one oscillator with another, but true PD has a distinct character.", | |
| 114 | + | ], | |
| 115 | + | ), | |
| 116 | + | ( | |
| 117 | + | "Processing drums creatively", | |
| 118 | + | "Beyond standard mixing, how do you use creative processing on drums?", | |
| 119 | + | &[ | |
| 120 | + | "Bit-crush the snare slightly. Adds grit and lo-fi character.", | |
| 121 | + | "Send drums to a reverb, compress the reverb return hard. Massive room sound.", | |
| 122 | + | "Granular processing on a drum loop. Freeze interesting moments, stretch hits into textures.", | |
| 123 | + | "Run the whole drum bus through a guitar amp sim. Instant rock energy.", | |
| 124 | + | ], | |
| 125 | + | ), | |
| 126 | + | ( | |
| 127 | + | "Additive synthesis: is it practical?", | |
| 128 | + | "Additive seems powerful in theory but is it actually useful for sound design?", | |
| 129 | + | &[ | |
| 130 | + | "Very useful for evolving pads and organ-like tones. Direct control over individual harmonics.", | |
| 131 | + | "Resynthesis is where additive shines. Analyze a sound, then modify individual partials.", | |
| 132 | + | "In practice, subtractive and wavetable get you there faster. Additive is more of a research tool.", | |
| 133 | + | ], | |
| 134 | + | ), | |
| 135 | + | ( | |
| 136 | + | "Sound design for games vs music", | |
| 137 | + | "Anyone here do game audio? How is the process different from music production?", | |
| 138 | + | &[ | |
| 139 | + | "Everything needs to loop without a click. And sounds need to work at different pitches and speeds for real-time variation.", | |
| 140 | + | "Much more focus on functionality. A UI click needs to be satisfying but not distracting. Very different from making a synth lead.", | |
| 141 | + | "Middleware like FMOD or Wwise adds a whole layer of implementation. You end up programming behaviors, not making sounds.", | |
| 142 | + | ], | |
| 143 | + | ), | |
| 144 | + | ]; |
| @@ -1,0 +1,247 @@ | |||
| 1 | + | //! Seed initial forum data for development. Run with `--seed` flag. | |
| 2 | + | //! | |
| 3 | + | //! The orchestration lives here; the rows it writes are in `rows`, the people | |
| 4 | + | //! who write them in `users`, and the forum content itself in `content`, which | |
| 5 | + | //! is data rather than code and was most of this file's size. | |
| 6 | + | ||
| 7 | + | mod content; | |
| 8 | + | mod rows; | |
| 9 | + | mod users; | |
| 10 | + | ||
| 11 | + | use content::{seed_music_general, seed_music_mixing, seed_music_sound_design}; | |
| 12 | + | use rows::{ | |
| 13 | + | seed_category, seed_community, seed_membership, seed_membership_upsert, seed_post, seed_thread, | |
| 14 | + | }; | |
| 15 | + | use users::{seed_harness_users, seed_users}; | |
| 16 | + | ||
| 17 | + | use mt_core::types::CommunityRole; | |
| 18 | + | use sqlx::PgPool; | |
| 19 | + | ||
| 20 | + | pub async fn run(pool: &PgPool) { | |
| 21 | + | // Guard: skip if data already exists (threads have no unique constraint) | |
| 22 | + | let thread_count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM threads") | |
| 23 | + | .fetch_one(pool) | |
| 24 | + | .await | |
| 25 | + | .unwrap_or(0); | |
| 26 | + | if thread_count > 0 { | |
| 27 | + | tracing::info!("seed data already exists, skipping"); | |
| 28 | + | return; | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // --- users | |
| 32 | + | ||
| 33 | + | let users = seed_users(pool).await; | |
| 34 | + | let harness = seed_harness_users(pool).await; | |
| 35 | + | ||
| 36 | + | // --- communities | |
| 37 | + | ||
| 38 | + | let rust_id = seed_community( | |
| 39 | + | pool, | |
| 40 | + | "Rust Programming", | |
| 41 | + | "rust", | |
| 42 | + | Some("All things Rust: language, ecosystem, tooling."), | |
| 43 | + | ) | |
| 44 | + | .await; | |
| 45 | + | ||
| 46 | + | let music_id = seed_community( | |
| 47 | + | pool, | |
| 48 | + | "Music Production", | |
| 49 | + | "music", | |
| 50 | + | Some("DAWs, plugins, mixing, mastering, sound design."), | |
| 51 | + | ) | |
| 52 | + | .await; | |
| 53 | + | ||
| 54 | + | let selfhosted_id = seed_community( | |
| 55 | + | pool, | |
| 56 | + | "Self-Hosted", | |
| 57 | + | "selfhosted", | |
| 58 | + | Some("Running your own infrastructure. No cloud required."), | |
| 59 | + | ) | |
| 60 | + | .await; | |
| 61 | + | ||
| 62 | + | // --- categories | |
| 63 | + | ||
| 64 | + | // Rust | |
| 65 | + | let rust_general = seed_category( | |
| 66 | + | pool, | |
| 67 | + | rust_id, | |
| 68 | + | "General", | |
| 69 | + | "general", | |
| 70 | + | 0, | |
| 71 | + | Some("Anything that doesn't fit elsewhere."), | |
| 72 | + | ) | |
| 73 | + | .await; | |
| 74 | + | let rust_help = seed_category( | |
| 75 | + | pool, | |
| 76 | + | rust_id, | |
| 77 | + | "Help & Questions", | |
| 78 | + | "help", | |
| 79 | + | 1, | |
| 80 | + | Some("Ask for help, get answers."), | |
| 81 | + | ) | |
| 82 | + | .await; | |
| 83 | + | let _rust_show = seed_category( | |
| 84 | + | pool, | |
| 85 | + | rust_id, | |
| 86 | + | "Show & Tell", | |
| 87 | + | "show", | |
| 88 | + | 2, | |
| 89 | + | Some("Share what you've built."), | |
| 90 | + | ) | |
| 91 | + | .await; | |
| 92 | + | let _rust_meta = seed_category( | |
| 93 | + | pool, | |
| 94 | + | rust_id, | |
| 95 | + | "Meta", | |
| 96 | + | "meta", | |
| 97 | + | 3, | |
| 98 | + | Some("Discussion about this community itself."), | |
| 99 | + | ) | |
| 100 | + | .await; | |
| 101 | + | ||
| 102 | + | // Music, the main test community | |
| 103 | + | let music_general = seed_category( | |
| 104 | + | pool, | |
| 105 | + | music_id, | |
| 106 | + | "General", | |
| 107 | + | "general", | |
| 108 | + | 0, | |
| 109 | + | Some("General music production discussion."), | |
| 110 | + | ) | |
| 111 | + | .await; | |
| 112 | + | let music_mixing = seed_category( | |
| 113 | + | pool, | |
| 114 | + | music_id, | |
| 115 | + | "Mixing & Mastering", | |
| 116 | + | "mixing", | |
| 117 | + | 1, | |
| 118 | + | Some("Techniques for getting a polished mix."), | |
| 119 | + | ) | |
| 120 | + | .await; | |
| 121 | + | let music_sound = seed_category( | |
| 122 | + | pool, | |
| 123 | + | music_id, | |
| 124 | + | "Sound Design", | |
| 125 | + | "sound-design", | |
| 126 | + | 2, | |
| 127 | + | Some("Synthesis, sampling, and sound creation."), | |
| 128 | + | ) | |
| 129 | + | .await; | |
| 130 | + | ||
| 131 | + | // Self-Hosted | |
| 132 | + | let sh_general = seed_category( | |
| 133 | + | pool, | |
| 134 | + | selfhosted_id, | |
| 135 | + | "General", | |
| 136 | + | "general", | |
| 137 | + | 0, | |
| 138 | + | Some("General self-hosting discussion."), | |
| 139 | + | ) | |
| 140 | + | .await; | |
| 141 | + | let _sh_homelab = seed_category( | |
| 142 | + | pool, | |
| 143 | + | selfhosted_id, | |
| 144 | + | "Homelab", | |
| 145 | + | "homelab", | |
| 146 | + | 1, | |
| 147 | + | Some("Hardware, networking, and home server setups."), | |
| 148 | + | ) | |
| 149 | + | .await; | |
| 150 | + | ||
| 151 | + | // --- memberships | |
| 152 | + | ||
| 153 | + | for user in &users { | |
| 154 | + | seed_membership(pool, user.id, music_id, CommunityRole::Member).await; | |
| 155 | + | } | |
| 156 | + | // admin owns all, maxmj is moderator of music | |
| 157 | + | seed_membership_upsert(pool, users[0].id, rust_id, CommunityRole::Owner).await; | |
| 158 | + | seed_membership_upsert(pool, users[0].id, music_id, CommunityRole::Owner).await; | |
| 159 | + | seed_membership_upsert(pool, users[0].id, selfhosted_id, CommunityRole::Owner).await; | |
| 160 | + | seed_membership_upsert(pool, users[1].id, rust_id, CommunityRole::Member).await; | |
| 161 | + | seed_membership_upsert(pool, users[1].id, music_id, CommunityRole::Moderator).await; | |
| 162 | + | ||
| 163 | + | // The harness accounts get the roles the browser axis needs to write: | |
| 164 | + | // ordinary membership for thread/reply/flag, and Owner + Moderator on two | |
| 165 | + | // different communities so a moderation action has somewhere to land. The | |
| 166 | + | // roles are seeded rather than granted at login because login only ever | |
| 167 | + | // upserts identity and MNW perks, never membership. | |
| 168 | + | for account in &harness { | |
| 169 | + | seed_membership_upsert(pool, account.id, rust_id, account.rust_role).await; | |
| 170 | + | seed_membership_upsert(pool, account.id, music_id, account.music_role).await; | |
| 171 | + | seed_membership_upsert(pool, account.id, selfhosted_id, CommunityRole::Member).await; | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | // --- Rust community: a few threads | |
| 175 | + | ||
| 176 | + | let welcome_id = seed_thread( | |
| 177 | + | pool, | |
| 178 | + | rust_general, | |
| 179 | + | users[0].id, | |
| 180 | + | "Welcome, read before posting", | |
| 181 | + | true, | |
| 182 | + | false, | |
| 183 | + | ) | |
| 184 | + | .await; | |
| 185 | + | seed_post(pool, welcome_id, users[0].id, "Welcome to the Rust Programming community. Please be respectful, stay on topic, and use code blocks for code snippets.").await; | |
| 186 | + | ||
| 187 | + | let async_id = seed_thread( | |
| 188 | + | pool, | |
| 189 | + | rust_general, | |
| 190 | + | users[1].id, | |
| 191 | + | "How do I get started with async Rust?", | |
| 192 | + | false, | |
| 193 | + | false, | |
| 194 | + | ) | |
| 195 | + | .await; | |
| 196 | + | seed_post(pool, async_id, users[1].id, "I've been writing synchronous Rust for a few months and want to start using async/await. What runtime should I pick? Is tokio the only option?\n\nAny recommended tutorials or blog posts would be great.").await; | |
| 197 | + | seed_post(pool, async_id, users[0].id, "Tokio is the most popular and what most web frameworks (Axum, Actix) use. There's also `async-std` and `smol`, but the ecosystem gravitates toward tokio.\n\nStart with the tokio tutorial: it covers spawning tasks, channels, and I/O.").await; | |
| 198 | + | ||
| 199 | + | let error_id = seed_thread( | |
| 200 | + | pool, | |
| 201 | + | rust_help, | |
| 202 | + | users[1].id, | |
| 203 | + | "Best practices for error handling in Axum", | |
| 204 | + | false, | |
| 205 | + | false, | |
| 206 | + | ) | |
| 207 | + | .await; | |
| 208 | + | seed_post(pool, error_id, users[1].id, "What's the recommended way to handle errors in Axum handlers? Should I use `anyhow`, `thiserror`, or something else? I keep writing `.map_err(|e| ...)` everywhere.").await; | |
| 209 | + | ||
| 210 | + | // --- Self-Hosted: a few threads | |
| 211 | + | ||
| 212 | + | let caddy_id = seed_thread( | |
| 213 | + | pool, | |
| 214 | + | sh_general, | |
| 215 | + | users[0].id, | |
| 216 | + | "Caddy vs nginx for reverse proxy", | |
| 217 | + | false, | |
| 218 | + | false, | |
| 219 | + | ) | |
| 220 | + | .await; | |
| 221 | + | seed_post(pool, caddy_id, users[0].id, "I have been using nginx for years but Caddy's automatic HTTPS is tempting. Anyone made the switch? What are the tradeoffs?").await; | |
| 222 | + | seed_post(pool, caddy_id, users[3].id, "Switched last year. Caddy's config is so much simpler. Automatic cert renewal is great. Only downside is slightly higher memory usage but it is negligible for small setups.").await; | |
| 223 | + | ||
| 224 | + | // --- Music community: bulk seed | |
| 225 | + | ||
| 226 | + | seed_music_general(pool, music_general, &users).await; | |
| 227 | + | seed_music_mixing(pool, music_mixing, &users).await; | |
| 228 | + | seed_music_sound_design(pool, music_sound, &users).await; | |
| 229 | + | ||
| 230 | + | // reply_count is computed live at read time, nothing to backfill. | |
| 231 | + | ||
| 232 | + | let total_threads: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM threads") | |
| 233 | + | .fetch_one(pool) | |
| 234 | + | .await | |
| 235 | + | .unwrap_or(0); | |
| 236 | + | let total_posts: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM posts") | |
| 237 | + | .fetch_one(pool) | |
| 238 | + | .await | |
| 239 | + | .unwrap_or(0); | |
| 240 | + | ||
| 241 | + | tracing::info!( | |
| 242 | + | "seeded 3 communities, {} users, {} threads, {} posts", | |
| 243 | + | users.len(), | |
| 244 | + | total_threads, | |
| 245 | + | total_posts | |
| 246 | + | ); | |
| 247 | + | } |
| @@ -1,0 +1,144 @@ | |||
| 1 | + | //! The row writers every content module goes through: one function per table, | |
| 2 | + | //! each returning the id it wrote so the caller can hang children off it. | |
| 3 | + | ||
| 4 | + | use mt_core::types::CommunityRole; | |
| 5 | + | use sqlx::PgPool; | |
| 6 | + | use uuid::Uuid; | |
| 7 | + | ||
| 8 | + | pub(super) async fn seed_community( | |
| 9 | + | pool: &PgPool, | |
| 10 | + | name: &str, | |
| 11 | + | slug: &str, | |
| 12 | + | description: Option<&str>, | |
| 13 | + | ) -> Uuid { | |
| 14 | + | sqlx::query_scalar( | |
| 15 | + | "INSERT INTO communities (name, slug, description) | |
| 16 | + | VALUES ($1, $2, $3) | |
| 17 | + | ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name | |
| 18 | + | RETURNING id", | |
| 19 | + | ) | |
| 20 | + | .bind(name) | |
| 21 | + | .bind(slug) | |
| 22 | + | .bind(description) | |
| 23 | + | .fetch_one(pool) | |
| 24 | + | .await | |
| 25 | + | .expect("failed to seed community") | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | pub(super) async fn seed_category( | |
| 29 | + | pool: &PgPool, | |
| 30 | + | community_id: Uuid, | |
| 31 | + | name: &str, | |
| 32 | + | slug: &str, | |
| 33 | + | sort_order: i32, | |
| 34 | + | description: Option<&str>, | |
| 35 | + | ) -> Uuid { | |
| 36 | + | sqlx::query_scalar( | |
| 37 | + | "INSERT INTO categories (community_id, name, slug, description, sort_order) | |
| 38 | + | VALUES ($1, $2, $3, $4, $5) | |
| 39 | + | ON CONFLICT (community_id, slug) DO UPDATE SET name = EXCLUDED.name | |
| 40 | + | RETURNING id", | |
| 41 | + | ) | |
| 42 | + | .bind(community_id) | |
| 43 | + | .bind(name) | |
| 44 | + | .bind(slug) | |
| 45 | + | .bind(description) | |
| 46 | + | .bind(sort_order) | |
| 47 | + | .fetch_one(pool) | |
| 48 | + | .await | |
| 49 | + | .expect("failed to seed category") | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | pub(super) async fn seed_membership( | |
| 53 | + | pool: &PgPool, | |
| 54 | + | user_id: Uuid, | |
| 55 | + | community_id: Uuid, | |
| 56 | + | role: CommunityRole, | |
| 57 | + | ) { | |
| 58 | + | sqlx::query( | |
| 59 | + | "INSERT INTO memberships (user_id, community_id, role) | |
| 60 | + | VALUES ($1, $2, $3) | |
| 61 | + | ON CONFLICT (user_id, community_id) DO NOTHING", | |
| 62 | + | ) | |
| 63 | + | .bind(user_id) | |
| 64 | + | .bind(community_id) | |
| 65 | + | .bind(role.as_str()) | |
| 66 | + | .execute(pool) | |
| 67 | + | .await | |
| 68 | + | .expect("failed to seed membership"); | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | pub(super) async fn seed_membership_upsert( | |
| 72 | + | pool: &PgPool, | |
| 73 | + | user_id: Uuid, | |
| 74 | + | community_id: Uuid, | |
| 75 | + | role: CommunityRole, | |
| 76 | + | ) { | |
| 77 | + | sqlx::query( | |
| 78 | + | "INSERT INTO memberships (user_id, community_id, role) | |
| 79 | + | VALUES ($1, $2, $3) | |
| 80 | + | ON CONFLICT (user_id, community_id) DO UPDATE SET role = EXCLUDED.role", | |
| 81 | + | ) | |
| 82 | + | .bind(user_id) | |
| 83 | + | .bind(community_id) | |
| 84 | + | .bind(role.as_str()) | |
| 85 | + | .execute(pool) | |
| 86 | + | .await | |
| 87 | + | .expect("failed to seed membership"); | |
| 88 | + | } | |
| 89 | + | ||
| 90 | + | pub(super) async fn seed_thread( | |
| 91 | + | pool: &PgPool, | |
| 92 | + | category_id: Uuid, | |
| 93 | + | author_id: Uuid, | |
| 94 | + | title: &str, | |
| 95 | + | pinned: bool, | |
| 96 | + | locked: bool, | |
| 97 | + | ) -> Uuid { | |
| 98 | + | sqlx::query_scalar( | |
| 99 | + | "INSERT INTO threads (category_id, author_id, title, pinned, locked) | |
| 100 | + | VALUES ($1, $2, $3, $4, $5) | |
| 101 | + | RETURNING id", | |
| 102 | + | ) | |
| 103 | + | .bind(category_id) | |
| 104 | + | .bind(author_id) | |
| 105 | + | .bind(title) | |
| 106 | + | .bind(pinned) | |
| 107 | + | .bind(locked) | |
| 108 | + | .fetch_one(pool) | |
| 109 | + | .await | |
| 110 | + | .expect("failed to seed thread") | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | pub(super) async fn seed_post( | |
| 114 | + | pool: &PgPool, | |
| 115 | + | thread_id: Uuid, | |
| 116 | + | author_id: Uuid, | |
| 117 | + | body_markdown: &str, | |
| 118 | + | ) -> Uuid { | |
| 119 | + | // Render through the same strict markdown path production uses so seeded | |
| 120 | + | // posts render identically to real ones (previously a hand-rolled escape | |
| 121 | + | // diverged from docengine output). | |
| 122 | + | let body_html = crate::routes::helpers::render_markdown(body_markdown); | |
| 123 | + | ||
| 124 | + | let post_id: Uuid = sqlx::query_scalar( | |
| 125 | + | "INSERT INTO posts (thread_id, author_id, body_markdown, body_html) | |
| 126 | + | VALUES ($1, $2, $3, $4) | |
| 127 | + | RETURNING id", | |
| 128 | + | ) | |
| 129 | + | .bind(thread_id) | |
| 130 | + | .bind(author_id) | |
| 131 | + | .bind(body_markdown) | |
| 132 | + | .bind(&body_html) | |
| 133 | + | .fetch_one(pool) | |
| 134 | + | .await | |
| 135 | + | .expect("failed to seed post"); | |
| 136 | + | ||
| 137 | + | sqlx::query("UPDATE threads SET last_activity_at = now() WHERE id = $1") | |
| 138 | + | .bind(thread_id) | |
| 139 | + | .execute(pool) | |
| 140 | + | .await | |
| 141 | + | .expect("failed to update thread activity"); | |
| 142 | + | ||
| 143 | + | post_id | |
| 144 | + | } |
| @@ -1,0 +1,129 @@ | |||
| 1 | + | //! The seeded people: three fixed harness accounts and the ordinary users | |
| 2 | + | //! everything else is written as. | |
| 3 | + | ||
| 4 | + | use mt_core::types::CommunityRole; | |
| 5 | + | use sqlx::PgPool; | |
| 6 | + | use uuid::Uuid; | |
| 7 | + | ||
| 8 | + | pub(super) struct SeedUser { | |
| 9 | + | pub(super) id: Uuid, | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | /// A harness account: the local row plus the roles it carries per community. | |
| 13 | + | pub(super) struct HarnessUser { | |
| 14 | + | pub(super) id: Uuid, | |
| 15 | + | pub(super) rust_role: CommunityRole, | |
| 16 | + | pub(super) music_role: CommunityRole, | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | /// The three accounts the browser harness logs in as. | |
| 20 | + | /// | |
| 21 | + | /// `mnw_account_id` is the join to MNW and these values are fixed on both | |
| 22 | + | /// sides: the MNW example seed creates the same three ids in | |
| 23 | + | /// `server/src/seed/harness.rs`, which is what lets roles be assigned here | |
| 24 | + | /// before anyone has logged in. The two lists have to be edited together, and | |
| 25 | + | /// the MNW side has a test pinning the literals to make that hard to forget. | |
| 26 | + | /// | |
| 27 | + | /// `is_fan_plus`/`is_creator` mirror the perks MNW will report at login. They | |
| 28 | + | /// are denormalised onto the row for post rendering, and login overwrites them | |
| 29 | + | /// from userinfo, so seeding them wrong is cosmetic rather than a privilege | |
| 30 | + | /// question. Seed them right anyway: a pre-login render of a seeded post is one | |
| 31 | + | /// of the things the harness looks at. | |
| 32 | + | pub(super) async fn seed_harness_users(pool: &PgPool) -> Vec<HarnessUser> { | |
| 33 | + | let harness_data = [ | |
| 34 | + | ( | |
| 35 | + | "00000000-0000-0000-0000-00000000f001", | |
| 36 | + | "harness_fan", | |
| 37 | + | "Harness Fan", | |
| 38 | + | true, | |
| 39 | + | false, | |
| 40 | + | CommunityRole::Member, | |
| 41 | + | CommunityRole::Member, | |
| 42 | + | ), | |
| 43 | + | ( | |
| 44 | + | "00000000-0000-0000-0000-00000000f002", | |
| 45 | + | "harness_creator", | |
| 46 | + | "Harness Creator", | |
| 47 | + | false, | |
| 48 | + | true, | |
| 49 | + | CommunityRole::Member, | |
| 50 | + | CommunityRole::Member, | |
| 51 | + | ), | |
| 52 | + | ( | |
| 53 | + | "00000000-0000-0000-0000-00000000f003", | |
| 54 | + | "harness_owner", | |
| 55 | + | "Harness Owner", | |
| 56 | + | false, | |
| 57 | + | false, | |
| 58 | + | CommunityRole::Owner, | |
| 59 | + | CommunityRole::Moderator, | |
| 60 | + | ), | |
| 61 | + | ]; | |
| 62 | + | ||
| 63 | + | let mut users = Vec::new(); | |
| 64 | + | for (uuid_str, username, display_name, is_fan_plus, is_creator, rust_role, music_role) in | |
| 65 | + | harness_data | |
| 66 | + | { | |
| 67 | + | let id = Uuid::parse_str(uuid_str).unwrap(); | |
| 68 | + | sqlx::query( | |
| 69 | + | "INSERT INTO users (mnw_account_id, username, display_name, is_fan_plus, is_creator) | |
| 70 | + | VALUES ($1, $2, $3, $4, $5) | |
| 71 | + | ON CONFLICT (mnw_account_id) DO UPDATE | |
| 72 | + | SET username = EXCLUDED.username, | |
| 73 | + | display_name = EXCLUDED.display_name, | |
| 74 | + | is_fan_plus = EXCLUDED.is_fan_plus, | |
| 75 | + | is_creator = EXCLUDED.is_creator", | |
| 76 | + | ) | |
| 77 | + | .bind(id) | |
| 78 | + | .bind(username) | |
| 79 | + | .bind(display_name) | |
| 80 | + | .bind(is_fan_plus) | |
| 81 | + | .bind(is_creator) | |
| 82 | + | .execute(pool) | |
| 83 | + | .await | |
| 84 | + | .expect("failed to seed harness user"); | |
| 85 | + | users.push(HarnessUser { | |
| 86 | + | id, | |
| 87 | + | rust_role, | |
| 88 | + | music_role, | |
| 89 | + | }); | |
| 90 | + | } | |
| 91 | + | users | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | pub(super) async fn seed_users(pool: &PgPool) -> Vec<SeedUser> { | |
| 95 | + | let user_data = [ | |
| 96 | + | ("00000000-0000-0000-0000-000000000001", "admin", "Admin"), | |
| 97 | + | ("00000000-0000-0000-0000-000000000002", "maxmj", "Max"), | |
| 98 | + | ( | |
| 99 | + | "00000000-0000-0000-0000-000000000003", | |
| 100 | + | "synthwave99", | |
| 101 | + | "Juno", | |
| 102 | + | ), | |
| 103 | + | ("00000000-0000-0000-0000-000000000004", "basshunter", "Erik"), | |
| 104 | + | ("00000000-0000-0000-0000-000000000005", "tape_hiss", "Rae"), | |
| 105 | + | ("00000000-0000-0000-0000-000000000006", "drumroom", "Cole"), | |
| 106 | + | ("00000000-0000-0000-0000-000000000007", "patchwork", "Lina"), | |
| 107 | + | ("00000000-0000-0000-0000-000000000008", "detuned", "Kai"), | |
| 108 | + | ("00000000-0000-0000-0000-000000000009", "resampled", "Noor"), | |
| 109 | + | ("00000000-0000-0000-0000-00000000000a", "clipgain", "Wren"), | |
| 110 | + | ]; | |
| 111 | + | ||
| 112 | + | let mut users = Vec::new(); | |
| 113 | + | for (uuid_str, username, display_name) in &user_data { | |
| 114 | + | let id = Uuid::parse_str(uuid_str).unwrap(); | |
| 115 | + | sqlx::query( | |
| 116 | + | "INSERT INTO users (mnw_account_id, username, display_name) | |
| 117 | + | VALUES ($1, $2, $3) | |
| 118 | + | ON CONFLICT (mnw_account_id) DO NOTHING", | |
| 119 | + | ) | |
| 120 | + | .bind(id) | |
| 121 | + | .bind(username) | |
| 122 | + | .bind(display_name) | |
| 123 | + | .execute(pool) | |
| 124 | + | .await | |
| 125 | + | .expect("failed to seed user"); | |
| 126 | + | users.push(SeedUser { id }); | |
| 127 | + | } | |
| 128 | + | users | |
| 129 | + | } |