diff options
-rw-r--r-- | common/common.js | 10 | ||||
-rw-r--r-- | static/client.js | 4 | ||||
-rw-r--r-- | static/site.css | 69 | ||||
-rw-r--r-- | upd8.js | 430 |
4 files changed, 296 insertions, 217 deletions
diff --git a/common/common.js b/common/common.js index 5db5ad95..4ee73a14 100644 --- a/common/common.js +++ b/common/common.js @@ -77,12 +77,16 @@ const C = { ABOUT_DIRECTORY: 'about', FEEDBACK_DIRECTORY: 'feedback', CHANGELOG_DIRECTORY: 'changelog', + DISCORD_DIRECTORY: 'discord', + DONATE_DIRECTORY: 'donate', FLASH_DIRECTORY: 'flash', NEWS_DIRECTORY: 'news', GROUP_DIRECTORY: 'group', JS_DISABLED_DIRECTORY: 'js-disabled', UNRELEASED_TRACKS_DIRECTORY: 'unreleased-tracks', + OFFICIAL_GROUP_DIRECTORY: 'official', + FANDOM_GROUP_DIRECTORY: 'fandom', // This function was originally made to sort just al8um data, 8ut its exact // code works fine for sorting tracks too, so I made the varia8les and names @@ -124,7 +128,11 @@ const C = { // "directories", we just reformat the artist's name. getArtistDirectory: artistName => C.getKebabCase(artistName), - getArtistNumContributions: artist => (artist.tracks.length + artist.albums.length + artist.flashes.length), + getArtistNumContributions: artist => ( + artist.tracks.asAny.length + + artist.albums.asCoverArtist.length + + artist.flashes.asContributor.length + ), getArtistCommentary: (artist, {justEverythingMan}) => justEverythingMan.filter(thing => thing.commentary && thing.commentary.replace(/<\/?b>/g, '').includes('<i>' + artist.name + ':</i>')) }; diff --git a/static/client.js b/static/client.js index 549fde29..83c82a29 100644 --- a/static/client.js +++ b/static/client.js @@ -167,8 +167,8 @@ fetch(rebase('data.json')).then(data => data.json()).then(data => { artistData = data.artistData; flashData = data.flashData; - officialAlbumData = albumData.filter(album => !album.isFanon); - fandomAlbumData = albumData.filter(album => album.isFanon); + officialAlbumData = albumData.filter(album => album.groups.includes('group:official')); + fandomAlbumData = albumData.filter(album => !album.groups.includes('group:official')); artistNames = artistData.filter(artist => !artist.alias).map(artist => artist.name); for (const element of elements1) element.style.display = 'none'; diff --git a/static/site.css b/static/site.css index 90f8ed3e..e76054ab 100644 --- a/static/site.css +++ b/static/site.css @@ -31,7 +31,7 @@ body::before { height: 100%; z-index: -1; - background-image: url("https://www.homestuck.com/images/desktops/johnhouse_1920x1080.jpg"); + background-image: url("bg.jpg"); background-position: center; background-size: cover; opacity: 0.5; @@ -41,7 +41,7 @@ body::before { background-color: var(--bg-color); color: rgb(var(--fg-shade), var(--fg-shade), var(--fg-shade)); - max-width: 1200px; + max-width: 1100px; margin: 10px auto 50px; padding: 15px; @@ -120,8 +120,12 @@ a:hover { } #header .chronology { - display: inline; - white-space: nowrap; + display: inline-block; +} + +#header .chronology .heading, +#header .chronology .buttons { + display: inline-block; } .nowrap { @@ -166,32 +170,6 @@ a:hover { font-size: 1em; } -@media (max-width: 780px) { - .sidebar:not(.no-hide) { - display: none; - } - - .layout-columns.vertical-when-thin { - flex-direction: column; - } - - .layout-columns.vertical-when-thin > *:not(:last-child) { - margin-bottom: 10px; - } - - .sidebar.no-hide { - max-width: unset !important; - flex-basis: unset !important; - margin-right: 0; - } -} - -@media (max-width: 600px) { - .content-columns { - columns: 1; - } -} - .sidebar, #content, #header, #skippers { background-color: rgba(var(--bg-shade), var(--bg-shade), var(--bg-shade), 0.6); border: 1px dotted var(--fg-color); @@ -729,3 +707,34 @@ li > ul { content: '.'; } } + +@media (max-width: 900px) { + .sidebar-column:not(.no-hide) { + display: none; + } + + .layout-columns.vertical-when-thin { + flex-direction: column; + } + + .layout-columns.vertical-when-thin > *:not(:last-child) { + margin-bottom: 10px; + } + + .sidebar-column.no-hide { + max-width: unset !important; + flex-basis: unset !important; + margin-right: 0 !important; + margin-left: 0 !important; + } + + .sidebar .news-entry:not(.first-news-entry) { + display: none; + } +} + +@media (max-width: 600px) { + .content-columns { + columns: 1; + } +} diff --git a/upd8.js b/upd8.js index 64a311ce..94c054d8 100644 --- a/upd8.js +++ b/upd8.js @@ -130,6 +130,8 @@ function readDataFile(file) { const SITE_ABOUT = readDataFile('about.html'); const SITE_CHANGELOG = readDataFile('changelog.html'); +const SITE_DISCORD = readDataFile('discord.html'); +const SITE_DONATE = readDataFile('donate.html'); const SITE_FEEDBACK = readDataFile('feedback.html'); const SITE_JS_DISABLED = readDataFile('js-disabled.html'); @@ -278,7 +280,7 @@ function getMultilineField(lines, name) { }; function transformInline(text) { - return text.replace(/\[\[(album:|artist:|flash:|track:|tag:)?(.+?)\]\]/g, (match, category, ref, offset) => { + return text.replace(/\[\[(album:|artist:|flash:|track:|tag:|group:)?(.+?)\]\]/g, (match, category, ref, offset) => { if (category === 'album:') { const album = getLinkedAlbum(ref); if (album) { @@ -334,6 +336,16 @@ function transformInline(text) { console.warn(`\x1b[33mThe linked tag ${match} does not exist!\x1b[0m`); return ref; } + } else if (category === 'group:') { + const group = getLinkedGroup(ref); + if (group) { + return fixWS` + <a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a> + `; + } else { + console.warn(`\x1b[33mThe linked group ${group} does not exist!\x1b[0m`); + return ref; + } } else { const track = getLinkedTrack(ref); if (track) { @@ -497,13 +509,6 @@ async function processAlbumDataFile(file) { album.urls = getListField(albumSection, 'URLs') || []; album.groups = getListField(albumSection, 'Groups') || []; album.directory = getBasicField(albumSection, 'Directory'); - - const canon = getBasicField(albumSection, 'Canon'); - album.isCanon = canon === 'Canon' || !canon; - album.isBeyond = canon === 'Beyond'; - album.isOfficial = album.isCanon || album.isBeyond; - album.isFanon = canon === 'Fanon'; - album.isMajorRelease = getBasicField(albumSection, 'Major Release') === 'yes'; if (album.artists && album.artists.error) { @@ -873,7 +878,14 @@ async function processGroupDataFile(file) { const contentLines = contents.split('\n'); const sections = Array.from(getSections(contentLines)); + let category, color; return sections.map(section => { + if (getBasicField(section, 'Category')) { + category = getBasicField(section, 'Category'); + color = getBasicField(section, 'Color'); + return {isCategory: true, name: category, color}; + } + const name = getBasicField(section, 'Group'); if (!name) { return {error: 'Expected "Group" field!'}; @@ -897,12 +909,14 @@ async function processGroupDataFile(file) { const urls = (getListField(section, 'URLs') || []).filter(Boolean); return { + isGroup: true, name, directory, description, descriptionShort, urls, - color: '#00ffff' + category, + color }; }); } @@ -955,21 +969,31 @@ function getTotalDuration(tracks) { const stringifyIndent = 0; -const toRefs = (label, array) => array.filter(Boolean).map(x => `${label}:${x.directory}`); +const toRefs = (label, objectOrArray) => { + if (Array.isArray(objectOrArray)) { + return objectOrArray.filter(Boolean).map(x => `${label}:${x.directory}`); + } else if (objectOrArray.directory) { + throw new Error('toRefs should not be passed a single object with directory'); + } else if (typeof objectOrArray === 'object') { + return Object.fromEntries(Object.entries(objectOrArray) + .map(([ key, value ]) => [key, toRefs(key, value)])); + } else { + throw new Error('toRefs should be passed an array or object of arrays'); + } +}; 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 'albums': return toRefs('album', value); case 'flashes': return toRefs('flash', value); case 'groups': return toRefs('group', value); case 'artTags': return toRefs('tag', value); @@ -1018,8 +1042,6 @@ function stringifyFlashData() { function stringifyArtistData() { 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': @@ -1119,16 +1141,17 @@ async function writePage(directoryParts, { main = { classes: [], - collapseSidebars: true, content: '' }, sidebar = { + collapse: true, classes: [], content: '' }, sidebarRight = { + collapse: true, classes: [], content: '' }, @@ -1149,9 +1172,7 @@ async function writePage(directoryParts, { } const canonical = SITE_CANONICAL_BASE + targetPath; - const { - collapseSidebars = true - } = main; + const collapseSidebars = (sidebar.collapse !== false) && (sidebarRight.collapse !== false); const mainHTML = main.content && fixWS` <main id="content" ${classes(...main.classes || [])}> @@ -1163,13 +1184,14 @@ async function writePage(directoryParts, { content, multiple, classes: sidebarClasses = [], + collapse = true, wide = false }) => (content ? fixWS` <div id="${id}" ${classes( 'sidebar-column', 'sidebar', wide && 'wide', - !collapseSidebars && 'no-hide', + !collapse && 'no-hide', ...sidebarClasses )}> ${content} @@ -1179,7 +1201,7 @@ async function writePage(directoryParts, { 'sidebar-column', 'sidebar-multiple', wide && 'wide', - !collapseSidebars && 'no-hide' + !collapse && 'no-hide' )}> ${multiple.map(content => fixWS` <div ${classes( @@ -1202,11 +1224,13 @@ async function writePage(directoryParts, { ] } + const links = (nav.links || []).filter(Boolean); + const navLinkParts = []; - for (let i = 0; i < nav.links?.length; i++) { - const link = nav.links[i]; - const prev = nav.links[i - 1]; - const next = nav.links[i + 1]; + for (let i = 0; i < links.length; i++) { + const link = links[i]; + const prev = links[i - 1]; + const next = links[i + 1]; const [ href, title ] = link; let part = ''; if (href) { @@ -1331,7 +1355,7 @@ function getFlashGridHTML(props) { function getNewReleases(numReleases) { const latestFirst = albumData.slice().reverse(); - const majorReleases = latestFirst.filter(album => album.isOfficial || album.isMajorRelease); + const majorReleases = latestFirst.filter(album => album.groups.some(g => g.directory === C.OFFICIAL_GROUP_DIRECTORY) || album.isMajorRelease); majorReleases.splice(1); const otherReleases = latestFirst @@ -1373,7 +1397,6 @@ function writeMiscellaneousPages() { }, main: { classes: ['top-index'], - collapseSidebars: false, content: fixWS` <h1>${SITE_TITLE}</h1> <h2>New Releases</h2> @@ -1387,7 +1410,7 @@ function writeMiscellaneousPages() { <div class="grid-listing"> ${getAlbumGridHTML({ entries: (albumData - .filter(album => album.isFanon) + .filter(album => album.groups.some(g => g.directory === C.FANDOM_GROUP_DIRECTORY)) .reverse() .slice(0, 6) .concat([albumData.find(album => album.directory === C.UNRELEASED_TRACKS_DIRECTORY)]) @@ -1395,7 +1418,7 @@ function writeMiscellaneousPages() { lazy: true })} <div class="grid-actions"> - <a class="box grid-item" href="albums/fandom/" style="--fg-color: #ffffff">Explore Fandom!</a> + <a class="box grid-item" href="${C.GROUP_DIRECTORY}/${C.FANDOM_GROUP_DIRECTORY}/gallery/" style="--fg-color: #ffffff">Explore Fandom!</a> <a class="box grid-item" href="${C.FEEDBACK_DIRECTORY}/" style="--fg-color: #ffffff">Share an album!</a> </div> </div> @@ -1403,30 +1426,32 @@ function writeMiscellaneousPages() { <div class="grid-listing"> ${getAlbumGridHTML({ entries: (albumData - .filter(album => album.isOfficial) + .filter(album => album.groups.some(g => g.directory === C.OFFICIAL_GROUP_DIRECTORY)) .reverse() .slice(0, 11) .map(album => ({item: album}))), lazy: true })} <div class="grid-actions"> - <a class="box grid-item" href="albums/official/" style="--fg-color: #ffffff">Explore Official!</a> + <a class="box grid-item" href="${C.GROUP_DIRECTORY}/${C.OFFICIAL_GROUP_DIRECTORY}/gallery/" style="--fg-color: #ffffff">Explore Official!</a> </div> </div> ` }, sidebar: { wide: true, + collapse: false, content: fixWS` <h1>Get involved!</h1> <ul> <li><a href="${C.FEEDBACK_DIRECTORY}/">Send feedback</a></li> - ${SITE_DONATE_LINK && `<li><a href="${SITE_DONATE_LINK}">Donate</a></li>`} + <li><a href="${C.DISCORD_DIRECTORY}/">Join the Discord server</a></li> + <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> </ul> <hr> <h1>News</h1> - ${newsData.slice(0, 3).map(entry => fixWS` - <article> + ${newsData.slice(0, 3).map((entry, i) => fixWS` + <article ${classes('news-entry', i === 0 && 'first-news-entry')}> <h2><time>${getDateString(entry)}</time> <a href="${C.NEWS_DIRECTORY}/#${entry.id}">${entry.name}</a></h2> ${entry.bodyShort} ${entry.bodyShort !== entry.body && `<a href="${C.NEWS_DIRECTORY}/#${entry.id}">(View rest of entry!)</a>`} @@ -1439,61 +1464,23 @@ function writeMiscellaneousPages() { <h2 class="dot-between-spans"> <span><a class="current" href="./">${SITE_SHORT_TITLE}</a></span> <span><a href="${C.LISTING_DIRECTORY}/">Listings</a></span> + <span><a href="${C.NEWS_DIRECTORY}/">News</a></span> <span><a href="${C.FLASH_DIRECTORY}/">Flashes & Games</a></span> <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.DONATE_DIRECTORY}/">Donate</a></span> </h2> ` } }), - writePage(['albums', 'fandom'], { - title: `Albums - Fandom`, - main: { - classes: ['top-index'], - content: fixWS` - <h1>Albums - Fandom</h1> - <p class="quick-info"><a href="list/">More listings!</a></p> - <div class="grid-listing"> - ${getAlbumGridHTML({ - details: true, - entries: (albumData - .filter(album => album.isFanon) - .reverse() - .map(album => ({item: album}))), - lazy: 4 - })} - </div> - ` - }, - sidebar: { - content: generateSidebarForGroup(true, null) - }, - nav: {simple: true} - }), + mkdirp(path.join(C.SITE_DIRECTORY, 'albums', 'fandom')) + .then(() => writeFile(path.join(C.SITE_DIRECTORY, 'albums', 'fandom', 'index.html'), + generateRedirectPage('Fandom - Gallery', `/${C.GROUP_DIRECTORY}/fandom/gallery/`))), - writePage(['albums', 'official'], { - title: `Albums - Official`, - main: { - classes: ['top-index'], - content: fixWS` - <h1>Albums - Official</h1> - <p class="quick-info"><a href="list/">More listings!</a></p> - <div class="grid-listing"> - ${getAlbumGridHTML({ - details: true, - entries: (albumData - .filter(album => album.isOfficial) - .reverse() - .map(album => ({item: album}))), - lazy: 4 - })} - </div> - ` - }, - nav: {simple: true} - }), + mkdirp(path.join(C.SITE_DIRECTORY, 'albums', 'official')) + .then(() => writeFile(path.join(C.SITE_DIRECTORY, 'albums', 'official', 'index.html'), + generateRedirectPage('Official - Gallery', `/${C.GROUP_DIRECTORY}/official/gallery/`))), writePage([C.FLASH_DIRECTORY], { title: `Flashes & Games`, @@ -1571,6 +1558,32 @@ function writeMiscellaneousPages() { nav: {simple: true} }), + writePage([C.DONATE_DIRECTORY], { + title: `Donate`, + main: { + content: fixWS` + <div class="long-content"> + <h1>Donate</h1> + ${SITE_DONATE} + </div> + ` + }, + nav: {simple: true} + }), + + writePage([C.DISCORD_DIRECTORY], { + title: `Discord`, + main: { + content: fixWS` + <div class="long-content"> + <h1>HSMusic Community Discord Server</h1> + ${SITE_DISCORD} + </div> + ` + }, + nav: {simple: true} + }), + writePage([C.JS_DISABLED_DIRECTORY], { title: 'JavaScript Disabled', main: { @@ -1735,8 +1748,8 @@ function writeTrackPages() { async function writeTrackPage(track) { const { album } = track; const tracksThatReference = track.referencedBy; - const ttrFanon = tracksThatReference.filter(t => t.album.isFanon); - const ttrOfficial = tracksThatReference.filter(t => t.album.isOfficial); + const ttrFanon = tracksThatReference.filter(t => t.album.groups.every(group => group.directory !== C.OFFICIAL_GROUP_DIRECTORY)); + const ttrOfficial = tracksThatReference.filter(t => t.album.groups.some(group => group.directory === C.OFFICIAL_GROUP_DIRECTORY)); const tracksReferenced = track.references; const otherReleases = track.otherReleases; const listTag = getAlbumListTag(track.album); @@ -2013,7 +2026,7 @@ async function writeArtistPage(artist) { [`${C.LISTING_DIRECTORY}/`, 'Listings'], [null, 'Artist:'], [`${C.ARTIST_DIRECTORY}/${kebab}/`, name], - [null, `(${[ + artThings.length && [null, `(${[ `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/" class="current">Info</a>`, `<a href="${C.ARTIST_DIRECTORY}/${artist.directory}/gallery/">Gallery</a>` ].join(', ')})`] @@ -2068,11 +2081,15 @@ async function writeArtistAliasPage(artist) { const target = `/${C.ARTIST_DIRECTORY}/${alias.directory}/`; await mkdirp(directory); - await writeFile(file, fixWS` + await writeFile(file, generateRedirectPage(alias.name, target)); +} + +function generateRedirectPage(title, target) { + return fixWS` <!DOCTYPE html> <html> <head> - <title>Moved to ${alias.name}</title> + <title>Moved to ${title}</title> <meta charset="utf-8"> <meta http-equiv="refresh" content="0;url=${target}"> <link rel="canonical" href="${target}"> @@ -2080,12 +2097,12 @@ async function writeArtistAliasPage(artist) { </head> <body> <main> - <h1>Moved to ${alias.name}</h1> + <h1>Moved to ${title}</h1> <p>This page has been moved to <a href="${target}">${target}</a>.</p> </main> </body> </html> - `); + `; } function albumChunkedList(tracks, getLI, showDate = true, datePropertyOrFn = 'date') { @@ -2291,8 +2308,10 @@ function writeListingPages() { `; const sortByName = (a, b) => { - const an = a.name.toLowerCase(); - const bn = b.name.toLowerCase(); + let an = a.name.toLowerCase(); + let bn = b.name.toLowerCase(); + if (an.startsWith('the ')) an = an.slice(4); + if (bn.startsWith('the ')) bn = bn.slice(4); return an < bn ? -1 : an > bn ? 1 : 0; }; @@ -2300,15 +2319,15 @@ function writeListingPages() { [['albums', 'by-name'], `Albums - by Name`, albumData.slice() .sort(sortByName) .map(album => getAlbumLI(album, `(${album.tracks.length} tracks)`))], - [['albums', 'by-date'], `Albums - by Date`, C.sortByDate(albumData.filter(album => album.directory !== C.UNRELEASED_TRACKS_DIRECTORY)) - .map(album => getAlbumLI(album, `(${getDateString(album)})`))], + [['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')})`))], [['albums', 'by-duration'], `Albums - by Duration`, albumData.slice() .map(album => ({album, duration: getTotalDuration(album.tracks)})) .sort((a, b) => b.duration - a.duration) .map(({ album, duration }) => getAlbumLI(album, `(${getDurationString(duration)})`))], - [['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')})`))], + [['albums', 'by-date'], `Albums - by Date`, C.sortByDate(albumData.filter(album => album.directory !== C.UNRELEASED_TRACKS_DIRECTORY)) + .map(album => getAlbumLI(album, `(${getDateString(album)})`))], [['artists', 'by-name'], `Artists - by Name`, artistData .filter(artist => !artist.alias) .sort(sortByName) @@ -2318,17 +2337,6 @@ function writeListingPages() { (${'' + 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(({ artist, commentary }) => fixWS` - <li> - <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/#commentary">${artist.name}</a> - (${commentary} ${commentary === 1 ? 'entry' : 'entries'}) - </li> - `)], [['artists', 'by-contribs'], `Artists - by Contributions`, fixWS` <div class="content-columns"> <div class="column"> @@ -2338,12 +2346,10 @@ function writeListingPages() { .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 === artist)).length + contribs: ( + artist.tracks.asContributor.length + + artist.tracks.asArtist.length + ) })) .sort((a, b) => b.contribs - a.contribs) .filter(({ contribs }) => contribs) @@ -2364,13 +2370,11 @@ function writeListingPages() { .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 === artist) - )).length + contribs: ( + artist.tracks.asCoverArtist.length + + artist.albums.asCoverArtist.length + + artist.flashes.asContributor.length + ) })) .sort((a, b) => b.contribs - a.contribs) .filter(({ contribs }) => contribs) @@ -2386,6 +2390,17 @@ function writeListingPages() { </div> </div> `], + [['artists', 'by-commentary'], `Artists - by Commentary Entries`, 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(({ artist, commentary }) => fixWS` + <li> + <a href="${C.ARTIST_DIRECTORY}/${artist.directory}/#commentary">${artist.name}</a> + (${commentary} ${commentary === 1 ? 'entry' : 'entries'}) + </li> + `)], [['artists', 'by-duration'], `Artists - by Duration`, artistData .filter(artist => !artist.alias) .map(artist => ({artist, duration: getTotalDuration( @@ -2452,6 +2467,62 @@ function writeListingPages() { </div> </div> `], + [['groups', 'by-name'], `Groups - by Name`, groupData + .filter(x => x.isGroup) + .sort(sortByName) + .map(group => fixWS` + <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a></li> + `)], + [['groups', 'by-category'], `Groups - by Category`, fixWS` + <dl> + ${groupData.filter(x => x.isCategory).map(category => fixWS` + <dt><a href="${C.GROUP_DIRECTORY}/${category.groups[0].directory}/" style="${getThemeString(category)}">${category.name}</a></li> + <dd><ul> + ${category.groups.map(group => fixWS` + <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/" style="${getThemeString(group)}">${group.name}</a></li> + `).join('\n')} + </ul></dd> + `).join('\n')} + </dl> + `], + [['groups', 'by-albums'], `Groups - by Albums`, groupData + .filter(x => x.isGroup) + .map(group => ({group, albums: group.albums.length})) + .sort((a, b) => b.albums - a.albums) + .map(({ group, albums }) => fixWS` + <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a> (${s(albums, 'album')})</li> + `)], + [['groups', 'by-tracks'], `Groups - by Tracks`, groupData + .filter(x => x.isGroup) + .map(group => ({group, tracks: group.albums.reduce((acc, album) => acc + album.tracks.length, 0)})) + .sort((a, b) => b.tracks - a.tracks) + .map(({ group, tracks }) => fixWS` + <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a> (${s(tracks, 'track')})</li> + `)], + [['groups', 'by-duration'], `Groups - by Duration`, groupData + .filter(x => x.isGroup) + .map(group => ({group, duration: getTotalDuration(group.albums.flatMap(album => album.tracks))})) + .sort((a, b) => b.duration - a.duration) + .map(({ group, duration }) => fixWS` + <li><a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a> (${getDurationString(duration)})</li> + `)], + [['groups', 'by-latest'], `Groups - by Latest Album`, C.sortByDate(groupData + .filter(x => x.isGroup) + .map(group => ({group, date: group.albums[group.albums.length - 1].date})) + // So this is kinda tough to explain, 8ut 8asically, when we reverse the list after sorting it 8y d8te + // (so that the latest d8tes come first), it also flips the order of groups which share the same d8te. + // This happens mostly when a single al8um is the l8test in two groups. So, say one such al8um is in + // the groups "Fandom" and "UMSPAF". Per category order, Fandom is meant to show up 8efore UMSPAF, 8ut + // when we do the reverse l8ter, that flips them, and UMSPAF ends up displaying 8efore Fandom. So we do + // an extra reverse here, which will fix that and only affect groups that share the same d8te (8ecause + // groups that don't will 8e moved 8y the sortByDate call surrounding this). + .reverse() + ).reverse().map(({ group, date }) => fixWS` + <li> + <a href="${C.GROUP_DIRECTORY}/${group.directory}/" style="${getThemeString(group)}">${group.name}</a> + (${getDateString({date})}) + </li> + `)], [['tracks', 'by-name'], `Tracks - by Name`, trackData.slice() .sort(sortByName) .map(track => fixWS` @@ -2996,6 +3067,7 @@ function fancifyURL(url, {album = false} = {}) { url.includes('wikipedia.org') ? 'Wikipedia' : url.includes('poetryfoundation.org') ? 'Poetry Foundation' : url.includes('instagram.com') ? 'Instagram' : + url.includes('patreon.com') ? 'Patreon' : new URL(url).hostname }</a>`; } @@ -3159,6 +3231,7 @@ function generateSidebarRightForAlbum(album, currentTrack = null) { const { groups } = album; if (groups.length) { return { + collapse: false, multiple: groups.map(group => { const index = group.albums.indexOf(album); const next = group.albums[index + 1]; @@ -3167,7 +3240,7 @@ function generateSidebarRightForAlbum(album, currentTrack = null) { }).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> + ${group.urls.length && `<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>`} @@ -3180,18 +3253,27 @@ function generateSidebarRightForAlbum(album, currentTrack = null) { 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> - ` + <dl> + ${groupData.filter(x => x.isCategory).map(category => [ + fixWS` + <dt ${classes(currentGroup && category === currentGroup.category && 'current')}> + <a href="${C.GROUP_DIRECTORY}/${groupData.find(x => x.isGroup && x.category === category).directory}/${isGallery ? 'gallery/' : ''}" style="${getThemeString(category)}">${category.name}</a> + </dt> + <dd><ul> + ${category.groups.map(group => fixWS` + <li ${classes(group === currentGroup && 'current')} style="${getThemeString(group)}"> + <a href="${C.GROUP_DIRECTORY}/${group.directory}/${isGallery && 'gallery/'}">${group.name}</a> + </li> + `).join('\n')} + </ul></dd> + ` + ]).join('\n')} + </dl> + `; } function writeGroupPages() { - return progressPromiseAll(`Writing group pages.`, queue(groupData.map(curry(writeGroupPage)), queueSize)); + return progressPromiseAll(`Writing group pages.`, queue(groupData.filter(x => x.isGroup).map(curry(writeGroupPage)), queueSize)); } async function writeGroupPage(group) { @@ -3199,6 +3281,19 @@ async function writeGroupPage(group) { const releasedTracks = releasedAlbums.flatMap(album => album.tracks); const totalDuration = getTotalDuration(releasedTracks); + const groups = groupData.filter(x => x.isGroup); + const index = groups.indexOf(group); + const previous = groups[index - 1]; + const next = groups[index + 1]; + + const generateNextPrevious = isGallery => [ + previous && `<a href="${C.GROUP_DIRECTORY}/${previous.directory}/${isGallery ? 'gallery/' : ''}" id="previous-button" title="${previous.name}">Previous</a>`, + next && `<a href="${C.GROUP_DIRECTORY}/${next.directory}/${isGallery ? 'gallery/' : ''}" id="next-button" title="${next.name}">Next</a>` + ].filter(Boolean).join(', '); + + const npInfo = generateNextPrevious(false); + const npGallery = generateNextPrevious(true); + await writePage([C.GROUP_DIRECTORY, group.directory], { title: group.name, body: { @@ -3207,10 +3302,12 @@ async function writeGroupPage(group) { main: { content: fixWS` <h1>${group.name}</h1> + ${group.urls.length && `<p>Visit on ${joinNoOxford(group.urls.map(fancifyURL), 'or')}.</p>`} <blockquote> ${transformMultiline(group.description)} </blockquote> - <p>Albums:</p> + <h2>Albums</h2> + <p>View <a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/">album gallery</a>! Or browse the list:</p> <ul> ${group.albums.map(album => fixWS` <li> @@ -3233,7 +3330,7 @@ async function writeGroupPage(group) { [null, `(${[ `<a href="${C.GROUP_DIRECTORY}/${group.directory}/" class="current">Info</a>`, `<a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/">Gallery</a>` - ].join(', ')})`] + ].join(', ') + (npInfo.length ? '; ' + npInfo : '')})`] ] } }); @@ -3248,11 +3345,13 @@ async function writeGroupPage(group) { 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> + ${group.directory === C.FANDOM_GROUP_DIRECTORY && `<p class="quick-info">(<a href="${C.LISTING_DIRECTORY}/groups/by-category/">Choose a group to filter by!</a>)</p>`} <div class="grid-listing"> ${getGridHTML({ entries: C.sortByDate(group.albums.map(item => ({item}))).reverse(), srcFn: getAlbumCover, - hrefFn: album => `${C.ALBUM_DIRECTORY}/${album.directory}/` + hrefFn: album => `${C.ALBUM_DIRECTORY}/${album.directory}/`, + details: true })} </div> ` @@ -3269,7 +3368,7 @@ async function writeGroupPage(group) { [null, `(${[ `<a href="${C.GROUP_DIRECTORY}/${group.directory}/">Info</a>`, `<a href="${C.GROUP_DIRECTORY}/${group.directory}/gallery/" class="current">Gallery</a>` - ].join(', ')})`] + ].join(', ') + (npGallery.length ? '; ' + npGallery : '')})`] ] } }); @@ -3503,8 +3602,6 @@ async function main() { artistNames.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : a.toLowerCase() > b.toLowerCase() ? 1 : 0); - officialAlbumData = albumData.filter(album => !album.isFanon); - fandomAlbumData = albumData.filter(album => album.isFanon); 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)); @@ -3668,64 +3765,29 @@ async function main() { ].filter(Boolean)); artistData.forEach(artist => { - const filterProp = prop => trackData.filter(track => track[prop]?.some(({ who }) => who === artist)); + const filterProp = (array, prop) => array.filter(thing => thing[prop]?.some(({ who }) => who === artist)); artist.tracks = { - asArtist: filterProp('artists'), - asContributor: filterProp('contributors'), - asCoverArtist: filterProp('coverArtists'), + asArtist: filterProp(trackData, 'artists'), + asContributor: filterProp(trackData, 'contributors'), + asCoverArtist: filterProp(trackData, '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) - )); + artist.albums = { + asArtist: filterProp(albumData, 'artists'), + asCoverArtist: filterProp(albumData, 'coverArtists') + }; + artist.flashes = { + asContributor: filterProp(flashData, 'contributors') + }; }); - /* - console.log(artistData - .filter(a => !a.alias) - .map(a => ({ - artist: a.name, - things: C.getThingsArtistContributedTo(a.name, {trackData, albumData, flashData}), - urls: a.urls.filter(url => - url.includes('youtu') || - url.includes('soundcloud') || - url.includes('bandcamp') || - url.includes('tumblr') || - url.includes('twitter') - ) - })) - .filter(a => a.urls.length === 0) - .map(a => ({ - ...a, - 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, '.') + - ' ' + - [ - [a.urls.some(u => u.includes('youtu')), 'YT'], - [a.urls.some(u => u.includes('bandcamp')), 'BC'], - [a.urls.some(u => u.includes('soundcloud')), 'SC'], - [a.urls.some(u => u.includes('tumblr')), 'TM'], - [a.urls.some(u => u.includes('twitter')), 'TW'] - ].map(([ match, label ]) => `[${match ? '+' : ' '}] ${label}`).join(' '), - ...[ - [a.urls.find(u => u.includes('youtu')), 'YT'], - [a.urls.find(u => u.includes('bandcamp')), 'BC'], - [a.urls.find(u => u.includes('soundcloud')), 'SC'], - [a.urls.find(u => u.includes('tumblr')), 'TM'], - [a.urls.find(u => u.includes('twitter')), 'TW'] - ].filter(([ match ]) => match).map(([ match, label ]) => ` [${match ? '+' : ' '}] ${label}: ${match || '?'}`) - ].join('\n')) - .join('\n') - ); - process.exit(); - */ + groupData.filter(x => x.isGroup).forEach(group => group.category = groupData.find(x => x.isCategory && x.name === group.category)); + groupData.filter(x => x.isCategory).forEach(category => category.groups = groupData.filter(x => x.isGroup && x.category === category)); + + officialAlbumData = albumData.filter(album => album.groups.some(group => group.directory === C.OFFICIAL_GROUP_DIRECTORY)); + fandomAlbumData = albumData.filter(album => album.groups.every(group => group.directory !== C.OFFICIAL_GROUP_DIRECTORY)); const miscOptions = await parseOptions(process.argv.slice(2), { 'queue-size': { |