diff options
-rw-r--r-- | common/common.js | 20 | ||||
-rwxr-xr-x | publish.sh | 1 | ||||
-rw-r--r-- | static/client.js | 95 | ||||
-rw-r--r-- | static/site.css | 142 | ||||
-rw-r--r-- | upd8-util.js | 143 | ||||
-rw-r--r-- | upd8.js | 946 |
6 files changed, 1014 insertions, 333 deletions
diff --git a/common/common.js b/common/common.js index 6c21dfcb..5db5ad95 100644 --- a/common/common.js +++ b/common/common.js @@ -79,6 +79,7 @@ const C = { CHANGELOG_DIRECTORY: 'changelog', FLASH_DIRECTORY: 'flash', NEWS_DIRECTORY: 'news', + GROUP_DIRECTORY: 'group', JS_DISABLED_DIRECTORY: 'js-disabled', UNRELEASED_TRACKS_DIRECTORY: 'unreleased-tracks', @@ -123,22 +124,9 @@ const C = { // "directories", we just reformat the artist's name. getArtistDirectory: artistName => C.getKebabCase(artistName), - getThingsArtistContributedTo: (artistName, {allTracks, albumData, flashData}) => [ - ...allTracks.filter(track => [ - ...track.artists, - ...track.contributors, - ...track.coverArtists || [] - ].some(({ who }) => who === artistName)), - ...flashData.filter(flash => (flash.contributors || []).some(({ who }) => who === artistName)), - ...albumData.filter(album => - (album.coverArtists || []).some(({ who }) => who === artistName)) - ], - - getArtistNumContributions: (artistName, {allTracks, albumData, flashData}) => ( - C.getThingsArtistContributedTo(artistName, {allTracks, albumData, flashData}).length - ), - - getArtistCommentary: (artistName, {justEverythingMan}) => justEverythingMan.filter(thing => thing.commentary && thing.commentary.replace(/<\/?b>/g, '').includes('<i>' + artistName + ':</i>')) + getArtistNumContributions: artist => (artist.tracks.length + artist.albums.length + artist.flashes.length), + + getArtistCommentary: (artist, {justEverythingMan}) => justEverythingMan.filter(thing => thing.commentary && thing.commentary.replace(/<\/?b>/g, '').includes('<i>' + artist.name + ':</i>')) }; if (typeof module === 'object') { diff --git a/publish.sh b/publish.sh index 4021b9bd..5a3cacd0 100755 --- a/publish.sh +++ b/publish.sh @@ -4,4 +4,5 @@ # So, don't even try, if you aren't. # 8ut you can tweak it to your own server if that's your vi8e. +node upd8.js --all rsync -avhL site/ --info=progress2 nebula@ed1.club:/home/nebula/hsmusic diff --git a/static/client.js b/static/client.js index 8247a42c..549fde29 100644 --- a/static/client.js +++ b/static/client.js @@ -5,10 +5,10 @@ 'use strict'; -const officialAlbumData = albumData.filter(album => !album.isFanon); -const fandomAlbumData = albumData.filter(album => album.isFanon); -const artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name); -const allTracks = C.getAllTracks(albumData); +let albumData, artistData, flashData; +let officialAlbumData, fandomAlbumData, artistNames; + +let ready = false; function rebase(href) { const relative = document.documentElement.dataset.rebase; @@ -27,6 +27,10 @@ function cssProp(el, key) { return getComputedStyle(el).getPropertyValue(key).trim(); } +function getRefDirectory(ref) { + return ref.split(':')[1]; +} + function getAlbum(el) { const directory = cssProp(el, '--album-directory'); return albumData.find(album => album.directory === directory); @@ -37,16 +41,16 @@ function getFlash(el) { return flashData.find(flash => flash.directory === directory); } -function openAlbum(album) { - return rebase(`${C.ALBUM_DIRECTORY}/${album.directory}/`); +function openAlbum(directory) { + return rebase(`${C.ALBUM_DIRECTORY}/${directory}/`); } -function openTrack(track) { - return rebase(`${C.TRACK_DIRECTORY}/${track.directory}/`); +function openTrack(directory) { + return rebase(`${C.TRACK_DIRECTORY}/${directory}/`); } -function openArtist(artist) { - return rebase(`${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist)}/`); +function openArtist(directory) { + return rebase(`${C.ARTIST_DIRECTORY}/${directory}/`); } function openFlash(flash) { @@ -76,20 +80,6 @@ function getTrackListAndIndex() { return {list: album.tracks, index: trackIndex}; } -function openNextTrack() { - const { list, index } = getTrackListAndIndex(); - if (!list) return; - if (index === list.length) return; - return openTrack(list[index + 1]); -} - -function openPreviousTrack() { - const { list, index } = getTrackListAndIndex(); - if (!list) return; - if (index === 0) return; - return openTrack(list[index - 1]); -} - function openRandomTrack() { const { list } = getTrackListAndIndex(); if (!list) return; @@ -104,33 +94,26 @@ function getFlashListAndIndex() { return {list, index: flashIndex}; } -function openNextFlash() { - const { list, index } = getFlashListAndIndex(); - if (index === list.length) return; - return openFlash(list[index + 1]); -} - -function openPreviousFlash() { - const { list, index } = getFlashListAndIndex(); - if (index === 0) return; - return openFlash(list[index - 1]); -} - for (const a of document.body.querySelectorAll('[data-random]')) { a.addEventListener('click', evt => { + if (!ready) { + evt.preventDefault(); + return; + } + setTimeout(() => { a.href = rebase(C.JS_DISABLED_DIRECTORY); }); switch (a.dataset.random) { - case 'album': return a.href = openAlbum(pick(albumData)); - case 'album-in-fandom': return a.href = openAlbum(pick(fandomAlbumData)); - case 'album-in-official': return a.href = openAlbum(pick(officialAlbumData)); - case 'track': return a.href = openTrack(pick(allTracks)); - case 'track-in-album': return a.href = openTrack(pick(getAlbum(a).tracks)); - case 'track-in-fandom': return a.href = openTrack(pick(fandomAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))); - case 'track-in-official': return a.href = openTrack(pick(officialAlbumData.reduce((acc, album) => acc.concat(album.tracks), []))); - case 'artist': return a.href = openArtist(pick(artistNames)); - case 'artist-more-than-one-contrib': return a.href = openArtist(pick(artistNames.filter(name => C.getArtistNumContributions(name, {albumData, allTracks, flashData}) > 1))); + case 'album': return a.href = openAlbum(pick(albumData).directory); + case 'album-in-fandom': return a.href = openAlbum(pick(fandomAlbumData).directory); + case 'album-in-official': return a.href = openAlbum(pick(officialAlbumData).directory); + case 'track': return a.href = openTrack(getRefDirectory(pick(albumData.map(a => a.tracks).reduce((a, b) => a.concat(b), [])))); + case 'track-in-album': return a.href = openTrack(getRefDirectory(pick(getAlbum(a).tracks))); + case 'track-in-fandom': return a.href = openTrack(getRefDirectory(pick(fandomAlbumData.reduce((acc, album) => acc.concat(album.tracks), [])))); + case 'track-in-official': return a.href = openTrack(getRefDirectory(pick(officialAlbumData.reduce((acc, album) => acc.concat(album.tracks), [])))); + case 'artist': return a.href = openArtist(pick(artistData).directory); + case 'artist-more-than-one-contrib': return a.href = openArtist(pick(artistData.filter(artist => C.getArtistNumContributions(artist) > 1)).directory); } }); } @@ -159,7 +142,7 @@ document.addEventListener('keypress', event => { } else if (event.charCode === 'P'.charCodeAt(0)) { if (previous) previous.click(); } else if (event.charCode === 'R'.charCodeAt(0)) { - if (random) random.click(); + if (random && ready) random.click(); } } }); @@ -173,3 +156,23 @@ for (const reveal of document.querySelectorAll('.reveal')) { } }); } + +const elements1 = document.getElementsByClassName('js-hide-once-data'); +const elements2 = document.getElementsByClassName('js-show-once-data'); + +for (const element of elements1) element.style.display = 'block'; + +fetch(rebase('data.json')).then(data => data.json()).then(data => { + albumData = data.albumData; + artistData = data.artistData; + flashData = data.flashData; + + officialAlbumData = albumData.filter(album => !album.isFanon); + fandomAlbumData = albumData.filter(album => album.isFanon); + artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name); + + for (const element of elements1) element.style.display = 'none'; + for (const element of elements2) element.style.display = 'block'; + + ready = true; +}); diff --git a/static/site.css b/static/site.css index 016f74c4..90f8ed3e 100644 --- a/static/site.css +++ b/static/site.css @@ -15,14 +15,37 @@ } body { - background-color: var(--bg-color); - --bg-shade: calc(255 * var(--theme)); --fg-shade: calc(255 * (1 - var(--theme))); + background: black; + margin: 10px; + overflow-y: scroll; +} + +body::before { + content: ""; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + + background-image: url("https://www.homestuck.com/images/desktops/johnhouse_1920x1080.jpg"); + background-position: center; + background-size: cover; + opacity: 0.5; +} +#page-container { + background-color: var(--bg-color); color: rgb(var(--fg-shade), var(--fg-shade), var(--fg-shade)); + max-width: 1200px; + margin: 10px auto 50px; padding: 15px; + + box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); } a { @@ -34,14 +57,36 @@ a:hover { text-decoration: underline; } +#skippers { + position: absolute; + left: -100px; + top: auto; + width: 1px; + height: 1px; +} + +#skippers:focus-within { + position: static; + width: unset; + height: unset; +} + +#skippers > .skipper:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; +} + .layout-columns { display: flex; } -#header { +#header, #skippers { margin-bottom: 10px; padding: 5px; font-size: 0.85em; +} + +#header { display: flex; } @@ -122,7 +167,7 @@ a:hover { } @media (max-width: 780px) { - #sidebar:not(.no-hide) { + .sidebar:not(.no-hide) { display: none; } @@ -134,7 +179,7 @@ a:hover { margin-bottom: 10px; } - #sidebar.no-hide { + .sidebar.no-hide { max-width: unset !important; flex-basis: unset !important; margin-right: 0; @@ -147,25 +192,43 @@ a:hover { } } -#sidebar, #content, #header { +.sidebar, #content, #header, #skippers { background-color: rgba(var(--bg-shade), var(--bg-shade), var(--bg-shade), 0.6); border: 1px dotted var(--fg-color); border-radius: 3px; } -#sidebar { +.sidebar-column { flex: 1 1 20%; min-width: 150px; max-width: 250px; flex-basis: 250px; - float: left; + height: 100%; +} + +.sidebar-multiple { + display: flex; + flex-direction: column; +} + +.sidebar-multiple .sidebar:not(:first-child) { + margin-top: 10px; +} + +.sidebar { padding: 5px; - margin-right: 10px; font-size: 0.85em; - height: 100%; } -#sidebar.wide { +#sidebar-left { + margin-right: 10px; +} + +#sidebar-right { + margin-left: 10px; +} + +.sidebar.wide { max-width: 350px; flex-basis: 300px; flex-shrink: 0; @@ -178,23 +241,23 @@ a:hover { flex-shrink: 3; } -#sidebar > h1, -#sidebar > h2, -#sidebar > h3, -#sidebar > p { +.sidebar > h1, +.sidebar > h2, +.sidebar > h3, +.sidebar > p { text-align: center; } -#sidebar h1 { +.sidebar h1 { font-size: 1.25em; } -#sidebar h2 { +.sidebar h2 { font-size: 1.1em; margin: 0; } -#sidebar h3 { +.sidebar h3 { font-size: 1.1em; font-style: oblique; font-variant: small-caps; @@ -202,73 +265,70 @@ a:hover { margin-bottom: 0em; } -#sidebar > p { +.sidebar > p { margin: 0.5em 0; + padding: 0 5px; } -#sidebar p:last-child { - margin-bottom: 0; -} - -#sidebar hr { +.sidebar hr { color: #555; margin: 10px 5px; } -#sidebar > ol, #sidebar > ul { +.sidebar > ol, .sidebar > ul { padding-left: 30px; padding-right: 15px; } -#sidebar > dl { +.sidebar > dl { padding-right: 15px; padding-left: 0; } -#sidebar > dl dt { +.sidebar > dl dt { padding-left: 10px; margin-top: 0.5em; } -#sidebar > dl dt.current { +.sidebar > dl dt.current { font-weight: 800; } -#sidebar > dl dd { +.sidebar > dl dd { margin-left: 0; } -#sidebar > dl dd ul { +.sidebar > dl dd ul { padding-left: 30px; margin-left: 0; } -#sidebar > dl .side { +.sidebar > dl .side { padding-left: 10px; } -#sidebar li.current { +.sidebar li.current { font-weight: 800; } -#sidebar li { +.sidebar li { overflow-wrap: break-word; } -#sidebar article { +.sidebar article { text-align: left; margin: 5px 5px 15px 5px; } -#sidebar article:last-child { +.sidebar article:last-child { margin-bottom: 5px; } -#sidebar article h2 { +.sidebar article h2 { border-bottom: 1px dotted white; } -#sidebar article h2 time { +.sidebar article h2 time { float: right; font-weight: normal; } @@ -320,7 +380,9 @@ img { */ } -.js-hide { +.js-hide, +.js-show-once-data, +.js-hide-once-data { display: none; } @@ -596,6 +658,10 @@ dl ul, dl ol { margin-left: 0; } +.group-chronology-link { + font-style: oblique; +} + hr.split::before { content: "(split)"; color: #808080; diff --git a/upd8-util.js b/upd8-util.js index b24b3b7f..28504eaf 100644 --- a/upd8-util.js +++ b/upd8-util.js @@ -62,7 +62,11 @@ module.exports.progressPromiseAll = function (msg, array) { }))); }; -module.exports.queue = function (array, max = 10) { +module.exports.queue = function (array, max = 50) { + if (max === 0) { + return array.map(fn => fn()); + } + const begin = []; let current = 0; const ret = array.map(fn => new Promise((resolve, reject) => { @@ -155,3 +159,140 @@ decorateTime.displayTime = function() { }; module.exports.decorateTime = decorateTime; + +// Stolen as #@CK from mtui! +const parseOptions = async function(options, optionDescriptorMap) { + // This function is sorely lacking in comments, but the basic usage is + // as such: + // + // options is the array of options you want to process; + // optionDescriptorMap is a mapping of option names to objects that describe + // the expected value for their corresponding options. + // Returned is a mapping of any specified option names to their values, or + // a process.exit(1) and error message if there were any issues. + // + // Here are examples of optionDescriptorMap to cover all the things you can + // do with it: + // + // optionDescriptorMap: { + // 'telnet-server': {type: 'flag'}, + // 't': {alias: 'telnet-server'} + // } + // + // options: ['t'] -> result: {'telnet-server': true} + // + // optionDescriptorMap: { + // 'directory': { + // type: 'value', + // validate(name) { + // // const whitelistedDirectories = ['apple', 'banana'] + // if (whitelistedDirectories.includes(name)) { + // return true + // } else { + // return 'a whitelisted directory' + // } + // } + // }, + // 'files': {type: 'series'} + // } + // + // ['--directory', 'apple'] -> {'directory': 'apple'} + // ['--directory', 'artichoke'] -> (error) + // ['--files', 'a', 'b', 'c', ';'] -> {'files': ['a', 'b', 'c']} + // + // TODO: Be able to validate the values in a series option. + + const handleDashless = optionDescriptorMap[parseOptions.handleDashless]; + const handleUnknown = optionDescriptorMap[parseOptions.handleUnknown]; + const result = Object.create(null); + for (let i = 0; i < options.length; i++) { + const option = options[i]; + if (option.startsWith('--')) { + // --x can be a flag or expect a value or series of values + let name = option.slice(2).split('=')[0]; // '--x'.split('=') = ['--x'] + let descriptor = optionDescriptorMap[name]; + if (!descriptor) { + if (handleUnknown) { + handleUnknown(option); + } else { + console.error(`Unknown option name: ${name}`); + process.exit(1); + } + continue; + } + if (descriptor.alias) { + name = descriptor.alias; + descriptor = optionDescriptorMap[name]; + } + if (descriptor.type === 'flag') { + result[name] = true; + } else if (descriptor.type === 'value') { + let value = option.slice(2).split('=')[1]; + if (!value) { + value = options[++i]; + if (!value || value.startsWith('-')) { + value = null; + } + } + if (!value) { + console.error(`Expected a value for --${name}`); + process.exit(1); + } + result[name] = value; + } else if (descriptor.type === 'series') { + if (!options.slice(i).includes(';')) { + console.error(`Expected a series of values concluding with ; (\\;) for --${name}`); + process.exit(1); + } + const endIndex = i + options.slice(i).indexOf(';'); + result[name] = options.slice(i + 1, endIndex); + i = endIndex; + } + if (descriptor.validate) { + const validation = await descriptor.validate(result[name]); + if (validation !== true) { + console.error(`Expected ${validation} for --${name}`); + process.exit(1); + } + } + } else if (option.startsWith('-')) { + // mtui doesn't use any -x=y or -x y format optionuments + // -x will always just be a flag + let name = option.slice(1); + let descriptor = optionDescriptorMap[name]; + if (!descriptor) { + if (handleUnknown) { + handleUnknown(option); + } else { + console.error(`Unknown option name: ${name}`); + process.exit(1); + } + continue; + } + if (descriptor.alias) { + name = descriptor.alias; + descriptor = optionDescriptorMap[name]; + } + if (descriptor.type === 'flag') { + result[name] = true; + } else { + console.error(`Use --${name} (value) to specify ${name}`); + process.exit(1); + } + } else if (handleDashless) { + handleDashless(option); + } + } + return result; +} + +parseOptions.handleDashless = Symbol(); +parseOptions.handleUnknown = Symbol(); + +module.exports.parseOptions = parseOptions; + +// Cheap FP for a cheap dyke! +// I have no idea if this is what curry actually means. +module.exports.curry = f => x => (...args) => f(x, ...args); + +module.exports.mapInPlace = (array, fn) => array.splice(0, array.length, ...array.map(fn)); diff --git a/upd8.js b/upd8.js index 86aeb53f..64a311ce 100644 --- a/upd8.js +++ b/upd8.js @@ -51,6 +51,14 @@ // // Use these wisely, which is to say all the time and instead of whatever // terri8le new pseudo structure you're trying to invent!!!!!!!! +// +// Upd8 2021-01-03: Soooooooo we didn't actually really end up using these, +// lol? Well there's still only one anyway. Kinda ended up doing a 8ig refactor +// of all the o8ject structures today. It's not *especially* relevant 8ut feels +// worth mentioning? I'd get rid of this comment 8lock 8ut I like it too much! +// Even though I haven't actually reread it, lol. 8ut yeah, hopefully in the +// spirit of this "make things more consistent" attitude I 8rought up 8ack in +// August, stuff's lookin' 8etter than ever now. W00t! 'use strict'; @@ -94,8 +102,11 @@ const unlink = util.promisify(fs.unlink); const { cacheOneArg, + curry, decorateTime, joinNoOxford, + mapInPlace, + parseOptions, progressPromiseAll, queue, s, @@ -110,9 +121,6 @@ const SITE_TITLE = 'Homestuck Music Wiki'; const SITE_SHORT_TITLE = 'HSMusic'; const SITE_DESCRIPTION = `Expansive resource for anyone interested in fan-made and official Homestuck music alike; an archive for all things related.`; -const SITE_VERSION = 'launch of hsmusic.wiki'; -const SITE_RELEASE = '12 December 2020'; - const SITE_DONATE_LINK = 'https://liberapay.com/nebula'; function readDataFile(file) { @@ -133,6 +141,7 @@ const ARTIST_DATA_FILE = 'artists.txt'; const FLASH_DATA_FILE = 'flashes.txt'; const NEWS_DATA_FILE = 'news.txt'; const TAG_DATA_FILE = 'tags.txt'; +const GROUP_DATA_FILE = 'groups.txt'; const CSS_FILE = 'site.css'; @@ -143,10 +152,11 @@ const CSS_FILE = 'site.css'; // Upd8: Okay yeah these aren't actually any different. Still cleaner than // passing around a data object containing all this, though. let albumData; -let allTracks; +let trackData; let flashData; let newsData; let tagData; +let groupData; let artistNames; let artistData; @@ -155,6 +165,9 @@ let officialAlbumData; let fandomAlbumData; let justEverythingMan; // tracks, albums, flashes -- don't forget to upd8 getHrefOfAnythingMan! let justEverythingSortedByArtDateMan; +let contributionData; + +let queueSize; // Note there isn't a 'find track data files' function. I plan on including the // data for all tracks within an al8um collected in the single metadata file @@ -312,7 +325,7 @@ function transformInline(text) { return ref; } } else if (category === 'tag:') { - const tag = tagData.find(tag => tag.directory === ref); + const tag = getLinkedTag(ref); if (tag) { return fixWS` <a href="${C.TAG_DIRECTORY}/${tag.directory}/" style="${getThemeString(tag)}">${tag.name}</a> @@ -481,7 +494,8 @@ async function processAlbumDataFile(file) { album.trackCoverArtists = getContributionField(albumSection, 'Track Art'); album.artTags = getListField(albumSection, 'Art Tags') || []; album.commentary = getCommentaryField(albumSection); - album.urls = (getListField(albumSection, 'URLs') || []).filter(Boolean); + album.urls = getListField(albumSection, 'URLs') || []; + album.groups = getListField(albumSection, 'Groups') || []; album.directory = getBasicField(albumSection, 'Directory'); const canon = getBasicField(albumSection, 'Canon'); @@ -490,6 +504,8 @@ async function processAlbumDataFile(file) { album.isOfficial = album.isCanon || album.isBeyond; album.isFanon = canon === 'Fanon'; + album.isMajorRelease = getBasicField(albumSection, 'Major Release') === 'yes'; + if (album.artists && album.artists.error) { return {error: `${album.artists.error} (in ${album.name})`}; } @@ -580,7 +596,7 @@ async function processAlbumDataFile(file) { track.aka = getBasicField(section, 'AKA'); if (!track.name) { - return {error: 'A track section is missing the "Track" (name) field (in ${album.name)}.'}; + return {error: `A track section is missing the "Track" (name) field (in ${album.name}, previous: ${album.tracks[album.tracks.length - 1]?.name}).`}; } let durationString = getBasicField(section, 'Duration') || '0:00'; @@ -676,12 +692,21 @@ async function processArtistDataFile(file) { const urls = (getListField(section, 'URLs') || []).filter(Boolean); const alias = getBasicField(section, 'Alias'); const note = getMultilineField(section, 'Note'); + let directory = getBasicField(section, 'Directory'); if (!name) { return {error: 'Expected "Artist" (name) field!'}; } - return {name, urls, alias, note}; + if (!directory) { + directory = C.getArtistDirectory(name); + } + + if (alias) { + return {name, directory, alias}; + } else { + return {name, directory, urls, note}; + } }); } @@ -837,6 +862,51 @@ async function processTagDataFile(file) { }); } +async function processGroupDataFile(file) { + let contents; + try { + contents = await readFile(file, 'utf-8'); + } catch (error) { + return {error: `Could not read ${file} (${error.code}).`}; + } + + const contentLines = contents.split('\n'); + const sections = Array.from(getSections(contentLines)); + + return sections.map(section => { + const name = getBasicField(section, 'Group'); + if (!name) { + return {error: 'Expected "Group" field!'}; + } + + let directory = getBasicField(section, 'Directory'); + if (!directory) { + directory = C.getKebabCase(name); + } + + let description = getMultilineField(section, 'Description'); + if (!description) { + return {error: 'Expected "Description" field!'}; + } + + let descriptionShort = description.split('<hr class="split">')[0]; + + description = transformMultiline(description); + descriptionShort = transformMultiline(descriptionShort); + + const urls = (getListField(section, 'URLs') || []).filter(Boolean); + + return { + name, + directory, + description, + descriptionShort, + urls, + color: '#00ffff' + }; + }); +} + function getDateString({ date }) { /* const pad = val => val.toString().padStart(2, '0'); @@ -885,28 +955,81 @@ function getTotalDuration(tracks) { const stringifyIndent = 0; +const toRefs = (label, array) => array.filter(Boolean).map(x => `${label}:${x.directory}`); + +function stringifyRefs(key, value) { + switch (key) { + case 'albums': return toRefs('album', value); + case 'tracks': + case 'references': + case 'referencedBy': + if (!Array.isArray(value)) console.log(Object.keys(value)); + return toRefs('track', value); + case 'artists': + case 'contributors': + case 'coverArtists': + case 'trackCoverArtists': + return value && value.map(({ who, what }) => ({who: `artist:${who.directory}`, what})); + case 'flashes': return toRefs('flash', value); + case 'groups': return toRefs('group', value); + case 'artTags': return toRefs('tag', value); + case 'aka': return value && `track:${value.directory}`; + default: + return value; + } +} + function stringifyAlbumData() { return JSON.stringify(albumData, (key, value) => { - if (['album', 'commentary'].includes(key)) { - return undefined; + switch (key) { + case 'commentary': + return ''; + default: + return stringifyRefs(key, value); } + }, stringifyIndent); +} - return value; +function stringifyTrackData() { + return JSON.stringify(trackData, (key, value) => { + switch (key) { + case 'album': + case 'commentary': + case 'otherReleases': + return undefined; + default: + return stringifyRefs(key, value); + } }, stringifyIndent); } function stringifyFlashData() { return JSON.stringify(flashData, (key, value) => { - if (['act', 'commentary'].includes(key)) { - return undefined; + switch (key) { + case 'act': + case 'commentary': + return undefined; + default: + return stringifyRefs(key, value); } - - return value; }, stringifyIndent); } function stringifyArtistData() { - return JSON.stringify(artistData, null, stringifyIndent); + return JSON.stringify(artistData, (key, value) => { + switch (key) { + case 'tracks': // skip stringifyRefs handling 'tracks' key as an array + return value; + case 'asAny': + return; + case 'asArtist': + case 'asContributor': + case 'asCoverArtist': + return toRefs('track', value); + default: + return stringifyRefs(key, value); + } + }, stringifyIndent); } function escapeAttributeValue(value) { @@ -996,11 +1119,16 @@ async function writePage(directoryParts, { main = { classes: [], + collapseSidebars: true, content: '' }, sidebar = { - collapse: true, + classes: [], + content: '' + }, + + sidebarRight = { classes: [], content: '' }, @@ -1021,26 +1149,51 @@ async function writePage(directoryParts, { } const canonical = SITE_CANONICAL_BASE + targetPath; + const { + collapseSidebars = true + } = main; + const mainHTML = main.content && fixWS` <main id="content" ${classes(...main.classes || [])}> ${main.content} </main> `; - const { - collapse = true, + const generateSidebarHTML = (id, { + content, + multiple, + classes: sidebarClasses = [], wide = false - } = sidebar; - - const sidebarHTML = sidebar.content && fixWS` - <div id="sidebar" ${classes( + }) => (content ? fixWS` + <div id="${id}" ${classes( + 'sidebar-column', + 'sidebar', wide && 'wide', - !collapse && 'no-hide', - ...sidebar.classes || [] + !collapseSidebars && 'no-hide', + ...sidebarClasses )}> - ${sidebar.content} + ${content} </div> - `; + ` : multiple ? fixWS` + <div id="${id}" ${classes( + 'sidebar-column', + 'sidebar-multiple', + wide && 'wide', + !collapseSidebars && 'no-hide' + )}> + ${multiple.map(content => fixWS` + <div ${classes( + 'sidebar', + ...sidebarClasses + )}> + ${content} + </div> + `).join('\n')} + </div> + ` : ''); + + const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebar); + const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight); if (nav.simple) { nav.links = [ @@ -1087,10 +1240,11 @@ async function writePage(directoryParts, { const layoutHTML = [ navHTML, - sidebar.content ? fixWS` - <div ${classes('layout-columns', !collapse && 'vertical-when-thin')}> - ${sidebarHTML} + (sidebarLeftHTML || sidebarRightHTML) ? fixWS` + <div ${classes('layout-columns', !collapseSidebars && 'vertical-when-thin')}> + ${sidebarLeftHTML} ${mainHTML} + ${sidebarRightHTML} </div> ` : mainHTML ].filter(Boolean).join('\n'); @@ -1109,9 +1263,16 @@ async function writePage(directoryParts, { <script src="${C.STATIC_DIRECTORY}/lazy-loading.js"></script> </head> <body ${attributes({style: body.style || ''})}> - ${layoutHTML} + <div id="page-container"> + ${mainHTML && fixWS` + <div id="skippers"> + <span class="skipper"><a href="#content">Skip to content</a></span> + ${sidebar.content && `<span class="skipper"><a href="#sidebar-left">Skip to sidebar</a></span>`} + </div> + `} + ${layoutHTML} + </div> <script src="${C.COMMON_DIRECTORY}/common.js"></script> - <script src="data.js"></script> <script src="${C.STATIC_DIRECTORY}/client.js"></script> </body> </html> @@ -1133,7 +1294,7 @@ function getGridHTML({ alt: altFn(item), lazy: (typeof lazy === 'number' ? i >= lazy : lazy), square: true, - reveal: getRevealString(getTagsUsedIn(item)) + reveal: getRevealString(item.artTags) })} <span>${item.name}</span> ${details && fixWS` @@ -1170,10 +1331,8 @@ function getFlashGridHTML(props) { function getNewReleases(numReleases) { const latestFirst = albumData.slice().reverse(); - - // TODO: Major fan albums - const majorReleases = []; - majorReleases.push(latestFirst.find(album => album.isOfficial)); + const majorReleases = latestFirst.filter(album => album.isOfficial || album.isMajorRelease); + majorReleases.splice(1); const otherReleases = latestFirst .filter(album => !majorReleases.includes(album)) @@ -1214,6 +1373,7 @@ function writeMiscellaneousPages() { }, main: { classes: ['top-index'], + collapseSidebars: false, content: fixWS` <h1>${SITE_TITLE}</h1> <h2>New Releases</h2> @@ -1256,7 +1416,6 @@ function writeMiscellaneousPages() { ` }, sidebar: { - collapse: false, wide: true, content: fixWS` <h1>Get involved!</h1> @@ -1284,7 +1443,6 @@ function writeMiscellaneousPages() { <span><a href="${C.ABOUT_DIRECTORY}/">About & Credits</a></span> <span><a href="${C.FEEDBACK_DIRECTORY}/">Feedback & Suggestions</a></span> <span><a href="${SITE_DONATE_LINK}">Donate</a></span> - <span><a href="${C.CHANGELOG_DIRECTORY}/">Changelog</a> (${SITE_RELEASE}: ${SITE_VERSION})</span> </h2> ` } @@ -1309,6 +1467,9 @@ function writeMiscellaneousPages() { </div> ` }, + sidebar: { + content: generateSidebarForGroup(true, null) + }, nav: {simple: true} }), @@ -1439,11 +1600,12 @@ function writeMiscellaneousPages() { nav: {simple: true} }), - writeFile(path.join(C.SITE_DIRECTORY, 'data.js'), fixWS` - // Yo, this file is gener8ted. Don't mess around with it! - window.albumData = ${stringifyAlbumData()}; - window.flashData = ${stringifyFlashData()}; - window.artistData = ${stringifyArtistData()}; + writeFile(path.join(C.SITE_DIRECTORY, 'data.json'), fixWS` + { + "albumData": ${stringifyAlbumData()}, + "flashData": ${stringifyFlashData()}, + "artistData": ${stringifyArtistData()} + } `) ]); } @@ -1479,12 +1641,18 @@ function generateCoverLink({ // This function title is my gr8test work of art. // (The 8ehavior... well, um. Don't tell anyone, 8ut it's even 8etter.) +/* // RIP, 2k20-2k20. function writeIndexAndTrackPagesForAlbum(album) { return [ () => writeAlbumPage(album), ...album.tracks.map(track => () => writeTrackPage(track)) ]; } +*/ + +function writeAlbumPages() { + return progressPromiseAll(`Writing album pages.`, queue(albumData.map(curry(writeAlbumPage)), queueSize)); +} async function writeAlbumPage(album) { const trackToListItem = track => fixWS` @@ -1507,7 +1675,7 @@ async function writeAlbumPage(album) { ${generateCoverLink({ src: getAlbumCover(album), alt: 'album cover', - tags: getTagsUsedIn(album) + tags: album.artTags })} <h1>${album.name}</h1> <p> @@ -1543,9 +1711,8 @@ async function writeAlbumPage(album) { ` || `<!-- (here: Full-album commentary) -->`} ` }, - sidebar: { - content: generateSidebarForAlbum(album) - }, + sidebar: generateSidebarForAlbum(album), + sidebarRight: generateSidebarRightForAlbum(album), nav: { links: [ ['./', SITE_SHORT_TITLE], @@ -1561,17 +1728,21 @@ async function writeAlbumPage(album) { }); } +function writeTrackPages() { + return progressPromiseAll(`Writing track pages.`, queue(trackData.map(curry(writeTrackPage)), queueSize)); +} + async function writeTrackPage(track) { const { album } = track; - const tracksThatReference = getTracksThatReference(track); + const tracksThatReference = track.referencedBy; const ttrFanon = tracksThatReference.filter(t => t.album.isFanon); const ttrOfficial = tracksThatReference.filter(t => t.album.isOfficial); - const tracksReferenced = getTracksReferencedBy(track); - const otherReleases = getOtherReleasesOf(track); + const tracksReferenced = track.references; + const otherReleases = track.otherReleases; const listTag = getAlbumListTag(track.album); const flashesThatFeature = C.sortByDate([track, ...otherReleases] - .flatMap(track => getFlashesThatFeature(track).map(flash => ({flash, as: track})))); + .flatMap(track => track.flashes.map(flash => ({flash, as: track})))); const generateTrackList = tracks => fixWS` <ul> @@ -1601,9 +1772,8 @@ async function writeTrackPage(track) { style: `${getThemeString(track)}; --album-directory: ${album.directory}; --track-directory: ${track.directory}` }, - sidebar: { - content: generateSidebarForAlbum(album, track) - }, + sidebar: generateSidebarForAlbum(album, track), + sidebarRight: generateSidebarRightForAlbum(album, track), nav: { links: [ @@ -1625,7 +1795,7 @@ async function writeTrackPage(track) { ${generateCoverLink({ src: getTrackCover(track), alt: 'track cover', - tags: getTagsUsedIn(track) + tags: track.artTags })} <h1>${track.name}</h1> <p> @@ -1708,13 +1878,7 @@ async function writeTrackPage(track) { } async function writeArtistPages() { - await progressPromiseAll('Writing artist pages.', queue(artistData.map(artist => () => writeArtistPage(artist)))); -} - -function getTracksByArtist(artistName) { - return allTracks.filter(track => ( - [...track.artists, ...track.contributors].some(({ who }) => who === artistName) - )); + await progressPromiseAll('Writing artist pages.', queue(artistData.map(curry(writeArtistPage)), queueSize)); } async function writeArtistPage(artist) { @@ -1728,25 +1892,26 @@ async function writeArtistPage(artist) { note = '' } = artist; - const tracks = getTracksByArtist(name); - const artThings = justEverythingMan.filter(thing => (thing.coverArtists || []).some(({ who }) => who === name)); - const flashes = flashData.filter(flash => (flash.contributors || []).some(({ who }) => who === name)); + const artThings = justEverythingMan.filter(thing => (thing.coverArtists || []).some(({ who }) => who === artist)); + const flashes = flashData.filter(flash => (flash.contributors || []).some(({ who }) => who === artist)); const commentaryThings = justEverythingMan.filter(thing => thing.commentary && thing.commentary.replace(/<\/?b>/g, '').includes('<i>' + name + ':</i>')); - const unreleasedTracks = tracks.filter(track => track.album.directory === C.UNRELEASED_TRACKS_DIRECTORY); - const releasedTracks = tracks.filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY); + const unreleasedTracks = [...artist.tracks.asArtist, ...artist.tracks.asContributor] + .filter(track => track.album.directory === C.UNRELEASED_TRACKS_DIRECTORY); + const releasedTracks = [...artist.tracks.asArtist, ...artist.tracks.asContributor] + .filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY); const generateTrackList = tracks => albumChunkedList(tracks, (track, i) => { const contrib = { - who: name, - what: track.contributors.filter(({ who }) => who === name).map(({ what }) => what).join(', ') + who: artist, + what: track.contributors.filter(({ who }) => who === artist).map(({ what }) => what).join(', ') }; - const flashes = getFlashesThatFeature(track); + const { flashes } = track; return fixWS` <li ${classes(track.aka && 'rerelease')} title="${th(i + 1)} track by ${name}; ${th(track.album.tracks.indexOf(track) + 1)} in ${track.album.name}"> ${track.duration && `(${getDurationString(track.duration)})` || `<!-- (here: Duration) -->`} <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a> - ${track.artists.some(({ who }) => who === name) && track.artists.length > 1 && `<span class="contributed">(with ${getArtistString(track.artists.filter(({ who }) => who !== name))})</span>` || `<!-- (here: Co-artist credits) -->`} + ${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) -->`} ${contrib.what && `<span class="contributed">(${getContributionString(contrib) || 'contributed'})</span>` || `<!-- (here: Contribution details) -->`} ${flashes.length && `<br><span class="flashes">(Featured in ${joinNoOxford(flashes.map(flash => getFlashLinkHTML(flash)))})</span></br>` || `<!-- (here: Flashes featuring this track) -->`} ${track.aka && `<span class="rerelease-label">(re-release)</span>`} @@ -1774,16 +1939,17 @@ async function writeArtistPage(artist) { <hr> `} ${urls.length && `<p>Visit on ${joinNoOxford(urls.map(fancifyURL), 'or')}.</p>`} + ${artThings.length && `<p>View <a href="${C.ARTIST_DIRECTORY}/${kebab}/gallery/">art gallery</a>!</p>`} <p>Jump to: ${[ [ - tracks.length && `<a href="${index}#tracks">Tracks</a>`, + [...releasedTracks, ...unreleasedTracks].length && `<a href="${index}#tracks">Tracks</a>`, unreleasedTracks.length && `<a href="${index}#unreleased-tracks">(Unreleased Tracks)</a>` ].filter(Boolean).join(' '), artThings.length && `<a href="${index}#art">Art</a>`, flashes.length && `<a href="${index}#flashes">Flashes & Games</a>`, commentaryThings.length && `<a href="${index}#commentary">Commentary</a>` ].filter(Boolean).join(', ')}.</p> - ${tracks.length && fixWS` + ${[...releasedTracks, ...unreleasedTracks].length && fixWS` <h2 id="tracks">Tracks</h2> `} ${releasedTracks.length && fixWS` @@ -1796,14 +1962,15 @@ async function writeArtistPage(artist) { `} ${artThings.length && fixWS` <h2 id="art">Art</h2> + <p>View <a href="${C.ARTIST_DIRECTORY}/${kebab}/gallery/">art gallery</a>! Or browse the list:</p> ${albumChunkedList(artThings, (thing, i) => { - const contrib = thing.coverArtists.find(({ who }) => who === name); + const contrib = thing.coverArtists.find(({ who }) => who === artist); return fixWS` <li title="${th(i + 1)} art by ${name}${thing.album && `; ${th(thing.album.tracks.indexOf(thing) + 1)} track in ${thing.album.name}`}"> ${thing.album ? fixWS` <a href="${C.TRACK_DIRECTORY}/${thing.directory}/" style="${getThemeString(thing)}">${thing.name}</a> ` : '<i>(cover art)</i>'} - ${thing.coverArtists.length > 1 && `<span class="contributed">(with ${getArtistString(thing.coverArtists.filter(({ who }) => who !== name))})</span>`} + ${thing.coverArtists.length > 1 && `<span class="contributed">(with ${getArtistString(thing.coverArtists.filter(({ who }) => who !== artist))})</span>`} ${contrib.what && `<span class="contributed">(${getContributionString(contrib)})</span>`} </li> `; @@ -1812,7 +1979,7 @@ async function writeArtistPage(artist) { ${flashes.length && fixWS` <h2 id="flashes">Flashes & Games</h2> ${actChunkedList(flashes, flash => { - const contributionString = flash.contributors.filter(({ who }) => who === name).map(getContributionString).join(' '); + const contributionString = flash.contributors.filter(({ who }) => who === artist).map(getContributionString).join(' '); return fixWS` <li> <a href="${C.FLASH_DIRECTORY}/${flash.directory}/" style="${getThemeString(flash)}">${flash.name}</a> @@ -1825,13 +1992,13 @@ async function writeArtistPage(artist) { ${commentaryThings.length && fixWS` <h2 id="commentary">Commentary</h2> ${albumChunkedList(commentaryThings, thing => { - const flashes = getFlashesThatFeature(thing); + const { flashes } = thing; return fixWS` <li> ${thing.album ? fixWS` <a href="${C.TRACK_DIRECTORY}/${thing.directory}/" style="${getThemeString(thing)}">${thing.name}</a> ` : '(album commentary)'} - ${flashes.length && `<br><span class="flashes">(Featured in ${joinNoOxford(flashes.map(flash => getFlashLinkHTML(flash)))})</span></br>`} + ${flashes?.length && `<br><span class="flashes">(Featured in ${joinNoOxford(flashes.map(flash => getFlashLinkHTML(flash)))})</span></br>`} </li> ` }, false)} @@ -1845,27 +2012,67 @@ async function writeArtistPage(artist) { ['./', SITE_SHORT_TITLE], [`${C.LISTING_DIRECTORY}/`, 'Listings'], [null, 'Artist:'], - [`${C.ARTIST_DIRECTORY}/${kebab}/`, name] + [`${C.ARTIST_DIRECTORY}/${kebab}/`, name], + [null, `(${[ + `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/" class="current">Info</a>`, + `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/gallery/">Gallery</a>` + ].join(', ')})`] ] } }); + + if (artThings.length) { + await writePage([C.ARTIST_DIRECTORY, kebab, 'gallery'], { + title: name + ' - Gallery', + + main: { + classes: ['top-index'], + content: fixWS` + <h1>${name} - Gallery</h1> + <p class="quick-info">(Contributed to ${s(artThings.length, 'cover art')})</p> + <div class="grid-listing"> + ${getGridHTML({ + entries: artThings.map(item => ({item})), + srcFn: thing => (thing.album + ? getTrackCover(thing) + : getAlbumCover(thing)), + hrefFn: thing => (thing.album + ? `${C.TRACK_DIRECTORY}/${thing.directory}/` + : `${C.ALBUM_DIRECTORY}/${thing.directory}`) + })} + </div> + ` + }, + + nav: { + links: [ + ['./', SITE_SHORT_TITLE], + [`${C.LISTING_DIRECTORY}/`, 'Listings'], + [null, 'Artist:'], + [`${C.ARTIST_DIRECTORY}/${kebab}/`, name], + [null, `(${[ + `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/">Info</a>`, + `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/gallery/" class="current">Gallery</a>` + ].join(', ')})`] + ] + } + }); + } } async function writeArtistAliasPage(artist) { - const { name, alias } = artist; - const kebab1 = C.getArtistDirectory(name); - const kebab2 = C.getArtistDirectory(alias); + const { alias } = artist; - const directory = path.join(C.SITE_DIRECTORY, C.ARTIST_DIRECTORY, kebab1); + const directory = path.join(C.SITE_DIRECTORY, C.ARTIST_DIRECTORY, artist.directory); const file = path.join(directory, 'index.html'); - const target = `/${C.ARTIST_DIRECTORY}/${kebab2}/`; + const target = `/${C.ARTIST_DIRECTORY}/${alias.directory}/`; await mkdirp(directory); await writeFile(file, fixWS` <!DOCTYPE html> <html> <head> - <title>Moved to ${alias}</title> + <title>Moved to ${alias.name}</title> <meta charset="utf-8"> <meta http-equiv="refresh" content="0;url=${target}"> <link rel="canonical" href="${target}"> @@ -1873,7 +2080,7 @@ async function writeArtistAliasPage(artist) { </head> <body> <main> - <h1>Moved to ${alias}</h1> + <h1>Moved to ${alias.name}</h1> <p>This page has been moved to <a href="${target}">${target}</a>.</p> </main> </body> @@ -1941,7 +2148,9 @@ function actChunkedList(flashes, getLI, showDate = true, dateProperty = 'date') } async function writeFlashPages() { - await progressPromiseAll('Writing Flash pages.', queue(flashData.map(flash => () => !flash.act8r8k && writeFlashPage(flash)).filter(Boolean))); + await progressPromiseAll('Writing Flash pages.', queue(flashData + .filter(flash => !flash.act8r8k) + .map(curry(writeFlashPage)), queueSize)); } async function writeFlashPage(flash) { @@ -1980,26 +2189,12 @@ async function writeFlashPage(flash) { ${flash.tracks.length && fixWS` <p>Tracks featured in <i>${flash.name.replace(/\.$/, '')}</i>:</p> <ul> - ${flash.tracks.map(ref => { - const track = getLinkedTrack(ref); - if (track) { - return fixWS` - <li> - <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a> - <span class="by">by ${getArtistString(track.artists)}</span> - </li> - `; - } else { - const by = ref.match(/\(by .*\)/); - if (by) { - const name = ref.replace(by, '').trim(); - const contribs = by[0].replace(/\(by |\)/g, '').split(',').map(w => ({who: w.trim()})); - return `<li>${name} <span class="by">by ${getArtistString(contribs)}</span></li>`; - } else { - return `<li>${ref}</li>`; - } - } - }).join('\n')} + ${flash.tracks.map(track => fixWS` + <li> + <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a> + <span class="by">by ${getArtistString(track.artists)}</span> + </li> + `).join('\n')} </ul> ` || `<!-- (here: Flash track listing) -->`} ${flash.contributors.length && fixWS` @@ -2085,8 +2280,7 @@ function generateSidebarForFlashes(flash) { } function writeListingPages() { - const allArtists = artistNames.slice().sort(); - const reversedTracks = allTracks.slice().reverse(); + const reversedTracks = trackData.slice().reverse(); const reversedThings = justEverythingMan.slice().reverse(); const getAlbumLI = (album, extraText = '') => fixWS` @@ -2096,13 +2290,6 @@ function writeListingPages() { </li> `; - const getArtistLI = artistName => fixWS` - <li> - <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artistName)}/">${artistName}</a> - (${'' + C.getArtistNumContributions(artistName, {allTracks, albumData, flashData})} <abbr title="contributions (to music, art, and flashes)">c.</abbr>) - </li> - `; - const sortByName = (a, b) => { const an = a.name.toLowerCase(); const bn = b.name.toLowerCase(); @@ -2122,18 +2309,23 @@ function writeListingPages() { [['albums', 'by-tracks'], `Albums - by Tracks`, albumData.slice() .sort((a, b) => b.tracks.length - a.tracks.length) .map(album => getAlbumLI(album, `(${s(album.tracks.length, 'track')})`))], - [['artists', 'by-name'], `Artists - by Name`, allArtists - .map(name => ({name})) + [['artists', 'by-name'], `Artists - by Name`, artistData + .filter(artist => !artist.alias) .sort(sortByName) - .map(({ name }) => name) - .map(getArtistLI)], - [['artists', 'by-commentary'], `Artists - by Commentary`, allArtists - .map(name => ({name, commentary: C.getArtistCommentary(name, {justEverythingMan}).length})) + .map(artist => fixWS` + <li> + <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/">${artist.name}</a> + (${'' + C.getArtistNumContributions(artist)} <abbr title="contributions (to music, art, and flashes)">c.</abbr>) + </li> + `)], + [['artists', 'by-commentary'], `Artists - by Commentary`, artistData + .filter(artist => !artist.alias) + .map(artist => ({artist, commentary: C.getArtistCommentary(artist, {justEverythingMan}).length})) .filter(({ commentary }) => commentary > 0) .sort((a, b) => b.commentary - a.commentary) - .map(({ name, commentary }) => fixWS` + .map(({ artist, commentary }) => fixWS` <li> - <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/#commentary">${name}</a> + <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/#commentary">${artist.name}</a> (${commentary} ${commentary === 1 ? 'entry' : 'entries'}) </li> `)], @@ -2142,15 +2334,16 @@ function writeListingPages() { <div class="column"> <h2>Track Contributors</h2> <ul> - ${allArtists - .map(name => ({ - name, - contribs: allTracks.filter(({ album, artists, contributors }) => + ${artistData + .filter(artist => !artist.alias) + .map(artist => ({ + name: artist.name, + contribs: trackData.filter(({ album, artists, contributors }) => album?.directory !== C.UNRELEASED_TRACKS_DIRECTORY && [ ...artists, ...contributors - ].some(({ who }) => who === name)).length + ].some(({ who }) => who === artist)).length })) .sort((a, b) => b.contribs - a.contribs) .filter(({ contribs }) => contribs) @@ -2167,22 +2360,23 @@ function writeListingPages() { <div class="column"> <h2>Art & Flash Contributors</h2> <ul> - ${allArtists - .map(name => ({ - name, + ${artistData + .filter(artist => !artist.alias) + .map(artist => ({ + artist, contribs: justEverythingMan.filter(({ album, contributors, coverArtists }) => ( album?.directory !== C.UNRELEASED_TRACKS_DIRECTORY && [ ...!album && contributors || [], ...coverArtists || [] - ].some(({ who }) => who === name) + ].some(({ who }) => who === artist) )).length })) .sort((a, b) => b.contribs - a.contribs) .filter(({ contribs }) => contribs) - .map(({ name, contribs }) => fixWS` + .map(({ artist, contribs }) => fixWS` <li> - <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}">${name}</a> + <a href="${C.ARTIST_DIRECTORY}/${artist.directory}">${artist.name}</a> (${contribs} <abbr title="contributions (to art and flashes)">c.</abbr>) </li> `) @@ -2192,15 +2386,16 @@ function writeListingPages() { </div> </div> `], - [['artists', 'by-duration'], `Artists - by Duration`, allArtists - .map(name => ({name, duration: getTotalDuration( - getTracksByArtist(name).filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY)) + [['artists', 'by-duration'], `Artists - by Duration`, artistData + .filter(artist => !artist.alias) + .map(artist => ({artist, duration: getTotalDuration( + [...artist.tracks.asArtist, ...artist.tracks.asContributor].filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY)) })) .filter(({ duration }) => duration > 0) .sort((a, b) => b.duration - a.duration) - .map(({ name, duration }) => fixWS` + .map(({ artist, duration }) => fixWS` <li> - <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/#tracks">${name}</a> + <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/#tracks">${artist.name}</a> (~${getDurationString(duration)}) </li> `)], @@ -2209,19 +2404,20 @@ function writeListingPages() { <div class="column"> <h2>Track Contributors</h2> <ul> - ${C.sortByDate(allArtists - .map(name => ({ - name, + ${C.sortByDate(artistData + .filter(artist => !artist.alias) + .map(artist => ({ + artist, date: reversedTracks.find(({ album, artists, contributors }) => ( album.directory !== C.UNRELEASED_TRACKS_DIRECTORY && - [...artists, ...contributors].some(({ who }) => who === name) + [...artists, ...contributors].some(({ who }) => who === artist) ))?.date })) .filter(({ date }) => date) .sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0) - ).reverse().map(({ name, date }) => fixWS` + ).reverse().map(({ artist, date }) => fixWS` <li> - <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/">${name}</a> + <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/">${artist.name}</a> (${getDateString({date})}) </li> `).join('\n')} @@ -2230,24 +2426,25 @@ function writeListingPages() { <div class="column"> <h2>Art & Flash Contributors</h2> <ul> - ${C.sortByDate(allArtists - .map(name => { + ${C.sortByDate(artistData + .filter(artist => !artist.alias) + .map(artist => { const thing = reversedThings.find(({ album, coverArtists, contributors }) => ( album?.directory !== C.UNRELEASED_TRACKS_DIRECTORY && - [...coverArtists || [], ...!album && contributors || []].some(({ who }) => who === name) + [...coverArtists || [], ...!album && contributors || []].some(({ who }) => who === artist) )); return thing && { - name, - date: (thing.coverArtists?.some(({ who }) => who === name) + artist, + date: (thing.coverArtists?.some(({ who }) => who === artist) ? thing.coverArtDate : thing.date) }; }) .filter(Boolean) .sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0) - ).reverse().map(({ name, date }) => fixWS` + ).reverse().map(({ artist, date }) => fixWS` <li> - <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(name)}/">${name}</a> + <a href="${C.ARTIST_DIRECTORY}/${artist.directory}">${artist.name}</a> (${getDateString({date})}) </li> `).join('\n')} @@ -2255,7 +2452,7 @@ function writeListingPages() { </div> </div> `], - [['tracks', 'by-name'], `Tracks - by Name`, allTracks.slice() + [['tracks', 'by-name'], `Tracks - by Name`, trackData.slice() .sort(sortByName) .map(track => fixWS` <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li> @@ -2273,11 +2470,11 @@ function writeListingPages() { </dl> `], [['tracks', 'by-date'], `Tracks - by Date`, albumChunkedList( - C.sortByDate(allTracks.filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY)), + C.sortByDate(trackData.filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY)), track => fixWS` <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> `)], - [['tracks', 'by-duration'], `Tracks - by Duration`, C.sortByDate(allTracks.slice()) + [['tracks', 'by-duration'], `Tracks - by Duration`, C.sortByDate(trackData.slice()) .filter(track => track.duration > 0) .sort((a, b) => b.duration - a.duration) .map(track => fixWS` @@ -2300,24 +2497,37 @@ function writeListingPages() { `, false, null)], - [['tracks', 'by-times-referenced'], `Tracks - by Times Referenced`, C.sortByDate(allTracks.slice()) - .filter(track => getTracksThatReference(track).length > 0) - .sort((a, b) => getTracksThatReference(b).length - getTracksThatReference(a).length) + [['tracks', 'by-times-referenced'], `Tracks - by Times Referenced`, C.sortByDate(trackData.slice()) + .filter(track => track.referencedBy.length > 0) + .sort((a, b) => b.referencedBy.length - a.referencedBy.length) .map(track => fixWS` <li> <a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a> - (${s(getTracksThatReference(track).length, 'time')} referenced) + (${s(track.referencedBy.length, 'time')} referenced) </li> `)], [['tracks', 'in-flashes', 'by-album'], `Tracks - in Flashes & Games (by Album)`, albumChunkedList( - C.sortByDate(allTracks.slice()).filter(track => getFlashesThatFeature(track).length > 0), + C.sortByDate(trackData.slice()).filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY && track.flashes.length > 0), track => `<li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>`)], - [['tracks', 'in-flashes', 'by-flash'], `Tracks - in Flashes & Games (by First Feature)`, - Array.from(new Set(flashData.filter(flash => !flash.act8r8k).flatMap(flash => getTracksFeaturedByFlash(flash)))) - .filter(Boolean) - .map(track => `<li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li>`)], + [['tracks', 'in-flashes', 'by-flash'], `Tracks - in Flashes & Games (by Flash)`, fixWS` + <dl> + ${C.sortByDate(flashData.filter(flash => !flash.act8r8k)) + .map(flash => fixWS` + <dt> + <a href="${C.FLASH_DIRECTORY}/${flash.directory}/" style="${getThemeString(flash)}">${flash.name}</a> + (${getDateString(flash)}) + </dt> + <dd><ul> + ${flash.tracks.map(track => fixWS` + <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li> + `).join('\n')} + </ul></dd> + `) + .join('\n')} + </dl> + `], [['tracks', 'with-lyrics'], `Tracks - with Lyrics`, albumChunkedList( - C.sortByDate(allTracks.slice()) + C.sortByDate(trackData.slice()) .filter(track => track.lyrics), track => fixWS` <li><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></li> @@ -2327,7 +2537,7 @@ function writeListingPages() { .map(tag => `<li><a href="${C.TAG_DIRECTORY}/${tag.directory}/" style="${getThemeString(tag)}">${tag.name}</a></li>`)], [['tags', 'by-uses'], 'Tags - by Uses', tagData.slice().sort(sortByName) .filter(tag => !tag.isCW) - .map(tag => ({tag, timesUsed: getThingsThatUseTag(tag).length})) + .map(tag => ({tag, timesUsed: tag.things.length})) .sort((a, b) => b.timesUsed - a.timesUsed) .map(({ tag, timesUsed }) => `<li><a href="${C.TAG_DIRECTORY}/${tag.directory}/" style="${getThemeString(tag)}">${tag.name}</a> (${s(timesUsed, 'time')})</li>`)] ]; @@ -2337,7 +2547,7 @@ function writeListingPages() { return `${Math.floor(wordCount / 100) / 10}k`; }; - const releasedTracks = allTracks.filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY); + const releasedTracks = trackData.filter(track => track.album.directory !== C.UNRELEASED_TRACKS_DIRECTORY); const releasedAlbums = albumData.filter(album => album.directory !== C.UNRELEASED_TRACKS_DIRECTORY); return progressPromiseAll(`Writing listing pages.`, [ @@ -2402,8 +2612,8 @@ function writeListingPages() { </blockquote> ` || `<!-- (here: Full-album commentary) -->`} ${tracks.filter(t => t.commentary).map(track => fixWS` - <h3 id="${track.directory}"><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(album)}">${track.name}</a></h3> - <blockquote style="${getThemeString(album)}"> + <h3 id="${track.directory}"><a href="${C.TRACK_DIRECTORY}/${track.directory}/" style="${getThemeString(track)}">${track.name}</a></h3> + <blockquote style="${getThemeString(track)}"> ${transformMultiline(track.commentary)} </blockquote> `).join('\n') || `<!-- (here: Per-track commentary) -->`} @@ -2433,6 +2643,8 @@ function writeListingPages() { content: fixWS` <h1>Random Pages</h1> <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> + <p class="js-hide-once-data">(Data files are downloading in the background! Please wait for data to load.)</p> + <p class="js-show-once-data">(Data files have finished being downloaded. The links should work!)</p> <dl> <dt>Miscellaneous:</dt> <dd><ul> @@ -2528,13 +2740,13 @@ function generateLinkIndexForListings(listingDescriptors, currentDirectoryParts) } function writeTagPages() { - return progressPromiseAll(`Writing tag pages.`, tagData + return progressPromiseAll(`Writing tag pages.`, queue(tagData .filter(tag => !tag.isCW) - .map(writeTagPage)); + .map(curry(writeTagPage)), queueSize)); } function writeTagPage(tag) { - const things = getThingsThatUseTag(tag); + const { things } = tag; return writePage([C.TAG_DIRECTORY, tag.directory], { title: tag.name, @@ -2577,71 +2789,53 @@ function writeTagPage(tag) { function getContributionString({ what }) { return what ? what.replace(/\[(.*?)\]/g, (match, name) => - allTracks.some(track => track.name === name) - ? `<i><a href="${C.TRACK_DIRECTORY}/${allTracks.find(track => track.name === name).directory}/">${name}</a></i>` + trackData.some(track => track.name === name) + ? `<i><a href="${C.TRACK_DIRECTORY}/${trackData.find(track => track.name === name).directory}/">${name}</a></i>` : `<i>${name}</i>`) : ''; } -const getTracksThatReference = cacheOneArg(track => - allTracks.filter(t => getTracksReferencedBy(t).includes(track))); - -const getTracksReferencedBy = cacheOneArg(track => - track.references.map(ref => getLinkedTrack(ref)).filter(Boolean)); - -const getThingsThatUseTag = cacheOneArg(tag => - C.sortByArtDate([...albumData, ...allTracks]).filter(thing => thing.artTags.includes(tag.name))); - -const getTagsUsedIn = cacheOneArg(thing => - (thing.artTags || []).map(tagName => { - if (tagName.startsWith('cw: ')) { - tagName = tagName.slice(4); - } - tagName = tagName.toLowerCase() - return tagData.find(tag => tag.name.toLowerCase() === tagName); - }).filter(Boolean)); - -const getOtherReleasesOf = cacheOneArg(track => [ - track.aka && getLinkedTrack(track.aka), - ...allTracks.filter(({ aka }) => aka && getLinkedTrack(aka) === track) -].filter(Boolean)); - function getLinkedTrack(ref) { + if (!ref) return null; + if (ref.includes('track:')) { ref = ref.replace('track:', ''); - return allTracks.find(track => track.directory === ref); + return trackData.find(track => track.directory === ref); } const match = ref.match(/\S:(.*)/); if (match) { const dir = match[1]; - return allTracks.find(track => track.directory === dir); + return trackData.find(track => track.directory === dir); } let track; - track = allTracks.find(track => track.directory === ref); + track = trackData.find(track => track.directory === ref); if (track) { return track; } - track = allTracks.find(track => track.name === ref); + track = trackData.find(track => track.name === ref); if (track) { return track; } - track = allTracks.find(track => track.name.toLowerCase() === ref.toLowerCase()); + track = trackData.find(track => track.name.toLowerCase() === ref.toLowerCase()); if (track) { console.warn(`\x1b[33mBad capitalization:\x1b[0m`); console.warn(`\x1b[31m- ${ref}\x1b[0m`); console.warn(`\x1b[32m+ ${track.name}\x1b[0m`); return track; } + + return null; } function getLinkedAlbum(ref) { + if (!ref) return null; ref = ref.replace('album:', ''); - let album + let album; album = albumData.find(album => album.directory === ref); if (!album) album = albumData.find(album => album.name === ref); if (!album) { @@ -2656,7 +2850,28 @@ function getLinkedAlbum(ref) { return album; } +function getLinkedGroup(ref) { + if (!ref) return null; + ref = ref.replace('group:', ''); + let group; + group = groupData.find(group => group.directory === ref); + if (!group) group = groupData.find(group => group.name === ref); + if (!group) { + group = groupData.find(group => group.name.toLowerCase() === ref.toLowerCase()); + if (group) { + console.warn(`\x1b[33mBad capitalization:\x1b[0m`); + console.warn(`\x1b[31m- ${ref}\x1b[0m`); + console.warn(`\x1b[32m+ ${group.name}\x1b[0m`); + return group; + } + } + return group; +} + function getLinkedArtist(ref) { + if (!ref) return null; + ref = ref.replace('artist:', ''); + let artist = artistData.find(artist => C.getArtistDirectory(artist.name) === ref); if (artist) { return artist; @@ -2666,24 +2881,44 @@ function getLinkedArtist(ref) { if (artist) { return artist; } + + return null; } function getLinkedFlash(ref) { + if (!ref) return null; ref = ref.replace('flash:', ''); return flashData.find(flash => flash.directory === ref); } -const getFlashesThatFeature = cacheOneArg(track => - flashData.filter(flash => (getTracksFeaturedByFlash(flash) || []).includes(track))); +function getLinkedTag(ref) { + if (!ref) return null; + + ref = ref.replace('tag:', ''); + + let tag = tagData.find(tag => tag.directory === ref); + if (tag) { + return tag; + } -const getTracksFeaturedByFlash = cacheOneArg(flash => - flash.tracks && flash.tracks.map(t => getLinkedTrack(t))); + if (ref.startsWith('cw: ')) { + ref = ref.slice(4); + } + + tag = tagData.find(tag => tag.name === ref); + if (tag) { + return tag; + } + + return null; +} function getArtistString(artists, showIcons = false) { return joinNoOxford(artists.map(({ who, what }) => { - const { urls = [] } = artistData.find(({ name }) => name === who) || {}; + if (!who) console.log(artists); + const { urls, directory, name } = who; return ( - `<a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(who)}/">${who}</a>` + + `<a href="${C.ARTIST_DIRECTORY}/${directory}/">${name}</a>` + (what ? ` (${getContributionString({what})})` : '') + (showIcons && urls.length ? ` <span class="icons">(${urls.map(iconifyURL).join(', ')})</span>` : '') ); @@ -2806,8 +3041,6 @@ function chronologyLinks(currentTrack, { return `<div class="chronology">(See artist pages for chronology info!)</div>`; } return artists.map(artist => { - if (!artistNames.includes(artist)) return ''; - const releasedThings = sourceData.filter(thing => { const album = albumData.includes(thing) ? thing : thing.album; if (album && album.directory === C.UNRELEASED_TRACKS_DIRECTORY) { @@ -2830,7 +3063,7 @@ function chronologyLinks(currentTrack, { next && `<a href="${getHrefOfAnythingMan(next)}" title="${next.name}">Next</a>` ].filter(Boolean); - const heading = `${th(index + 1)} ${headingWord} by <a href="${C.ARTIST_DIRECTORY}/${C.getArtistDirectory(artist)}/">${artist}</a>`; + const heading = `${th(index + 1)} ${headingWord} by <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/">${artist.name}</a>`; return fixWS` <div class="chronology"> @@ -2857,9 +3090,9 @@ function generateAlbumNavLinks(album, currentTrack = null) { ]; if (previousLine || nextLine) { - return `(${[previousLine, nextLine].filter(Boolean).join(', ')}<span class="js-hide">, ${randomLine}</span>)`; + return `(${[previousLine, nextLine].filter(Boolean).join(', ')}<span class="js-hide-until-data">, ${randomLine}</span>)`; } else { - return `<span class="js-hide">(${randomLine})</span>`; + return `<span class="js-hide-until-data">(${randomLine})</span>`; } } @@ -2867,7 +3100,7 @@ function generateAlbumChronologyLinks(album, currentTrack = null) { return [ currentTrack && chronologyLinks(currentTrack, { headingWord: 'track', - sourceData: allTracks, + sourceData: trackData, filters: [ { mapProperty: 'artists', @@ -2895,7 +3128,7 @@ function generateAlbumChronologyLinks(album, currentTrack = null) { function generateSidebarForAlbum(album, currentTrack = null) { const trackToListItem = track => `<li ${classes(track === currentTrack && 'current')}><a href="${C.TRACK_DIRECTORY}/${track.directory}/">${track.name}</a></li>`; const listTag = getAlbumListTag(album); - return fixWS` + return {content: fixWS` <h1><a href="${C.ALBUM_DIRECTORY}/${album.directory}/">${album.name}</a></h1> ${album.usesGroups ? fixWS` <dl> @@ -2919,13 +3152,133 @@ function generateSidebarForAlbum(album, currentTrack = null) { ${album.tracks.map(trackToListItem).join('\n')} </${listTag}> `} + `}; +} + +function generateSidebarRightForAlbum(album, currentTrack = null) { + const { groups } = album; + if (groups.length) { + return { + multiple: groups.map(group => { + const index = group.albums.indexOf(album); + const next = group.albums[index + 1]; + const previous = group.albums[index - 1]; + return {group, next, previous}; + }).map(({group, next, previous}) => fixWS` + <h1><a href="${C.GROUP_DIRECTORY}/${group.directory}/">${group.name}</a></h1> + ${!currentTrack && group.descriptionShort} + <p>Visit on ${joinNoOxford(group.urls.map(fancifyURL), 'or')}.</p> + ${!currentTrack && fixWS` + ${next && `<p class="group-chronology-link">Next: <a href="${C.ALBUM_DIRECTORY}/${next.directory}/" style="${getThemeString(next)}">${next.name}</a></p>`} + ${previous && `<p class="group-chronology-link">Previous: <a href="${C.ALBUM_DIRECTORY}/${previous.directory}/" style="${getThemeString(previous)}">${previous.name}</a></p>`} + `} + `) + }; + }; +} + +function generateSidebarForGroup(isGallery = false, currentGroup = null) { + return ` + <h1>Groups</h1> + <ul> + ${groupData.map(group => fixWS` + <li ${classes(group === currentGroup && 'current')}> + <a href="${C.GROUP_DIRECTORY}/${group.directory}/${isGallery && 'gallery/'}">${group.name}</a> + </li> + `).join('\n')} + </ul> ` } +function writeGroupPages() { + return progressPromiseAll(`Writing group pages.`, queue(groupData.map(curry(writeGroupPage)), queueSize)); +} + +async function writeGroupPage(group) { + const releasedAlbums = group.albums.filter(album => album.directory !== C.UNRELEASED_TRACKS_DIRECTORY); + const releasedTracks = releasedAlbums.flatMap(album => album.tracks); + const totalDuration = getTotalDuration(releasedTracks); + + await writePage([C.GROUP_DIRECTORY, group.directory], { + title: group.name, + body: { + style: getThemeString(group) + }, + main: { + content: fixWS` + <h1>${group.name}</h1> + <blockquote> + ${transformMultiline(group.description)} + </blockquote> + <p>Albums:</p> + <ul> + ${group.albums.map(album => fixWS` + <li> + (${album.date.getFullYear()}) + <a href="${C.ALBUM_DIRECTORY}/${album.directory}/" style="${getThemeString(album)}">${album.name}</a> + </li> + `).join('\n')} + </ul> + ` + }, + sidebar: { + content: generateSidebarForGroup(false, group) + }, + nav: { + links: [ + ['./', SITE_SHORT_TITLE], + [`${C.LISTING_DIRECTORY}/`, 'Listings'], + [null, 'Group:'], + [`${C.GROUP_DIRECTORY}/${group.directory}/`, group.name], + [null, `(${[ + `<a href="${C.GROUP_DIRECTORY}/${group.directory}/" class="current">Info</a>`, + `<a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/">Gallery</a>` + ].join(', ')})`] + ] + } + }); + + await writePage([C.GROUP_DIRECTORY, group.directory, 'gallery'], { + title: `${group.name} - Gallery`, + body: { + style: getThemeString(group) + }, + main: { + classes: ['top-index'], + content: fixWS` + <h1>${group.name} - Gallery</h1> + <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> + <div class="grid-listing"> + ${getGridHTML({ + entries: C.sortByDate(group.albums.map(item => ({item}))).reverse(), + srcFn: getAlbumCover, + hrefFn: album => `${C.ALBUM_DIRECTORY}/${album.directory}/` + })} + </div> + ` + }, + sidebar: { + content: generateSidebarForGroup(true, group) + }, + nav: { + links: [ + ['./', SITE_SHORT_TITLE], + [`${C.LISTING_DIRECTORY}/`, 'Listings'], + [null, 'Group:'], + [`${C.GROUP_DIRECTORY}/${group.directory}/`, group.name], + [null, `(${[ + `<a href="${C.GROUP_DIRECTORY}/${group.directory}/">Info</a>`, + `<a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/" class="current">Gallery</a>` + ].join(', ')})`] + ] + } + }); +} + function getHrefOfAnythingMan(anythingMan) { return ( albumData.includes(anythingMan) ? C.ALBUM_DIRECTORY : - allTracks.includes(anythingMan) ? C.TRACK_DIRECTORY : + trackData.includes(anythingMan) ? C.TRACK_DIRECTORY : flashData.includes(anythingMan) ? C.FLASH_DIRECTORY : 'idk-bud' ) + '/' + ( @@ -3050,7 +3403,7 @@ async function main() { } } - allTracks = C.getAllTracks(albumData); + trackData = C.getAllTracks(albumData); flashData = await processFlashDataFile(path.join(C.DATA_DIRECTORY, FLASH_DATA_FILE)); if (flashData.error) { @@ -3100,6 +3453,22 @@ async function main() { } } + groupData = await processGroupDataFile(path.join(C.DATA_DIRECTORY, GROUP_DATA_FILE)); + if (groupData.error) { + console.log(`\x1b[31;1m${groupData.error}\x1b[0m`); + return; + } + + { + const errors = groupData.filter(obj => obj.error); + if (errors.length) { + for (const error of errors) { + console.log(`\x1b[31;1m${error.error}\x1b[0m`); + } + return; + } + } + newsData = await processNewsDataFile(path.join(C.DATA_DIRECTORY, NEWS_DATA_FILE)); if (newsData.error) { console.log(`\x1b[31;1m${newsData.error}\x1b[0m`); @@ -3115,7 +3484,7 @@ async function main() { } { - const tagNames = new Set([...allTracks, ...albumData].flatMap(thing => thing.artTags)); + const tagNames = new Set([...trackData, ...albumData].flatMap(thing => thing.artTags)); for (let { name, isCW } of tagData) { if (isCW) { @@ -3136,7 +3505,7 @@ async function main() { officialAlbumData = albumData.filter(album => !album.isFanon); fandomAlbumData = albumData.filter(album => album.isFanon); - justEverythingMan = C.sortByDate(albumData.concat(allTracks, flashData.filter(flash => !flash.act8r8k))); + justEverythingMan = C.sortByDate(albumData.concat(trackData, flashData.filter(flash => !flash.act8r8k))); justEverythingSortedByArtDateMan = C.sortByArtDate(justEverythingMan.slice()); // console.log(JSON.stringify(justEverythingSortedByArtDateMan.map(getHrefOfAnythingMan), null, 2)); @@ -3206,7 +3575,7 @@ async function main() { { const directories = []; const where = {}; - for (const { directory, album } of allTracks) { + for (const { directory, album } of trackData) { if (directories.includes(directory)) { console.log(`\x1b[31;1mDuplicate track directory "${directory}"\x1b[0m`); console.log(`Shows up in:`); @@ -3234,7 +3603,7 @@ async function main() { } { - for (const { references, name, album } of allTracks) { + for (const { references, name, album } of trackData) { for (const ref of references) { // Skip these, for now. if (ref.includes("by")) { @@ -3247,12 +3616,81 @@ async function main() { } } + contributionData = Array.from(new Set([ + ...trackData.flatMap(track => [...track.artists || [], ...track.contributors || [], ...track.coverArtists || []]), + ...albumData.flatMap(album => [...album.coverArtists || [], ...album.artists || []]), + ...flashData.flatMap(flash => [...flash.contributors || []]) + ])); + + // Now that we have all the data, resolve references all 8efore actually + // gener8ting any of the pages, 8ecause page gener8tion is going to involve + // accessing these references a lot, and there's no reason to resolve them + // more than once. (We 8uild a few additional links that can't 8e cre8ted + // at initial data processing time here too.) + + const filterNull = (parent, key) => { + for (const obj of parent) { + const array = obj[key]; + for (let i = 0; i < array.length; i++) { + if (!Boolean(array[i])) { + const prev = array[i - 1] && array[i - 1].name; + const next = array[i + 1] && array[i + 1].name; + console.log(`\x1b[33mUnexpected null in ${obj.name} (${key}) - prev: ${prev}, next: ${next}\x1b[0m`); + } + } + array.splice(0, array.length, ...array.filter(Boolean)); + } + }; + + const actlessFlashData = flashData.filter(flash => !flash.act8r8k); + + trackData.forEach(track => mapInPlace(track.references, getLinkedTrack)); + trackData.forEach(track => track.aka = getLinkedTrack(track.aka)); + trackData.forEach(track => mapInPlace(track.artTags, getLinkedTag)); + albumData.forEach(album => mapInPlace(album.groups, getLinkedGroup)); + albumData.forEach(album => mapInPlace(album.artTags, getLinkedTag)); + artistData.forEach(artist => artist.alias = getLinkedArtist(artist.alias)); + actlessFlashData.forEach(flash => mapInPlace(flash.tracks, getLinkedTrack)); + contributionData.forEach(contrib => contrib.who = getLinkedArtist(contrib.who)); + + filterNull(trackData, 'references'); + filterNull(albumData, 'groups'); + filterNull(actlessFlashData, 'tracks'); + + trackData.forEach(track1 => track1.referencedBy = trackData.filter(track2 => track2.references.includes(track1))); + trackData.forEach(track => track.flashes = actlessFlashData.filter(flash => flash.tracks.includes(track))); + groupData.forEach(group => group.albums = albumData.filter(album => album.groups.includes(group))); + tagData.forEach(tag => tag.things = C.sortByArtDate([...albumData, ...trackData]).filter(thing => thing.artTags.includes(tag))); + + trackData.forEach(track => track.otherReleases = [ + track.aka, + ...trackData.filter(({ aka }) => aka === track) + ].filter(Boolean)); + + artistData.forEach(artist => { + const filterProp = prop => trackData.filter(track => track[prop]?.some(({ who }) => who === artist)); + artist.tracks = { + asArtist: filterProp('artists'), + asContributor: filterProp('contributors'), + asCoverArtist: filterProp('coverArtists'), + asAny: trackData.filter(track => ( + [...track.artists, ...track.contributors, ...track.coverArtists || []].some(({ who }) => who === artist) + )) + }; + artist.albums = albumData.filter(album => ( + [...album.coverArtists].some(({ who }) => who === artist) + )); + artist.flashes = flashData.filter(flash => ( + [...flash.contributors || []].some(({ who }) => who === artist) + )); + }); + /* console.log(artistData .filter(a => !a.alias) .map(a => ({ artist: a.name, - things: C.getThingsArtistContributedTo(a.name, {allTracks, albumData, flashData}), + things: C.getThingsArtistContributedTo(a.name, {trackData, albumData, flashData}), urls: a.urls.filter(url => url.includes('youtu') || url.includes('soundcloud') || @@ -3264,7 +3702,7 @@ async function main() { .filter(a => a.urls.length === 0) .map(a => ({ ...a, - things: (C.getThingsArtistContributedTo(a.artist, {allTracks, albumData, flashData}))})) + things: (C.getThingsArtistContributedTo(a.artist, {trackData, albumData, flashData}))})) .sort((a, b) => b.things.length - a.things.length) .map(a => [ `* ${a.artist} (${a.things.length} c.)`// .padEnd(40, '.') + @@ -3289,13 +3727,57 @@ async function main() { process.exit(); */ + const miscOptions = await parseOptions(process.argv.slice(2), { + 'queue-size': { + type: 'value', + validate(size) { + if (parseInt(size) !== parseFloat(size)) return 'an integer'; + if (parseInt(size) < 0) return 'a counting number or zero'; + return true; + } + }, + queue: {alias: 'queue-size'}, + + [parseOptions.handleUnknown]: () => {} + }); + + // Makes writing a little nicer on CPU theoretically, 8ut also costs in + // performance right now 'cuz it'll w8 for file writes to 8e completed + // 8efore moving on to more data processing. So, defaults to zero, which + // disa8les the queue feature altogether. + queueSize = +(miscOptions['queue-size'] ?? 0); + + // NOT for ena8ling or disa8ling specific features of the site! + // This is only in charge of what general groups of files to 8uild. + // They're here to make development quicker when you're only working + // on some particular area(s) of the site rather than making changes + // across all of them. + const buildFlags = await parseOptions(process.argv.slice(2), { + all: {type: 'flag'}, // Defaults to true if none 8elow specified. + + album: {type: 'flag'}, + artist: {type: 'flag'}, + flash: {type: 'flag'}, + group: {type: 'flag'}, + list: {type: 'flag'}, + misc: {type: 'flag'}, + tag: {type: 'flag'}, + track: {type: 'flag'}, + + [parseOptions.handleUnknown]: () => {} + }); + + const buildAll = !Object.keys(buildFlags).length || buildFlags.all; + await writeSymlinks(); - await writeMiscellaneousPages(); - await writeListingPages(); - await writeTagPages(); - await progressPromiseAll(`Writing album & track pages.`, queue(albumData.map(album => writeIndexAndTrackPagesForAlbum(album)).reduce((a, b) => a.concat(b)))); - await writeArtistPages(); - await writeFlashPages(); + if (buildAll || buildFlags.misc) await writeMiscellaneousPages(); + if (buildAll || buildFlags.list) await writeListingPages(); + if (buildAll || buildFlags.tag) await writeTagPages(); + if (buildAll || buildFlags.group) await writeGroupPages(); + if (buildAll || buildFlags.album) await writeAlbumPages(); + if (buildAll || buildFlags.track) await writeTrackPages(); + if (buildAll || buildFlags.artist) await writeArtistPages(); + if (buildAll || buildFlags.flash) await writeFlashPages(); decorateTime.displayTime(); |