| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
use uuid::Uuid; |
| 13 |
|
| 14 |
use super::{EXAMPLE_EMAIL_DOMAIN, SeedError}; |
| 15 |
use crate::auth; |
| 16 |
use crate::db::{self, DbUser, Email, ItemType, Username}; |
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
|
| 22 |
pub enum ItemPricing { |
| 23 |
|
| 24 |
Free, |
| 25 |
|
| 26 |
BuyOnce(i32), |
| 27 |
|
| 28 |
Pwyw { min: i32 }, |
| 29 |
} |
| 30 |
|
| 31 |
|
| 32 |
pub enum ProjectPricing { |
| 33 |
Free, |
| 34 |
BuyOnce(i32), |
| 35 |
Pwyw { |
| 36 |
min: i32, |
| 37 |
}, |
| 38 |
|
| 39 |
Subscription, |
| 40 |
} |
| 41 |
|
| 42 |
|
| 43 |
pub struct TierSpec { |
| 44 |
pub name: &'static str, |
| 45 |
pub description: &'static str, |
| 46 |
pub price_cents: i32, |
| 47 |
} |
| 48 |
|
| 49 |
|
| 50 |
pub struct BlogSpec { |
| 51 |
pub title: &'static str, |
| 52 |
|
| 53 |
pub body: &'static str, |
| 54 |
} |
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
pub struct ItemSpec { |
| 59 |
pub title: &'static str, |
| 60 |
pub description: &'static str, |
| 61 |
pub item_type: ItemType, |
| 62 |
pub pricing: ItemPricing, |
| 63 |
|
| 64 |
|
| 65 |
pub tags: &'static [&'static str], |
| 66 |
|
| 67 |
pub body: Option<&'static str>, |
| 68 |
} |
| 69 |
|
| 70 |
|
| 71 |
pub struct ProjectSpec { |
| 72 |
|
| 73 |
pub slug: &'static str, |
| 74 |
|
| 75 |
pub title: &'static str, |
| 76 |
|
| 77 |
pub description: &'static str, |
| 78 |
|
| 79 |
pub features: &'static [&'static str], |
| 80 |
|
| 81 |
pub pricing: ProjectPricing, |
| 82 |
|
| 83 |
pub tiers: &'static [TierSpec], |
| 84 |
|
| 85 |
pub items: &'static [ItemSpec], |
| 86 |
|
| 87 |
pub blog: &'static [BlogSpec], |
| 88 |
} |
| 89 |
|
| 90 |
|
| 91 |
struct CreatorSpec { |
| 92 |
|
| 93 |
handle: &'static str, |
| 94 |
|
| 95 |
display_name: &'static str, |
| 96 |
|
| 97 |
bio: &'static str, |
| 98 |
|
| 99 |
project: ProjectSpec, |
| 100 |
} |
| 101 |
|
| 102 |
|
| 103 |
pub struct SeededCreator { |
| 104 |
|
| 105 |
pub user: DbUser, |
| 106 |
|
| 107 |
pub project: &'static ProjectSpec, |
| 108 |
} |
| 109 |
|
| 110 |
|
| 111 |
|
| 112 |
const SLOW_READING_BODY: &str = "\ |
| 113 |
There is a kind of reading that resists hurry. It asks you to sit with a sentence \ |
| 114 |
until it gives up its second meaning, and then its third. |
| 115 |
|
| 116 |
We have built our tools for the opposite habit — to skim, to extract, to move on. \ |
| 117 |
This press is a small argument against that. The editions here are set with wide \ |
| 118 |
margins on purpose: room for a pencil, room for a second thought. |
| 119 |
|
| 120 |
Read slowly. Read the same page twice. The commons is patient, and so are its books."; |
| 121 |
|
| 122 |
|
| 123 |
|
| 124 |
|
| 125 |
const ROSTER: &[CreatorSpec] = &[ |
| 126 |
CreatorSpec { |
| 127 |
handle: "openreels", |
| 128 |
display_name: "Open Reels", |
| 129 |
bio: "A small label restoring and reissuing public-domain recordings. \ |
| 130 |
Every release is free to share; pay what it's worth if it moves you.", |
| 131 |
project: ProjectSpec { |
| 132 |
slug: "restored-reels-vol-1", |
| 133 |
blog: &[ |
| 134 |
BlogSpec { |
| 135 |
title: "Vol. 1 is out", |
| 136 |
body: "The first volume of Restored Reels is up. Three recordings, cleaned from public-domain sources and remastered for easy listening.\n\nEverything here is free to stream and share. If a restoration moves you, name your price — it keeps the next volume coming.", |
| 137 |
}, |
| 138 |
BlogSpec { |
| 139 |
title: "How we restore a reel", |
| 140 |
body: "Restoration is mostly patience: de-noise gently, level the dynamics, and leave the character of the original intact.\n\nWe never \"improve\" a performance. The goal is to let a public-domain recording sound like itself, only clearer.", |
| 141 |
}, |
| 142 |
], |
| 143 |
title: "Restored Reels, Vol. 1", |
| 144 |
description: "A first volume of public-domain recordings, cleaned up \ |
| 145 |
and remastered for easy listening. Free to stream and \ |
| 146 |
share; name your price if you'd like to support the \ |
| 147 |
restoration work.", |
| 148 |
features: &["audio"], |
| 149 |
pricing: ProjectPricing::Pwyw { min: 100 }, |
| 150 |
tiers: &[], |
| 151 |
items: &[ |
| 152 |
ItemSpec { |
| 153 |
title: "Restoration No. 1 (Full Mix)", |
| 154 |
description: "The complete restored recording, remastered from \ |
| 155 |
a public-domain source. Name your price.", |
| 156 |
item_type: ItemType::Audio, |
| 157 |
pricing: ItemPricing::Pwyw { min: 100 }, |
| 158 |
tags: &["audio", "audio.format.music"], |
| 159 |
body: None, |
| 160 |
}, |
| 161 |
ItemSpec { |
| 162 |
title: "Stem Pack: Strings", |
| 163 |
description: "Isolated string stems from the restoration, for \ |
| 164 |
sampling and study. WAV, ready for your DAW.", |
| 165 |
item_type: ItemType::Sample, |
| 166 |
pricing: ItemPricing::Pwyw { min: 200 }, |
| 167 |
tags: &["audio.format.samples", "audio.technique.sampling"], |
| 168 |
body: None, |
| 169 |
}, |
| 170 |
ItemSpec { |
| 171 |
title: "Session Take (Video)", |
| 172 |
description: "A short archival video of the session, restored \ |
| 173 |
and captioned.", |
| 174 |
item_type: ItemType::Video, |
| 175 |
pricing: ItemPricing::Pwyw { min: 100 }, |
| 176 |
tags: &["video", "video.genre.music-video"], |
| 177 |
body: None, |
| 178 |
}, |
| 179 |
], |
| 180 |
}, |
| 181 |
}, |
| 182 |
CreatorSpec { |
| 183 |
handle: "deskriver", |
| 184 |
display_name: "Deskriver Tools", |
| 185 |
bio: "Source-available desktop tools that stay out of your way. No \ |
| 186 |
accounts required to run them, no engagement tricks, no cloud \ |
| 187 |
lock-in.", |
| 188 |
project: ProjectSpec { |
| 189 |
slug: "deskriver-suite", |
| 190 |
blog: &[ |
| 191 |
BlogSpec { |
| 192 |
title: "One download, buy it once", |
| 193 |
body: "The Deskriver Suite is a single download. Pay once and it is yours — no account to run it, no subscription, no cloud lock-in.\n\nUpdates are free for the life of the release. When there is something new, you will see it in the app.", |
| 194 |
}, |
| 195 |
BlogSpec { |
| 196 |
title: "Why the tools stay offline", |
| 197 |
body: "Every tool in the suite runs fully offline and stores your data locally. There is nothing to sign in to and nothing phoning home.\n\nThat is a deliberate choice: the tools should keep working whether or not we do.", |
| 198 |
}, |
| 199 |
], |
| 200 |
title: "Deskriver Suite", |
| 201 |
description: "A bundle of small, source-available desktop utilities. \ |
| 202 |
Runs offline with no account required, stores \ |
| 203 |
everything locally, and stays out of your way. One \ |
| 204 |
download, buy it once.", |
| 205 |
features: &["downloads", "license_keys"], |
| 206 |
pricing: ProjectPricing::BuyOnce(900), |
| 207 |
tiers: &[], |
| 208 |
items: &[ |
| 209 |
ItemSpec { |
| 210 |
title: "Deskriver Focus (Plugin)", |
| 211 |
description: "A focus-timer plugin for the suite. Signed \ |
| 212 |
builds, offline, no telemetry.", |
| 213 |
item_type: ItemType::Plugin, |
| 214 |
pricing: ItemPricing::BuyOnce(1200), |
| 215 |
tags: &["software.format.plugin", "software.format.vst3"], |
| 216 |
body: None, |
| 217 |
}, |
| 218 |
ItemSpec { |
| 219 |
title: "Minimal Preset Pack", |
| 220 |
description: "A set of restrained presets for the suite — \ |
| 221 |
sensible defaults, nothing flashy.", |
| 222 |
item_type: ItemType::Preset, |
| 223 |
pricing: ItemPricing::BuyOnce(500), |
| 224 |
tags: &["software", "software.platform.macos"], |
| 225 |
body: None, |
| 226 |
}, |
| 227 |
ItemSpec { |
| 228 |
title: "Weekly-Review Template", |
| 229 |
description: "A ready-to-use weekly-review layout. Import it \ |
| 230 |
and adapt it to your week.", |
| 231 |
item_type: ItemType::Template, |
| 232 |
pricing: ItemPricing::BuyOnce(300), |
| 233 |
tags: &["writing.topic.productivity", "software"], |
| 234 |
body: None, |
| 235 |
}, |
| 236 |
ItemSpec { |
| 237 |
title: "Deskriver Utility (Download)", |
| 238 |
description: "The core command-line utility. Signed and \ |
| 239 |
notarized for macOS; runs fully offline.", |
| 240 |
item_type: ItemType::Digital, |
| 241 |
pricing: ItemPricing::BuyOnce(900), |
| 242 |
tags: &[ |
| 243 |
"software.format.cli", |
| 244 |
"software.platform.macos", |
| 245 |
"software.format.desktop", |
| 246 |
], |
| 247 |
body: None, |
| 248 |
}, |
| 249 |
], |
| 250 |
}, |
| 251 |
}, |
| 252 |
CreatorSpec { |
| 253 |
handle: "stillfield", |
| 254 |
display_name: "Stillfield", |
| 255 |
bio: "Quiet landscape and still-life photography, released to the public \ |
| 256 |
domain. Download, print, remix — no permission needed.", |
| 257 |
project: ProjectSpec { |
| 258 |
slug: "cc0-field-library", |
| 259 |
blog: &[ |
| 260 |
BlogSpec { |
| 261 |
title: "Opening the field library", |
| 262 |
body: "The CC0 Field Library is open. Landscape and still-life studies, released to the public domain, added to over time.\n\nDownload the full-resolution files and use them however you like. No attribution required, though it is always welcome.", |
| 263 |
}, |
| 264 |
BlogSpec { |
| 265 |
title: "Why these are CC0", |
| 266 |
body: "Public-domain photography should be genuinely usable — in a book, a zine, a website, a wall.\n\nSo everything here is CC0: no rights reserved, no permission to ask for, no strings.", |
| 267 |
}, |
| 268 |
], |
| 269 |
title: "CC0 Field Library", |
| 270 |
description: "A growing library of landscape and still-life \ |
| 271 |
photography released to the public domain. Download the \ |
| 272 |
full-resolution files and use them however you like, no \ |
| 273 |
attribution required.", |
| 274 |
features: &["downloads"], |
| 275 |
pricing: ProjectPricing::Free, |
| 276 |
tiers: &[], |
| 277 |
items: &[ItemSpec { |
| 278 |
title: "Field Study 01 (Print)", |
| 279 |
description: "A high-resolution landscape study, released CC0. \ |
| 280 |
Free to download, print, and remix.", |
| 281 |
item_type: ItemType::Image, |
| 282 |
pricing: ItemPricing::Free, |
| 283 |
tags: &["visual.medium.photography", "visual"], |
| 284 |
body: None, |
| 285 |
}], |
| 286 |
}, |
| 287 |
}, |
| 288 |
CreatorSpec { |
| 289 |
handle: "marginalia", |
| 290 |
display_name: "Marginalia Press", |
| 291 |
bio: "A one-person press typesetting public-domain literature into clean, \ |
| 292 |
readable editions. New volumes for subscribers each month.", |
| 293 |
project: ProjectSpec { |
| 294 |
slug: "the-marginalia-reader", |
| 295 |
blog: &[ |
| 296 |
BlogSpec { |
| 297 |
title: "The first monthly volume", |
| 298 |
body: "The first monthly volume of The Marginalia Reader is ready for subscribers. Public-domain literature, re-typeset into clean editions, delivered as EPUB and PDF.\n\nA new volume goes out each month. Read slowly.", |
| 299 |
}, |
| 300 |
BlogSpec { |
| 301 |
title: "On typesetting the commons", |
| 302 |
body: "A good edition disappears: you notice the text, not the type. That takes careful measure — line length, leading, generous margins.\n\nEvery volume in the reader is set by hand from a public-domain source, then proofed twice before it ships.", |
| 303 |
}, |
| 304 |
], |
| 305 |
title: "The Marginalia Reader", |
| 306 |
description: "Public-domain literature, re-typeset into clean and \ |
| 307 |
readable editions. Subscribers get a new volume every \ |
| 308 |
month, delivered as EPUB and PDF.", |
| 309 |
features: &["text", "blog", "subscriptions"], |
| 310 |
pricing: ProjectPricing::Subscription, |
| 311 |
tiers: &[ |
| 312 |
TierSpec { |
| 313 |
name: "Reader", |
| 314 |
description: "The monthly edition, EPUB and PDF.", |
| 315 |
price_cents: 300, |
| 316 |
}, |
| 317 |
TierSpec { |
| 318 |
name: "Patron", |
| 319 |
description: "The monthly edition plus the working notes and \ |
| 320 |
early proofs.", |
| 321 |
price_cents: 600, |
| 322 |
}, |
| 323 |
TierSpec { |
| 324 |
name: "Benefactor", |
| 325 |
description: "Everything, plus a printed copy of the annual \ |
| 326 |
collection.", |
| 327 |
price_cents: 1200, |
| 328 |
}, |
| 329 |
], |
| 330 |
items: &[ |
| 331 |
ItemSpec { |
| 332 |
title: "On Slow Reading", |
| 333 |
description: "The opening essay of the reader — an argument \ |
| 334 |
for reading twice.", |
| 335 |
item_type: ItemType::Text, |
| 336 |
pricing: ItemPricing::Free, |
| 337 |
tags: &["writing.format.essay", "writing.topic.creativity"], |
| 338 |
body: Some(SLOW_READING_BODY), |
| 339 |
}, |
| 340 |
ItemSpec { |
| 341 |
title: "Typesetting the Commons", |
| 342 |
description: "A short course on preparing public-domain texts \ |
| 343 |
as clean digital editions.", |
| 344 |
item_type: ItemType::Course, |
| 345 |
pricing: ItemPricing::Free, |
| 346 |
tags: &["education.format.course", "education.topic.writing"], |
| 347 |
body: None, |
| 348 |
}, |
| 349 |
], |
| 350 |
}, |
| 351 |
}, |
| 352 |
CreatorSpec { |
| 353 |
handle: "commonshare", |
| 354 |
display_name: "Commonshare", |
| 355 |
bio: "A benefit account: everything here is free, and any support routed \ |
| 356 |
through it passes straight to community projects. Value to the \ |
| 357 |
commons, not to us.", |
| 358 |
project: ProjectSpec { |
| 359 |
slug: "commons-sampler", |
| 360 |
blog: &[ |
| 361 |
BlogSpec { |
| 362 |
title: "What the account funds", |
| 363 |
body: "Commonshare is a benefit account. Everything here is free, and any support routed through it passes straight to community projects.\n\nThe value goes to the commons, not to us. That is the whole point of the account.", |
| 364 |
}, |
| 365 |
BlogSpec { |
| 366 |
title: "All of this is free", |
| 367 |
body: "The Commons Sampler collects freely licensed work — music and art — in one place to download.\n\nNothing here costs anything. If you want to give back, the benefit account passes it along.", |
| 368 |
}, |
| 369 |
], |
| 370 |
title: "Commons Sampler", |
| 371 |
description: "A free sampler of work shared through the Commonshare \ |
| 372 |
benefit account. Everything here is free to download; \ |
| 373 |
anything routed through the account passes straight to \ |
| 374 |
community projects.", |
| 375 |
features: &["downloads"], |
| 376 |
pricing: ProjectPricing::Free, |
| 377 |
tiers: &[], |
| 378 |
items: &[ItemSpec { |
| 379 |
title: "Community Bundle Vol. 1", |
| 380 |
description: "A free bundle collecting highlights shared through \ |
| 381 |
the account — music and art, all freely licensed.", |
| 382 |
item_type: ItemType::Bundle, |
| 383 |
pricing: ItemPricing::Free, |
| 384 |
tags: &["audio", "visual"], |
| 385 |
body: None, |
| 386 |
}], |
| 387 |
}, |
| 388 |
}, |
| 389 |
]; |
| 390 |
|
| 391 |
|
| 392 |
|
| 393 |
|
| 394 |
|
| 395 |
|
| 396 |
|
| 397 |
|
| 398 |
pub async fn seed_creators(pool: &sqlx::PgPool) -> Result<Vec<SeededCreator>, SeedError> { |
| 399 |
let mut seeded = Vec::with_capacity(ROSTER.len()); |
| 400 |
for spec in ROSTER { |
| 401 |
|
| 402 |
|
| 403 |
let password_hash = |
| 404 |
auth::hash_password_async(format!("example-seed_{}", Uuid::new_v4())).await?; |
| 405 |
let username = Username::from_trusted(spec.handle.to_string()); |
| 406 |
let email = Email::from_trusted(format!("{}@{EXAMPLE_EMAIL_DOMAIN}", spec.handle)); |
| 407 |
|
| 408 |
let user = |
| 409 |
db::users::create_example_creator(pool, &username, &email, &password_hash).await?; |
| 410 |
let user = |
| 411 |
db::users::update_user_profile(pool, user.id, Some(spec.display_name), Some(spec.bio)) |
| 412 |
.await?; |
| 413 |
|
| 414 |
tracing::info!( |
| 415 |
handle = spec.handle, |
| 416 |
user_id = %user.id, |
| 417 |
"example seed: created creator" |
| 418 |
); |
| 419 |
seeded.push(SeededCreator { |
| 420 |
user, |
| 421 |
project: &spec.project, |
| 422 |
}); |
| 423 |
} |
| 424 |
Ok(seeded) |
| 425 |
} |
| 426 |
|