upd8.js 155 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901
  1. // HEY N8RDS!
  2. //
  3. // This is one of the 8ACKEND FILES. It's not used anywhere on the actual site
  4. // you are pro8a8ly using right now.
  5. //
  6. // Specifically, this one does all the actual work of the music wiki. The
  7. // process looks something like this:
  8. //
  9. // 1. Crawl the music directories. Well, not so much "crawl" as "look inside
  10. // the folders for each al8um, and read the metadata file descri8ing that
  11. // al8um and the tracks within."
  12. //
  13. // 2. Read that metadata. I'm writing this 8efore actually doing any of the
  14. // code, and I've gotta admit I have no idea what file format they're
  15. // going to 8e in. May8e JSON, 8ut more likely some weird custom format
  16. // which will 8e a lot easier to edit.
  17. //
  18. // 3. Generate the page files! They're just static index.html files, and are
  19. // what gh-pages (or wherever this is hosted) will show to clients.
  20. // Hopefully pretty minimalistic HTML, 8ut like, shrug. They'll reference
  21. // CSS (and maaaaaaaay8e JS) files, hard-coded somewhere near the root.
  22. //
  23. // 4. Print an awesome message which says the process is done. This is the
  24. // most important step.
  25. //
  26. // Oh yeah, like. Just run this through some relatively recent version of
  27. // node.js and you'll 8e fine. ...Within the project root. O8viously.
  28. // HEY FUTURE ME!!!!!!!! Don't forget to implement artist pages! Those are,
  29. // like, the coolest idea you've had yet, so DO NOT FORGET. (Remem8er, link
  30. // from track listings, etc!) --- Thanks, past me. To futurerer me: an al8um
  31. // listing page (a list of all the al8ums)! Make sure to sort these 8y date -
  32. // we'll need a new field for al8ums.
  33. // ^^^^^^^^ DID THAT! 8ut also, artist images. Pro8a8ly stolen from the fandom
  34. // wiki (I found half those images anywayz).
  35. // TRACK ART CREDITS. This is a must.
  36. // 2020-08-23
  37. // ATTENTION ALL 8*TCHES AND OTHER GENDER TRUCKERS: AS IT TURNS OUT, THIS CODE
  38. // ****SUCKS****. I DON'T THINK ANYTHING WILL EVER REDEEM IT, 8UT THAT DOESN'T
  39. // MEAN WE CAN'T TAKE SOME ACTION TO MAKE WRITING IT A LITTLE LESS TERRI8LE.
  40. // We're gonna start defining STRUCTURES to make things suck less!!!!!!!!
  41. // No classes 8ecause those are a huge pain and like, pro8a8ly 8ad performance
  42. // or whatever -- just some standard structures that should 8e followed
  43. // wherever reasona8le. Only one I need today is the contri8 one 8ut let's put
  44. // any new general-purpose structures here too, ok?
  45. //
  46. // Contri8ution: {who, what, date, thing}. D8 and thing are the new fields.
  47. //
  48. // Use these wisely, which is to say all the time and instead of whatever
  49. // terri8le new pseudo structure you're trying to invent!!!!!!!!
  50. //
  51. // Upd8 2021-01-03: Soooooooo we didn't actually really end up using these,
  52. // lol? Well there's still only one anyway. Kinda ended up doing a 8ig refactor
  53. // of all the o8ject structures today. It's not *especially* relevant 8ut feels
  54. // worth mentioning? I'd get rid of this comment 8lock 8ut I like it too much!
  55. // Even though I haven't actually reread it, lol. 8ut yeah, hopefully in the
  56. // spirit of this "make things more consistent" attitude I 8rought up 8ack in
  57. // August, stuff's lookin' 8etter than ever now. W00t!
  58. 'use strict';
  59. const fs = require('fs');
  60. const path = require('path');
  61. const util = require('util');
  62. // I made this dependency myself! A long, long time ago. It is pro8a8ly my
  63. // most useful li8rary ever. I'm not sure 8esides me actually uses it, though.
  64. const fixWS = require('fix-whitespace');
  65. // Wait nevermind, I forgot a8out why-do-kids-love-the-taste-of-cinnamon-toast-
  66. // crunch. THAT is my 8est li8rary.
  67. // The require function just returns whatever the module exports, so there's
  68. // no reason you can't wrap it in some decorator right out of the 8ox. Which is
  69. // exactly what we do here.
  70. const mkdirp = util.promisify(require('mkdirp'));
  71. // This is the dum8est name for a function possi8le. Like, SURE, fine, may8e
  72. // the UNIX people had some valid reason to go with the weird truncated
  73. // lowercased convention they did. 8ut Node didn't have to ALSO use that
  74. // convention! Would it have 8een so hard to just name the function something
  75. // like fs.readDirectory???????? No, it wouldn't have 8een.
  76. const readdir = util.promisify(fs.readdir);
  77. // 8ut okay, like, look at me. DOING THE SAME THING. See, *I* could have named
  78. // my promisified function differently, and yet I did not. I literally cannot
  79. // explain why. We are all used to following in the 8ad decisions of our
  80. // ancestors, and never never never never never never never consider that hey,
  81. // may8e we don't need to make the exact same decisions they did. Even when
  82. // we're perfectly aware th8t's exactly what we're doing! Programmers,
  83. // including me, are all pretty stupid.
  84. // 8ut I mean, come on. Look. Node decided to use readFile, instead of like,
  85. // what, cat? Why couldn't they rename readdir too???????? As Johannes Kepler
  86. // once so elegantly put it: "Shrug."
  87. const readFile = util.promisify(fs.readFile);
  88. const writeFile = util.promisify(fs.writeFile);
  89. const access = util.promisify(fs.access);
  90. const symlink = util.promisify(fs.symlink);
  91. const unlink = util.promisify(fs.unlink);
  92. const {
  93. cacheOneArg,
  94. curry,
  95. decorateTime,
  96. joinNoOxford,
  97. mapInPlace,
  98. parseOptions,
  99. progressPromiseAll,
  100. queue,
  101. s,
  102. splitArray,
  103. th
  104. } = require('./upd8-util');
  105. const C = require('./common/common');
  106. const CACHEBUST = 1;
  107. const SITE_CANONICAL_BASE = 'https://hsmusic.wiki/';
  108. const SITE_TITLE = 'Homestuck Music Wiki';
  109. const SITE_SHORT_TITLE = 'HSMusic';
  110. const SITE_DESCRIPTION = `Expansive resource for anyone interested in fan-made and official Homestuck music alike; an archive for all things related.`;
  111. const SITE_DONATE_LINK = 'https://liberapay.com/nebula';
  112. function readDataFile(file) {
  113. // fight me bro
  114. return fs.readFileSync(path.join(C.DATA_DIRECTORY, file)).toString().trim();
  115. }
  116. const SITE_ABOUT = readDataFile('about.html');
  117. const SITE_CHANGELOG = readDataFile('changelog.html');
  118. const SITE_DISCORD = readDataFile('discord.html');
  119. const SITE_DONATE = readDataFile('donate.html');
  120. const SITE_FEEDBACK = readDataFile('feedback.html');
  121. const SITE_JS_DISABLED = readDataFile('js-disabled.html');
  122. // Might ena8le this later... we'll see! Eventually. May8e.
  123. const ENABLE_ARTIST_AVATARS = false;
  124. const ARTIST_AVATAR_DIRECTORY = 'artist-avatar';
  125. const ARTIST_DATA_FILE = 'artists.txt';
  126. const FLASH_DATA_FILE = 'flashes.txt';
  127. const NEWS_DATA_FILE = 'news.txt';
  128. const TAG_DATA_FILE = 'tags.txt';
  129. const GROUP_DATA_FILE = 'groups.txt';
  130. const CSS_FILE = 'site.css';
  131. // Shared varia8les! These are more efficient to access than a shared varia8le
  132. // (or at least I h8pe so), and are easier to pass across functions than a
  133. // 8unch of specific arguments.
  134. //
  135. // Upd8: Okay yeah these aren't actually any different. Still cleaner than
  136. // passing around a data object containing all this, though.
  137. let albumData;
  138. let trackData;
  139. let flashData;
  140. let newsData;
  141. let tagData;
  142. let groupData;
  143. let artistNames;
  144. let artistData;
  145. let officialAlbumData;
  146. let fandomAlbumData;
  147. let justEverythingMan; // tracks, albums, flashes -- don't forget to upd8 getHrefOfAnythingMan!
  148. let justEverythingSortedByArtDateMan;
  149. let contributionData;
  150. let queueSize;
  151. // Note there isn't a 'find track data files' function. I plan on including the
  152. // data for all tracks within an al8um collected in the single metadata file
  153. // for that al8um. Otherwise there'll just 8e way too many files, and I'd also
  154. // have to worry a8out linking track files to al8um files (which would contain
  155. // only the track listing, not track data itself), and dealing with errors of
  156. // missing track files (or track files which are not linked to al8ums). All a
  157. // 8unch of stuff that's a pain to deal with for no apparent 8enefit.
  158. async function findAlbumDataFiles() {
  159. return (await readdir(path.join(C.DATA_DIRECTORY, C.DATA_ALBUM_DIRECTORY)))
  160. .map(albumFile => path.join(C.DATA_DIRECTORY, C.DATA_ALBUM_DIRECTORY, albumFile));
  161. }
  162. function* getSections(lines) {
  163. // ::::)
  164. const isSeparatorLine = line => /^-{8,}$/.test(line);
  165. yield* splitArray(lines, isSeparatorLine);
  166. }
  167. function getBasicField(lines, name) {
  168. const line = lines.find(line => line.startsWith(name + ':'));
  169. return line && line.slice(name.length + 1).trim();
  170. };
  171. function getListField(lines, name) {
  172. let startIndex = lines.findIndex(line => line.startsWith(name + ':'));
  173. // If callers want to default to an empty array, they should stick
  174. // "|| []" after the call.
  175. if (startIndex === -1) {
  176. return null;
  177. }
  178. // We increment startIndex 8ecause we don't want to include the
  179. // "heading" line (e.g. "URLs:") in the actual data.
  180. startIndex++;
  181. let endIndex = lines.findIndex((line, index) => index >= startIndex && !line.startsWith('- '));
  182. if (endIndex === -1) {
  183. endIndex = lines.length;
  184. }
  185. if (endIndex === startIndex) {
  186. // If there is no list that comes after the heading line, treat the
  187. // heading line itself as the comma-separ8ted array value, using
  188. // the 8asic field function to do that. (It's l8 and my 8rain is
  189. // sleepy. Please excuse any unhelpful comments I may write, or may
  190. // have already written, in this st8. Thanks!)
  191. const value = getBasicField(lines, name);
  192. return value && value.split(',').map(val => val.trim());
  193. }
  194. const listLines = lines.slice(startIndex, endIndex);
  195. return listLines.map(line => line.slice(2));
  196. };
  197. function getContributionField(section, name) {
  198. let contributors = getListField(section, name);
  199. if (!contributors) {
  200. return null;
  201. }
  202. if (contributors.length === 1 && contributors[0].startsWith('<i>')) {
  203. const arr = [];
  204. arr.textContent = contributors[0];
  205. return arr;
  206. }
  207. contributors = contributors.map(contrib => {
  208. // 8asically, the format is "Who (What)", or just "Who". 8e sure to
  209. // keep in mind that "what" doesn't necessarily have a value!
  210. const match = contrib.match(/^(.*?)( \((.*)\))?$/);
  211. if (!match) {
  212. return contrib;
  213. }
  214. const who = match[1];
  215. const what = match[3] || null;
  216. return {who, what};
  217. });
  218. const badContributor = contributors.find(val => typeof val === 'string');
  219. if (badContributor) {
  220. return {error: `An entry has an incorrectly formatted contributor, "${badContributor}".`};
  221. }
  222. if (contributors.length === 1 && contributors[0].who === 'none') {
  223. return null;
  224. }
  225. return contributors;
  226. };
  227. function getMultilineField(lines, name) {
  228. // All this code is 8asically the same as the getListText - just with a
  229. // different line prefix (four spaces instead of a dash and a space).
  230. let startIndex = lines.findIndex(line => line.startsWith(name + ':'));
  231. if (startIndex === -1) {
  232. return null;
  233. }
  234. startIndex++;
  235. let endIndex = lines.findIndex((line, index) => index >= startIndex && !line.startsWith(' '));
  236. if (endIndex === -1) {
  237. endIndex = lines.length;
  238. }
  239. // If there aren't any content lines, don't return anything!
  240. if (endIndex === startIndex) {
  241. return null;
  242. }
  243. // We also join the lines instead of returning an array.
  244. const listLines = lines.slice(startIndex, endIndex);
  245. return listLines.map(line => line.slice(4)).join('\n');
  246. };
  247. function transformInline(text) {
  248. return text.replace(/\[\[(album:|artist:|flash:|track:|tag:|group:)?(.+?)\]\]/g, (match, category, ref, offset) => {
  249. if (category === 'album:') {
  250. const album = getLinkedAlbum(ref);
  251. if (album) {
  252. return fixWS`
  253. <a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a>
  254. `;
  255. } else {
  256. console.warn(`\x1b[33mThe linked album ${match} does not exist!\x1b[0m`);
  257. return ref;
  258. }
  259. } else if (category === 'artist:') {
  260. const artist = getLinkedArtist(ref);
  261. if (artist) {
  262. return `<a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist.name)}/">${artist.name}</a>`;
  263. } else {
  264. console.warn(`\x1b[33mThe linked artist ${artist} does not exist!\x1b[0m`);
  265. return ref;
  266. }
  267. } else if (category === 'flash:') {
  268. const flash = getLinkedFlash(ref);
  269. if (flash) {
  270. let name = flash.name;
  271. const nextCharacter = text[offset + match.length];
  272. const lastCharacter = name[name.length - 1];
  273. if (
  274. ![' ', '\n', '<'].includes(nextCharacter) &&
  275. lastCharacter === '.'
  276. ) {
  277. name = name.slice(0, -1);
  278. }
  279. return getFlashLinkHTML(flash, name);
  280. } else {
  281. console.warn(`\x1b[33mThe linked flash ${match} does not exist!\x1b[0m`);
  282. return ref;
  283. }
  284. } else if (category === 'track:') {
  285. const track = getLinkedTrack(ref);
  286. if (track) {
  287. return fixWS`
  288. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
  289. `;
  290. } else {
  291. console.warn(`\x1b[33mThe linked track ${match} does not exist!\x1b[0m`);
  292. return ref;
  293. }
  294. } else if (category === 'tag:') {
  295. const tag = getLinkedTag(ref);
  296. if (tag) {
  297. return fixWS`
  298. <a href="${C.TAG_DIRECTORY}/${tag.directory}/" style="${getThemeString(tag)}">${tag.name}</a>
  299. `;
  300. } else {
  301. console.warn(`\x1b[33mThe linked tag ${match} does not exist!\x1b[0m`);
  302. return ref;
  303. }
  304. } else if (category === 'group:') {
  305. const group = getLinkedGroup(ref);
  306. if (group) {
  307. return fixWS`
  308. <a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a>
  309. `;
  310. } else {
  311. console.warn(`\x1b[33mThe linked group ${group} does not exist!\x1b[0m`);
  312. return ref;
  313. }
  314. } else {
  315. const track = getLinkedTrack(ref);
  316. if (track) {
  317. let name = ref.match(/(.*):/);
  318. if (name) {
  319. name = name[1];
  320. } else {
  321. name = track.name;
  322. }
  323. return fixWS`
  324. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${name}</a>
  325. `;
  326. } else {
  327. console.warn(`\x1b[33mThe linked track ${match} does not exist!\x1b[0m`);
  328. return ref;
  329. }
  330. }
  331. });
  332. }
  333. function parseAttributes(string) {
  334. const attributes = Object.create(null);
  335. const skipWhitespace = i => {
  336. const ws = /\s/;
  337. if (ws.test(string[i])) {
  338. const match = string.slice(i).match(/[^\s]/);
  339. if (match) {
  340. return i + match.index;
  341. } else {
  342. return string.length;
  343. }
  344. } else {
  345. return i;
  346. }
  347. };
  348. for (let i = 0; i < string.length;) {
  349. i = skipWhitespace(i);
  350. const aStart = i;
  351. const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
  352. const attribute = string.slice(aStart, aEnd);
  353. i = skipWhitespace(aEnd);
  354. if (string[i] === '=') {
  355. i = skipWhitespace(i + 1);
  356. let end, endOffset;
  357. if (string[i] === '"' || string[i] === "'") {
  358. end = string[i];
  359. endOffset = 1;
  360. i++;
  361. } else {
  362. end = '\\s';
  363. endOffset = 0;
  364. }
  365. const vStart = i;
  366. const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
  367. const value = string.slice(vStart, vEnd);
  368. i = vEnd + endOffset;
  369. attributes[attribute] = value;
  370. } else {
  371. attributes[attribute] = attribute;
  372. }
  373. }
  374. return Object.fromEntries(Object.entries(attributes).map(([ key, val ]) => [
  375. key,
  376. val === 'true' ? true :
  377. val === 'false' ? false :
  378. val === key ? true :
  379. val
  380. ]));
  381. }
  382. function transformMultiline(text, treatAsDocument=false) {
  383. // Heck yes, HTML magics.
  384. text = transformInline(text);
  385. if (treatAsDocument) {
  386. return text;
  387. }
  388. const outLines = [];
  389. let inList = false;
  390. for (let line of text.split(/\r|\n|\r\n/)) {
  391. line = line.replace(/<img (.*?)>/g, (match, attributes) => img({
  392. lazy: true,
  393. link: true,
  394. ...parseAttributes(attributes)
  395. }));
  396. if (line.startsWith('- ')) {
  397. if (!inList) {
  398. outLines.push('<ul>');
  399. inList = true;
  400. }
  401. outLines.push(` <li>${line.slice(1).trim()}</li>`);
  402. } else {
  403. if (inList) {
  404. outLines.push('</ul>');
  405. inList = false;
  406. }
  407. outLines.push(`<p>${line}</p>`);
  408. }
  409. }
  410. return outLines.join('\n');
  411. }
  412. function transformLyrics(text) {
  413. // Different from transformMultiline 'cuz it joins multiple lines together
  414. // with line 8reaks (<br>); transformMultiline treats each line as its own
  415. // complete paragraph (or list, etc).
  416. // If it looks like old data, then like, oh god.
  417. // Use the normal transformMultiline tool.
  418. if (text.includes('<br')) {
  419. return transformMultiline(text);
  420. }
  421. text = transformInline(text.trim());
  422. let buildLine = '';
  423. const addLine = () => outLines.push(`<p>${buildLine}</p>`);
  424. const outLines = [];
  425. for (const line of text.split('\n')) {
  426. if (line.length) {
  427. if (buildLine.length) {
  428. buildLine += '<br>';
  429. }
  430. buildLine += line;
  431. } else if (buildLine.length) {
  432. addLine();
  433. buildLine = '';
  434. }
  435. }
  436. if (buildLine.length) {
  437. addLine();
  438. }
  439. return outLines.join('\n');
  440. }
  441. function getCommentaryField(lines) {
  442. const text = getMultilineField(lines, 'Commentary');
  443. if (text) {
  444. const lines = text.split('\n');
  445. if (!lines[0].replace(/<\/b>/g, '').includes(':</i>')) {
  446. return {error: `An entry is missing commentary citation: "${lines[0].slice(0, 40)}..."`};
  447. }
  448. return text;
  449. } else {
  450. return null;
  451. }
  452. };
  453. async function processAlbumDataFile(file) {
  454. let contents;
  455. try {
  456. contents = await readFile(file, 'utf-8');
  457. } catch (error) {
  458. // This function can return "error o8jects," which are really just
  459. // ordinary o8jects with an error message attached. I'm not 8othering
  460. // with error codes here or anywhere in this function; while this would
  461. // normally 8e 8ad coding practice, it doesn't really matter here,
  462. // 8ecause this isn't an API getting consumed 8y other services (e.g.
  463. // translaction functions). If we return an error, the caller will just
  464. // print the attached message in the output summary.
  465. return {error: `Could not read ${file} (${error.code}).`};
  466. }
  467. // We're pro8a8ly supposed to, like, search for a header somewhere in the
  468. // al8um contents, to make sure it's trying to 8e the intended structure
  469. // and is a valid utf-8 (or at least ASCII) file. 8ut like, whatever.
  470. // We'll just return more specific errors if it's missing necessary data
  471. // fields.
  472. const contentLines = contents.split('\n');
  473. // In this line of code I defeat the purpose of using a generator in the
  474. // first place. Sorry!!!!!!!!
  475. const sections = Array.from(getSections(contentLines));
  476. const albumSection = sections[0];
  477. const album = {};
  478. album.name = getBasicField(albumSection, 'Album');
  479. album.artists = getContributionField(albumSection, 'Artists') || getContributionField(albumSection, 'Artist');
  480. album.wallpaperArtists = getContributionField(albumSection, 'Wallpaper Art');
  481. album.wallpaperStyle = getMultilineField(albumSection, 'Wallpaper Style');
  482. album.date = getBasicField(albumSection, 'Date');
  483. album.trackArtDate = getBasicField(albumSection, 'Track Art Date') || album.date;
  484. album.coverArtDate = getBasicField(albumSection, 'Cover Art Date') || album.date;
  485. album.coverArtists = getContributionField(albumSection, 'Cover Art');
  486. album.hasTrackArt = (getBasicField(albumSection, 'Has Track Art') !== 'no');
  487. album.trackCoverArtists = getContributionField(albumSection, 'Track Art');
  488. album.artTags = getListField(albumSection, 'Art Tags') || [];
  489. album.commentary = getCommentaryField(albumSection);
  490. album.urls = getListField(albumSection, 'URLs') || [];
  491. album.groups = getListField(albumSection, 'Groups') || [];
  492. album.directory = getBasicField(albumSection, 'Directory');
  493. album.isMajorRelease = getBasicField(albumSection, 'Major Release') === 'yes';
  494. if (album.artists && album.artists.error) {
  495. return {error: `${album.artists.error} (in ${album.name})`};
  496. }
  497. if (album.coverArtists && album.coverArtists.error) {
  498. return {error: `${album.coverArtists.error} (in ${album.name})`};
  499. }
  500. if (album.commentary && album.commentary.error) {
  501. return {error: `${album.commentary.error} (in ${album.name})`};
  502. }
  503. if (album.trackCoverArtists && album.trackCoverArtists.error) {
  504. return {error: `${album.trackCoverArtists.error} (in ${album.name})`};
  505. }
  506. if (!album.coverArtists) {
  507. return {error: `The album "${album.name}" is missing the "Cover Art" field.`};
  508. }
  509. album.color = getBasicField(albumSection, 'FG') || '#0088ff';
  510. if (!album.name) {
  511. return {error: 'Expected "Album" (name) field!'};
  512. }
  513. if (!album.date) {
  514. return {error: 'Expected "Date" field!'};
  515. }
  516. if (isNaN(Date.parse(album.date))) {
  517. return {error: `Invalid Date field: "${album.date}"`};
  518. }
  519. album.date = new Date(album.date);
  520. album.trackArtDate = new Date(album.trackArtDate);
  521. album.coverArtDate = new Date(album.coverArtDate);
  522. if (isNaN(Date.parse(album.trackArtDate))) {
  523. return {error: `Invalid Track Art Date field: "${album.trackArtDate}"`};
  524. }
  525. if (isNaN(Date.parse(album.coverArtDate))) {
  526. return {error: `Invalid Cover Art Date field: "${album.coverArtDate}"`};
  527. }
  528. if (!album.directory) {
  529. album.directory = C.getKebabCase(album.name);
  530. }
  531. album.tracks = [];
  532. // will be overwritten if a group section is found!
  533. album.usesGroups = false;
  534. let group = '';
  535. let groupColor = album.color;
  536. for (const section of sections.slice(1)) {
  537. // Just skip empty sections. Sometimes I paste a 8unch of dividers,
  538. // and this lets the empty sections doing that creates (temporarily)
  539. // exist without raising an error.
  540. if (!section.filter(Boolean).length) {
  541. continue;
  542. }
  543. const groupName = getBasicField(section, 'Group');
  544. if (groupName) {
  545. group = groupName;
  546. groupColor = getBasicField(section, 'FG') || album.color;
  547. album.usesGroups = true;
  548. continue;
  549. }
  550. const track = {};
  551. track.name = getBasicField(section, 'Track');
  552. track.commentary = getCommentaryField(section);
  553. track.lyrics = getMultilineField(section, 'Lyrics');
  554. track.originalDate = getBasicField(section, 'Original Date');
  555. track.coverArtDate = getBasicField(section, 'Cover Art Date') || track.originalDate || album.trackArtDate;
  556. track.references = getListField(section, 'References') || [];
  557. track.artists = getContributionField(section, 'Artists') || getContributionField(section, 'Artist');
  558. track.coverArtists = getContributionField(section, 'Track Art');
  559. track.artTags = getListField(section, 'Art Tags') || [];
  560. track.contributors = getContributionField(section, 'Contributors') || [];
  561. track.directory = getBasicField(section, 'Directory');
  562. track.aka = getBasicField(section, 'AKA');
  563. if (!track.name) {
  564. return {error: `A track section is missing the "Track" (name) field (in ${album.name}, previous: ${album.tracks[album.tracks.length - 1]?.name}).`};
  565. }
  566. let durationString = getBasicField(section, 'Duration') || '0:00';
  567. track.duration = getDurationInSeconds(durationString);
  568. if (track.contributors.error) {
  569. return {error: `${track.contributors.error} (in ${track.name}, ${album.name})`};
  570. }
  571. if (track.commentary && track.commentary.error) {
  572. return {error: `${track.commentary.error} (in ${track.name}, ${album.name})`};
  573. }
  574. if (!track.artists) {
  575. // If an al8um has an artist specified (usually 8ecause it's a solo
  576. // al8um), let tracks inherit that artist. We won't display the
  577. // "8y <artist>" string on the al8um listing.
  578. if (album.artists) {
  579. track.artists = album.artists;
  580. } else {
  581. return {error: `The track "${track.name}" is missing the "Artist" field (in ${album.name}).`};
  582. }
  583. }
  584. if (!track.coverArtists) {
  585. if (getBasicField(section, 'Track Art') !== 'none' && album.hasTrackArt) {
  586. if (album.trackCoverArtists) {
  587. track.coverArtists = album.trackCoverArtists;
  588. } else {
  589. return {error: `The track "${track.name}" is missing the "Track Art" field (in ${album.name}).`};
  590. }
  591. }
  592. }
  593. if (track.coverArtists && track.coverArtists.length && track.coverArtists[0] === 'none') {
  594. track.coverArtists = null;
  595. }
  596. if (!track.directory) {
  597. track.directory = C.getKebabCase(track.name);
  598. }
  599. if (track.originalDate) {
  600. if (isNaN(Date.parse(track.originalDate))) {
  601. return {error: `The track "${track.name}"'s has an invalid "Original Date" field: "${track.originalDate}"`};
  602. }
  603. track.date = new Date(track.originalDate);
  604. } else {
  605. track.date = album.date;
  606. }
  607. track.coverArtDate = new Date(track.coverArtDate);
  608. const hasURLs = getBasicField(section, 'Has URLs') !== 'no';
  609. track.urls = hasURLs && (getListField(section, 'URLs') || []).filter(Boolean);
  610. if (hasURLs && !track.urls.length) {
  611. return {error: `The track "${track.name}" should have at least one URL specified.`};
  612. }
  613. // 8ack-reference the al8um o8ject! This is very useful for when
  614. // we're outputting the track pages.
  615. track.album = album;
  616. track.group = group;
  617. if (group) {
  618. track.color = groupColor;
  619. } else {
  620. track.color = album.color;
  621. }
  622. album.tracks.push(track);
  623. }
  624. return album;
  625. }
  626. async function processArtistDataFile(file) {
  627. let contents;
  628. try {
  629. contents = await readFile(file, 'utf-8');
  630. } catch (error) {
  631. return {error: `Could not read ${file} (${error.code}).`};
  632. }
  633. const contentLines = contents.split('\n');
  634. const sections = Array.from(getSections(contentLines));
  635. return sections.map(section => {
  636. const name = getBasicField(section, 'Artist');
  637. const urls = (getListField(section, 'URLs') || []).filter(Boolean);
  638. const alias = getBasicField(section, 'Alias');
  639. const note = getMultilineField(section, 'Note');
  640. let directory = getBasicField(section, 'Directory');
  641. if (!name) {
  642. return {error: 'Expected "Artist" (name) field!'};
  643. }
  644. if (!directory) {
  645. directory = C.getArtistDirectory(name);
  646. }
  647. if (alias) {
  648. return {name, directory, alias};
  649. } else {
  650. return {name, directory, urls, note};
  651. }
  652. });
  653. }
  654. async function processFlashDataFile(file) {
  655. let contents;
  656. try {
  657. contents = await readFile(file, 'utf-8');
  658. } catch (error) {
  659. return {error: `Could not read ${file} (${error.code}).`};
  660. }
  661. const contentLines = contents.split('\n');
  662. const sections = Array.from(getSections(contentLines));
  663. let act, color;
  664. return sections.map(section => {
  665. if (getBasicField(section, 'ACT')) {
  666. act = getBasicField(section, 'ACT');
  667. color = getBasicField(section, 'FG');
  668. const anchor = getBasicField(section, 'Anchor');
  669. const jump = getBasicField(section, 'Jump');
  670. const jumpColor = getBasicField(section, 'Jump Color') || color;
  671. return {act8r8k: true, act, color, anchor, jump, jumpColor};
  672. }
  673. const name = getBasicField(section, 'Flash');
  674. let page = getBasicField(section, 'Page');
  675. let directory = getBasicField(section, 'Directory');
  676. let date = getBasicField(section, 'Date');
  677. const jiff = getBasicField(section, 'Jiff');
  678. const tracks = getListField(section, 'Tracks') || [];
  679. const contributors = getContributionField(section, 'Contributors') || [];
  680. const urls = (getListField(section, 'URLs') || []).filter(Boolean);
  681. if (!name) {
  682. return {error: 'Expected "Flash" (name) field!'};
  683. }
  684. if (!page && !directory) {
  685. return {error: 'Expected "Page" or "Directory" field!'};
  686. }
  687. if (!directory) {
  688. directory = page;
  689. }
  690. if (!date) {
  691. return {error: 'Expected "Date" field!'};
  692. }
  693. if (isNaN(Date.parse(date))) {
  694. return {error: `Invalid Date field: "${date}"`};
  695. }
  696. date = new Date(date);
  697. return {name, page, directory, date, contributors, tracks, urls, act, color, jiff};
  698. });
  699. }
  700. async function processNewsDataFile(file) {
  701. let contents;
  702. try {
  703. contents = await readFile(file, 'utf-8');
  704. } catch (error) {
  705. return {error: `Could not read ${file} (${error.code}).`};
  706. }
  707. const contentLines = contents.split('\n');
  708. const sections = Array.from(getSections(contentLines));
  709. return sections.map(section => {
  710. const name = getBasicField(section, 'Name');
  711. if (!name) {
  712. return {error: 'Expected "Name" field!'};
  713. }
  714. const id = getBasicField(section, 'ID');
  715. if (!id) {
  716. return {error: 'Expected "ID" field!'};
  717. }
  718. let body = getMultilineField(section, 'Body');
  719. if (!body) {
  720. return {error: 'Expected "Body" field!'};
  721. }
  722. let date = getBasicField(section, 'Date');
  723. if (!date) {
  724. return {error: 'Expected "Date" field!'};
  725. }
  726. if (isNaN(Date.parse(date))) {
  727. return {error: `Invalid date field: "${date}"`};
  728. }
  729. date = new Date(date);
  730. let bodyShort = body.split('<hr class="split">')[0];
  731. return {
  732. name,
  733. body,
  734. bodyShort,
  735. date,
  736. id
  737. };
  738. });
  739. }
  740. async function processTagDataFile(file) {
  741. let contents;
  742. try {
  743. contents = await readFile(file, 'utf-8');
  744. } catch (error) {
  745. return {error: `Could not read ${file} (${error.code}).`};
  746. }
  747. const contentLines = contents.split('\n');
  748. const sections = Array.from(getSections(contentLines));
  749. return sections.map(section => {
  750. let isCW = false;
  751. let name = getBasicField(section, 'Tag');
  752. if (!name) {
  753. name = getBasicField(section, 'CW');
  754. isCW = true;
  755. if (!name) {
  756. return {error: 'Expected "Tag" or "CW" field!'};
  757. }
  758. }
  759. let color;
  760. if (!isCW) {
  761. color = getBasicField(section, 'Color');
  762. if (!color) {
  763. return {error: 'Expected "Color" field!'};
  764. }
  765. }
  766. const directory = C.getKebabCase(name);
  767. return {
  768. name,
  769. directory,
  770. isCW,
  771. color
  772. };
  773. });
  774. }
  775. async function processGroupDataFile(file) {
  776. let contents;
  777. try {
  778. contents = await readFile(file, 'utf-8');
  779. } catch (error) {
  780. return {error: `Could not read ${file} (${error.code}).`};
  781. }
  782. const contentLines = contents.split('\n');
  783. const sections = Array.from(getSections(contentLines));
  784. let category, color;
  785. return sections.map(section => {
  786. if (getBasicField(section, 'Category')) {
  787. category = getBasicField(section, 'Category');
  788. color = getBasicField(section, 'Color');
  789. return {isCategory: true, name: category, color};
  790. }
  791. const name = getBasicField(section, 'Group');
  792. if (!name) {
  793. return {error: 'Expected "Group" field!'};
  794. }
  795. let directory = getBasicField(section, 'Directory');
  796. if (!directory) {
  797. directory = C.getKebabCase(name);
  798. }
  799. let description = getMultilineField(section, 'Description');
  800. if (!description) {
  801. return {error: 'Expected "Description" field!'};
  802. }
  803. let descriptionShort = description.split('<hr class="split">')[0];
  804. const urls = (getListField(section, 'URLs') || []).filter(Boolean);
  805. return {
  806. isGroup: true,
  807. name,
  808. directory,
  809. description,
  810. descriptionShort,
  811. urls,
  812. category,
  813. color
  814. };
  815. });
  816. }
  817. function getDateString({ date }) {
  818. /*
  819. const pad = val => val.toString().padStart(2, '0');
  820. return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
  821. */
  822. const months = [
  823. 'January', 'February', 'March', 'April', 'May', 'June',
  824. 'July', 'August', 'September', 'October', 'November', 'December'
  825. ]
  826. date = new Date(date);
  827. return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`
  828. }
  829. function getDurationString(secTotal) {
  830. if (secTotal === 0) {
  831. return '_:__'
  832. }
  833. let hour = Math.floor(secTotal / 3600)
  834. let min = Math.floor((secTotal - hour * 3600) / 60)
  835. let sec = Math.floor(secTotal - hour * 3600 - min * 60)
  836. const pad = val => val.toString().padStart(2, '0')
  837. if (hour > 0) {
  838. return `${hour}:${pad(min)}:${pad(sec)}`
  839. } else {
  840. return `${min}:${pad(sec)}`
  841. }
  842. }
  843. function getDurationInSeconds(string) {
  844. const parts = string.split(':').map(n => parseInt(n))
  845. if (parts.length === 3) {
  846. return parts[0] * 3600 + parts[1] * 60 + parts[2]
  847. } else if (parts.length === 2) {
  848. return parts[0] * 60 + parts[1]
  849. } else {
  850. return 0
  851. }
  852. }
  853. function getTotalDuration(tracks) {
  854. return tracks.reduce((duration, track) => duration + track.duration, 0);
  855. }
  856. const stringifyIndent = 0;
  857. const toRefs = (label, objectOrArray) => {
  858. if (Array.isArray(objectOrArray)) {
  859. return objectOrArray.filter(Boolean).map(x => `${label}:${x.directory}`);
  860. } else if (objectOrArray.directory) {
  861. throw new Error('toRefs should not be passed a single object with directory');
  862. } else if (typeof objectOrArray === 'object') {
  863. return Object.fromEntries(Object.entries(objectOrArray)
  864. .map(([ key, value ]) => [key, toRefs(key, value)]));
  865. } else {
  866. throw new Error('toRefs should be passed an array or object of arrays');
  867. }
  868. };
  869. function stringifyRefs(key, value) {
  870. switch (key) {
  871. case 'tracks':
  872. case 'references':
  873. case 'referencedBy':
  874. return toRefs('track', value);
  875. case 'artists':
  876. case 'contributors':
  877. case 'coverArtists':
  878. case 'trackCoverArtists':
  879. return value && value.map(({ who, what }) => ({who: `artist:${who.directory}`, what}));
  880. case 'albums': return toRefs('album', value);
  881. case 'flashes': return toRefs('flash', value);
  882. case 'groups': return toRefs('group', value);
  883. case 'artTags': return toRefs('tag', value);
  884. case 'aka': return value && `track:${value.directory}`;
  885. default:
  886. return value;
  887. }
  888. }
  889. function stringifyAlbumData() {
  890. return JSON.stringify(albumData, (key, value) => {
  891. switch (key) {
  892. case 'commentary':
  893. return '';
  894. default:
  895. return stringifyRefs(key, value);
  896. }
  897. }, stringifyIndent);
  898. }
  899. function stringifyTrackData() {
  900. return JSON.stringify(trackData, (key, value) => {
  901. switch (key) {
  902. case 'album':
  903. case 'commentary':
  904. case 'otherReleases':
  905. return undefined;
  906. default:
  907. return stringifyRefs(key, value);
  908. }
  909. }, stringifyIndent);
  910. }
  911. function stringifyFlashData() {
  912. return JSON.stringify(flashData, (key, value) => {
  913. switch (key) {
  914. case 'act':
  915. case 'commentary':
  916. return undefined;
  917. default:
  918. return stringifyRefs(key, value);
  919. }
  920. }, stringifyIndent);
  921. }
  922. function stringifyArtistData() {
  923. return JSON.stringify(artistData, (key, value) => {
  924. switch (key) {
  925. case 'asAny':
  926. return;
  927. case 'asArtist':
  928. case 'asContributor':
  929. case 'asCoverArtist':
  930. return toRefs('track', value);
  931. default:
  932. return stringifyRefs(key, value);
  933. }
  934. }, stringifyIndent);
  935. }
  936. function escapeAttributeValue(value) {
  937. return value.toString().replace(/"/g, '&quot;');
  938. }
  939. function attributes(attribs) {
  940. return Object.entries(attribs)
  941. .filter(([ key, val ]) => val !== '')
  942. .map(([ key, val ]) => `${key}="${escapeAttributeValue(val)}"`)
  943. .join(' ');
  944. }
  945. function img({
  946. src = '',
  947. alt = '',
  948. reveal = '',
  949. id = '',
  950. width = '',
  951. height = '',
  952. link = false,
  953. lazy = false,
  954. square = false
  955. }) {
  956. const willSquare = square;
  957. const willLink = typeof link === 'string' || link;
  958. const imgAttributes = attributes({
  959. id: link ? '' : id,
  960. alt,
  961. width,
  962. height
  963. });
  964. const nonlazyHTML = wrap(`<img src="${src}" ${imgAttributes}>`);
  965. const lazyHTML = lazy && wrap(`<img class="lazy" data-original="${src}" ${imgAttributes}>`, true);
  966. if (lazy) {
  967. return fixWS`
  968. <noscript>${nonlazyHTML}</noscript>
  969. ${lazyHTML}
  970. `;
  971. } else {
  972. return nonlazyHTML;
  973. }
  974. function wrap(html, hide = false) {
  975. html = fixWS`
  976. <div class="image-inner-area">${html}</div>
  977. `;
  978. html = fixWS`
  979. <div class="image-container">${html}</div>
  980. `;
  981. if (reveal) {
  982. html = fixWS`
  983. <div class="reveal">
  984. ${html}
  985. <span class="reveal-text">${reveal}</span>
  986. </div>
  987. `;
  988. }
  989. if (willSquare) {
  990. html = fixWS`<div ${classes('square', hide && !willLink && 'js-hide')}><div class="square-content">${html}</div></div>`;
  991. }
  992. if (willLink) {
  993. html = `<a ${classes('box', hide && 'js-hide')} ${attributes({
  994. id,
  995. href: typeof link === 'string' ? link : src
  996. })}>${html}</a>`;
  997. }
  998. return html;
  999. }
  1000. }
  1001. async function writePage(directoryParts, {
  1002. title = '',
  1003. meta = {},
  1004. body = {
  1005. style: ''
  1006. },
  1007. main = {
  1008. classes: [],
  1009. content: ''
  1010. },
  1011. sidebar = {
  1012. collapse: true,
  1013. classes: [],
  1014. content: ''
  1015. },
  1016. sidebarRight = {
  1017. collapse: true,
  1018. classes: [],
  1019. content: ''
  1020. },
  1021. nav = {
  1022. links: [],
  1023. classes: [],
  1024. content: ''
  1025. }
  1026. }) {
  1027. const directory = path.join(C.SITE_DIRECTORY, ...directoryParts);
  1028. const file = path.join(directory, 'index.html');
  1029. const href = path.join(...directoryParts, 'index.html');
  1030. let targetPath = directoryParts.join('/');
  1031. if (directoryParts.length) {
  1032. targetPath += '/';
  1033. }
  1034. const canonical = SITE_CANONICAL_BASE + targetPath;
  1035. const collapseSidebars = (sidebar.collapse !== false) && (sidebarRight.collapse !== false);
  1036. const mainHTML = main.content && fixWS`
  1037. <main id="content" ${classes(...main.classes || [])}>
  1038. ${main.content}
  1039. </main>
  1040. `;
  1041. const generateSidebarHTML = (id, {
  1042. content,
  1043. multiple,
  1044. classes: sidebarClasses = [],
  1045. collapse = true,
  1046. wide = false
  1047. }) => (content ? fixWS`
  1048. <div id="${id}" ${classes(
  1049. 'sidebar-column',
  1050. 'sidebar',
  1051. wide && 'wide',
  1052. !collapse && 'no-hide',
  1053. ...sidebarClasses
  1054. )}>
  1055. ${content}
  1056. </div>
  1057. ` : multiple ? fixWS`
  1058. <div id="${id}" ${classes(
  1059. 'sidebar-column',
  1060. 'sidebar-multiple',
  1061. wide && 'wide',
  1062. !collapse && 'no-hide'
  1063. )}>
  1064. ${multiple.map(content => fixWS`
  1065. <div ${classes(
  1066. 'sidebar',
  1067. ...sidebarClasses
  1068. )}>
  1069. ${content}
  1070. </div>
  1071. `).join('\n')}
  1072. </div>
  1073. ` : '');
  1074. const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebar);
  1075. const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight);
  1076. if (nav.simple) {
  1077. nav.links = [
  1078. ['./', SITE_SHORT_TITLE],
  1079. [href, title]
  1080. ]
  1081. }
  1082. const links = (nav.links || []).filter(Boolean);
  1083. const navLinkParts = [];
  1084. for (let i = 0; i < links.length; i++) {
  1085. const link = links[i];
  1086. const prev = links[i - 1];
  1087. const next = links[i + 1];
  1088. const [ href, title ] = link;
  1089. let part = '';
  1090. if (href) {
  1091. if (prev && prev[0]) {
  1092. part = '/ ';
  1093. }
  1094. part += `<a href="${href}">${title}</a>`;
  1095. } else {
  1096. if (next && prev) {
  1097. part = '/ ';
  1098. }
  1099. part += `<span>${title}</span>`;
  1100. }
  1101. navLinkParts.push(part);
  1102. }
  1103. const navContentHTML = [
  1104. nav.links && fixWS`
  1105. <h2 class="highlight-last-link">
  1106. ${navLinkParts.join('\n')}
  1107. </h2>
  1108. `,
  1109. nav.content
  1110. ].filter(Boolean).join('\n');
  1111. const navHTML = navContentHTML && fixWS`
  1112. <nav id="header" ${classes(...nav.classes || [])}>
  1113. ${navContentHTML}
  1114. </nav>
  1115. `;
  1116. const layoutHTML = [
  1117. navHTML,
  1118. (sidebarLeftHTML || sidebarRightHTML) ? fixWS`
  1119. <div ${classes('layout-columns', !collapseSidebars && 'vertical-when-thin')}>
  1120. ${sidebarLeftHTML}
  1121. ${mainHTML}
  1122. ${sidebarRightHTML}
  1123. </div>
  1124. ` : mainHTML
  1125. ].filter(Boolean).join('\n');
  1126. await mkdirp(directory);
  1127. await writeFile(file, rebaseURLs(directory, fixWS`
  1128. <!DOCTYPE html>
  1129. <html data-rebase="${path.relative(directory, C.SITE_DIRECTORY)}">
  1130. <head>
  1131. <title>${title}</title>
  1132. <meta charset="utf-8">
  1133. <meta name="viewport" content="width=device-width, initial-scale=1">
  1134. ${Object.entries(meta).map(([ key, value ]) => `<meta ${key}="${escapeAttributeValue(value)}">`).join('\n')}
  1135. <link rel="canonical" href="${canonical}">
  1136. <link rel="stylesheet" href="${C.STATIC_DIRECTORY}/site.css?${CACHEBUST}">
  1137. <script src="${C.STATIC_DIRECTORY}/lazy-loading.js?${CACHEBUST}"></script>
  1138. </head>
  1139. <body ${attributes({style: body.style || ''})}>
  1140. <div id="page-container">
  1141. ${mainHTML && fixWS`
  1142. <div id="skippers">
  1143. <span class="skipper"><a href="#content">Skip to content</a></span>
  1144. ${sidebarLeftHTML && `<span class="skipper"><a href="#sidebar-left">Skip to sidebar ${sidebarRightHTML && '(left)'}</a></span>`}
  1145. ${sidebarRightHTML && `<span class="skipper"><a href="#sidebar-right">Skip to sidebar ${sidebar.content && '(right)'}</a></span>`}
  1146. </div>
  1147. `}
  1148. ${layoutHTML}
  1149. </div>
  1150. <script src="${C.COMMON_DIRECTORY}/common.js?${CACHEBUST}"></script>
  1151. <script src="${C.STATIC_DIRECTORY}/client.js?${CACHEBUST}"></script>
  1152. </body>
  1153. </html>
  1154. `));
  1155. }
  1156. function getGridHTML({
  1157. entries,
  1158. srcFn,
  1159. hrefFn,
  1160. altFn = () => '',
  1161. details = false,
  1162. lazy = true
  1163. }) {
  1164. return entries.map(({ large, item }, i) => fixWS`
  1165. <a ${classes('grid-item', 'box', large && 'large-grid-item')} href="${hrefFn(item)}" style="${getThemeString(item)}">
  1166. ${img({
  1167. src: srcFn(item),
  1168. alt: altFn(item),
  1169. lazy: (typeof lazy === 'number' ? i >= lazy : lazy),
  1170. square: true,
  1171. reveal: getRevealString(item.artTags)
  1172. })}
  1173. <span>${item.name}</span>
  1174. ${details && fixWS`
  1175. <span>(${s(item.tracks.length, 'track')}, ${getDurationString(getTotalDuration(item.tracks))})</span>
  1176. `}
  1177. </a>
  1178. `).join('\n');
  1179. }
  1180. function getAlbumGridHTML(props) {
  1181. return getGridHTML({
  1182. srcFn: getAlbumCover,
  1183. hrefFn: album => `${C.ALBUM_DIRECTORY}/${album.directory}/`,
  1184. ...props
  1185. });
  1186. }
  1187. function getAlbumGridHTML(props) {
  1188. return getGridHTML({
  1189. srcFn: getAlbumCover,
  1190. hrefFn: album => `${C.ALBUM_DIRECTORY}/${album.directory}/`,
  1191. ...props
  1192. });
  1193. }
  1194. function getFlashGridHTML(props) {
  1195. return getGridHTML({
  1196. srcFn: getFlashCover,
  1197. hrefFn: flash => `${C.FLASH_DIRECTORY}/${flash.directory}/`,
  1198. altFn: () => 'flash art',
  1199. ...props
  1200. });
  1201. }
  1202. function getNewReleases(numReleases) {
  1203. const latestFirst = albumData.slice().reverse();
  1204. const majorReleases = latestFirst.filter(album => album.groups.some(g => g.directory === C.OFFICIAL_GROUP_DIRECTORY) || album.isMajorRelease);
  1205. majorReleases.splice(1);
  1206. const otherReleases = latestFirst
  1207. .filter(album => !majorReleases.includes(album))
  1208. .slice(0, numReleases - majorReleases.length);
  1209. return [
  1210. ...majorReleases.map(album => ({large: true, item: album})),
  1211. ...otherReleases.map(album => ({large: false, item: album}))
  1212. ];
  1213. }
  1214. function writeSymlinks() {
  1215. return progressPromiseAll('Building site symlinks.', [
  1216. link(C.COMMON_DIRECTORY),
  1217. link(C.STATIC_DIRECTORY),
  1218. link(C.MEDIA_DIRECTORY)
  1219. ]);
  1220. async function link(directory) {
  1221. const file = path.join(C.SITE_DIRECTORY, directory);
  1222. try {
  1223. await unlink(file);
  1224. } catch (error) {
  1225. if (error.code !== 'ENOENT') {
  1226. throw error;
  1227. }
  1228. }
  1229. await symlink(path.join('..', directory), file);
  1230. }
  1231. }
  1232. function writeMiscellaneousPages() {
  1233. return progressPromiseAll('Writing miscellaneous pages.', [
  1234. writePage([], {
  1235. title: SITE_TITLE,
  1236. meta: {
  1237. description: SITE_DESCRIPTION
  1238. },
  1239. main: {
  1240. classes: ['top-index'],
  1241. content: fixWS`
  1242. <h1>${SITE_TITLE}</h1>
  1243. <h2>New Releases</h2>
  1244. <div class="grid-listing">
  1245. ${getAlbumGridHTML({
  1246. entries: getNewReleases(4),
  1247. lazy: false
  1248. })}
  1249. </div>
  1250. <h2>Fandom</h2>
  1251. <div class="grid-listing">
  1252. ${getAlbumGridHTML({
  1253. entries: (albumData
  1254. .filter(album => album.groups.some(g => g.directory === C.FANDOM_GROUP_DIRECTORY))
  1255. .reverse()
  1256. .slice(0, 6)
  1257. .concat([albumData.find(album => album.directory === C.UNRELEASED_TRACKS_DIRECTORY)])
  1258. .map(album => ({item: album}))),
  1259. lazy: true
  1260. })}
  1261. <div class="grid-actions">
  1262. <a class="box grid-item" href="${C.GROUP_DIRECTORY}/${C.FANDOM_GROUP_DIRECTORY}/gallery/" style="--fg-color: #ffffff">Explore Fandom!</a>
  1263. <a class="box grid-item" href="${C.FEEDBACK_DIRECTORY}/" style="--fg-color: #ffffff">Share an album!</a>
  1264. </div>
  1265. </div>
  1266. <h2>Official</h2>
  1267. <div class="grid-listing">
  1268. ${getAlbumGridHTML({
  1269. entries: (albumData
  1270. .filter(album => album.groups.some(g => g.directory === C.OFFICIAL_GROUP_DIRECTORY))
  1271. .reverse()
  1272. .slice(0, 11)
  1273. .map(album => ({item: album}))),
  1274. lazy: true
  1275. })}
  1276. <div class="grid-actions">
  1277. <a class="box grid-item" href="${C.GROUP_DIRECTORY}/${C.OFFICIAL_GROUP_DIRECTORY}/gallery/" style="--fg-color: #ffffff">Explore Official!</a>
  1278. </div>
  1279. </div>
  1280. `
  1281. },
  1282. sidebar: {
  1283. wide: true,
  1284. collapse: false,
  1285. content: fixWS`
  1286. <h1>Get involved!</h1>
  1287. <ul>
  1288. <li><a href="${C.FEEDBACK_DIRECTORY}/">Send feedback</a></li>
  1289. <li><a href="${C.DISCORD_DIRECTORY}/">Join the Discord server</a></li>
  1290. <li><a href="${C.DONATE_DIRECTORY}/">Donate</a> (<a href="https://www.patreon.com/qznebula">Patreon</a>, <a href="https://liberapay.com/nebula">Liberapay</a>)</li>
  1291. </ul>
  1292. <hr>
  1293. <h1>News</h1>
  1294. ${newsData.slice(0, 3).map((entry, i) => fixWS`
  1295. <article ${classes('news-entry', i === 0 && 'first-news-entry')}>
  1296. <h2><time>${getDateString(entry)}</time> <a href="${C.NEWS_DIRECTORY}/#${entry.id}">${entry.name}</a></h2>
  1297. ${entry.bodyShort}
  1298. ${entry.bodyShort !== entry.body && `<a href="${C.NEWS_DIRECTORY}/#${entry.id}">(View rest of entry!)</a>`}
  1299. </article>
  1300. `).join('\n')}
  1301. `
  1302. },
  1303. nav: {
  1304. content: fixWS`
  1305. <h2 class="dot-between-spans">
  1306. <span><a class="current" href="./">${SITE_SHORT_TITLE}</a></span>
  1307. <span><a href="${C.LISTING_DIRECTORY}/">Listings</a></span>
  1308. <span><a href="${C.NEWS_DIRECTORY}/">News</a></span>
  1309. <span><a href="${C.FLASH_DIRECTORY}/">Flashes &amp; Games</a></span>
  1310. <span><a href="${C.ABOUT_DIRECTORY}/">About &amp; Credits</a></span>
  1311. <span><a href="${C.FEEDBACK_DIRECTORY}/">Feedback &amp; Suggestions</a></span>
  1312. <span><a href="${C.DONATE_DIRECTORY}/">Donate</a></span>
  1313. </h2>
  1314. `
  1315. }
  1316. }),
  1317. mkdirp(path.join(C.SITE_DIRECTORY, 'albums', 'fandom'))
  1318. .then(() => writeFile(path.join(C.SITE_DIRECTORY, 'albums', 'fandom', 'index.html'),
  1319. generateRedirectPage('Fandom - Gallery', `/${C.GROUP_DIRECTORY}/fandom/gallery/`))),
  1320. mkdirp(path.join(C.SITE_DIRECTORY, 'albums', 'official'))
  1321. .then(() => writeFile(path.join(C.SITE_DIRECTORY, 'albums', 'official', 'index.html'),
  1322. generateRedirectPage('Official - Gallery', `/${C.GROUP_DIRECTORY}/official/gallery/`))),
  1323. writePage([C.FLASH_DIRECTORY], {
  1324. title: `Flashes & Games`,
  1325. main: {
  1326. classes: ['flash-index'],
  1327. content: fixWS`
  1328. <h1>Flashes &amp; Games</h1>
  1329. <div class="long-content">
  1330. <p class="quick-info">Jump to:</p>
  1331. <ul class="quick-info">
  1332. ${flashData.filter(act => act.act8r8k && act.jump).map(({ anchor, jump, jumpColor }) => fixWS`
  1333. <li><a href="#${anchor}" style="${getThemeString({color: jumpColor})}">${jump}</a></li>
  1334. `).join('\n')}
  1335. </ul>
  1336. </div>
  1337. ${flashData.filter(flash => flash.act8r8k).map((act, i) => fixWS`
  1338. <h2 id="${act.anchor}" style="${getThemeString(act)}"><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act === act.act))}/">${act.act}</a></h2>
  1339. <div class="grid-listing">
  1340. ${getFlashGridHTML({
  1341. entries: (flashData
  1342. .filter(flash => !flash.act8r8k && flash.act === act.act)
  1343. .map(flash => ({item: flash}))),
  1344. lazy: i === 0 ? 4 : true
  1345. })}
  1346. </div>
  1347. `).join('\n')}
  1348. `
  1349. },
  1350. /*
  1351. sidebar: {
  1352. content: generateSidebarForFlashes(null)
  1353. },
  1354. */
  1355. nav: {simple: true}
  1356. }),
  1357. writePage([C.ABOUT_DIRECTORY], {
  1358. title: `About &amp; Credits`,
  1359. main: {
  1360. content: fixWS`
  1361. <div class="long-content">
  1362. <h1>${SITE_TITLE}</h1>
  1363. ${transformMultiline(SITE_ABOUT, true)}
  1364. </div>
  1365. `
  1366. },
  1367. nav: {simple: true}
  1368. }),
  1369. writePage([C.CHANGELOG_DIRECTORY], {
  1370. title: `Changelog`,
  1371. main: {
  1372. content: fixWS`
  1373. <div class="long-content">
  1374. <h1>Changelog</h1>
  1375. ${transformMultiline(SITE_CHANGELOG, true)}
  1376. </div>
  1377. `
  1378. },
  1379. nav: {simple: true}
  1380. }),
  1381. writePage([C.FEEDBACK_DIRECTORY], {
  1382. title: `Feedback &amp; Suggestions!`,
  1383. main: {
  1384. content: fixWS`
  1385. <div class="long-content">
  1386. <h1>Feedback &amp; Suggestions!</h1>
  1387. ${SITE_FEEDBACK}
  1388. </div>
  1389. `
  1390. },
  1391. nav: {simple: true}
  1392. }),
  1393. writePage([C.DONATE_DIRECTORY], {
  1394. title: `Donate`,
  1395. main: {
  1396. content: fixWS`
  1397. <div class="long-content">
  1398. <h1>Donate</h1>
  1399. ${SITE_DONATE}
  1400. </div>
  1401. `
  1402. },
  1403. nav: {simple: true}
  1404. }),
  1405. writePage([C.DISCORD_DIRECTORY], {
  1406. title: `Discord`,
  1407. main: {
  1408. content: fixWS`
  1409. <div class="long-content">
  1410. <h1>HSMusic Community Discord Server</h1>
  1411. ${SITE_DISCORD}
  1412. </div>
  1413. `
  1414. },
  1415. nav: {simple: true}
  1416. }),
  1417. writePage([C.JS_DISABLED_DIRECTORY], {
  1418. title: 'JavaScript Disabled',
  1419. main: {
  1420. content: fixWS`
  1421. <h1>JavaScript Disabled (or out of date)</h1>
  1422. ${SITE_JS_DISABLED}
  1423. `
  1424. },
  1425. nav: {simple: true}
  1426. }),
  1427. writePage([C.NEWS_DIRECTORY], {
  1428. title: 'News',
  1429. main: {
  1430. content: fixWS`
  1431. <div class="long-content">
  1432. <h1>News</h1>
  1433. ${newsData.map(entry => fixWS`
  1434. <article id="${entry.id}">
  1435. <h2><a href="#${entry.id}">${getDateString(entry)} - ${entry.name}</a></h2>
  1436. ${transformMultiline(entry.body)}
  1437. </article>
  1438. `).join('\n')}
  1439. </div>
  1440. `
  1441. },
  1442. nav: {simple: true}
  1443. }),
  1444. writeFile(path.join(C.SITE_DIRECTORY, 'data.json'), fixWS`
  1445. {
  1446. "albumData": ${stringifyAlbumData()},
  1447. "flashData": ${stringifyFlashData()},
  1448. "artistData": ${stringifyArtistData()}
  1449. }
  1450. `)
  1451. ]);
  1452. }
  1453. function getRevealString(tags = []) {
  1454. return tags.some(tag => tag.isCW) && (
  1455. 'cw: ' + tags.filter(tag => tag.isCW).map(tag => `<span class="reveal-tag">${tag.name}</span>`).join(', ')) + '<br><span class="reveal-interaction">click to show</span>'
  1456. }
  1457. function generateCoverLink({
  1458. src,
  1459. alt,
  1460. tags = []
  1461. }) {
  1462. return fixWS`
  1463. <div id="cover-art-container">
  1464. ${img({
  1465. src,
  1466. alt,
  1467. id: 'cover-art',
  1468. link: true,
  1469. square: true,
  1470. reveal: getRevealString(tags)
  1471. })}
  1472. ${tags.filter(tag => !tag.isCW).length && `<p class="tags">Tags:
  1473. ${tags.filter(tag => !tag.isCW).map(tag => fixWS`
  1474. <a href="${C.TAG_DIRECTORY}/${tag.directory}/" style="${getThemeString(tag)}">${tag.name}</a>
  1475. `).join(',\n')}
  1476. </p>`}
  1477. </div>
  1478. `;
  1479. }
  1480. // This function title is my gr8test work of art.
  1481. // (The 8ehavior... well, um. Don't tell anyone, 8ut it's even 8etter.)
  1482. /* // RIP, 2k20-2k20.
  1483. function writeIndexAndTrackPagesForAlbum(album) {
  1484. return [
  1485. () => writeAlbumPage(album),
  1486. ...album.tracks.map(track => () => writeTrackPage(track))
  1487. ];
  1488. }
  1489. */
  1490. function writeAlbumPages() {
  1491. return progressPromiseAll(`Writing album pages.`, queue(albumData.map(curry(writeAlbumPage)), queueSize));
  1492. }
  1493. async function writeAlbumPage(album) {
  1494. const trackToListItem = track => fixWS`
  1495. <li style="${getThemeString(track)}">
  1496. (${getDurationString(track.duration)})
  1497. <a href="${C.TRACK_DIRECTORY}/${track.directory}/">${track.name}</a>
  1498. ${track.artists !== album.artists && fixWS`
  1499. <span class="by">by ${getArtistString(track.artists)}</span>
  1500. ` || `<!-- (here: Track-specific musician credits) -->`}
  1501. </li>
  1502. `;
  1503. const listTag = getAlbumListTag(album);
  1504. await writePage([C.ALBUM_DIRECTORY, album.directory], {
  1505. title: album.name,
  1506. stylesheet: getAlbumStylesheet(album),
  1507. theme: `${getThemeString(album)}; --album-directory: ${album.directory}`,
  1508. main: {
  1509. content: fixWS`
  1510. ${generateCoverLink({
  1511. src: getAlbumCover(album),
  1512. alt: 'album cover',
  1513. tags: album.artTags
  1514. })}
  1515. <h1>${album.name}</h1>
  1516. <p>
  1517. ${album.artists && `By ${getArtistString(album.artists, true)}.<br>` || `<!-- (here: Full-album musician credits) -->`}
  1518. ${album.coverArtists && `Cover art by ${getArtistString(album.coverArtists, true)}.<br>` || `<!-- (here: Cover art credits) -->`}
  1519. Released ${getDateString(album)}.
  1520. ${+album.coverArtDate !== +album.date && `<br>Art released ${getDateString({date: album.coverArtDate})}.` || `<!-- (here: Cover art release date) -->`}
  1521. <br>Duration: ~${getDurationString(getTotalDuration(album.tracks))}.</p>
  1522. </p>
  1523. ${album.urls.length && `<p>Listen on ${joinNoOxford(album.urls.map(url => fancifyURL(url, {album: true})), 'or')}.</p>` || `<!-- (here: Listen on...) -->`}
  1524. ${album.usesGroups ? fixWS`
  1525. <dl class="album-group-list">
  1526. ${album.tracks.flatMap((track, i, arr) => [
  1527. (i > 0 && track.group !== arr[i - 1].group) && `</${listTag}></dd>`,
  1528. (i === 0 || track.group !== arr[i - 1].group) && fixWS`
  1529. ${track.group && `<dt>${track.group} (~${getDurationString(getTotalDuration(album.tracks.filter(({ group }) => group === track.group)))}):</dt>`}
  1530. <dd><${listTag === 'ol' ? `ol start="${i + 1}"` : listTag}>
  1531. `,
  1532. trackToListItem(track),
  1533. i === arr.length && `</${listTag}></dd>`
  1534. ].filter(Boolean)).join('\n')}
  1535. </dl>
  1536. ` : fixWS`
  1537. <${listTag}>
  1538. ${album.tracks.map(trackToListItem).join('\n')}
  1539. </${listTag}>
  1540. `}
  1541. ${album.commentary && fixWS`
  1542. <p>Artist commentary:</p>
  1543. <blockquote>
  1544. ${transformMultiline(album.commentary)}
  1545. </blockquote>
  1546. ` || `<!-- (here: Full-album commentary) -->`}
  1547. `
  1548. },
  1549. sidebar: generateSidebarForAlbum(album),
  1550. sidebarRight: generateSidebarRightForAlbum(album),
  1551. nav: {
  1552. links: [
  1553. ['./', SITE_SHORT_TITLE],
  1554. [`${C.ALBUM_DIRECTORY}/${album.directory}/`, album.name],
  1555. [null, generateAlbumNavLinks(album)]
  1556. ],
  1557. content: fixWS`
  1558. <div>
  1559. ${generateAlbumChronologyLinks(album)}
  1560. </div>
  1561. `
  1562. }
  1563. });
  1564. }
  1565. function getAlbumStylesheet(album) {
  1566. if (album.wallpaperStyle) {
  1567. return fixWS`
  1568. body::before {
  1569. ${album.wallpaperStyle}
  1570. }
  1571. `;
  1572. } else {
  1573. return '';
  1574. }
  1575. }
  1576. function writeTrackPages() {
  1577. return progressPromiseAll(`Writing track pages.`, queue(trackData.map(curry(writeTrackPage)), queueSize));
  1578. }
  1579. async function writeTrackPage(track) {
  1580. const { album } = track;
  1581. const tracksThatReference = track.referencedBy;
  1582. const ttrFanon = tracksThatReference.filter(t => t.album.groups.every(group => group.directory !== C.OFFICIAL_GROUP_DIRECTORY));
  1583. const ttrOfficial = tracksThatReference.filter(t => t.album.groups.some(group => group.directory === C.OFFICIAL_GROUP_DIRECTORY));
  1584. const tracksReferenced = track.references;
  1585. const otherReleases = track.otherReleases;
  1586. const listTag = getAlbumListTag(track.album);
  1587. const flashesThatFeature = C.sortByDate([track, ...otherReleases]
  1588. .flatMap(track => track.flashes.map(flash => ({flash, as: track}))));
  1589. const generateTrackList = tracks => fixWS`
  1590. <ul>
  1591. ${tracks.map(track => fixWS`
  1592. <li ${classes(track.aka && 'rerelease')}>
  1593. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
  1594. <span class="by">by ${getArtistString(track.artists)}</span>
  1595. ${track.aka && `<span class="rerelease-label">(re-release)</span>`}
  1596. </li>
  1597. `).join('\n')}
  1598. </ul>
  1599. `;
  1600. const commentary = [
  1601. track.commentary,
  1602. ...otherReleases.map(track =>
  1603. (track.commentary?.split('\n')
  1604. .filter(line => line.replace(/<\/b>/g, '').includes(':</i>'))
  1605. .flatMap(line => [line, `<i>See <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>!</i>`])
  1606. .join('\n')))
  1607. ].filter(Boolean).join('\n');
  1608. await writePage([C.TRACK_DIRECTORY, track.directory], {
  1609. title: track.name,
  1610. stylesheet: getAlbumStylesheet(track.album),
  1611. theme: `${getThemeString(track)}; --album-directory: ${album.directory}; --track-directory: ${track.directory}`,
  1612. sidebar: generateSidebarForAlbum(album, track),
  1613. sidebarRight: generateSidebarRightForAlbum(album, track),
  1614. nav: {
  1615. links: [
  1616. ['./', SITE_SHORT_TITLE],
  1617. [`${C.ALBUM_DIRECTORY}/${album.directory}/`, album.name],
  1618. listTag === 'ol' && [null, album.tracks.indexOf(track) + 1 + '.'],
  1619. [`${C.TRACK_DIRECTORY}/${track.directory}/`, track.name],
  1620. [null, generateAlbumNavLinks(album, track)]
  1621. ].filter(Boolean),
  1622. content: fixWS`
  1623. <div>
  1624. ${generateAlbumChronologyLinks(album, track)}
  1625. </div>
  1626. `
  1627. },
  1628. main: {
  1629. content: fixWS`
  1630. ${generateCoverLink({
  1631. src: getTrackCover(track),
  1632. alt: 'track cover',
  1633. tags: track.artTags
  1634. })}
  1635. <h1>${track.name}</h1>
  1636. <p>
  1637. By ${getArtistString(track.artists, true)}.
  1638. ${track.coverArtists && `<br>Cover art by ${getArtistString(track.coverArtists, true)}.` || `<!-- (here: Cover art credits) -->`}
  1639. ${album.directory !== C.UNRELEASED_TRACKS_DIRECTORY && `<br>Released ${getDateString(track)}.` || `<!-- (here: Track release date) -->`}
  1640. ${+track.coverArtDate !== +track.date && `<br>Art released ${getDateString({date: track.coverArtDate})}.` || `<!-- (here: Cover art release date, if it differs) -->`}
  1641. ${track.duration && `<br>Duration: ${getDurationString(track.duration)}.` || `<!-- (here: Track duration) -->`}
  1642. </p>
  1643. ${track.urls.length ? fixWS`
  1644. <p>Listen on ${joinNoOxford(track.urls.map(fancifyURL), 'or')}.</p>
  1645. ` : fixWS`
  1646. <p>This track has no URLs at which it can be listened.</p>
  1647. `}
  1648. ${otherReleases.length && fixWS`
  1649. <p>Also released as:</p>
  1650. <ul>
  1651. ${otherReleases.map(track => fixWS`
  1652. <li>
  1653. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
  1654. (on <a href="${C.ALBUM_DIRECTORY}/${track.album.directory}/" style="${getThemeString(track.album)}">${track.album.name}</a>)
  1655. </li>
  1656. `).join('\n')}
  1657. </ul>
  1658. `}
  1659. ${track.contributors.textContent && fixWS`
  1660. <p>Contributors:<br>${transformInline(track.contributors.textContent)}</p>
  1661. `}
  1662. ${track.contributors.length && fixWS`
  1663. <p>Contributors:</p>
  1664. <ul>
  1665. ${track.contributors.map(contrib => `<li>${getArtistString([contrib], true)}</li>`).join('\n')}
  1666. </ul>
  1667. ` || `<!-- (here: Track contributor credits) -->`}
  1668. ${tracksReferenced.length && fixWS`
  1669. <p>Tracks that <i>${track.name}</i> references:</p>
  1670. ${generateTrackList(tracksReferenced)}
  1671. ` || `<!-- (here: List of tracks referenced) -->`}
  1672. ${tracksThatReference.length && fixWS`
  1673. <p>Tracks that reference <i>${track.name}</i>:</p>
  1674. <dl>
  1675. ${ttrOfficial.length && fixWS`
  1676. <dt>Official:</dt>
  1677. <dd>${generateTrackList(ttrOfficial)}</dd>
  1678. ` || `<!-- (here: Official tracks) -->`}
  1679. ${ttrFanon.length && fixWS`
  1680. <dt>Fandom:</dt>
  1681. <dd>${generateTrackList(ttrFanon)}</dd>
  1682. ` || `<!-- (here: Fandom tracks) -->`}
  1683. </dl>
  1684. ` || `<!-- (here: Tracks that reference this track) -->`}
  1685. ${flashesThatFeature.length && fixWS`
  1686. <p>Flashes &amp; games that feature <i>${track.name}</i>:</p>
  1687. <ul>
  1688. ${flashesThatFeature.map(({ flash, as }) => fixWS`
  1689. <li ${classes(as !== track && 'rerelease')}>
  1690. ${getFlashLinkHTML(flash)}
  1691. ${as !== track && fixWS`
  1692. (as <a href="${C.TRACK_DIRECTORY}/${as.directory}/" style="${getThemeString(as)}">${as.name}</a>)
  1693. `}
  1694. </li>
  1695. `).join('\n')}
  1696. </ul>
  1697. ` || `<!-- (here: Flashes that feature this track) -->`}
  1698. ${track.lyrics && fixWS`
  1699. <p>Lyrics:</p>
  1700. <blockquote>
  1701. ${transformLyrics(track.lyrics)}
  1702. </blockquote>
  1703. ` || `<!-- (here: Track lyrics) -->`}
  1704. ${commentary && fixWS`
  1705. <p>Artist commentary:</p>
  1706. <blockquote>
  1707. ${transformMultiline(commentary)}
  1708. </blockquote>
  1709. ` || `<!-- (here: Track commentary) -->`}
  1710. `
  1711. }
  1712. });
  1713. }
  1714. async function writeArtistPages() {
  1715. await progressPromiseAll('Writing artist pages.', queue(artistData.map(curry(writeArtistPage)), queueSize));
  1716. }
  1717. async function writeArtistPage(artist) {
  1718. if (artist.alias) {
  1719. return writeArtistAliasPage(artist);
  1720. }
  1721. const {
  1722. name,
  1723. urls = [],
  1724. note = ''
  1725. } = artist;
  1726. const artThings = justEverythingMan.filter(thing => (thing.coverArtists || []).some(({ who }) => who === artist));
  1727. const flashes = flashData.filter(flash => (flash.contributors || []).some(({ who }) => who === artist));
  1728. const commentaryThings = justEverythingMan.filter(thing => thing.commentary && thing.commentary.replace(/<\/?b>/g, '').includes('<i>' + name + ':</i>'));
  1729. const unreleasedTracks = [...artist.tracks.asArtist, ...artist.tracks.asContributor]
  1730. .filter(track => track.album.directory === C.UNRELEASED_TRACKS_DIRECTORY);
  1731. const releasedTracks = [...artist.tracks.asArtist, ...artist.tracks.asContributor]
  1732. .filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY);
  1733. const generateTrackList = tracks => albumChunkedList(tracks, (track, i) => {
  1734. const contrib = {
  1735. who: artist,
  1736. what: track.contributors.filter(({ who }) => who === artist).map(({ what }) => what).join(', ')
  1737. };
  1738. const { flashes } = track;
  1739. return fixWS`
  1740. <li ${classes(track.aka && 'rerelease')} title="${th(i + 1)} track by ${name}; ${th(track.album.tracks.indexOf(track) + 1)} in ${track.album.name}">
  1741. ${track.duration && `(${getDurationString(track.duration)})` || `<!-- (here: Duration) -->`}
  1742. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
  1743. ${track.artists.some(({ who }) => who === artist) && track.artists.length > 1 && `<span class="contributed">(with ${getArtistString(track.artists.filter(({ who }) => who !== artist))})</span>` || `<!-- (here: Co-artist credits) -->`}
  1744. ${contrib.what && `<span class="contributed">(${getContributionString(contrib) || 'contributed'})</span>` || `<!-- (here: Contribution details) -->`}
  1745. ${flashes.length && `<br><span class="flashes">(Featured in ${joinNoOxford(flashes.map(flash => getFlashLinkHTML(flash)))})</span></br>` || `<!-- (here: Flashes featuring this track) -->`}
  1746. ${track.aka && `<span class="rerelease-label">(re-release)</span>`}
  1747. </li>
  1748. `;
  1749. });
  1750. // Shish!
  1751. const kebab = C.getArtistDirectory(name);
  1752. const index = `${C.ARTIST_DIRECTORY}/${kebab}/`;
  1753. await writePage([C.ARTIST_DIRECTORY, kebab], {
  1754. title: name,
  1755. main: {
  1756. content: fixWS`
  1757. ${ENABLE_ARTIST_AVATARS && await access(path.join(C.ARTIST_AVATAR_DIRECTORY, kebab + '.jpg')).then(() => true, () => false) && fixWS`
  1758. <a id="cover-art" href="${C.ARTIST_AVATAR_DIRECTORY}/${C.getArtistDirectory(name)}.jpg"><img src="${ARTIST_AVATAR_DIRECTORY}/${C.getArtistDirectory(name)}.jpg" alt="Artist avatar"></a>
  1759. `}
  1760. <h1>${name}</h1>
  1761. ${note && fixWS`
  1762. <p>Note:</p>
  1763. <blockquote>
  1764. ${transformMultiline(note)}
  1765. </blockquote>
  1766. <hr>
  1767. `}
  1768. ${urls.length && `<p>Visit on ${joinNoOxford(urls.map(fancifyURL), 'or')}.</p>`}
  1769. ${artThings.length && `<p>View <a href="${C.ARTIST_DIRECTORY}/${kebab}/gallery/">art gallery</a>!</p>`}
  1770. <p>Jump to: ${[
  1771. [
  1772. [...releasedTracks, ...unreleasedTracks].length && `<a href="${index}#tracks">Tracks</a>`,
  1773. unreleasedTracks.length && `<a href="${index}#unreleased-tracks">(Unreleased Tracks)</a>`
  1774. ].filter(Boolean).join(' '),
  1775. artThings.length && `<a href="${index}#art">Art</a>`,
  1776. flashes.length && `<a href="${index}#flashes">Flashes &amp; Games</a>`,
  1777. commentaryThings.length && `<a href="${index}#commentary">Commentary</a>`
  1778. ].filter(Boolean).join(', ')}.</p>
  1779. ${[...releasedTracks, ...unreleasedTracks].length && fixWS`
  1780. <h2 id="tracks">Tracks</h2>
  1781. `}
  1782. ${releasedTracks.length && fixWS`
  1783. <p>${name} has contributed ~${getDurationString(getTotalDuration(releasedTracks))} ${getTotalDuration(releasedTracks) > 3600 ? 'hours' : 'minutes'} of music collected on this wiki.</p>
  1784. ${generateTrackList(releasedTracks)}
  1785. `}
  1786. ${unreleasedTracks.length && fixWS`
  1787. <h3 id="unreleased-tracks">Unreleased Tracks</h3>
  1788. ${generateTrackList(unreleasedTracks)}
  1789. `}
  1790. ${artThings.length && fixWS`
  1791. <h2 id="art">Art</h2>
  1792. <p>View <a href="${C.ARTIST_DIRECTORY}/${kebab}/gallery/">art gallery</a>! Or browse the list:</p>
  1793. ${albumChunkedList(artThings, (thing, i) => {
  1794. const contrib = thing.coverArtists.find(({ who }) => who === artist);
  1795. return fixWS`
  1796. <li title="${th(i + 1)} art by ${name}${thing.album && `; ${th(thing.album.tracks.indexOf(thing) + 1)} track in ${thing.album.name}`}">
  1797. ${thing.album ? fixWS`
  1798. <a href="${C.TRACK_DIRECTORY}/${thing.directory}/" style="${getThemeString(thing)}">${thing.name}</a>
  1799. ` : '<i>(cover art)</i>'}
  1800. ${thing.coverArtists.length > 1 && `<span class="contributed">(with ${getArtistString(thing.coverArtists.filter(({ who }) => who !== artist))})</span>`}
  1801. ${contrib.what && `<span class="contributed">(${getContributionString(contrib)})</span>`}
  1802. </li>
  1803. `;
  1804. }, true, 'coverArtDate')}
  1805. `}
  1806. ${flashes.length && fixWS`
  1807. <h2 id="flashes">Flashes &amp; Games</h2>
  1808. ${actChunkedList(flashes, flash => {
  1809. const contributionString = flash.contributors.filter(({ who }) => who === artist).map(getContributionString).join(' ');
  1810. return fixWS`
  1811. <li>
  1812. <a href="${C.FLASH_DIRECTORY}/${flash.directory}/" style="${getThemeString(flash)}">${flash.name}</a>
  1813. ${contributionString && `<span class="contributed">(${contributionString})</span>`}
  1814. (${getDateString({date: flash.date})})
  1815. </li>
  1816. `
  1817. })}
  1818. `}
  1819. ${commentaryThings.length && fixWS`
  1820. <h2 id="commentary">Commentary</h2>
  1821. ${albumChunkedList(commentaryThings, thing => {
  1822. const { flashes } = thing;
  1823. return fixWS`
  1824. <li>
  1825. ${thing.album ? fixWS`
  1826. <a href="${C.TRACK_DIRECTORY}/${thing.directory}/" style="${getThemeString(thing)}">${thing.name}</a>
  1827. ` : '(album commentary)'}
  1828. ${flashes?.length && `<br><span class="flashes">(Featured in ${joinNoOxford(flashes.map(flash => getFlashLinkHTML(flash)))})</span></br>`}
  1829. </li>
  1830. `
  1831. }, false)}
  1832. </ul>
  1833. `}
  1834. `
  1835. },
  1836. nav: {
  1837. links: [
  1838. ['./', SITE_SHORT_TITLE],
  1839. [`${C.LISTING_DIRECTORY}/`, 'Listings'],
  1840. [null, 'Artist:'],
  1841. [`${C.ARTIST_DIRECTORY}/${kebab}/`, name],
  1842. artThings.length && [null, `(${[
  1843. `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/" class="current">Info</a>`,
  1844. `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/gallery/">Gallery</a>`
  1845. ].join(', ')})`]
  1846. ]
  1847. }
  1848. });
  1849. if (artThings.length) {
  1850. await writePage([C.ARTIST_DIRECTORY, kebab, 'gallery'], {
  1851. title: name + ' - Gallery',
  1852. main: {
  1853. classes: ['top-index'],
  1854. content: fixWS`
  1855. <h1>${name} - Gallery</h1>
  1856. <p class="quick-info">(Contributed to ${s(artThings.length, 'cover art')})</p>
  1857. <div class="grid-listing">
  1858. ${getGridHTML({
  1859. entries: artThings.map(item => ({item})),
  1860. srcFn: thing => (thing.album
  1861. ? getTrackCover(thing)
  1862. : getAlbumCover(thing)),
  1863. hrefFn: thing => (thing.album
  1864. ? `${C.TRACK_DIRECTORY}/${thing.directory}/`
  1865. : `${C.ALBUM_DIRECTORY}/${thing.directory}`)
  1866. })}
  1867. </div>
  1868. `
  1869. },
  1870. nav: {
  1871. links: [
  1872. ['./', SITE_SHORT_TITLE],
  1873. [`${C.LISTING_DIRECTORY}/`, 'Listings'],
  1874. [null, 'Artist:'],
  1875. [`${C.ARTIST_DIRECTORY}/${kebab}/`, name],
  1876. [null, `(${[
  1877. `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/">Info</a>`,
  1878. `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/gallery/" class="current">Gallery</a>`
  1879. ].join(', ')})`]
  1880. ]
  1881. }
  1882. });
  1883. }
  1884. }
  1885. async function writeArtistAliasPage(artist) {
  1886. const { alias } = artist;
  1887. const directory = path.join(C.SITE_DIRECTORY, C.ARTIST_DIRECTORY, artist.directory);
  1888. const file = path.join(directory, 'index.html');
  1889. const target = `/${C.ARTIST_DIRECTORY}/${alias.directory}/`;
  1890. await mkdirp(directory);
  1891. await writeFile(file, generateRedirectPage(alias.name, target));
  1892. }
  1893. function generateRedirectPage(title, target) {
  1894. return fixWS`
  1895. <!DOCTYPE html>
  1896. <html>
  1897. <head>
  1898. <title>Moved to ${title}</title>
  1899. <meta charset="utf-8">
  1900. <meta http-equiv="refresh" content="0;url=${target}">
  1901. <link rel="canonical" href="${target}">
  1902. <link rel="stylesheet" href="static/site-basic.css">
  1903. </head>
  1904. <body>
  1905. <main>
  1906. <h1>Moved to ${title}</h1>
  1907. <p>This page has been moved to <a href="${target}">${target}</a>.</p>
  1908. </main>
  1909. </body>
  1910. </html>
  1911. `;
  1912. }
  1913. function albumChunkedList(tracks, getLI, showDate = true, datePropertyOrFn = 'date') {
  1914. const getAlbum = thing => thing.album ? thing.album : thing;
  1915. const dateFn = (typeof datePropertyOrFn === 'function'
  1916. ? datePropertyOrFn
  1917. : track => track[datePropertyOrFn]);
  1918. return fixWS`
  1919. <dl>
  1920. ${tracks.slice().sort((a, b) => dateFn(a) - dateFn(b)).map((thing, i, sorted) => {
  1921. const li = getLI(thing, i);
  1922. const album = getAlbum(thing);
  1923. const previous = sorted[i - 1];
  1924. if (i === 0 || album !== getAlbum(previous) || (showDate && +dateFn(thing) !== +dateFn(previous))) {
  1925. const heading = fixWS`
  1926. <dt>
  1927. <a href="${C.ALBUM_DIRECTORY}/${getAlbum(thing).directory}/" style="${getThemeString(getAlbum(thing))}">${getAlbum(thing).name}</a>
  1928. ${showDate && `(${getDateString({date: dateFn(thing)})})`}
  1929. </dt>
  1930. <dd><ul>
  1931. `;
  1932. if (i > 0) {
  1933. return ['</ul></dd>', heading, li];
  1934. } else {
  1935. return [heading, li];
  1936. }
  1937. } else {
  1938. return [li];
  1939. }
  1940. }).reduce((acc, arr) => acc.concat(arr), []).join('\n')}
  1941. </dl>
  1942. `;
  1943. }
  1944. function actChunkedList(flashes, getLI, showDate = true, dateProperty = 'date') {
  1945. return fixWS`
  1946. <dl>
  1947. ${flashes.slice().sort((a, b) => a[dateProperty] - b[dateProperty]).map((flash, i, sorted) => {
  1948. const li = getLI(flash, i);
  1949. const act = flash.act;
  1950. const previous = sorted[i - 1];
  1951. if (i === 0 || act !== previous.act) {
  1952. const heading = fixWS`
  1953. <dt>
  1954. <a href="${C.FLASH_DIRECTORY}/${sorted.find(flash => !flash.act8r8k && flash.act === act).directory}/" style="${getThemeString(flash)}">${flash.act}</a>
  1955. </dt>
  1956. <dd><ul>
  1957. `;
  1958. if (i > 0) {
  1959. return ['</ul></dd>', heading, li];
  1960. } else {
  1961. return [heading, li];
  1962. }
  1963. } else {
  1964. return [li];
  1965. }
  1966. }).reduce((acc, arr) => acc.concat(arr), []).join('\n')}
  1967. </dl>
  1968. `;
  1969. }
  1970. async function writeFlashPages() {
  1971. await progressPromiseAll('Writing Flash pages.', queue(flashData
  1972. .filter(flash => !flash.act8r8k)
  1973. .map(curry(writeFlashPage)), queueSize));
  1974. }
  1975. async function writeFlashPage(flash) {
  1976. const kebab = getFlashDirectory(flash);
  1977. const flashes = flashData.filter(flash => !flash.act8r8k);
  1978. const index = flashes.indexOf(flash);
  1979. const previous = flashes[index - 1];
  1980. const next = flashes[index + 1];
  1981. const parts = [
  1982. previous && `<a href="${getHrefOfAnythingMan(previous)}" id="previous-button" title="${previous.name}">Previous</a>`,
  1983. next && `<a href="${getHrefOfAnythingMan(next)}" id="next-button" title="${next.name}">Next</a>`
  1984. ].filter(Boolean);
  1985. await writePage([C.FLASH_DIRECTORY, kebab], {
  1986. title: flash.name,
  1987. theme: `${getThemeString(flash)}; --flash-directory: ${flash.directory}`,
  1988. main: {
  1989. content: fixWS`
  1990. <h1>${flash.name}</h1>
  1991. ${generateCoverLink({
  1992. src: getFlashCover(flash),
  1993. alt: 'cover art'
  1994. })}
  1995. <p>Released ${getDateString(flash)}.</p>
  1996. ${(flash.page || flash.urls.length) && `<p>Play on ${joinNoOxford(
  1997. [
  1998. flash.page && getFlashLink(flash),
  1999. ...flash.urls
  2000. ].map(url => fancifyFlashURL(url, flash)), 'or')}.</p>` || `<!-- (here: Play-online links) -->`}
  2001. ${flash.contributors.textContent && fixWS`
  2002. <p>Contributors:<br>${transformInline(flash.contributors.textContent)}</p>
  2003. `}
  2004. ${flash.tracks.length && fixWS`
  2005. <p>Tracks featured in <i>${flash.name.replace(/\.$/, '')}</i>:</p>
  2006. <ul>
  2007. ${flash.tracks.map(track => fixWS`
  2008. <li>
  2009. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
  2010. <span class="by">by ${getArtistString(track.artists)}</span>
  2011. </li>
  2012. `).join('\n')}
  2013. </ul>
  2014. ` || `<!-- (here: Flash track listing) -->`}
  2015. ${flash.contributors.length && fixWS`
  2016. <p>Contributors:</p>
  2017. <ul>
  2018. ${flash.contributors.map(contrib => fixWS`<li>${getArtistString([contrib], true)}</li>`).join('\n')}
  2019. </ul>
  2020. ` || `<!-- (here: Flash contributor details) -->`}
  2021. `
  2022. },
  2023. sidebar: {
  2024. content: generateSidebarForFlashes(flash)
  2025. },
  2026. nav: {
  2027. links: [
  2028. ['./', SITE_SHORT_TITLE],
  2029. [`${C.FLASH_DIRECTORY}/`, `Flashes &amp; Games`],
  2030. [`${C.FLASH_DIRECTORY}/${kebab}/`, flash.name],
  2031. parts.length && [null, `(${parts.join(', ')})`]
  2032. ].filter(Boolean),
  2033. content: fixWS`
  2034. <div>
  2035. ${chronologyLinks(flash, {
  2036. headingWord: 'flash/game',
  2037. sourceData: flashData,
  2038. filters: [
  2039. {
  2040. mapProperty: 'contributors',
  2041. toArtist: ({ who }) => who
  2042. }
  2043. ]
  2044. }) || `<!-- (here: Contributor chronology links) -->`}
  2045. </div>
  2046. `
  2047. }
  2048. });
  2049. }
  2050. function generateSidebarForFlashes(flash) {
  2051. const act6 = flashData.findIndex(f => f.act.startsWith('Act 6'));
  2052. const postCanon = flashData.findIndex(f => f.act.includes('Post Canon'));
  2053. const outsideCanon = postCanon + flashData.slice(postCanon).findIndex(f => !f.act.includes('Post Canon'));
  2054. const index = flashData.indexOf(flash);
  2055. const side = (
  2056. (index < 0) ? 0 :
  2057. (index < act6) ? 1 :
  2058. (index <= outsideCanon) ? 2 :
  2059. 3
  2060. );
  2061. const currentAct = flash && flash.act;
  2062. return fixWS`
  2063. <h1><a href="${C.FLASH_DIRECTORY}/">Flashes &amp; Games</a></h1>
  2064. <dl>
  2065. ${flashData.filter(f => f.act8r8k).filter(({ act }) =>
  2066. act.startsWith('Act 1') ||
  2067. act.startsWith('Act 6 Act 1') ||
  2068. act.startsWith('Hiveswap') ||
  2069. (
  2070. flashData.findIndex(f => f.act === act) < act6 ? side === 1 :
  2071. flashData.findIndex(f => f.act === act) < outsideCanon ? side === 2 :
  2072. true
  2073. )
  2074. ).flatMap(({ act, color }) => [
  2075. act.startsWith('Act 1') && `<dt ${classes('side', side === 1 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Act 1')))}/" style="--fg-color: #4ac925">Side 1 (Acts 1-5)</a></dt>`
  2076. || act.startsWith('Act 6 Act 1') && `<dt ${classes('side', side === 2 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Act 6')))}/" style="--fg-color: #1076a2">Side 2 (Acts 6-7)</a></dt>`
  2077. || act.startsWith('Hiveswap Act 1') && `<dt ${classes('side', side === 3 && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act.startsWith('Hiveswap')))}/" style="--fg-color: #008282">Outside Canon (Misc. Games)</a></dt>`,
  2078. (
  2079. flashData.findIndex(f => f.act === act) < act6 ? side === 1 :
  2080. flashData.findIndex(f => f.act === act) < outsideCanon ? side === 2 :
  2081. true
  2082. ) && `<dt ${classes(act === currentAct && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flashData.find(f => !f.act8r8k && f.act === act))}/" style="${getThemeString({color})}">${act}</a></dt>`,
  2083. act === currentAct && fixWS`
  2084. <dd><ul>
  2085. ${flashData.filter(f => !f.act8r8k && f.act === act).map(f => fixWS`
  2086. <li ${classes(f === flash && 'current')}><a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(f)}/" style="${getThemeString(f)}">${f.name}</a></li>
  2087. `).join('\n')}
  2088. </ul></dd>
  2089. `
  2090. ]).filter(Boolean).join('\n')}
  2091. </dl>
  2092. `;
  2093. }
  2094. function writeListingPages() {
  2095. const reversedTracks = trackData.slice().reverse();
  2096. const reversedThings = justEverythingMan.slice().reverse();
  2097. const getAlbumLI = (album, extraText = '') => fixWS`
  2098. <li>
  2099. <a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a>
  2100. ${extraText}
  2101. </li>
  2102. `;
  2103. const sortByName = (a, b) => {
  2104. let an = a.name.toLowerCase();
  2105. let bn = b.name.toLowerCase();
  2106. if (an.startsWith('the ')) an = an.slice(4);
  2107. if (bn.startsWith('the ')) bn = bn.slice(4);
  2108. return an < bn ? -1 : an > bn ? 1 : 0;
  2109. };
  2110. const listingDescriptors = [
  2111. [['albums', 'by-name'], `Albums - by Name`, albumData.slice()
  2112. .sort(sortByName)
  2113. .map(album => getAlbumLI(album, `(${album.tracks.length} tracks)`))],
  2114. [['albums', 'by-tracks'], `Albums - by Tracks`, albumData.slice()
  2115. .sort((a, b) => b.tracks.length - a.tracks.length)
  2116. .map(album => getAlbumLI(album, `(${s(album.tracks.length, 'track')})`))],
  2117. [['albums', 'by-duration'], `Albums - by Duration`, albumData.slice()
  2118. .map(album => ({album, duration: getTotalDuration(album.tracks)}))
  2119. .sort((a, b) => b.duration - a.duration)
  2120. .map(({ album, duration }) => getAlbumLI(album, `(${getDurationString(duration)})`))],
  2121. [['albums', 'by-date'], `Albums - by Date`, C.sortByDate(albumData.filter(album => album.directory !== C.UNRELEASED_TRACKS_DIRECTORY))
  2122. .map(album => getAlbumLI(album, `(${getDateString(album)})`))],
  2123. [['artists', 'by-name'], `Artists - by Name`, artistData
  2124. .filter(artist => !artist.alias)
  2125. .sort(sortByName)
  2126. .map(artist => fixWS`
  2127. <li>
  2128. <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/">${artist.name}</a>
  2129. (${'' + C.getArtistNumContributions(artist)} <abbr title="contributions (to music, art, and flashes)">c.</abbr>)
  2130. </li>
  2131. `)],
  2132. [['artists', 'by-contribs'], `Artists - by Contributions`, fixWS`
  2133. <div class="content-columns">
  2134. <div class="column">
  2135. <h2>Track Contributors</h2>
  2136. <ul>
  2137. ${artistData
  2138. .filter(artist => !artist.alias)
  2139. .map(artist => ({
  2140. name: artist.name,
  2141. contribs: (
  2142. artist.tracks.asContributor.length +
  2143. artist.tracks.asArtist.length
  2144. )
  2145. }))
  2146. .sort((a, b) => b.contribs - a.contribs)
  2147. .filter(({ contribs }) => contribs)
  2148. .map(({ name, contribs }) => fixWS`
  2149. <li>
  2150. <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}">${name}</a>
  2151. (${contribs} <abbr title="contributions (to track music)">c.</abbr>)
  2152. </li>
  2153. `)
  2154. .join('\n')
  2155. }
  2156. </ul>
  2157. </div>
  2158. <div class="column">
  2159. <h2>Art &amp; Flash Contributors</h2>
  2160. <ul>
  2161. ${artistData
  2162. .filter(artist => !artist.alias)
  2163. .map(artist => ({
  2164. artist,
  2165. contribs: (
  2166. artist.tracks.asCoverArtist.length +
  2167. artist.albums.asCoverArtist.length +
  2168. artist.flashes.asContributor.length
  2169. )
  2170. }))
  2171. .sort((a, b) => b.contribs - a.contribs)
  2172. .filter(({ contribs }) => contribs)
  2173. .map(({ artist, contribs }) => fixWS`
  2174. <li>
  2175. <a href="${C.ARTIST_DIRECTORY}/${artist.directory}">${artist.name}</a>
  2176. (${contribs} <abbr title="contributions (to art and flashes)">c.</abbr>)
  2177. </li>
  2178. `)
  2179. .join('\n')
  2180. }
  2181. </ul>
  2182. </div>
  2183. </div>
  2184. `],
  2185. [['artists', 'by-commentary'], `Artists - by Commentary Entries`, artistData
  2186. .filter(artist => !artist.alias)
  2187. .map(artist => ({artist, commentary: C.getArtistCommentary(artist, {justEverythingMan}).length}))
  2188. .filter(({ commentary }) => commentary > 0)
  2189. .sort((a, b) => b.commentary - a.commentary)
  2190. .map(({ artist, commentary }) => fixWS`
  2191. <li>
  2192. <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/#commentary">${artist.name}</a>
  2193. (${commentary} ${commentary === 1 ? 'entry' : 'entries'})
  2194. </li>
  2195. `)],
  2196. [['artists', 'by-duration'], `Artists - by Duration`, artistData
  2197. .filter(artist => !artist.alias)
  2198. .map(artist => ({artist, duration: getTotalDuration(
  2199. [...artist.tracks.asArtist, ...artist.tracks.asContributor].filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY))
  2200. }))
  2201. .filter(({ duration }) => duration > 0)
  2202. .sort((a, b) => b.duration - a.duration)
  2203. .map(({ artist, duration }) => fixWS`
  2204. <li>
  2205. <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/#tracks">${artist.name}</a>
  2206. (~${getDurationString(duration)})
  2207. </li>
  2208. `)],
  2209. [['artists', 'by-latest'], `Artists - by Latest Contribution`, fixWS`
  2210. <div class="content-columns">
  2211. <div class="column">
  2212. <h2>Track Contributors</h2>
  2213. <ul>
  2214. ${C.sortByDate(artistData
  2215. .filter(artist => !artist.alias)
  2216. .map(artist => ({
  2217. artist,
  2218. date: reversedTracks.find(({ album, artists, contributors }) => (
  2219. album.directory !== C.UNRELEASED_TRACKS_DIRECTORY &&
  2220. [...artists, ...contributors].some(({ who }) => who === artist)
  2221. ))?.date
  2222. }))
  2223. .filter(({ date }) => date)
  2224. .sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0)
  2225. ).reverse().map(({ artist, date }) => fixWS`
  2226. <li>
  2227. <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/">${artist.name}</a>
  2228. (${getDateString({date})})
  2229. </li>
  2230. `).join('\n')}
  2231. </ul>
  2232. </div>
  2233. <div class="column">
  2234. <h2>Art &amp; Flash Contributors</h2>
  2235. <ul>
  2236. ${C.sortByDate(artistData
  2237. .filter(artist => !artist.alias)
  2238. .map(artist => {
  2239. const thing = reversedThings.find(({ album, coverArtists, contributors }) => (
  2240. album?.directory !== C.UNRELEASED_TRACKS_DIRECTORY &&
  2241. [...coverArtists || [], ...!album && contributors || []].some(({ who }) => who === artist)
  2242. ));
  2243. return thing && {
  2244. artist,
  2245. date: (thing.coverArtists?.some(({ who }) => who === artist)
  2246. ? thing.coverArtDate
  2247. : thing.date)
  2248. };
  2249. })
  2250. .filter(Boolean)
  2251. .sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0)
  2252. ).reverse().map(({ artist, date }) => fixWS`
  2253. <li>
  2254. <a href="${C.ARTIST_DIRECTORY}/${artist.directory}">${artist.name}</a>
  2255. (${getDateString({date})})
  2256. </li>
  2257. `).join('\n')}
  2258. </ul>
  2259. </div>
  2260. </div>
  2261. `],
  2262. [['groups', 'by-name'], `Groups - by Name`, groupData
  2263. .filter(x => x.isGroup)
  2264. .sort(sortByName)
  2265. .map(group => fixWS`
  2266. <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a></li>
  2267. `)],
  2268. [['groups', 'by-category'], `Groups - by Category`, fixWS`
  2269. <dl>
  2270. ${groupData.filter(x => x.isCategory).map(category => fixWS`
  2271. <dt><a href="${C.GROUP_DIRECTORY}/${category.groups[0].directory}/" style="${getThemeString(category)}">${category.name}</a></li>
  2272. <dd><ul>
  2273. ${category.groups.map(group => fixWS`
  2274. <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/" style="${getThemeString(group)}">${group.name}</a></li>
  2275. `).join('\n')}
  2276. </ul></dd>
  2277. `).join('\n')}
  2278. </dl>
  2279. `],
  2280. [['groups', 'by-albums'], `Groups - by Albums`, groupData
  2281. .filter(x => x.isGroup)
  2282. .map(group => ({group, albums: group.albums.length}))
  2283. .sort((a, b) => b.albums - a.albums)
  2284. .map(({ group, albums }) => fixWS`
  2285. <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a> (${s(albums, 'album')})</li>
  2286. `)],
  2287. [['groups', 'by-tracks'], `Groups - by Tracks`, groupData
  2288. .filter(x => x.isGroup)
  2289. .map(group => ({group, tracks: group.albums.reduce((acc, album) => acc + album.tracks.length, 0)}))
  2290. .sort((a, b) => b.tracks - a.tracks)
  2291. .map(({ group, tracks }) => fixWS`
  2292. <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a> (${s(tracks, 'track')})</li>
  2293. `)],
  2294. [['groups', 'by-duration'], `Groups - by Duration`, groupData
  2295. .filter(x => x.isGroup)
  2296. .map(group => ({group, duration: getTotalDuration(group.albums.flatMap(album => album.tracks))}))
  2297. .sort((a, b) => b.duration - a.duration)
  2298. .map(({ group, duration }) => fixWS`
  2299. <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a> (${getDurationString(duration)})</li>
  2300. `)],
  2301. [['groups', 'by-latest'], `Groups - by Latest Album`, C.sortByDate(groupData
  2302. .filter(x => x.isGroup)
  2303. .map(group => ({group, date: group.albums[group.albums.length - 1].date}))
  2304. // So this is kinda tough to explain, 8ut 8asically, when we reverse the list after sorting it 8y d8te
  2305. // (so that the latest d8tes come first), it also flips the order of groups which share the same d8te.
  2306. // This happens mostly when a single al8um is the l8test in two groups. So, say one such al8um is in
  2307. // the groups "Fandom" and "UMSPAF". Per category order, Fandom is meant to show up 8efore UMSPAF, 8ut
  2308. // when we do the reverse l8ter, that flips them, and UMSPAF ends up displaying 8efore Fandom. So we do
  2309. // an extra reverse here, which will fix that and only affect groups that share the same d8te (8ecause
  2310. // groups that don't will 8e moved 8y the sortByDate call surrounding this).
  2311. .reverse()
  2312. ).reverse().map(({ group, date }) => fixWS`
  2313. <li>
  2314. <a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a>
  2315. (${getDateString({date})})
  2316. </li>
  2317. `)],
  2318. [['tracks', 'by-name'], `Tracks - by Name`, trackData.slice()
  2319. .sort(sortByName)
  2320. .map(track => fixWS`
  2321. <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>
  2322. `)],
  2323. [['tracks', 'by-album'], `Tracks - by Album`, fixWS`
  2324. <dl>
  2325. ${albumData.map(album => fixWS`
  2326. <dt><a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a></dt>
  2327. <dd><ol>
  2328. ${album.tracks.map(track => fixWS`
  2329. <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>
  2330. `).join('\n')}
  2331. </ol></dd>
  2332. `).join('\n')}
  2333. </dl>
  2334. `],
  2335. [['tracks', 'by-date'], `Tracks - by Date`, albumChunkedList(
  2336. C.sortByDate(trackData.filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY)),
  2337. track => fixWS`
  2338. <li ${classes(track.aka && 'rerelease')}><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a> ${track.aka && `<span class="rerelease-label">(re-release)</span>`}</li>
  2339. `)],
  2340. [['tracks', 'by-duration'], `Tracks - by Duration`, C.sortByDate(trackData.slice())
  2341. .filter(track => track.duration > 0)
  2342. .sort((a, b) => b.duration - a.duration)
  2343. .map(track => fixWS`
  2344. <li>
  2345. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
  2346. (${getDurationString(track.duration)})
  2347. </li>
  2348. `)],
  2349. [['tracks', 'by-duration-in-album'], `Tracks - by Duration (in Album)`, albumChunkedList(albumData.flatMap(album => album.tracks)
  2350. .filter(track => track.duration > 0)
  2351. .sort((a, b) => (
  2352. b.album !== a.album ? 0 :
  2353. b.duration - a.duration
  2354. )),
  2355. track => fixWS`
  2356. <li>
  2357. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
  2358. (${getDurationString(track.duration)})
  2359. </li>
  2360. `,
  2361. false,
  2362. null)],
  2363. [['tracks', 'by-times-referenced'], `Tracks - by Times Referenced`, C.sortByDate(trackData.slice())
  2364. .filter(track => track.referencedBy.length > 0)
  2365. .sort((a, b) => b.referencedBy.length - a.referencedBy.length)
  2366. .map(track => fixWS`
  2367. <li>
  2368. <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a>
  2369. (${s(track.referencedBy.length, 'time')} referenced)
  2370. </li>
  2371. `)],
  2372. [['tracks', 'in-flashes', 'by-album'], `Tracks - in Flashes &amp; Games (by Album)`, albumChunkedList(
  2373. C.sortByDate(trackData.slice()).filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY && track.flashes.length > 0),
  2374. track => `<li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>`)],
  2375. [['tracks', 'in-flashes', 'by-flash'], `Tracks - in Flashes &amp; Games (by Flash)`, fixWS`
  2376. <dl>
  2377. ${C.sortByDate(flashData.filter(flash => !flash.act8r8k))
  2378. .map(flash => fixWS`
  2379. <dt>
  2380. <a href="${C.FLASH_DIRECTORY}/${flash.directory}/" style="${getThemeString(flash)}">${flash.name}</a>
  2381. (${getDateString(flash)})
  2382. </dt>
  2383. <dd><ul>
  2384. ${flash.tracks.map(track => fixWS`
  2385. <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>
  2386. `).join('\n')}
  2387. </ul></dd>
  2388. `)
  2389. .join('\n')}
  2390. </dl>
  2391. `],
  2392. [['tracks', 'with-lyrics'], `Tracks - with Lyrics`, albumChunkedList(
  2393. C.sortByDate(trackData.slice())
  2394. .filter(track => track.lyrics),
  2395. track => fixWS`
  2396. <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>
  2397. `)],
  2398. [['tags', 'by-name'], 'Tags - by Name', tagData.slice().sort(sortByName)
  2399. .filter(tag => !tag.isCW)
  2400. .map(tag => `<li><a href="${C.TAG_DIRECTORY}/${tag.directory}/" style="${getThemeString(tag)}">${tag.name}</a></li>`)],
  2401. [['tags', 'by-uses'], 'Tags - by Uses', tagData.slice().sort(sortByName)
  2402. .filter(tag => !tag.isCW)
  2403. .map(tag => ({tag, timesUsed: tag.things.length}))
  2404. .sort((a, b) => b.timesUsed - a.timesUsed)
  2405. .map(({ tag, timesUsed }) => `<li><a href="${C.TAG_DIRECTORY}/${tag.directory}/" style="${getThemeString(tag)}">${tag.name}</a> (${s(timesUsed, 'time')})</li>`)]
  2406. ];
  2407. const getWordCount = str => {
  2408. const wordCount = str.split(' ').length;
  2409. return `${Math.floor(wordCount / 100) / 10}k`;
  2410. };
  2411. const releasedTracks = trackData.filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY);
  2412. const releasedAlbums = albumData.filter(album => album.directory !== C.UNRELEASED_TRACKS_DIRECTORY);
  2413. return progressPromiseAll(`Writing listing pages.`, [
  2414. writePage([C.LISTING_DIRECTORY], {
  2415. title: `Listings Index`,
  2416. main: {
  2417. content: fixWS`
  2418. <h1>Listings</h1>
  2419. <p>${SITE_TITLE}: <b>${releasedTracks.length}</b> tracks across <b>${releasedAlbums.length}</b> albums, totaling <b>~${getDurationString(getTotalDuration(releasedTracks))}</b> ${getTotalDuration(releasedTracks) > 3600 ? 'hours' : 'minutes'}.</p>
  2420. <hr>
  2421. <p>Feel free to explore any of the listings linked below and in the sidebar!</p>
  2422. ${generateLinkIndexForListings(listingDescriptors)}
  2423. `
  2424. },
  2425. sidebar: {
  2426. content: generateSidebarForListings(listingDescriptors)
  2427. },
  2428. nav: {
  2429. links: [
  2430. ['./', SITE_SHORT_TITLE],
  2431. [`${C.LISTINGS_DIRECTORY}/`, 'Listings']
  2432. ]
  2433. }
  2434. }),
  2435. writePage([C.LISTING_DIRECTORY, 'all-commentary'], {
  2436. title: 'All Commentary',
  2437. main: {
  2438. content: fixWS`
  2439. <h1>All Commentary</h1>
  2440. <p><strong>${getWordCount(albumData.reduce((acc, a) => acc + [a, ...a.tracks].filter(x => x.commentary).map(x => x.commentary).join(' '), ''))}</strong> words, in all.<br>Jump to a particular album:</p>
  2441. <ul>
  2442. ${C.sortByDate(albumData.slice())
  2443. .filter(album => [album, ...album.tracks].some(x => x.commentary))
  2444. .map(album => fixWS`
  2445. <li>
  2446. <a href="${C.LISTING_DIRECTORY}/all-commentary/#${album.directory}" style="${getThemeString(album)}">${album.name}</a>
  2447. (${(() => {
  2448. const things = [album, ...album.tracks];
  2449. const cThings = things.filter(x => x.commentary);
  2450. // const numStr = album.tracks.every(t => t.commentary) ? 'full commentary' : `${cThings.length} entries`;
  2451. const numStr = `${cThings.length}/${things.length} entries`;
  2452. return `${numStr}; ${getWordCount(cThings.map(x => x.commentary).join(' '))} words`;
  2453. })()})
  2454. </li>
  2455. `)
  2456. .join('\n')
  2457. }
  2458. </ul>
  2459. ${C.sortByDate(albumData.slice())
  2460. .map(album => [album, ...album.tracks])
  2461. .filter(x => x.some(y => y.commentary))
  2462. .map(([ album, ...tracks ]) => fixWS`
  2463. <h2 id="${album.directory}"><a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a></h2>
  2464. ${album.commentary && fixWS`
  2465. <blockquote style="${getThemeString(album)}">
  2466. ${transformMultiline(album.commentary)}
  2467. </blockquote>
  2468. ` || `<!-- (here: Full-album commentary) -->`}
  2469. ${tracks.filter(t => t.commentary).map(track => fixWS`
  2470. <h3 id="${track.directory}"><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></h3>
  2471. <blockquote style="${getThemeString(track)}">
  2472. ${transformMultiline(track.commentary)}
  2473. </blockquote>
  2474. `).join('\n') || `<!-- (here: Per-track commentary) -->`}
  2475. `)
  2476. .join('\n')
  2477. }
  2478. `
  2479. },
  2480. sidebar: {
  2481. content: generateSidebarForListings(listingDescriptors, 'all-commentary')
  2482. },
  2483. nav: {
  2484. links: [
  2485. ['./', SITE_SHORT_TITLE],
  2486. [`${C.LISTING_DIRECTORY}/`, 'Listings'],
  2487. [`${C.LISTING_DIRECTORY}/all-commentary`, 'All Commentary']
  2488. ]
  2489. }
  2490. }),
  2491. writePage([C.LISTING_DIRECTORY, 'random'], {
  2492. title: 'Random Pages',
  2493. main: {
  2494. content: fixWS`
  2495. <h1>Random Pages</h1>
  2496. <p>Choose a link to go to a random page in that category or album! If your browser doesn't support relatively modern JavaScript or you've disabled it, these links won't work - sorry.</p>
  2497. <p class="js-hide-once-data">(Data files are downloading in the background! Please wait for data to load.)</p>
  2498. <p class="js-show-once-data">(Data files have finished being downloaded. The links should work!)</p>
  2499. <dl>
  2500. <dt>Miscellaneous:</dt>
  2501. <dd><ul>
  2502. <li>
  2503. <a href="${C.JS_DISABLED_DIRECTORY}/" data-random="artist">Random Artist</a>
  2504. (<a href="${C.JS_DISABLED_DIRECTORY}/" data-random="artist-more-than-one-contrib">&gt;1 contribution</a>)
  2505. </li>
  2506. <li><a href="${C.JS_DISABLED_DIRECTORY}/" data-random="album">Random Album (whole site)</a></li>
  2507. <li><a href="${C.JS_DISABLED_DIRECTORY}/" data-random="track">Random Track (whole site)</a></li>
  2508. </ul></dd>
  2509. ${[
  2510. {name: 'Official', albumData: officialAlbumData, code: 'official'},
  2511. {name: 'Fandom', albumData: fandomAlbumData, code: 'fandom'}
  2512. ].map(category => fixWS`
  2513. <dt>${category.name}: (<a href="${C.JS_DISABLED_DIRECTORY}/" data-random="album-in-${category.code}">Random Album</a>, <a href="${C.JS_DISABLED_DIRECTORY}/" data-random="track-in-${category.code}">Random Track</a>)</dt>
  2514. <dd><ul>${category.albumData.map(album => fixWS`
  2515. <li><a style="${getThemeString(album)}; --album-directory: ${album.directory}" href="${C.JS_DISABLED_DIRECTORY}/" data-random="track-in-album">${album.name}</a></li>
  2516. `).join('\n')}</ul></dd>
  2517. `).join('\n')}
  2518. </dl>
  2519. `
  2520. },
  2521. sidebar: {
  2522. content: generateSidebarForListings(listingDescriptors, 'all-commentary')
  2523. },
  2524. nav: {
  2525. links: [
  2526. ['./', SITE_SHORT_TITLE],
  2527. [`${C.LISTING_DIRECTORY}/`, 'Listings'],
  2528. [`${C.LISTING_DIRECTORY}/random`, 'Random Pages']
  2529. ]
  2530. }
  2531. }),
  2532. ...listingDescriptors.map(entry => writeListingPage(...entry, listingDescriptors))
  2533. ]);
  2534. }
  2535. function writeListingPage(directoryParts, title, items, listingDescriptors) {
  2536. return writePage([C.LISTING_DIRECTORY, ...directoryParts], {
  2537. title,
  2538. main: {
  2539. content: fixWS`
  2540. <h1>${title}</h1>
  2541. ${typeof items === 'string' ? items : fixWS`
  2542. <ul>
  2543. ${items.join('\n')}
  2544. </ul>
  2545. `}
  2546. `
  2547. },
  2548. sidebar: {
  2549. content: generateSidebarForListings(listingDescriptors, directoryParts)
  2550. },
  2551. nav: {
  2552. links: [
  2553. ['./', SITE_SHORT_TITLE],
  2554. [`${C.LISTING_DIRECTORY}/`, 'Listings'],
  2555. [`${C.LISTING_DIRECTORY}/${directoryParts.join('/')}/`, title]
  2556. ]
  2557. }
  2558. });
  2559. }
  2560. function generateSidebarForListings(listingDescriptors, currentDirectoryParts) {
  2561. return fixWS`
  2562. <h1><a href="${C.LISTING_DIRECTORY}/">Listings</a></h1>
  2563. ${generateLinkIndexForListings(listingDescriptors, currentDirectoryParts)}
  2564. `;
  2565. }
  2566. function generateLinkIndexForListings(listingDescriptors, currentDirectoryParts) {
  2567. return fixWS`
  2568. <ul>
  2569. ${listingDescriptors.map(([ ldDirectoryParts, ldTitle ]) => fixWS`
  2570. <li ${classes(currentDirectoryParts === ldDirectoryParts && 'current')}>
  2571. <a href="${C.LISTING_DIRECTORY}/${ldDirectoryParts.join('/')}/">${ldTitle}</a>
  2572. </li>
  2573. `).join('\n')}
  2574. <li ${classes(currentDirectoryParts === 'all-commentary' && 'current')}>
  2575. <a href="${C.LISTING_DIRECTORY}/all-commentary/">All Commentary</a>
  2576. </li>
  2577. <li ${classes(currentDirectoryParts === 'random' && 'current')}>
  2578. <a href="${C.LISTING_DIRECTORY}/random/">Random Pages</a>
  2579. </li>
  2580. </ul>
  2581. `;
  2582. }
  2583. function writeTagPages() {
  2584. return progressPromiseAll(`Writing tag pages.`, queue(tagData
  2585. .filter(tag => !tag.isCW)
  2586. .map(curry(writeTagPage)), queueSize));
  2587. }
  2588. function writeTagPage(tag) {
  2589. const { things } = tag;
  2590. return writePage([C.TAG_DIRECTORY, tag.directory], {
  2591. title: tag.name,
  2592. theme: getThemeString(tag),
  2593. main: {
  2594. classes: ['top-index'],
  2595. content: fixWS`
  2596. <h1>${tag.name}</h1>
  2597. <p class="quick-info">(Appears in ${s(things.length, 'cover art')})</p>
  2598. <div class="grid-listing">
  2599. ${getGridHTML({
  2600. entries: things.map(item => ({item})),
  2601. srcFn: thing => (thing.album
  2602. ? getTrackCover(thing)
  2603. : getAlbumCover(thing)),
  2604. hrefFn: thing => (thing.album
  2605. ? `${C.TRACK_DIRECTORY}/${thing.directory}/`
  2606. : `${C.ALBUM_DIRECTORY}/${thing.directory}`)
  2607. })}
  2608. </div>
  2609. `
  2610. },
  2611. nav: {
  2612. links: [
  2613. ['./', SITE_SHORT_TITLE],
  2614. [`${C.LISTING_DIRECTORY}/`, 'Listings'],
  2615. [null, 'Tag:'],
  2616. [`${C.TAG_DIRECTORY}/${tag.directory}/`, tag.name]
  2617. ]
  2618. }
  2619. });
  2620. }
  2621. // This function is terri8le. Sorry!
  2622. function getContributionString({ what }) {
  2623. return what
  2624. ? what.replace(/\[(.*?)\]/g, (match, name) =>
  2625. trackData.some(track => track.name === name)
  2626. ? `<i><a href="${C.TRACK_DIRECTORY}/${trackData.find(track => track.name === name).directory}/">${name}</a></i>`
  2627. : `<i>${name}</i>`)
  2628. : '';
  2629. }
  2630. function getLinkedTrack(ref) {
  2631. if (!ref) return null;
  2632. if (ref.includes('track:')) {
  2633. ref = ref.replace('track:', '');
  2634. return trackData.find(track => track.directory === ref);
  2635. }
  2636. const match = ref.match(/\S:(.*)/);
  2637. if (match) {
  2638. const dir = match[1];
  2639. return trackData.find(track => track.directory === dir);
  2640. }
  2641. let track;
  2642. track = trackData.find(track => track.directory === ref);
  2643. if (track) {
  2644. return track;
  2645. }
  2646. track = trackData.find(track => track.name === ref);
  2647. if (track) {
  2648. return track;
  2649. }
  2650. track = trackData.find(track => track.name.toLowerCase() === ref.toLowerCase());
  2651. if (track) {
  2652. console.warn(`\x1b[33mBad capitalization:\x1b[0m`);
  2653. console.warn(`\x1b[31m- ${ref}\x1b[0m`);
  2654. console.warn(`\x1b[32m+ ${track.name}\x1b[0m`);
  2655. return track;
  2656. }
  2657. return null;
  2658. }
  2659. function getLinkedAlbum(ref) {
  2660. if (!ref) return null;
  2661. ref = ref.replace('album:', '');
  2662. let album;
  2663. album = albumData.find(album => album.directory === ref);
  2664. if (!album) album = albumData.find(album => album.name === ref);
  2665. if (!album) {
  2666. album = albumData.find(album => album.name.toLowerCase() === ref.toLowerCase());
  2667. if (album) {
  2668. console.warn(`\x1b[33mBad capitalization:\x1b[0m`);
  2669. console.warn(`\x1b[31m- ${ref}\x1b[0m`);
  2670. console.warn(`\x1b[32m+ ${album.name}\x1b[0m`);
  2671. return album;
  2672. }
  2673. }
  2674. return album;
  2675. }
  2676. function getLinkedGroup(ref) {
  2677. if (!ref) return null;
  2678. ref = ref.replace('group:', '');
  2679. let group;
  2680. group = groupData.find(group => group.directory === ref);
  2681. if (!group) group = groupData.find(group => group.name === ref);
  2682. if (!group) {
  2683. group = groupData.find(group => group.name.toLowerCase() === ref.toLowerCase());
  2684. if (group) {
  2685. console.warn(`\x1b[33mBad capitalization:\x1b[0m`);
  2686. console.warn(`\x1b[31m- ${ref}\x1b[0m`);
  2687. console.warn(`\x1b[32m+ ${group.name}\x1b[0m`);
  2688. return group;
  2689. }
  2690. }
  2691. return group;
  2692. }
  2693. function getLinkedArtist(ref) {
  2694. if (!ref) return null;
  2695. ref = ref.replace('artist:', '');
  2696. let artist = artistData.find(artist => C.getArtistDirectory(artist.name) === ref);
  2697. if (artist) {
  2698. return artist;
  2699. }
  2700. artist = artistData.find(artist => artist.name === ref);
  2701. if (artist) {
  2702. return artist;
  2703. }
  2704. return null;
  2705. }
  2706. function getLinkedFlash(ref) {
  2707. if (!ref) return null;
  2708. ref = ref.replace('flash:', '');
  2709. return flashData.find(flash => flash.directory === ref);
  2710. }
  2711. function getLinkedTag(ref) {
  2712. if (!ref) return null;
  2713. ref = ref.replace('tag:', '');
  2714. let tag = tagData.find(tag => tag.directory === ref);
  2715. if (tag) {
  2716. return tag;
  2717. }
  2718. if (ref.startsWith('cw: ')) {
  2719. ref = ref.slice(4);
  2720. }
  2721. tag = tagData.find(tag => tag.name === ref);
  2722. if (tag) {
  2723. return tag;
  2724. }
  2725. return null;
  2726. }
  2727. function getArtistString(artists, showIcons = false) {
  2728. return joinNoOxford(artists.map(({ who, what }) => {
  2729. if (!who) console.log(artists);
  2730. const { urls, directory, name } = who;
  2731. return (
  2732. `<a href="${C.ARTIST_DIRECTORY}/${directory}/">${name}</a>` +
  2733. (what ? ` (${getContributionString({what})})` : '') +
  2734. (showIcons && urls.length ? ` <span class="icons">(${urls.map(iconifyURL).join(', ')})</span>` : '')
  2735. );
  2736. }));
  2737. }
  2738. /*
  2739. function getThemeString({fg, bg, theme}) {
  2740. return [
  2741. [fg, `--fg-color: ${fg}`],
  2742. [bg, `--bg-color: ${bg}`],
  2743. [theme, `--theme: ${theme + ''}`]
  2744. ].filter(pair => pair[0] !== undefined).map(pair => pair[1]).join('; ');
  2745. }
  2746. */
  2747. // Graciously stolen from https://stackoverflow.com/a/54071699! ::::)
  2748. // in: r,g,b in [0,1], out: h in [0,360) and s,l in [0,1]
  2749. function rgb2hsl(r,g,b) {
  2750. let a=Math.max(r,g,b), n=a-Math.min(r,g,b), f=(1-Math.abs(a+a-n-1));
  2751. let h= n && ((a==r) ? (g-b)/n : ((a==g) ? 2+(b-r)/n : 4+(r-g)/n));
  2752. return [60*(h<0?h+6:h), f ? n/f : 0, (a+a-n)/2];
  2753. }
  2754. function getThemeString(thing) {
  2755. const {color} = thing;
  2756. const [ r, g, b ] = color.slice(1)
  2757. .match(/[0-9a-fA-F]{2,2}/g)
  2758. .slice(0, 3)
  2759. .map(val => parseInt(val, 16) / 255);
  2760. const [ h, s, l ] = rgb2hsl(r, g, b);
  2761. const dim = `hsl(${Math.round(h)}deg, ${Math.round(s * 50)}%, ${Math.round(l * 80)}%)`;
  2762. const album = (
  2763. trackData.includes(thing) ? thing.album :
  2764. albumData.includes(thing) ? thing :
  2765. null
  2766. );
  2767. let bgUrl = '';
  2768. if (album?.wallpaperArtists) {
  2769. // The 8ack-directory (..) here is necessary 8ecause CSS doesn't want
  2770. // to consider the fact that this is, like, not talking a8out a URL
  2771. // relative to the CSS source file. Really, what SHOULD 8e happening
  2772. // here is, we use path.relative to get the URL relative to the HTML
  2773. // file! 8ut I guess that's not what CSS spec says, or whatever.
  2774. // Pretty cringe t8h.
  2775. bgUrl = `../${C.MEDIA_DIRECTORY}/${C.MEDIA_ALBUM_ART_DIRECTORY}/${album.directory}/bg.jpg`;
  2776. }
  2777. return [
  2778. color && `--fg-color: ${color}; --dim-color: ${dim}`,
  2779. bgUrl && `--bg: url("${bgUrl}")`
  2780. ].filter(Boolean).join('; ');
  2781. }
  2782. function getFlashDirectory(flash) {
  2783. // const kebab = getKebabCase(flash.name.replace('[S] ', ''));
  2784. // return flash.page + (kebab ? '-' + kebab : '');
  2785. // return '' + flash.page;
  2786. return '' + flash.directory;
  2787. }
  2788. function getTagDirectory({name}) {
  2789. return C.getKebabCase(name);
  2790. }
  2791. function getAlbumListTag(album) {
  2792. if (album.directory === C.UNRELEASED_TRACKS_DIRECTORY) {
  2793. return 'ul';
  2794. } else {
  2795. return 'ol';
  2796. }
  2797. }
  2798. function fancifyURL(url, {album = false} = {}) {
  2799. return fixWS`<a href="${url}" class="nowrap">${
  2800. url.includes('bandcamp.com') ? 'Bandcamp' :
  2801. (
  2802. url.includes('music.solatrus.com')
  2803. ) ? `Bandcamp (${new URL(url).hostname})` :
  2804. (
  2805. url.includes('types.pl')
  2806. ) ? `Mastodon (${new URL(url).hostname})` :
  2807. url.includes('youtu') ? (album ? (
  2808. url.includes('list=') ? 'YouTube (Playlist)' : 'YouTube (Full Album)'
  2809. ) : 'YouTube') :
  2810. url.includes('soundcloud') ? 'SoundCloud' :
  2811. url.includes('tumblr.com') ? 'Tumblr' :
  2812. url.includes('twitter.com') ? 'Twitter' :
  2813. url.includes('deviantart.com') ? 'DeviantArt' :
  2814. url.includes('wikipedia.org') ? 'Wikipedia' :
  2815. url.includes('poetryfoundation.org') ? 'Poetry Foundation' :
  2816. url.includes('instagram.com') ? 'Instagram' :
  2817. url.includes('patreon.com') ? 'Patreon' :
  2818. new URL(url).hostname
  2819. }</a>`;
  2820. }
  2821. function fancifyFlashURL(url, flash) {
  2822. return `<span class="nowrap">${fancifyURL(url)}` + (
  2823. url.includes('homestuck.com') ? ` (${isNaN(Number(flash.page)) ? 'secret page' : `page ${flash.page}`})` :
  2824. url.includes('bgreco.net') ? ` (HQ audio)` :
  2825. url.includes('youtu') ? ` (on any device)` :
  2826. ''
  2827. ) + `</span>`;
  2828. }
  2829. function iconifyURL(url) {
  2830. const [ id, msg ] = (
  2831. url.includes('bandcamp.com') ? ['bandcamp', 'Bandcamp'] :
  2832. (
  2833. url.includes('music.solatrus.com')
  2834. ) ? ['bandcamp', `Bandcamp (${new URL(url).hostname})`] :
  2835. (
  2836. url.includes('types.pl')
  2837. ) ? ['mastodon', `Mastodon (${new URL(url).hostname})`] :
  2838. url.includes('youtu') ? ['youtube', 'YouTube'] :
  2839. url.includes('soundcloud') ? ['soundcloud', 'SoundCloud'] :
  2840. url.includes('tumblr.com') ? ['tumblr', 'Tumblr'] :
  2841. url.includes('twitter.com') ? ['twitter', 'Twitter'] :
  2842. url.includes('deviantart.com') ? ['deviantart', 'DeviantArt'] :
  2843. url.includes('instagram.com') ? ['instagram', 'Instagram'] :
  2844. ['globe', `External (${new URL(url).hostname})`]
  2845. );
  2846. return fixWS`<a href="${url}" class="icon"><svg><title>${msg}</title><use href="${C.STATIC_DIRECTORY}/icons.svg#icon-${id}"></use></svg></a>`;
  2847. }
  2848. function chronologyLinks(currentTrack, {
  2849. mapProperty,
  2850. toArtist,
  2851. filters, // {property, toArtist}
  2852. headingWord,
  2853. sourceData = justEverythingMan
  2854. }) {
  2855. const artists = Array.from(new Set(filters.flatMap(({ mapProperty, toArtist }) => currentTrack[mapProperty] && currentTrack[mapProperty].map(toArtist))));
  2856. if (artists.length > 8) {
  2857. return `<div class="chronology">(See artist pages for chronology info!)</div>`;
  2858. }
  2859. return artists.map(artist => {
  2860. const releasedThings = sourceData.filter(thing => {
  2861. const album = albumData.includes(thing) ? thing : thing.album;
  2862. if (album && album.directory === C.UNRELEASED_TRACKS_DIRECTORY) {
  2863. return false;
  2864. }
  2865. return filters.some(({ mapProperty, toArtist }) => (
  2866. thing[mapProperty] &&
  2867. thing[mapProperty].map(toArtist).includes(artist)
  2868. ));
  2869. });
  2870. const index = releasedThings.indexOf(currentTrack);
  2871. if (index === -1) return '';
  2872. const previous = releasedThings[index - 1];
  2873. const next = releasedThings[index + 1];
  2874. const parts = [
  2875. previous && `<a href="${getHrefOfAnythingMan(previous)}" title="${previous.name}">Previous</a>`,
  2876. next && `<a href="${getHrefOfAnythingMan(next)}" title="${next.name}">Next</a>`
  2877. ].filter(Boolean);
  2878. const heading = `${th(index + 1)} ${headingWord} by <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/">${artist.name}</a>`;
  2879. return fixWS`
  2880. <div class="chronology">
  2881. <span class="heading">${heading}</span>
  2882. ${parts.length && `<span class="buttons">(${parts.join(', ')})</span>` || `<!-- (here: Next/previous links) -->`}
  2883. </div>
  2884. `;
  2885. }).filter(Boolean).join('\n');
  2886. }
  2887. function generateAlbumNavLinks(album, currentTrack = null) {
  2888. if (album.tracks.length <= 1) {
  2889. return '';
  2890. }
  2891. const index = currentTrack && album.tracks.indexOf(currentTrack)
  2892. const previous = currentTrack && album.tracks[index - 1]
  2893. const next = currentTrack && album.tracks[index + 1]
  2894. const [ previousLine, nextLine, randomLine ] = [
  2895. previous && `<a href="${C.TRACK_DIRECTORY}/${previous.directory}/" id="previous-button" title="${previous.name}">Previous</a>`,
  2896. next && `<a href="${C.TRACK_DIRECTORY}/${next.directory}/" id="next-button" title="${next.name}">Next</a>`,
  2897. `<a href="${C.JS_DISABLED_DIRECTORY}/" data-random="track-in-album" id="random-button">${currentTrack ? 'Random' : 'Random Track'}</a>`
  2898. ];
  2899. if (previousLine || nextLine) {
  2900. return `(${[previousLine, nextLine].filter(Boolean).join(', ')}<span class="js-hide-until-data">, ${randomLine}</span>)`;
  2901. } else {
  2902. return `<span class="js-hide-until-data">(${randomLine})</span>`;
  2903. }
  2904. }
  2905. function generateAlbumChronologyLinks(album, currentTrack = null) {
  2906. return [
  2907. currentTrack && chronologyLinks(currentTrack, {
  2908. headingWord: 'track',
  2909. sourceData: trackData,
  2910. filters: [
  2911. {
  2912. mapProperty: 'artists',
  2913. toArtist: ({ who }) => who
  2914. },
  2915. {
  2916. mapProperty: 'contributors',
  2917. toArtist: ({ who }) => who
  2918. }
  2919. ]
  2920. }),
  2921. chronologyLinks(currentTrack || album, {
  2922. headingWord: 'cover art',
  2923. sourceData: justEverythingSortedByArtDateMan,
  2924. filters: [
  2925. {
  2926. mapProperty: 'coverArtists',
  2927. toArtist: ({ who }) => who
  2928. }
  2929. ]
  2930. })
  2931. ].filter(Boolean).join('\n');
  2932. }
  2933. function generateSidebarForAlbum(album, currentTrack = null) {
  2934. const trackToListItem = track => `<li ${classes(track === currentTrack && 'current')}><a href="${C.TRACK_DIRECTORY}/${track.directory}/">${track.name}</a></li>`;
  2935. const listTag = getAlbumListTag(album);
  2936. return {content: fixWS`
  2937. <h1><a href="${C.ALBUM_DIRECTORY}/${album.directory}/">${album.name}</a></h1>
  2938. ${album.usesGroups ? fixWS`
  2939. <dl>
  2940. ${album.tracks.flatMap((track, i, arr) => [
  2941. (i > 0 && track.group !== arr[i - 1].group) && `</${listTag}></dd>`,
  2942. (i === 0 || track.group !== arr[i - 1].group) && fixWS`
  2943. ${track.group && fixWS`
  2944. <dt style="${getThemeString(track)}" ${classes(currentTrack && track.group === currentTrack.group && 'current')}>
  2945. <a href="${C.TRACK_DIRECTORY}/${track.directory}/">${track.group}</a>
  2946. ${listTag === 'ol' ? `(${i + 1}&ndash;${arr.length - arr.slice().reverse().findIndex(t => t.group === track.group)})` : `<!-- (here: track number range) -->`}
  2947. </dt>
  2948. `}
  2949. <dd style="${getThemeString(track)}"><${listTag === 'ol' ? `ol start="${i + 1}"` : listTag}>
  2950. `,
  2951. (!currentTrack || track.group === currentTrack.group) && trackToListItem(track),
  2952. i === arr.length && `</${listTag}></dd>`
  2953. ].filter(Boolean)).join('\n')}
  2954. </dl>
  2955. ` : fixWS`
  2956. <${listTag}>
  2957. ${album.tracks.map(trackToListItem).join('\n')}
  2958. </${listTag}>
  2959. `}
  2960. `};
  2961. }
  2962. function generateSidebarRightForAlbum(album, currentTrack = null) {
  2963. const { groups } = album;
  2964. if (groups.length) {
  2965. return {
  2966. collapse: false,
  2967. multiple: groups.map(group => {
  2968. const index = group.albums.indexOf(album);
  2969. const next = group.albums[index + 1];
  2970. const previous = group.albums[index - 1];
  2971. return {group, next, previous};
  2972. }).map(({group, next, previous}) => fixWS`
  2973. <h1><a href="${C.GROUP_DIRECTORY}/${group.directory}/">${group.name}</a></h1>
  2974. ${!currentTrack && transformMultiline(group.descriptionShort)}
  2975. ${group.urls.length && `<p>Visit on ${joinNoOxford(group.urls.map(fancifyURL), 'or')}.</p>`}
  2976. ${!currentTrack && fixWS`
  2977. ${next && `<p class="group-chronology-link">Next: <a href="${C.ALBUM_DIRECTORY}/${next.directory}/" style="${getThemeString(next)}">${next.name}</a></p>`}
  2978. ${previous && `<p class="group-chronology-link">Previous: <a href="${C.ALBUM_DIRECTORY}/${previous.directory}/" style="${getThemeString(previous)}">${previous.name}</a></p>`}
  2979. `}
  2980. `)
  2981. };
  2982. };
  2983. }
  2984. function generateSidebarForGroup(isGallery = false, currentGroup = null) {
  2985. return `
  2986. <h1>Groups</h1>
  2987. <dl>
  2988. ${groupData.filter(x => x.isCategory).map(category => [
  2989. fixWS`
  2990. <dt ${classes(currentGroup && category === currentGroup.category && 'current')}>
  2991. <a href="${C.GROUP_DIRECTORY}/${groupData.find(x => x.isGroup && x.category === category).directory}/${isGallery ? 'gallery/' : ''}" style="${getThemeString(category)}">${category.name}</a>
  2992. </dt>
  2993. <dd><ul>
  2994. ${category.groups.map(group => fixWS`
  2995. <li ${classes(group === currentGroup && 'current')} style="${getThemeString(group)}">
  2996. <a href="${C.GROUP_DIRECTORY}/${group.directory}/${isGallery && 'gallery/'}">${group.name}</a>
  2997. </li>
  2998. `).join('\n')}
  2999. </ul></dd>
  3000. `
  3001. ]).join('\n')}
  3002. </dl>
  3003. `;
  3004. }
  3005. function writeGroupPages() {
  3006. return progressPromiseAll(`Writing group pages.`, queue(groupData.filter(x => x.isGroup).map(curry(writeGroupPage)), queueSize));
  3007. }
  3008. async function writeGroupPage(group) {
  3009. const releasedAlbums = group.albums.filter(album => album.directory !== C.UNRELEASED_TRACKS_DIRECTORY);
  3010. const releasedTracks = releasedAlbums.flatMap(album => album.tracks);
  3011. const totalDuration = getTotalDuration(releasedTracks);
  3012. const groups = groupData.filter(x => x.isGroup);
  3013. const index = groups.indexOf(group);
  3014. const previous = groups[index - 1];
  3015. const next = groups[index + 1];
  3016. const generateNextPrevious = isGallery => [
  3017. previous && `<a href="${C.GROUP_DIRECTORY}/${previous.directory}/${isGallery ? 'gallery/' : ''}" id="previous-button" title="${previous.name}">Previous</a>`,
  3018. next && `<a href="${C.GROUP_DIRECTORY}/${next.directory}/${isGallery ? 'gallery/' : ''}" id="next-button" title="${next.name}">Next</a>`
  3019. ].filter(Boolean).join(', ');
  3020. const npInfo = generateNextPrevious(false);
  3021. const npGallery = generateNextPrevious(true);
  3022. await writePage([C.GROUP_DIRECTORY, group.directory], {
  3023. title: group.name,
  3024. theme: getThemeString(group),
  3025. main: {
  3026. content: fixWS`
  3027. <h1>${group.name}</h1>
  3028. ${group.urls.length && `<p>Visit on ${joinNoOxford(group.urls.map(fancifyURL), 'or')}.</p>`}
  3029. <blockquote>
  3030. ${transformMultiline(group.description)}
  3031. </blockquote>
  3032. <h2>Albums</h2>
  3033. <p>View <a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/">album gallery</a>! Or browse the list:</p>
  3034. <ul>
  3035. ${group.albums.map(album => fixWS`
  3036. <li>
  3037. (${album.date.getFullYear()})
  3038. <a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a>
  3039. </li>
  3040. `).join('\n')}
  3041. </ul>
  3042. `
  3043. },
  3044. sidebar: {
  3045. content: generateSidebarForGroup(false, group)
  3046. },
  3047. nav: {
  3048. links: [
  3049. ['./', SITE_SHORT_TITLE],
  3050. [`${C.LISTING_DIRECTORY}/`, 'Listings'],
  3051. [null, 'Group:'],
  3052. [`${C.GROUP_DIRECTORY}/${group.directory}/`, group.name],
  3053. [null, `(${[
  3054. `<a href="${C.GROUP_DIRECTORY}/${group.directory}/" class="current">Info</a>`,
  3055. `<a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/">Gallery</a>`
  3056. ].join(', ') + (npInfo.length ? '; ' + npInfo : '')})`]
  3057. ]
  3058. }
  3059. });
  3060. await writePage([C.GROUP_DIRECTORY, group.directory, 'gallery'], {
  3061. title: `${group.name} - Gallery`,
  3062. theme: getThemeString(group),
  3063. main: {
  3064. classes: ['top-index'],
  3065. content: fixWS`
  3066. <h1>${group.name} - Gallery</h1>
  3067. <p class="quick-info"><b>${releasedTracks.length}</b> track${releasedTracks.length === 1 ? '' : 's'} across <b>${releasedAlbums.length}</b> album${releasedAlbums.length === 1 ? '' : 's'}, totaling <b>~${getDurationString(totalDuration)}</b> ${totalDuration > 3600 ? 'hours' : 'minutes'}.</p>
  3068. <p class="quick-info">(<a href="${C.LISTING_DIRECTORY}/groups/by-category/">Choose another group to filter by!</a>)</p>
  3069. <div class="grid-listing">
  3070. ${getGridHTML({
  3071. entries: C.sortByDate(group.albums.map(item => ({item}))).reverse(),
  3072. srcFn: getAlbumCover,
  3073. hrefFn: album => `${C.ALBUM_DIRECTORY}/${album.directory}/`,
  3074. details: true
  3075. })}
  3076. </div>
  3077. `
  3078. },
  3079. sidebar: {
  3080. content: generateSidebarForGroup(true, group)
  3081. },
  3082. nav: {
  3083. links: [
  3084. ['./', SITE_SHORT_TITLE],
  3085. [`${C.LISTING_DIRECTORY}/`, 'Listings'],
  3086. [null, 'Group:'],
  3087. [`${C.GROUP_DIRECTORY}/${group.directory}/`, group.name],
  3088. [null, `(${[
  3089. `<a href="${C.GROUP_DIRECTORY}/${group.directory}/">Info</a>`,
  3090. `<a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/" class="current">Gallery</a>`
  3091. ].join(', ') + (npGallery.length ? '; ' + npGallery : '')})`]
  3092. ]
  3093. }
  3094. });
  3095. }
  3096. function getHrefOfAnythingMan(anythingMan) {
  3097. return (
  3098. albumData.includes(anythingMan) ? C.ALBUM_DIRECTORY :
  3099. trackData.includes(anythingMan) ? C.TRACK_DIRECTORY :
  3100. flashData.includes(anythingMan) ? C.FLASH_DIRECTORY :
  3101. 'idk-bud'
  3102. ) + '/' + (
  3103. flashData.includes(anythingMan) ? getFlashDirectory(anythingMan) :
  3104. anythingMan.directory
  3105. ) + '/';
  3106. }
  3107. function getAlbumCover(album) {
  3108. const file = 'cover.jpg';
  3109. return `${C.MEDIA_DIRECTORY}/${C.MEDIA_ALBUM_ART_DIRECTORY}/${album.directory}/${file}`;
  3110. }
  3111. function getTrackCover(track) {
  3112. // Some al8ums don't have any track art at all, and in those, every track
  3113. // just inherits the al8um's own cover art.
  3114. if (track.coverArtists === null) {
  3115. return getAlbumCover(track.album);
  3116. } else {
  3117. const file = `${track.directory}.jpg`;
  3118. return `${C.MEDIA_DIRECTORY}/${C.MEDIA_ALBUM_ART_DIRECTORY}/${track.album.directory}/${file}`;
  3119. }
  3120. }
  3121. function getFlashCover(flash) {
  3122. const file = `${getFlashDirectory(flash)}.${flash.jiff === 'Yeah' ? 'gif' : 'jpg'}`;
  3123. return `${C.MEDIA_DIRECTORY}/${C.MEDIA_FLASH_ART_DIRECTORY}/${file}`;
  3124. }
  3125. function getFlashLink(flash) {
  3126. return `https://homestuck.com/story/${flash.page}`;
  3127. }
  3128. function getFlashLinkHTML(flash, name = null) {
  3129. if (!name) {
  3130. name = flash.name;
  3131. }
  3132. return `<a href="${C.FLASH_DIRECTORY}/${getFlashDirectory(flash)}/" title="Page ${flash.page}" style="${getThemeString(flash)}">${name}</a>`;
  3133. }
  3134. function rebaseURLs(directory, html) {
  3135. if (directory === '') {
  3136. return html;
  3137. }
  3138. return html.replace(/(href|src|data-original)="(.*?)"/g, (match, attr, url) => {
  3139. if (url.startsWith('#')) {
  3140. return `${attr}="${url}"`;
  3141. }
  3142. try {
  3143. new URL(url);
  3144. // no error: it's a full url
  3145. } catch (error) {
  3146. // caught an error: it's a component!
  3147. url = path.relative(directory, path.join(C.SITE_DIRECTORY, url));
  3148. }
  3149. return `${attr}="${url}"`;
  3150. });
  3151. }
  3152. function classes(...args) {
  3153. const values = args.filter(Boolean);
  3154. // return values.length ? ` class="${values.join(' ')}"` : '';
  3155. return `class="${values.join(' ')}"`;
  3156. }
  3157. async function main() {
  3158. // 8ut wait, you might say, how do we know which al8um these data files
  3159. // correspond to???????? You wouldn't dare suggest we parse the actual
  3160. // paths returned 8y this function, which ought to 8e of effectively
  3161. // unknown format except for their purpose as reada8le data files!?
  3162. // To that, I would say, yeah, you're right. Thanks a 8unch, my projection
  3163. // of "you". We're going to read these files later, and contained within
  3164. // will 8e the actual directory names that the data correspond to. Yes,
  3165. // that's redundant in some ways - we COULD just return the directory name
  3166. // in addition to the data path, and duplicating that name within the file
  3167. // itself suggests we 8e careful to avoid mismatching it - 8ut doing it
  3168. // this way lets the data files themselves 8e more porta8le (meaning we
  3169. // could store them all in one folder, if we wanted, and this program would
  3170. // still output to the correct al8um directories), and also does make the
  3171. // function's signature simpler (an array of strings, rather than some kind
  3172. // of structure containing 8oth data file paths and output directories).
  3173. // This is o8jectively a good thing, 8ecause it means the function can stay
  3174. // truer to its name, and have a narrower purpose: it doesn't need to
  3175. // concern itself with where we *output* files, or whatever other reasons
  3176. // we might (hypothetically) have for knowing the containing directory.
  3177. // And, in the strange case where we DO really need to know that info, we
  3178. // callers CAN use path.dirname to find out that data. 8ut we'll 8e
  3179. // avoiding that in our code 8ecause, again, we want to avoid assuming the
  3180. // format of the returned paths here - they're only meant to 8e used for
  3181. // reading as-is.
  3182. const albumDataFiles = await findAlbumDataFiles();
  3183. // Technically, we could do the data file reading and output writing at the
  3184. // same time, 8ut that kinda makes the code messy, so I'm not 8othering
  3185. // with it.
  3186. albumData = await progressPromiseAll(`Reading & processing album files.`, albumDataFiles.map(processAlbumDataFile));
  3187. {
  3188. const errors = albumData.filter(obj => obj.error);
  3189. if (errors.length) {
  3190. for (const error of errors) {
  3191. console.log(`\x1b[31;1m${error.error}\x1b[0m`);
  3192. }
  3193. return;
  3194. }
  3195. }
  3196. C.sortByDate(albumData);
  3197. artistData = await processArtistDataFile(path.join(C.DATA_DIRECTORY, ARTIST_DATA_FILE));
  3198. if (artistData.error) {
  3199. console.log(`\x1b[31;1m${artistData.error}\x1b[0m`);
  3200. return;
  3201. }
  3202. {
  3203. const errors = artistData.filter(obj => obj.error);
  3204. if (errors.length) {
  3205. for (const error of errors) {
  3206. console.log(`\x1b[31;1m${error.error}\x1b[0m`);
  3207. }
  3208. return;
  3209. }
  3210. }
  3211. trackData = C.getAllTracks(albumData);
  3212. flashData = await processFlashDataFile(path.join(C.DATA_DIRECTORY, FLASH_DATA_FILE));
  3213. if (flashData.error) {
  3214. console.log(`\x1b[31;1m${flashData.error}\x1b[0m`);
  3215. return;
  3216. }
  3217. const flashErrors = flashData.filter(obj => obj.error);
  3218. if (flashErrors.length) {
  3219. for (const error of flashErrors) {
  3220. console.log(`\x1b[31;1m${error.error}\x1b[0m`);
  3221. }
  3222. return;
  3223. }
  3224. artistNames = Array.from(new Set([
  3225. ...artistData.filter(artist => !artist.alias).map(artist => artist.name),
  3226. ...[
  3227. ...albumData.flatMap(album => [
  3228. ...album.artists || [],
  3229. ...album.coverArtists || [],
  3230. ...album.tracks.flatMap(track => [
  3231. ...track.artists,
  3232. ...track.coverArtists || [],
  3233. ...track.contributors || []
  3234. ])
  3235. ]),
  3236. ...flashData.flatMap(flash => [
  3237. ...flash.contributors || []
  3238. ])
  3239. ].map(contribution => contribution.who)
  3240. ]));
  3241. tagData = await processTagDataFile(path.join(C.DATA_DIRECTORY, TAG_DATA_FILE));
  3242. if (tagData.error) {
  3243. console.log(`\x1b[31;1m${tagData.error}\x1b[0m`);
  3244. return;
  3245. }
  3246. {
  3247. const errors = tagData.filter(obj => obj.error);
  3248. if (errors.length) {
  3249. for (const error of errors) {
  3250. console.log(`\x1b[31;1m${error.error}\x1b[0m`);
  3251. }
  3252. return;
  3253. }
  3254. }
  3255. groupData = await processGroupDataFile(path.join(C.DATA_DIRECTORY, GROUP_DATA_FILE));
  3256. if (groupData.error) {
  3257. console.log(`\x1b[31;1m${groupData.error}\x1b[0m`);
  3258. return;
  3259. }
  3260. {
  3261. const errors = groupData.filter(obj => obj.error);
  3262. if (errors.length) {
  3263. for (const error of errors) {
  3264. console.log(`\x1b[31;1m${error.error}\x1b[0m`);
  3265. }
  3266. return;
  3267. }
  3268. }
  3269. newsData = await processNewsDataFile(path.join(C.DATA_DIRECTORY, NEWS_DATA_FILE));
  3270. if (newsData.error) {
  3271. console.log(`\x1b[31;1m${newsData.error}\x1b[0m`);
  3272. return;
  3273. }
  3274. const newsErrors = newsData.filter(obj => obj.error);
  3275. if (newsErrors.length) {
  3276. for (const error of newsErrors) {
  3277. console.log(`\x1b[31;1m${error.error}\x1b[0m`);
  3278. }
  3279. return;
  3280. }
  3281. {
  3282. const tagNames = new Set([...trackData, ...albumData].flatMap(thing => thing.artTags));
  3283. for (let { name, isCW } of tagData) {
  3284. if (isCW) {
  3285. name = 'cw: ' + name;
  3286. }
  3287. tagNames.delete(name);
  3288. }
  3289. if (tagNames.size) {
  3290. for (const name of Array.from(tagNames).sort()) {
  3291. console.log(`\x1b[33;1m- Missing tag: "${name}"\x1b[0m`);
  3292. }
  3293. return;
  3294. }
  3295. }
  3296. artistNames.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0);
  3297. justEverythingMan = C.sortByDate(albumData.concat(trackData, flashData.filter(flash => !flash.act8r8k)));
  3298. justEverythingSortedByArtDateMan = C.sortByArtDate(justEverythingMan.slice());
  3299. // console.log(JSON.stringify(justEverythingSortedByArtDateMan.map(getHrefOfAnythingMan), null, 2));
  3300. {
  3301. let buffer = [];
  3302. const clearBuffer = function() {
  3303. if (buffer.length) {
  3304. for (const entry of buffer.slice(0, -1)) {
  3305. console.log(`\x1b[2m... ${entry.name} ...\x1b[0m`);
  3306. }
  3307. const lastEntry = buffer[buffer.length - 1];
  3308. console.log(`\x1b[2m... \x1b[0m${lastEntry.name}\x1b[0;2m ...\x1b[0m`);
  3309. buffer = [];
  3310. }
  3311. };
  3312. const showWhere = (name, color) => {
  3313. const where = justEverythingMan.filter(thing => [
  3314. ...thing.coverArtists || [],
  3315. ...thing.contributors || [],
  3316. ...thing.artists || []
  3317. ].some(({ who }) => who === name));
  3318. for (const thing of where) {
  3319. console.log(`\x1b[${color}m- ` + (thing.album ? `(\x1b[1m${thing.album.name}\x1b[0;${color}m)` : '') + ` \x1b[1m${thing.name}\x1b[0m`);
  3320. }
  3321. };
  3322. let CR4SH = false;
  3323. for (let name of artistNames) {
  3324. const entry = artistData.find(entry => entry.name === name || entry.name.toLowerCase() === name.toLowerCase());
  3325. if (!entry) {
  3326. clearBuffer();
  3327. console.log(`\x1b[31mMissing entry for artist "\x1b[1m${name}\x1b[0;31m"\x1b[0m`);
  3328. showWhere(name, 31);
  3329. CR4SH = true;
  3330. } else if (entry.alias) {
  3331. console.log(`\x1b[33mArtist "\x1b[1m${name}\x1b[0;33m" should be named "\x1b[1m${entry.alias}\x1b[0;33m"\x1b[0m`);
  3332. showWhere(name, 33);
  3333. CR4SH = true;
  3334. } else if (entry.name !== name) {
  3335. console.log(`\x1b[33mArtist "\x1b[1m${name}\x1b[0;33m" should be named "\x1b[1m${entry.name}\x1b[0;33m"\x1b[0m`);
  3336. showWhere(name, 33);
  3337. CR4SH = true;
  3338. } else {
  3339. buffer.push(entry);
  3340. if (buffer.length > 3) {
  3341. buffer.shift();
  3342. }
  3343. }
  3344. }
  3345. if (CR4SH) {
  3346. return;
  3347. } else {
  3348. console.log(`All artist data is good!`);
  3349. }
  3350. }
  3351. {
  3352. const directories = [];
  3353. for (const { directory, name } of albumData) {
  3354. if (directories.includes(directory)) {
  3355. console.log(`\x1b[31;1mDuplicate album directory "${directory}" (${name})\x1b[0m`);
  3356. return;
  3357. }
  3358. directories.push(directory);
  3359. }
  3360. }
  3361. {
  3362. const directories = [];
  3363. const where = {};
  3364. for (const { directory, album } of trackData) {
  3365. if (directories.includes(directory)) {
  3366. console.log(`\x1b[31;1mDuplicate track directory "${directory}"\x1b[0m`);
  3367. console.log(`Shows up in:`);
  3368. console.log(`- ${album.name}`);
  3369. console.log(`- ${where[directory].name}`);
  3370. return;
  3371. }
  3372. directories.push(directory);
  3373. where[directory] = album;
  3374. }
  3375. }
  3376. {
  3377. const artists = [];
  3378. const artistsLC = [];
  3379. for (const name of artistNames) {
  3380. if (!artists.includes(name) && artistsLC.includes(name.toLowerCase())) {
  3381. const other = artists.find(oth => oth.toLowerCase() === name.toLowerCase());
  3382. console.log(`\x1b[31;1mMiscapitalized artist name: ${name}, ${other}\x1b[0m`);
  3383. return;
  3384. }
  3385. artists.push(name);
  3386. artistsLC.push(name.toLowerCase());
  3387. }
  3388. }
  3389. {
  3390. for (const { references, name, album } of trackData) {
  3391. for (const ref of references) {
  3392. // Skip these, for now.
  3393. if (ref.includes("by")) {
  3394. continue;
  3395. }
  3396. if (!getLinkedTrack(ref)) {
  3397. console.warn(`\x1b[33mTrack not found "${ref}" in ${name} (${album.name})\x1b[0m`);
  3398. }
  3399. }
  3400. }
  3401. }
  3402. contributionData = Array.from(new Set([
  3403. ...trackData.flatMap(track => [...track.artists || [], ...track.contributors || [], ...track.coverArtists || []]),
  3404. ...albumData.flatMap(album => [...album.coverArtists || [], ...album.artists || []]),
  3405. ...flashData.flatMap(flash => [...flash.contributors || []])
  3406. ]));
  3407. // Now that we have all the data, resolve references all 8efore actually
  3408. // gener8ting any of the pages, 8ecause page gener8tion is going to involve
  3409. // accessing these references a lot, and there's no reason to resolve them
  3410. // more than once. (We 8uild a few additional links that can't 8e cre8ted
  3411. // at initial data processing time here too.)
  3412. const filterNull = (parent, key) => {
  3413. for (const obj of parent) {
  3414. const array = obj[key];
  3415. for (let i = 0; i < array.length; i++) {
  3416. if (!Boolean(array[i])) {
  3417. const prev = array[i - 1] && array[i - 1].name;
  3418. const next = array[i + 1] && array[i + 1].name;
  3419. console.log(`\x1b[33mUnexpected null in ${obj.name} (${key}) - prev: ${prev}, next: ${next}\x1b[0m`);
  3420. }
  3421. }
  3422. array.splice(0, array.length, ...array.filter(Boolean));
  3423. }
  3424. };
  3425. const actlessFlashData = flashData.filter(flash => !flash.act8r8k);
  3426. trackData.forEach(track => mapInPlace(track.references, getLinkedTrack));
  3427. trackData.forEach(track => track.aka = getLinkedTrack(track.aka));
  3428. trackData.forEach(track => mapInPlace(track.artTags, getLinkedTag));
  3429. albumData.forEach(album => mapInPlace(album.groups, getLinkedGroup));
  3430. albumData.forEach(album => mapInPlace(album.artTags, getLinkedTag));
  3431. artistData.forEach(artist => artist.alias = getLinkedArtist(artist.alias));
  3432. actlessFlashData.forEach(flash => mapInPlace(flash.tracks, getLinkedTrack));
  3433. contributionData.forEach(contrib => contrib.who = getLinkedArtist(contrib.who));
  3434. filterNull(trackData, 'references');
  3435. filterNull(albumData, 'groups');
  3436. filterNull(actlessFlashData, 'tracks');
  3437. trackData.forEach(track1 => track1.referencedBy = trackData.filter(track2 => track2.references.includes(track1)));
  3438. trackData.forEach(track => track.flashes = actlessFlashData.filter(flash => flash.tracks.includes(track)));
  3439. groupData.forEach(group => group.albums = albumData.filter(album => album.groups.includes(group)));
  3440. tagData.forEach(tag => tag.things = C.sortByArtDate([...albumData, ...trackData]).filter(thing => thing.artTags.includes(tag)));
  3441. trackData.forEach(track => track.otherReleases = [
  3442. track.aka,
  3443. ...trackData.filter(({ aka }) => aka === track)
  3444. ].filter(Boolean));
  3445. artistData.forEach(artist => {
  3446. const filterProp = (array, prop) => array.filter(thing => thing[prop]?.some(({ who }) => who === artist));
  3447. artist.tracks = {
  3448. asArtist: filterProp(trackData, 'artists'),
  3449. asContributor: filterProp(trackData, 'contributors'),
  3450. asCoverArtist: filterProp(trackData, 'coverArtists'),
  3451. asAny: trackData.filter(track => (
  3452. [...track.artists, ...track.contributors, ...track.coverArtists || []].some(({ who }) => who === artist)
  3453. ))
  3454. };
  3455. artist.albums = {
  3456. asArtist: filterProp(albumData, 'artists'),
  3457. asCoverArtist: filterProp(albumData, 'coverArtists')
  3458. };
  3459. artist.flashes = {
  3460. asContributor: filterProp(flashData, 'contributors')
  3461. };
  3462. });
  3463. groupData.filter(x => x.isGroup).forEach(group => group.category = groupData.find(x => x.isCategory && x.name === group.category));
  3464. groupData.filter(x => x.isCategory).forEach(category => category.groups = groupData.filter(x => x.isGroup && x.category === category));
  3465. officialAlbumData = albumData.filter(album => album.groups.some(group => group.directory === C.OFFICIAL_GROUP_DIRECTORY));
  3466. fandomAlbumData = albumData.filter(album => album.groups.every(group => group.directory !== C.OFFICIAL_GROUP_DIRECTORY));
  3467. const miscOptions = await parseOptions(process.argv.slice(2), {
  3468. 'queue-size': {
  3469. type: 'value',
  3470. validate(size) {
  3471. if (parseInt(size) !== parseFloat(size)) return 'an integer';
  3472. if (parseInt(size) < 0) return 'a counting number or zero';
  3473. return true;
  3474. }
  3475. },
  3476. queue: {alias: 'queue-size'},
  3477. [parseOptions.handleUnknown]: () => {}
  3478. });
  3479. // Makes writing a little nicer on CPU theoretically, 8ut also costs in
  3480. // performance right now 'cuz it'll w8 for file writes to 8e completed
  3481. // 8efore moving on to more data processing. So, defaults to zero, which
  3482. // disa8les the queue feature altogether.
  3483. queueSize = +(miscOptions['queue-size'] ?? 0);
  3484. // NOT for ena8ling or disa8ling specific features of the site!
  3485. // This is only in charge of what general groups of files to 8uild.
  3486. // They're here to make development quicker when you're only working
  3487. // on some particular area(s) of the site rather than making changes
  3488. // across all of them.
  3489. const buildFlags = await parseOptions(process.argv.slice(2), {
  3490. all: {type: 'flag'}, // Defaults to true if none 8elow specified.
  3491. album: {type: 'flag'},
  3492. artist: {type: 'flag'},
  3493. flash: {type: 'flag'},
  3494. group: {type: 'flag'},
  3495. list: {type: 'flag'},
  3496. misc: {type: 'flag'},
  3497. tag: {type: 'flag'},
  3498. track: {type: 'flag'},
  3499. [parseOptions.handleUnknown]: () => {}
  3500. });
  3501. const buildAll = !Object.keys(buildFlags).length || buildFlags.all;
  3502. await writeSymlinks();
  3503. if (buildAll || buildFlags.misc) await writeMiscellaneousPages();
  3504. if (buildAll || buildFlags.list) await writeListingPages();
  3505. if (buildAll || buildFlags.tag) await writeTagPages();
  3506. if (buildAll || buildFlags.group) await writeGroupPages();
  3507. if (buildAll || buildFlags.album) await writeAlbumPages();
  3508. if (buildAll || buildFlags.track) await writeTrackPages();
  3509. if (buildAll || buildFlags.artist) await writeArtistPages();
  3510. if (buildAll || buildFlags.flash) await writeFlashPages();
  3511. decorateTime.displayTime();
  3512. // The single most important step.
  3513. console.log('Written!');
  3514. }
  3515. main().catch(error => console.error(error));