| 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 |
|
| 72 |
pub media: Option<&'static str>, |
| 73 |
|
| 74 |
pub cover: Option<&'static str>, |
| 75 |
} |
| 76 |
|
| 77 |
|
| 78 |
pub struct ProjectSpec { |
| 79 |
|
| 80 |
pub slug: &'static str, |
| 81 |
|
| 82 |
pub title: &'static str, |
| 83 |
|
| 84 |
pub description: &'static str, |
| 85 |
|
| 86 |
pub features: &'static [&'static str], |
| 87 |
|
| 88 |
|
| 89 |
|
| 90 |
|
| 91 |
|
| 92 |
|
| 93 |
|
| 94 |
|
| 95 |
|
| 96 |
|
| 97 |
|
| 98 |
|
| 99 |
|
| 100 |
|
| 101 |
|
| 102 |
pub pricing: ProjectPricing, |
| 103 |
|
| 104 |
pub tiers: &'static [TierSpec], |
| 105 |
|
| 106 |
pub items: &'static [ItemSpec], |
| 107 |
|
| 108 |
pub blog: &'static [BlogSpec], |
| 109 |
|
| 110 |
|
| 111 |
pub cover: Option<&'static str>, |
| 112 |
} |
| 113 |
|
| 114 |
|
| 115 |
struct CreatorSpec { |
| 116 |
|
| 117 |
handle: &'static str, |
| 118 |
|
| 119 |
display_name: &'static str, |
| 120 |
|
| 121 |
bio: &'static str, |
| 122 |
|
| 123 |
project: ProjectSpec, |
| 124 |
} |
| 125 |
|
| 126 |
|
| 127 |
pub struct SeededCreator { |
| 128 |
|
| 129 |
pub user: DbUser, |
| 130 |
|
| 131 |
pub project: &'static ProjectSpec, |
| 132 |
} |
| 133 |
|
| 134 |
|
| 135 |
|
| 136 |
const SLOW_READING_BODY: &str = "\ |
| 137 |
There is a kind of reading that resists hurry. It asks you to sit with a sentence \ |
| 138 |
until it gives up its second meaning, and then its third. |
| 139 |
|
| 140 |
We have built our tools for the opposite habit, to skim, to extract, to move on. \ |
| 141 |
This press is a small argument against that. The editions here are set with wide \ |
| 142 |
margins on purpose: room for a pencil, room for a second thought. |
| 143 |
|
| 144 |
Read slowly. Read the same page twice. The commons is patient, and so are its books."; |
| 145 |
|
| 146 |
|
| 147 |
const ON_EDITIONS_BODY: &str = "\ |
| 148 |
An edition is an argument about how a text should be met. Where the line breaks, \ |
| 149 |
how wide the margin runs, whether a note sits at the foot of the page or is exiled \ |
| 150 |
to the back: each is a claim about what the reader is for. |
| 151 |
|
| 152 |
The claims we make here are modest. Long measure tires the eye, so the measure is \ |
| 153 |
short. Notes belong where the sentence that needs them is, so they stay on the page. \ |
| 154 |
Nothing is set in a face that wants to be admired. |
| 155 |
|
| 156 |
A good edition disappears. You should finish it remembering the book."; |
| 157 |
|
| 158 |
|
| 159 |
const READER_ONE_BODY: &str = "\ |
| 160 |
The first number collects three short pieces from the commons, chosen for how they \ |
| 161 |
sit next to each other rather than for any theme. |
| 162 |
|
| 163 |
The oldest is a traveller's account, written by someone with no gift for landscape \ |
| 164 |
and an unusual ear for how people speak. The second is a letter that was never sent. \ |
| 165 |
The third is a fragment, and it stops where the manuscript does. |
| 166 |
|
| 167 |
Fragments are worth setting properly. A text that breaks off mid-thought is not a \ |
| 168 |
failure of the text."; |
| 169 |
|
| 170 |
|
| 171 |
const READER_TWO_BODY: &str = "\ |
| 172 |
Two essays this month, both on work, both by people who did not think of themselves \ |
| 173 |
as writers. |
| 174 |
|
| 175 |
The first was set down by a printer late in a long career, and reads like someone \ |
| 176 |
explaining a trade to a nephew who has not asked. The second is angrier and shorter, \ |
| 177 |
and was published anonymously, which was the ordinary caution of the time. |
| 178 |
|
| 179 |
Neither has been edited for taste. The spelling is regularised and nothing else."; |
| 180 |
|
| 181 |
|
| 182 |
|
| 183 |
|
| 184 |
const ROSTER: &[CreatorSpec] = &[ |
| 185 |
CreatorSpec { |
| 186 |
handle: "openreels", |
| 187 |
display_name: "Open Reels", |
| 188 |
bio: "A small label restoring and reissuing public-domain recordings. \ |
| 189 |
Every release is free to share; pay what it's worth if it moves you.", |
| 190 |
project: ProjectSpec { |
| 191 |
slug: "restored-reels-vol-1", |
| 192 |
cover: Some("openreels-project-cover"), |
| 193 |
blog: &[ |
| 194 |
BlogSpec { |
| 195 |
title: "Vol. 1 is out", |
| 196 |
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.", |
| 197 |
}, |
| 198 |
BlogSpec { |
| 199 |
title: "How we restore a reel", |
| 200 |
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.", |
| 201 |
}, |
| 202 |
], |
| 203 |
title: "Restored Reels, Vol. 1", |
| 204 |
description: "A first volume of public-domain recordings, cleaned up \ |
| 205 |
and remastered for easy listening. Free to stream and \ |
| 206 |
share; name your price if you'd like to support the \ |
| 207 |
restoration work.", |
| 208 |
features: &["audio"], |
| 209 |
|
| 210 |
|
| 211 |
|
| 212 |
pricing: ProjectPricing::Free, |
| 213 |
tiers: &[], |
| 214 |
items: &[ |
| 215 |
ItemSpec { |
| 216 |
title: "Restoration No. 1 (Full Mix)", |
| 217 |
description: "The complete restored recording, remastered from \ |
| 218 |
a public-domain source. Name your price.", |
| 219 |
item_type: ItemType::Audio, |
| 220 |
pricing: ItemPricing::Pwyw { min: 100 }, |
| 221 |
tags: &["audio", "audio.format.music"], |
| 222 |
body: None, |
| 223 |
media: Some("restoration-1-audio"), |
| 224 |
cover: Some("restoration-1-cover"), |
| 225 |
}, |
| 226 |
ItemSpec { |
| 227 |
title: "Stem Pack: Strings", |
| 228 |
description: "Isolated string stems from the restoration, for \ |
| 229 |
sampling and study. WAV, ready for your DAW.", |
| 230 |
item_type: ItemType::Sample, |
| 231 |
pricing: ItemPricing::Pwyw { min: 200 }, |
| 232 |
tags: &["audio.format.samples", "audio.technique.sampling"], |
| 233 |
body: None, |
| 234 |
media: Some("stem-pack-strings-audio"), |
| 235 |
cover: Some("stem-pack-strings-cover"), |
| 236 |
}, |
| 237 |
ItemSpec { |
| 238 |
title: "Session Take (Video)", |
| 239 |
description: "A short archival video of the session, restored \ |
| 240 |
and captioned.", |
| 241 |
item_type: ItemType::Video, |
| 242 |
pricing: ItemPricing::Pwyw { min: 100 }, |
| 243 |
tags: &["video", "video.genre.music-video"], |
| 244 |
body: None, |
| 245 |
media: Some("session-take-video"), |
| 246 |
cover: Some("session-take-cover"), |
| 247 |
}, |
| 248 |
ItemSpec { |
| 249 |
title: "Restoration No. 2 (Full Mix)", |
| 250 |
description: "The second restoration in the volume, from a \ |
| 251 |
later reel. Name your price.", |
| 252 |
item_type: ItemType::Audio, |
| 253 |
pricing: ItemPricing::Pwyw { min: 100 }, |
| 254 |
tags: &["audio", "audio.format.music"], |
| 255 |
body: None, |
| 256 |
media: Some("restoration-2-audio"), |
| 257 |
cover: Some("restoration-2-cover"), |
| 258 |
}, |
| 259 |
ItemSpec { |
| 260 |
title: "Stem Pack: Brass", |
| 261 |
description: "Isolated brass stems from the second restoration. \ |
| 262 |
WAV, ready for your DAW.", |
| 263 |
item_type: ItemType::Sample, |
| 264 |
pricing: ItemPricing::Pwyw { min: 200 }, |
| 265 |
tags: &["audio.format.samples", "audio.technique.sampling"], |
| 266 |
body: None, |
| 267 |
media: None, |
| 268 |
cover: Some("stem-pack-brass-cover"), |
| 269 |
}, |
| 270 |
], |
| 271 |
}, |
| 272 |
}, |
| 273 |
CreatorSpec { |
| 274 |
handle: "deskriver", |
| 275 |
display_name: "Deskriver Tools", |
| 276 |
bio: "Source-available desktop tools that stay out of your way. No \ |
| 277 |
accounts required to run them, no engagement tricks, no cloud \ |
| 278 |
lock-in.", |
| 279 |
project: ProjectSpec { |
| 280 |
slug: "deskriver-suite", |
| 281 |
cover: Some("deskriver-project-cover"), |
| 282 |
blog: &[ |
| 283 |
BlogSpec { |
| 284 |
title: "One download, buy it once", |
| 285 |
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.", |
| 286 |
}, |
| 287 |
BlogSpec { |
| 288 |
title: "Why the tools stay offline", |
| 289 |
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.", |
| 290 |
}, |
| 291 |
], |
| 292 |
title: "Deskriver Suite", |
| 293 |
description: "A bundle of small, source-available desktop utilities. \ |
| 294 |
Runs offline with no account required, stores \ |
| 295 |
everything locally, and stays out of your way. One \ |
| 296 |
download, buy it once.", |
| 297 |
features: &["downloads", "license_keys"], |
| 298 |
|
| 299 |
|
| 300 |
|
| 301 |
pricing: ProjectPricing::Free, |
| 302 |
tiers: &[], |
| 303 |
items: &[ |
| 304 |
ItemSpec { |
| 305 |
title: "Deskriver Focus (Plugin)", |
| 306 |
description: "A focus-timer plugin for the suite. Signed \ |
| 307 |
builds, offline, no telemetry.", |
| 308 |
item_type: ItemType::Plugin, |
| 309 |
pricing: ItemPricing::BuyOnce(1200), |
| 310 |
tags: &["software.format.plugin", "software.format.vst3"], |
| 311 |
body: None, |
| 312 |
media: None, |
| 313 |
cover: Some("deskriver-focus-cover"), |
| 314 |
}, |
| 315 |
ItemSpec { |
| 316 |
title: "Minimal Preset Pack", |
| 317 |
description: "A set of restrained presets for the suite, \ |
| 318 |
sensible defaults, nothing flashy.", |
| 319 |
item_type: ItemType::Preset, |
| 320 |
pricing: ItemPricing::BuyOnce(500), |
| 321 |
tags: &["software", "software.platform.macos"], |
| 322 |
body: None, |
| 323 |
media: None, |
| 324 |
cover: Some("deskriver-presets-cover"), |
| 325 |
}, |
| 326 |
ItemSpec { |
| 327 |
title: "Weekly-Review Template", |
| 328 |
description: "A ready-to-use weekly-review layout. Import it \ |
| 329 |
and adapt it to your week.", |
| 330 |
item_type: ItemType::Template, |
| 331 |
pricing: ItemPricing::BuyOnce(300), |
| 332 |
tags: &["writing.topic.productivity", "software"], |
| 333 |
body: None, |
| 334 |
media: None, |
| 335 |
cover: Some("deskriver-template-cover"), |
| 336 |
}, |
| 337 |
ItemSpec { |
| 338 |
title: "Deskriver Utility (Download)", |
| 339 |
description: "The core command-line utility. Signed and \ |
| 340 |
notarized for macOS; runs fully offline.", |
| 341 |
item_type: ItemType::Digital, |
| 342 |
pricing: ItemPricing::BuyOnce(900), |
| 343 |
tags: &[ |
| 344 |
"software.format.cli", |
| 345 |
"software.platform.macos", |
| 346 |
"software.format.desktop", |
| 347 |
], |
| 348 |
body: None, |
| 349 |
media: None, |
| 350 |
cover: Some("deskriver-utility-cover"), |
| 351 |
}, |
| 352 |
ItemSpec { |
| 353 |
title: "Deskriver Notes (Plugin)", |
| 354 |
description: "A plain-text notes panel for the suite. Local \ |
| 355 |
files, no database, no sync.", |
| 356 |
item_type: ItemType::Plugin, |
| 357 |
pricing: ItemPricing::BuyOnce(800), |
| 358 |
tags: &["software.format.plugin", "software.platform.macos"], |
| 359 |
body: None, |
| 360 |
media: None, |
| 361 |
cover: Some("deskriver-notes-cover"), |
| 362 |
}, |
| 363 |
], |
| 364 |
}, |
| 365 |
}, |
| 366 |
CreatorSpec { |
| 367 |
handle: "stillfield", |
| 368 |
display_name: "Stillfield", |
| 369 |
bio: "Quiet landscape and still-life photography, released to the public \ |
| 370 |
domain. Download, print, remix, no permission needed.", |
| 371 |
project: ProjectSpec { |
| 372 |
slug: "cc0-field-library", |
| 373 |
cover: Some("stillfield-project-cover"), |
| 374 |
blog: &[ |
| 375 |
BlogSpec { |
| 376 |
title: "Opening the field library", |
| 377 |
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.", |
| 378 |
}, |
| 379 |
BlogSpec { |
| 380 |
title: "Why these are CC0", |
| 381 |
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.", |
| 382 |
}, |
| 383 |
], |
| 384 |
title: "CC0 Field Library", |
| 385 |
description: "A growing library of landscape and still-life \ |
| 386 |
photography released to the public domain. Download the \ |
| 387 |
full-resolution files and use them however you like, no \ |
| 388 |
attribution required.", |
| 389 |
features: &["downloads"], |
| 390 |
pricing: ProjectPricing::Free, |
| 391 |
tiers: &[], |
| 392 |
items: &[ |
| 393 |
ItemSpec { |
| 394 |
title: "Field Study 01 (Print)", |
| 395 |
description: "A high-resolution landscape study, released CC0. \ |
| 396 |
Free to download, print, and remix.", |
| 397 |
item_type: ItemType::Image, |
| 398 |
pricing: ItemPricing::Free, |
| 399 |
tags: &["visual.medium.photography", "visual"], |
| 400 |
body: None, |
| 401 |
media: None, |
| 402 |
cover: Some("field-study-01-cover"), |
| 403 |
}, |
| 404 |
ItemSpec { |
| 405 |
title: "Field Study 02 (Print)", |
| 406 |
description: "A second landscape study from the same series. \ |
| 407 |
Full resolution, CC0.", |
| 408 |
item_type: ItemType::Image, |
| 409 |
pricing: ItemPricing::Free, |
| 410 |
tags: &["visual.medium.photography", "visual"], |
| 411 |
body: None, |
| 412 |
media: None, |
| 413 |
cover: Some("field-study-02-cover"), |
| 414 |
}, |
| 415 |
ItemSpec { |
| 416 |
title: "Field Study 03 (Print)", |
| 417 |
description: "Late light over open ground. Full resolution, \ |
| 418 |
CC0, no attribution required.", |
| 419 |
item_type: ItemType::Image, |
| 420 |
pricing: ItemPricing::Free, |
| 421 |
tags: &["visual.medium.photography", "visual"], |
| 422 |
body: None, |
| 423 |
media: None, |
| 424 |
cover: Some("field-study-03-cover"), |
| 425 |
}, |
| 426 |
ItemSpec { |
| 427 |
title: "Still Life 01 (Print)", |
| 428 |
description: "A quiet interior arrangement, shot in daylight. \ |
| 429 |
Full resolution, CC0.", |
| 430 |
item_type: ItemType::Image, |
| 431 |
pricing: ItemPricing::Free, |
| 432 |
tags: &["visual.medium.photography", "visual"], |
| 433 |
body: None, |
| 434 |
media: None, |
| 435 |
cover: Some("still-life-01-cover"), |
| 436 |
}, |
| 437 |
ItemSpec { |
| 438 |
title: "Still Life 02 (Print)", |
| 439 |
description: "The same table, a different hour. Full \ |
| 440 |
resolution, CC0.", |
| 441 |
item_type: ItemType::Image, |
| 442 |
pricing: ItemPricing::Free, |
| 443 |
tags: &["visual.medium.photography", "visual"], |
| 444 |
body: None, |
| 445 |
media: None, |
| 446 |
cover: Some("still-life-02-cover"), |
| 447 |
}, |
| 448 |
], |
| 449 |
}, |
| 450 |
}, |
| 451 |
CreatorSpec { |
| 452 |
handle: "marginalia", |
| 453 |
display_name: "Marginalia Press", |
| 454 |
bio: "A one-person press typesetting public-domain literature into clean, \ |
| 455 |
readable editions. New volumes for subscribers each month.", |
| 456 |
project: ProjectSpec { |
| 457 |
slug: "the-marginalia-reader", |
| 458 |
cover: Some("marginalia-project-cover"), |
| 459 |
blog: &[ |
| 460 |
BlogSpec { |
| 461 |
title: "The first monthly volume", |
| 462 |
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.", |
| 463 |
}, |
| 464 |
BlogSpec { |
| 465 |
title: "On typesetting the commons", |
| 466 |
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.", |
| 467 |
}, |
| 468 |
], |
| 469 |
title: "The Marginalia Reader", |
| 470 |
description: "Public-domain literature, re-typeset into clean and \ |
| 471 |
readable editions. Subscribers get a new volume every \ |
| 472 |
month, delivered as EPUB and PDF.", |
| 473 |
features: &["text", "blog", "subscriptions"], |
| 474 |
pricing: ProjectPricing::Subscription, |
| 475 |
tiers: &[ |
| 476 |
TierSpec { |
| 477 |
name: "Reader", |
| 478 |
description: "The monthly edition, EPUB and PDF.", |
| 479 |
price_cents: 300, |
| 480 |
}, |
| 481 |
TierSpec { |
| 482 |
name: "Patron", |
| 483 |
description: "The monthly edition plus the working notes and \ |
| 484 |
early proofs.", |
| 485 |
price_cents: 600, |
| 486 |
}, |
| 487 |
TierSpec { |
| 488 |
name: "Benefactor", |
| 489 |
description: "Everything, plus a printed copy of the annual \ |
| 490 |
collection.", |
| 491 |
price_cents: 1200, |
| 492 |
}, |
| 493 |
], |
| 494 |
items: &[ |
| 495 |
ItemSpec { |
| 496 |
title: "On Slow Reading", |
| 497 |
description: "The opening essay of the reader, an argument \ |
| 498 |
for reading twice.", |
| 499 |
item_type: ItemType::Text, |
| 500 |
pricing: ItemPricing::Free, |
| 501 |
tags: &["writing.format.essay", "writing.topic.creativity"], |
| 502 |
body: Some(SLOW_READING_BODY), |
| 503 |
media: None, |
| 504 |
cover: Some("on-slow-reading-cover"), |
| 505 |
}, |
| 506 |
ItemSpec { |
| 507 |
title: "Typesetting the Commons", |
| 508 |
description: "A short course on preparing public-domain texts \ |
| 509 |
as clean digital editions.", |
| 510 |
item_type: ItemType::Course, |
| 511 |
pricing: ItemPricing::Free, |
| 512 |
tags: &["education.format.course", "education.topic.writing"], |
| 513 |
body: None, |
| 514 |
media: None, |
| 515 |
cover: Some("typesetting-commons-cover"), |
| 516 |
}, |
| 517 |
ItemSpec { |
| 518 |
title: "The Reader, No. 1", |
| 519 |
description: "The first monthly number: three short pieces from \ |
| 520 |
the commons, newly set.", |
| 521 |
item_type: ItemType::Text, |
| 522 |
pricing: ItemPricing::Free, |
| 523 |
tags: &["writing.format.essay", "writing"], |
| 524 |
body: Some(READER_ONE_BODY), |
| 525 |
media: None, |
| 526 |
cover: Some("reader-one-cover"), |
| 527 |
}, |
| 528 |
ItemSpec { |
| 529 |
title: "The Reader, No. 2", |
| 530 |
description: "Two essays on work, by people who did not think of \ |
| 531 |
themselves as writers.", |
| 532 |
item_type: ItemType::Text, |
| 533 |
pricing: ItemPricing::Free, |
| 534 |
tags: &["writing.format.essay", "writing"], |
| 535 |
body: Some(READER_TWO_BODY), |
| 536 |
media: None, |
| 537 |
cover: Some("reader-two-cover"), |
| 538 |
}, |
| 539 |
ItemSpec { |
| 540 |
title: "Marginal Notes: On Editions", |
| 541 |
description: "What a margin, a measure, and a footnote each \ |
| 542 |
claim about the reader.", |
| 543 |
item_type: ItemType::Text, |
| 544 |
pricing: ItemPricing::Free, |
| 545 |
tags: &["writing.format.essay", "writing.topic.creativity"], |
| 546 |
body: Some(ON_EDITIONS_BODY), |
| 547 |
media: None, |
| 548 |
cover: Some("on-editions-cover"), |
| 549 |
}, |
| 550 |
], |
| 551 |
}, |
| 552 |
}, |
| 553 |
CreatorSpec { |
| 554 |
handle: "commonshare", |
| 555 |
display_name: "Commonshare", |
| 556 |
bio: "A benefit account: everything here is free, and any support routed \ |
| 557 |
through it passes straight to community projects. Value to the \ |
| 558 |
commons, not to us.", |
| 559 |
project: ProjectSpec { |
| 560 |
slug: "commons-sampler", |
| 561 |
cover: Some("commonshare-project-cover"), |
| 562 |
blog: &[ |
| 563 |
BlogSpec { |
| 564 |
title: "What the account funds", |
| 565 |
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.", |
| 566 |
}, |
| 567 |
BlogSpec { |
| 568 |
title: "All of this is free", |
| 569 |
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.", |
| 570 |
}, |
| 571 |
], |
| 572 |
title: "Commons Sampler", |
| 573 |
description: "A free sampler of work shared through the Commonshare \ |
| 574 |
benefit account. Everything here is free to download; \ |
| 575 |
anything routed through the account passes straight to \ |
| 576 |
community projects.", |
| 577 |
features: &["downloads"], |
| 578 |
pricing: ProjectPricing::Free, |
| 579 |
tiers: &[], |
| 580 |
items: &[ |
| 581 |
ItemSpec { |
| 582 |
title: "Community Bundle Vol. 1", |
| 583 |
description: "A free bundle collecting highlights shared through \ |
| 584 |
the account, music and art, all freely licensed.", |
| 585 |
item_type: ItemType::Bundle, |
| 586 |
pricing: ItemPricing::Free, |
| 587 |
tags: &["audio", "visual"], |
| 588 |
body: None, |
| 589 |
media: None, |
| 590 |
cover: Some("community-bundle-cover"), |
| 591 |
}, |
| 592 |
ItemSpec { |
| 593 |
title: "Community Bundle Vol. 2", |
| 594 |
description: "The second collection. Same terms: free to \ |
| 595 |
download, free to pass on.", |
| 596 |
item_type: ItemType::Bundle, |
| 597 |
pricing: ItemPricing::Free, |
| 598 |
tags: &["audio", "visual"], |
| 599 |
body: None, |
| 600 |
media: None, |
| 601 |
cover: Some("community-bundle-2-cover"), |
| 602 |
}, |
| 603 |
ItemSpec { |
| 604 |
title: "Sampler: Field Recordings", |
| 605 |
description: "Room tone, weather, and open ground. Freely \ |
| 606 |
licensed, for anything you like.", |
| 607 |
item_type: ItemType::Sample, |
| 608 |
pricing: ItemPricing::Free, |
| 609 |
tags: &["audio.format.samples", "audio"], |
| 610 |
body: None, |
| 611 |
media: None, |
| 612 |
cover: Some("field-recordings-cover"), |
| 613 |
}, |
| 614 |
ItemSpec { |
| 615 |
title: "Sampler: Public-Domain Loops", |
| 616 |
description: "Short loops cut from public-domain recordings, \ |
| 617 |
tempo-labelled and ready to drop in.", |
| 618 |
item_type: ItemType::Sample, |
| 619 |
pricing: ItemPricing::Free, |
| 620 |
tags: &["audio.format.samples", "audio.technique.sampling"], |
| 621 |
body: None, |
| 622 |
media: None, |
| 623 |
cover: Some("pd-loops-cover"), |
| 624 |
}, |
| 625 |
ItemSpec { |
| 626 |
title: "Commons Reader (Download)", |
| 627 |
description: "A collected PDF of the writing shared through the \ |
| 628 |
account this year.", |
| 629 |
item_type: ItemType::Digital, |
| 630 |
pricing: ItemPricing::Free, |
| 631 |
tags: &["writing", "writing.format.essay"], |
| 632 |
body: None, |
| 633 |
media: None, |
| 634 |
cover: Some("commons-reader-cover"), |
| 635 |
}, |
| 636 |
], |
| 637 |
}, |
| 638 |
}, |
| 639 |
]; |
| 640 |
|
| 641 |
|
| 642 |
|
| 643 |
|
| 644 |
|
| 645 |
|
| 646 |
|
| 647 |
|
| 648 |
pub async fn seed_creators(pool: &sqlx::PgPool) -> Result<Vec<SeededCreator>, SeedError> { |
| 649 |
let mut seeded = Vec::with_capacity(ROSTER.len()); |
| 650 |
for spec in ROSTER { |
| 651 |
|
| 652 |
|
| 653 |
let password_hash = |
| 654 |
auth::hash_password_async(format!("example-seed_{}", Uuid::new_v4())).await?; |
| 655 |
let username = Username::from_trusted(spec.handle.to_string()); |
| 656 |
let email = Email::from_trusted(format!("{}@{EXAMPLE_EMAIL_DOMAIN}", spec.handle)); |
| 657 |
|
| 658 |
let user = |
| 659 |
db::users::create_example_creator(pool, &username, &email, &password_hash).await?; |
| 660 |
let user = |
| 661 |
db::users::update_user_profile(pool, user.id, Some(spec.display_name), Some(spec.bio)) |
| 662 |
.await?; |
| 663 |
|
| 664 |
tracing::info!( |
| 665 |
handle = spec.handle, |
| 666 |
user_id = %user.id, |
| 667 |
"example seed: created creator" |
| 668 |
); |
| 669 |
seeded.push(SeededCreator { |
| 670 |
user, |
| 671 |
project: &spec.project, |
| 672 |
}); |
| 673 |
} |
| 674 |
Ok(seeded) |
| 675 |
} |
| 676 |
|
| 677 |
#[cfg(test)] |
| 678 |
mod tests { |
| 679 |
use super::*; |
| 680 |
use std::collections::HashSet; |
| 681 |
|
| 682 |
|
| 683 |
fn referenced_ids() -> Vec<&'static str> { |
| 684 |
let mut ids = Vec::new(); |
| 685 |
for creator in ROSTER { |
| 686 |
ids.extend(creator.project.cover); |
| 687 |
for item in creator.project.items { |
| 688 |
ids.extend(item.media); |
| 689 |
ids.extend(item.cover); |
| 690 |
} |
| 691 |
} |
| 692 |
ids |
| 693 |
} |
| 694 |
|
| 695 |
|
| 696 |
|
| 697 |
|
| 698 |
#[test] |
| 699 |
fn roster_and_manifest_agree() { |
| 700 |
let manifest = |
| 701 |
super::super::manifest::Manifest::load().expect("the manifest must be loadable"); |
| 702 |
let declared: HashSet<&str> = manifest.ids().collect(); |
| 703 |
let referenced: HashSet<&str> = referenced_ids().into_iter().collect(); |
| 704 |
|
| 705 |
let undeclared: Vec<&str> = referenced.difference(&declared).copied().collect(); |
| 706 |
assert!( |
| 707 |
undeclared.is_empty(), |
| 708 |
"roster references ids the manifest does not declare: {undeclared:?}" |
| 709 |
); |
| 710 |
|
| 711 |
let unused: Vec<&str> = declared.difference(&referenced).copied().collect(); |
| 712 |
assert!( |
| 713 |
unused.is_empty(), |
| 714 |
"manifest declares ids nothing references; curating them would upload \ |
| 715 |
files no page shows: {unused:?}" |
| 716 |
); |
| 717 |
} |
| 718 |
|
| 719 |
|
| 720 |
|
| 721 |
#[test] |
| 722 |
fn each_id_is_referenced_once() { |
| 723 |
let ids = referenced_ids(); |
| 724 |
let mut seen = HashSet::with_capacity(ids.len()); |
| 725 |
for id in ids { |
| 726 |
assert!(seen.insert(id), "asset id {id:?} is referenced twice"); |
| 727 |
} |
| 728 |
} |
| 729 |
|
| 730 |
|
| 731 |
|
| 732 |
#[test] |
| 733 |
fn image_items_carry_no_separate_media() { |
| 734 |
for creator in ROSTER { |
| 735 |
for item in creator.project.items { |
| 736 |
if matches!(item.item_type, ItemType::Image | ItemType::Text) { |
| 737 |
assert!( |
| 738 |
item.media.is_none(), |
| 739 |
"{:?} item {:?} declares media, which nothing uploads", |
| 740 |
item.item_type, |
| 741 |
item.title |
| 742 |
); |
| 743 |
} |
| 744 |
} |
| 745 |
} |
| 746 |
} |
| 747 |
} |
| 748 |
|