« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--static/client.js8
-rwxr-xr-xupd8.js401
2 files changed, 264 insertions, 145 deletions
diff --git a/static/client.js b/static/client.js
index 1f8d51ed..649ca312 100644
--- a/static/client.js
+++ b/static/client.js
@@ -277,10 +277,10 @@ const infoCard = (() => {
             link(nameLink, 'track', data);
 
             const albumLink = container.querySelector('.info-card-album a');
-            link(albumLink, 'album', data.links.album);
+            link(albumLink, 'album', data.album);
 
             const artistSpan = container.querySelector('.info-card-artists span');
-            artistSpan.innerHTML = joinElements('conjunction', data.links.artists.map(({ who: artist }) => {
+            artistSpan.innerHTML = joinElements('conjunction', data.artists.map(({ artist }) => {
                 const a = document.createElement('a');
                 a.href = getLinkHref('artist', artist.directory);
                 a.innerText = artist.name;
@@ -289,9 +289,9 @@ const infoCard = (() => {
 
             const coverArtistParagraph = container.querySelector('.info-card-cover-artists');
             const coverArtistSpan = coverArtistParagraph.querySelector('span');
-            if (data.links.coverArtists.length) {
+            if (data.coverArtists.length) {
                 coverArtistParagraph.style.display = 'block';
-                coverArtistSpan.innerHTML = joinElements('conjunction', data.links.coverArtists.map(({ who: artist }) => {
+                coverArtistSpan.innerHTML = joinElements('conjunction', data.coverArtists.map(({ artist }) => {
                     const a = document.createElement('a');
                     a.href = getLinkHref('artist', artist.directory);
                     a.innerText = artist.name;
diff --git a/upd8.js b/upd8.js
index f9727fa2..68d95ba3 100755
--- a/upd8.js
+++ b/upd8.js
@@ -191,6 +191,8 @@ const urlSpec = {
             root: '',
             path: '<>',
 
+            album: 'album/<>',
+            artist: 'artist/<>',
             track: 'track/<>'
         }
     },
@@ -1514,6 +1516,7 @@ async function processArtistDataFile(file) {
         const name = getBasicField(section, 'Artist');
         const urls = (getListField(section, 'URLs') || []).filter(Boolean);
         const alias = getBasicField(section, 'Alias');
+        const hasAvatar = getBooleanField(section, 'Has Avatar') ?? false;
         const note = getMultilineField(section, 'Note');
         let directory = getBasicField(section, 'Directory');
 
@@ -1528,7 +1531,7 @@ async function processArtistDataFile(file) {
         if (alias) {
             return {name, directory, alias};
         } else {
-            return {name, directory, urls, note};
+            return {name, directory, urls, note, hasAvatar};
         }
     });
 }
@@ -2122,18 +2125,20 @@ function serializeImagePaths(original) {
 }
 
 function serializeLink(thing) {
-    return Object.fromEntries([
-        ['name', thing.name],
-        ['directory', thing.directory],
-        ['color', thing.color]
-    ].filter(([ key, value ]) => value));
+    const ret = {};
+    ret.name = thing.name;
+    ret.directory = thing.directory;
+    if (thing.color) ret.color = thing.color;
+    return ret;
 }
 
 function serializeContribs(contribs) {
-    return contribs.map(({ who, what }) => ({
-        who: serializeLink(who),
-        what
-    }));
+    return contribs.map(({ who, what }) => {
+        const ret = {};
+        ret.artist = serializeLink(who);
+        if (what) ret.contribution = what;
+        return ret;
+    });
 }
 
 function serializeCover(thing, pathFunction) {
@@ -2153,6 +2158,29 @@ function serializeCover(thing, pathFunction) {
     };
 }
 
+function serializeGroupsForAlbum(album) {
+    return album.groups.map(group => {
+        const index = group.albums.indexOf(album);
+        const next = group.albums[index + 1] || null;
+        const previous = group.albums[index - 1] || null;
+        return {group, index, next, previous};
+    }).map(({group, index, next, previous}) => ({
+        link: serializeLink(group),
+        descriptionShort: group.descriptionShort,
+        albumIndex: index,
+        nextAlbum: next && serializeLink(next),
+        previousAlbum: previous && serializeLink(previous),
+        urls: group.urls
+    }));
+}
+
+function serializeGroupsForTrack(track) {
+    return track.album.groups.map(group => ({
+        link: serializeLink(group),
+        urls: group.urls,
+    }));
+}
+
 function validateWritePath(path, urlGroup) {
     if (!Array.isArray(path)) {
         return {error: `Expected array, got ${path}`};
@@ -3028,10 +3056,43 @@ function writeAlbumPage(album) {
     };
 
     const commentaryEntries = [album, ...album.tracks].filter(x => x.commentary).length;
+    const albumDuration = getTotalDuration(album.tracks);
 
     const listTag = getAlbumListTag(album);
 
-    return ({strings, writePage}) => writePage('album', album.directory, ({to}) => ({
+    const data = {
+        type: 'data',
+        path: ['album', album.directory],
+        data: () => ({
+            name: album.name,
+            directory: album.directory,
+            dates: {
+                released: album.date,
+                trackArtAdded: album.trackArtDate,
+                coverArtAdded: album.coverArtDate,
+                addedToWiki: album.dateAdded
+            },
+            duration: albumDuration,
+            color: album.color,
+            cover: serializeCover(album, getAlbumCover),
+            artists: serializeContribs(album.artists || []),
+            coverArtists: serializeContribs(album.coverArtists || []),
+            wallpaperArtists: serializeContribs(album.wallpaperArtists || []),
+            bannerArtists: serializeContribs(album.bannerArtists || []),
+            groups: serializeGroupsForAlbum(album),
+            trackGroups: album.trackGroups?.map(trackGroup => ({
+                name: trackGroup.name,
+                color: trackGroup.color,
+                tracks: trackGroup.tracks.map(track => track.directory)
+            })),
+            tracks: album.tracks.map(track => ({
+                link: serializeLink(track),
+                duration: track.duration
+            }))
+        })
+    };
+
+    const page = {type: 'page', path: ['album', album.directory], page: ({strings, to}) => ({
         title: strings('albumPage.title', {album: album.name}),
         stylesheet: getAlbumStylesheet(album, {to}),
         theme: getThemeString(album, [
@@ -3090,7 +3151,7 @@ function writeAlbumPage(album) {
                             date: strings.count.date(album.coverArtDate)
                         }),
                         strings('releaseInfo.duration', {
-                            duration: strings.count.duration(getTotalDuration(album.tracks), {approximate: album.tracks.length > 1})
+                            duration: strings.count.duration(albumDuration, {approximate: album.tracks.length > 1})
                         })
                     ].filter(Boolean).join('<br>\n')}
                 </p>
@@ -3142,7 +3203,6 @@ function writeAlbumPage(album) {
         },
 
         sidebarLeft: generateSidebarForAlbum(album, null, {strings, to}),
-        sidebarRight: generateSidebarRightForAlbum(album, null, {strings, to}),
 
         nav: {
             links: [
@@ -3166,7 +3226,9 @@ function writeAlbumPage(album) {
                 </div>
             `
         }
-    }));
+    })};
+
+    return [page, data];
 }
 
 function getAlbumStylesheet(album, {to}) {
@@ -3252,23 +3314,28 @@ function writeTrackPage(track) {
         data: () => ({
             name: track.name,
             directory: track.directory,
-            date: track.date,
+            dates: {
+                released: track.date,
+                originallyReleased: track.originalDate,
+                coverArtAdded: track.coverArtDate
+            },
             duration: track.duration,
             color: track.color,
             cover: serializeCover(track, getTrackCover),
-            links: {
-                artists: serializeContribs(track.artists),
-                contributors: serializeContribs(track.contributors),
-                coverArtists: serializeContribs(track.coverArtists || []),
-                album: serializeLink(track.album),
-                groups: track.album.groups.map(serializeLink),
-                references: track.references.map(serializeLink),
-                referencedBy: track.referencedBy.map(serializeLink)
-            }
+            artists: serializeContribs(track.artists),
+            contributors: serializeContribs(track.contributors),
+            coverArtists: serializeContribs(track.coverArtists || []),
+            album: serializeLink(track.album),
+            groups: serializeGroupsForTrack(track),
+            references: track.references.map(serializeLink),
+            referencedBy: track.referencedBy.map(serializeLink),
+            alsoReleasedAs: otherReleases.map(track => ({
+                track: serializeLink(track),
+                album: serializeLink(track.album)
+            }))
         })
     };
 
-    // const page = ({strings, writePage}) => writePage('track', track.directory, ({to}) => ({
     const page = {type: 'page', path: ['track', track.directory], page: ({strings, to}) => ({
         title: strings('trackPage.title', {track: track.name}),
         stylesheet: getAlbumStylesheet(album, {to}),
@@ -3413,7 +3480,6 @@ function writeTrackPage(track) {
         },
 
         sidebarLeft: generateSidebarForAlbum(album, track, {strings, to}),
-        sidebarRight: generateSidebarRightForAlbum(album, track, {strings, to}),
 
         nav: {
             links: [
@@ -3474,6 +3540,7 @@ function writeArtistPage(artist) {
     const getArtistsAndContrib = (thing, key) => ({
         artists: thing[key]?.filter(({ who }) => who !== artist),
         contrib: thing[key]?.find(({ who }) => who === artist),
+        thing,
         key
     });
 
@@ -3606,26 +3673,63 @@ function writeArtistPage(artist) {
         </dl>
     `;
 
-    const avatarPath = path.join(C.MEDIA_ARTIST_AVATAR_DIRECTORY, artist.directory + '.jpg');
-    let avatarFileExists = null;
+    const serializeArtistsAndContrib = key => thing => {
+        const { artists, contrib } = getArtistsAndContrib(thing, key);
+        const ret = {};
+        ret.link = serializeLink(thing);
+        if (contrib.what) ret.contribution = contrib.what;
+        if (artists.length) ret.otherArtists = serializeContribs(artists);
+        return ret;
+    };
 
-    return async ({strings, writePage}) => {
-        // The outer step, used for gathering data, is always sync. This is
-        // normally fine 8ecause pretty much all the data we will ever need
-        // across 8uilds is available for synchronous access - 8ut this here
-        // is an exception, and we have to evaluate it asynchronously. Still,
-        // we don't want to perform that access() oper8tion any more than
-        // necessary, so we cache the value in a varia8le shared across calls
-        // to this 8uild function.
-        avatarFileExists = avatarFileExists ?? (wikiInfo.features.artistAvatars &&
-            await access(path.join(mediaPath, avatarPath)).then(() => true, () => false));
-
-        await writePage('artist', artist.directory, ({to}) => ({
+    const serializeTrackListChunks = chunks =>
+        chunks.map(({date, album, chunk, duration}) => ({
+            album: serializeLink(album),
+            date,
+            duration,
+            tracks: chunk.map(({ track }) => ({
+                link: serializeLink(track),
+                duration: track.duration
+            }))
+        }));
+
+    const data = {
+        type: 'data',
+        path: ['artist', artist.directory],
+        data: () => ({
+            albums: {
+                asCoverArtist: artist.albums.asCoverArtist.map(serializeArtistsAndContrib('coverArtists')),
+                asWallpaperArtist: artist.albums.asWallpaperArtist.map(serializeArtistsAndContrib('wallpaperArtists')),
+                asBannerArtist: artist.albums.asBannerArtist.map(serializeArtistsAndContrib('bannerArtists'))
+            },
+            flashes: wikiInfo.features.flashesAndGames ? {
+                asContributor: artist.flashes.asContributor
+                    .map(flash => getArtistsAndContrib(flash, 'contributors'))
+                    .map(({ contrib, thing: flash }) => ({
+                        link: serializeLink(flash),
+                        contribution: contrib.what
+                    }))
+            } : null,
+            tracks: {
+                asArtist: artist.tracks.asArtist.map(serializeArtistsAndContrib('artists')),
+                asContributor: artist.tracks.asContributor.map(serializeArtistsAndContrib('contributors')),
+                chunked: {
+                    released: serializeTrackListChunks(releasedTrackListChunks),
+                    unreleased: serializeTrackListChunks(unreleasedTrackListChunks)
+                }
+            }
+        })
+    };
+
+    const infoPage = {
+        type: 'page',
+        path: ['artist', artist.directory],
+        page: ({strings, to}) => ({
             title: strings('artistPage.title', {artist: name}),
 
             main: {
                 content: fixWS`
-                    ${avatarFileExists && generateCoverLink({
+                    ${artist.hasAvatar && generateCoverLink({
                         strings, to,
                         src: to('localized.artistAvatar', artist.directory),
                         alt: strings('misc.alt.artistAvatar')
@@ -3768,38 +3872,42 @@ function writeArtistPage(artist) {
             },
 
             nav: generateNavForArtist(artist, {strings, to, isGallery: false, hasGallery})
-        }));
+        })
+    };
 
-        if (hasGallery) {
-            await writePage('artistGallery', artist.directory, ({to}) => ({
-                title: strings('artistGalleryPage.title', {artist: name}),
+    const galleryPage = hasGallery && {
+        type: 'page',
+        path: ['artistGallery', artist.directory],
+        page: ({strings, to}) => ({
+            title: strings('artistGalleryPage.title', {artist: name}),
 
-                main: {
-                    classes: ['top-index'],
-                    content: fixWS`
-                        <h1>${strings('artistGalleryPage.title', {artist: name})}</h1>
-                        <p class="quick-info">${strings('artistGalleryPage.infoLine', {
-                            coverArts: strings.count.coverArts(artThingsGallery.length, {unit: true})
-                        })}</p>
-                        <div class="grid-listing">
-                            ${getGridHTML({
-                                strings, to,
-                                entries: artThingsGallery.map(item => ({item})),
-                                srcFn: thing => (thing.album
-                                    ? getTrackCover(thing, {to})
-                                    : getAlbumCover(thing, {to})),
-                                hrefFn: thing => (thing.album
-                                    ? to('localized.track', thing.directory)
-                                    : to('localized.album', thing.directory))
-                            })}
-                        </div>
-                    `
-                },
+            main: {
+                classes: ['top-index'],
+                content: fixWS`
+                    <h1>${strings('artistGalleryPage.title', {artist: name})}</h1>
+                    <p class="quick-info">${strings('artistGalleryPage.infoLine', {
+                        coverArts: strings.count.coverArts(artThingsGallery.length, {unit: true})
+                    })}</p>
+                    <div class="grid-listing">
+                        ${getGridHTML({
+                            strings, to,
+                            entries: artThingsGallery.map(item => ({item})),
+                            srcFn: thing => (thing.album
+                                ? getTrackCover(thing, {to})
+                                : getAlbumCover(thing, {to})),
+                            hrefFn: thing => (thing.album
+                                ? to('localized.track', thing.directory)
+                                : to('localized.album', thing.directory))
+                        })}
+                    </div>
+                `
+            },
 
-                nav: generateNavForArtist(artist, {strings, to, isGallery: true, hasGallery})
-            }));
-        }
-    }
+            nav: generateNavForArtist(artist, {strings, to, isGallery: true, hasGallery})
+        })
+    };
+
+    return [data, infoPage, galleryPage].filter(Boolean);
 }
 
 function generateNavForArtist(artist, {strings, to, isGallery, hasGallery}) {
@@ -5311,79 +5419,90 @@ function generateSidebarForAlbum(album, currentTrack, {strings, to}) {
         })
     }</li>`;
 
-    return {
-        content: fixWS`
-            <h1><a href="${to('localized.album', album.directory)}">${album.name}</a></h1>
-            ${album.trackGroups ? fixWS`
-                <dl>
-                    ${album.trackGroups.map(({ name, color, startIndex, tracks }) => fixWS`
-                        <dt ${classes(tracks.includes(currentTrack) && 'current')}>${
-                            (listTag === 'ol'
-                                ? strings('albumSidebar.trackList.group.withRange', {
-                                    group: strings.link.track(tracks[0], {to, text: name}),
-                                    range: `${startIndex + 1}&ndash;${startIndex + tracks.length}`
-                                })
-                                : strings('albumSidebar.trackList.group', {
-                                    group: strings.link.track(tracks[0], {to, text: name})
-                                }))
-                        }</dt>
-                        ${(!currentTrack || tracks.includes(currentTrack)) && fixWS`
-                            <dd><${listTag === 'ol' ? `ol start="${startIndex + 1}"` : listTag}>
-                                ${tracks.map(trackToListItem).join('\n')}
-                            </${listTag}></dd>
-                        `}
-                    `).join('\n')}
-                </dl>
-            ` : fixWS`
-                <${listTag}>
-                    ${album.tracks.map(trackToListItem).join('\n')}
-                </${listTag}>
-            `}
-        `
-    };
-}
-
-function generateSidebarRightForAlbum(album, currentTrack, {strings, to}) {
-    if (!wikiInfo.features.groupUI) {
-        return null;
-    }
+    const trackListPart = fixWS`
+        <h1><a href="${to('localized.album', album.directory)}">${album.name}</a></h1>
+        ${album.trackGroups ? fixWS`
+            <dl>
+                ${album.trackGroups.map(({ name, color, startIndex, tracks }) => fixWS`
+                    <dt ${classes(tracks.includes(currentTrack) && 'current')}>${
+                        (listTag === 'ol'
+                            ? strings('albumSidebar.trackList.group.withRange', {
+                                group: strings.link.track(tracks[0], {to, text: name}),
+                                range: `${startIndex + 1}&ndash;${startIndex + tracks.length}`
+                            })
+                            : strings('albumSidebar.trackList.group', {
+                                group: strings.link.track(tracks[0], {to, text: name})
+                            }))
+                    }</dt>
+                    ${(!currentTrack || tracks.includes(currentTrack)) && fixWS`
+                        <dd><${listTag === 'ol' ? `ol start="${startIndex + 1}"` : listTag}>
+                            ${tracks.map(trackToListItem).join('\n')}
+                        </${listTag}></dd>
+                    `}
+                `).join('\n')}
+            </dl>
+        ` : fixWS`
+            <${listTag}>
+                ${album.tracks.map(trackToListItem).join('\n')}
+            </${listTag}>
+        `}
+    `;
 
     const { groups } = album;
-    if (groups.length) {
+
+    const groupParts = 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>${
+            strings('albumSidebar.groupBox.title', {
+                group: `<a href="${to('localized.groupInfo', group.directory)}">${group.name}</a>`
+            })
+        }</h1>
+        ${!currentTrack && transformMultiline(group.descriptionShort, {strings, to})}
+        ${group.urls.length && `<p>${
+            strings('releaseInfo.visitOn', {
+                links: strings.list.or(group.urls.map(url => fancifyURL(url, {strings})))
+            })
+        }</p>`}
+        ${!currentTrack && fixWS`
+            ${next && `<p class="group-chronology-link">${
+                strings('albumSidebar.groupBox.next', {
+                    album: `<a href="${to('localized.album', next.directory)}" style="${getLinkThemeString(next)}">${next.name}</a>`
+                })
+            }</p>`}
+            ${previous && `<p class="group-chronology-link">${
+                strings('albumSidebar.groupBox.previous', {
+                    album: `<a href="${to('localized.album', previous.directory)}" style="${getLinkThemeString(previous)}">${previous.name}</a>`
+                })
+            }</p>`}
+        `}
+    `);
+
+    if (groupParts.length) {
+        if (currentTrack) {
+            const combinedGroupPart = groupParts.join('\n<hr>\n');
+            return {
+                multiple: [
+                    trackListPart,
+                    combinedGroupPart
+                ]
+            };
+        } else {
+            return {
+                multiple: [
+                    ...groupParts,
+                    trackListPart
+                ]
+            };
+        }
+    } else {
         return {
-            collapse: false,
-            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>${
-                    strings('albumSidebar.groupBox.title', {
-                        group: `<a href="${to('localized.groupInfo', group.directory)}">${group.name}</a>`
-                    })
-                }</h1>
-                ${!currentTrack && transformMultiline(group.descriptionShort, {strings, to})}
-                ${group.urls.length && `<p>${
-                    strings('releaseInfo.visitOn', {
-                        links: strings.list.or(group.urls.map(url => fancifyURL(url, {strings})))
-                    })
-                }</p>`}
-                ${!currentTrack && fixWS`
-                    ${next && `<p class="group-chronology-link">${
-                        strings('albumSidebar.groupBox.next', {
-                            album: `<a href="${to('localized.album', next.directory)}" style="${getLinkThemeString(next)}">${next.name}</a>`
-                        })
-                    }</p>`}
-                    ${previous && `<p class="group-chronology-link">${
-                        strings('albumSidebar.groupBox.previous', {
-                            album: `<a href="${to('localized.album', previous.directory)}" style="${getLinkThemeString(previous)}">${previous.name}</a>`
-                        })
-                    }</p>`}
-                `}
-            `)
+            content: trackListPart
         };
-    };
+    }
 }
 
 function generateSidebarForGroup(currentGroup, {strings, to, isGallery}) {
@@ -6185,8 +6304,8 @@ async function main() {
 
     trackData.forEach(track => track.otherReleases = [
         track.aka,
-        ...trackData.filter(({ aka }) => aka === track)
-    ].filter(Boolean));
+        ...trackData.filter(({ aka }) => aka === track || (track.aka && aka === track.aka)),
+    ].filter(x => x && x !== track));
 
     if (wikiInfo.features.flashesAndGames) {
         flashData.forEach(flash => mapInPlace(flash.tracks, search.track));