diff options
Diffstat (limited to 'src/content/dependencies/generateArtistInfoPage.js')
-rw-r--r-- | src/content/dependencies/generateArtistInfoPage.js | 818 |
1 files changed, 818 insertions, 0 deletions
diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js new file mode 100644 index 00000000..a91eebf2 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -0,0 +1,818 @@ +import {empty, filterProperties, unique} from '../../util/sugar.js'; + +import { + chunkByProperties, + getTotalDuration, + sortAlbumsTracksChronologically, +} from '../../util/wiki-data.js'; + +export default { + contentDependencies: [ + 'generateArtistNavLinks', + 'generateContentHeading', + 'generatePageLayout', + 'linkAlbum', + 'linkArtist', + 'linkArtistGallery', + 'linkExternal', + 'linkGroup', + 'linkTrack', + 'transformContent', + ], + + extraDependencies: ['html', 'language'], + + relations(relation, artist) { + const relations = {}; + const sections = relations.sections = {}; + + relations.layout = + relation('generatePageLayout'); + + relations.artistNavLinks = + relation('generateArtistNavLinks', artist); + + const processContribs = (...contribArrays) => { + const properties = {}; + + const ownContribs = + contribArrays + .map(contribs => contribs.find(({who}) => who === artist)) + .filter(Boolean); + + const contributionDescriptions = + ownContribs + .map(({what}) => what) + .filter(Boolean); + + if (!empty(contributionDescriptions)) { + properties.contributionDescriptions = contributionDescriptions; + } + + const otherArtistContribs = + contribArrays + .map(contribs => contribs.filter(({who}) => who !== artist)) + .flat(); + + if (!empty(otherArtistContribs)) { + properties.otherArtistLinks = + otherArtistContribs + .map(({who}) => relation('linkArtist', who)); + } + + return properties; + }; + + const sortContributionEntries = (entries, sortFunction) => { + const things = unique(entries.map(({thing}) => thing)); + sortFunction(things); + + const outputArrays = []; + const thingToOutputArray = new Map(); + + for (const thing of things) { + const array = []; + thingToOutputArray.set(thing, array); + outputArrays.push(array); + } + + for (const entry of entries) { + thingToOutputArray.get(entry.thing).push(entry); + } + + entries.splice(0, entries.length, ...outputArrays.flat()); + }; + + const getGroupInfo = (entries) => { + const allGroups = new Set(); + const groupToDuration = new Map(); + const groupToCount = new Map(); + + for (const entry of entries) { + for (const group of entry.album.groups) { + allGroups.add(group); + groupToCount.set(group, (groupToCount.get(group) ?? 0) + 1); + groupToDuration.set(group, (groupToDuration.get(group) ?? 0) + entry.duration ?? 0); + } + } + + const groupInfo = + Array.from(allGroups) + .map(group => ({ + groupLink: relation('linkGroup', group), + duration: groupToDuration.get(group) ?? 0, + count: groupToCount.get(group), + })); + + groupInfo.sort((a, b) => b.count - a.count); + groupInfo.sort((a, b) => b.duration - a.duration); + + return groupInfo; + }; + + if (artist.contextNotes) { + const contextNotes = sections.contextNotes = {}; + contextNotes.content = relation('transformContent', artist.contextNotes); + } + + if (!empty(artist.urls)) { + const visit = sections.visit = {}; + visit.externalLinks = + artist.urls.map(url => + relation('linkExternal', url)); + } + + const trackContributionEntries = [ + ...artist.tracksAsArtist.map(track => ({ + date: track.date, + thing: track, + album: track.album, + duration: track.duration, + rerelease: track.originalReleaseTrack !== null, + trackLink: relation('linkTrack', track), + ...processContribs(track.artistContribs), + })), + + ...artist.tracksAsContributor.map(track => ({ + date: track.date, + thing: track, + album: track.album, + duration: track.duration, + rerelease: track.originalReleaseTrack !== null, + trackLink: relation('linkTrack', track), + ...processContribs(track.contributorContribs), + })), + ]; + + sortContributionEntries(trackContributionEntries, sortAlbumsTracksChronologically); + + const trackContributionChunks = + chunkByProperties(trackContributionEntries, ['album', 'date']) + .map(({album, date, chunk}) => ({ + albumLink: relation('linkAlbum', album), + date: +date, + duration: getTotalDuration(chunk), + entries: chunk + .map(entry => + filterProperties(entry, [ + 'contributionDescriptions', + 'duration', + 'otherArtistLinks', + 'rerelease', + 'trackLink', + ])), + })); + + const trackGroupInfo = getGroupInfo(trackContributionEntries, 'duration'); + + if (!empty(trackContributionChunks)) { + const tracks = sections.tracks = {}; + tracks.heading = relation('generateContentHeading'); + tracks.chunks = trackContributionChunks; + + if (!empty(trackGroupInfo)) { + tracks.groupInfo = trackGroupInfo; + } + } + + // TODO: Add and integrate wallpaper and banner date fields (#90) + const artContributionEntries = [ + ...artist.albumsAsCoverArtist.map(album => ({ + kind: 'albumCover', + date: album.coverArtDate, + thing: album, + album: album, + ...processContribs(album.coverArtistContribs), + })), + + ...artist.albumsAsWallpaperArtist.map(album => ({ + kind: 'albumWallpaper', + date: album.coverArtDate, + thing: album, + album: album, + ...processContribs(album.wallpaperArtistContribs), + })), + + ...artist.albumsAsBannerArtist.map(album => ({ + kind: 'albumBanner', + date: album.coverArtDate, + thing: album, + album: album, + ...processContribs(album.bannerArtistContribs), + })), + + ...artist.tracksAsCoverArtist.map(track => ({ + kind: 'trackCover', + date: track.coverArtDate, + thing: track, + album: track.album, + rerelease: track.originalReleaseTrack !== null, + trackLink: relation('linkTrack', track), + ...processContribs(track.coverArtistContribs), + })), + ]; + + sortContributionEntries(artContributionEntries, sortAlbumsTracksChronologically); + + const artContributionChunks = + chunkByProperties(artContributionEntries, ['album', 'date']) + .map(({album, date, chunk}) => ({ + albumLink: relation('linkAlbum', album), + date: +date, + entries: + chunk.map(entry => + filterProperties(entry, [ + 'contributionDescriptions', + 'kind', + 'otherArtistLinks', + 'rerelease', + 'trackLink', + ])), + })); + + const artGroupInfo = getGroupInfo(artContributionEntries, 'count'); + + if (!empty(artContributionChunks)) { + const artworks = sections.artworks = {}; + artworks.heading = relation('generateContentHeading'); + artworks.chunks = artContributionChunks; + + if ( + !empty(artist.albumsAsCoverArtist) || + !empty(artist.tracksAsCoverArtist) + ) { + artworks.artistGalleryLink = + relation('linkArtistGallery', artist); + } + + if (!empty(artGroupInfo)) { + artworks.groupInfo = artGroupInfo; + } + } + + // Commentary doesn't use the detailed contribution system where multiple + // artists are collaboratively credited for the same piece, so there isn't + // really anything special to do for processing or presenting it. + + const commentaryEntries = [ + ...artist.albumsAsCommentator.map(album => ({ + kind: 'albumCommentary', + date: album.date, + thing: album, + album: album, + })), + + ...artist.tracksAsCommentator.map(track => ({ + kind: 'trackCommentary', + date: track.date, + thing: track, + album: track.album, + trackLink: relation('linkTrack', track), + })), + ]; + + sortContributionEntries(commentaryEntries, sortAlbumsTracksChronologically); + + // We still pass through (and chunk by) date here, even though it doesn't + // actually get displayed on the album page. See issue #193. + const commentaryChunks = + chunkByProperties(commentaryEntries, ['album', 'date']) + .map(({album, date, chunk}) => ({ + albumLink: relation('linkAlbum', album), + date: +date, + entries: + chunk.map(entry => + filterProperties(entry, [ + 'kind', + 'trackLink', + ])), + })); + + if (!empty(commentaryChunks)) { + const commentary = sections.commentary = {}; + commentary.heading = relation('generateContentHeading'); + commentary.chunks = commentaryChunks; + } + + return relations; + }, + + data(artist) { + const data = {}; + + data.name = artist.name; + + const allTracks = unique([...artist.tracksAsArtist, ...artist.tracksAsContributor]); + data.totalTrackCount = allTracks.length; + data.totalDuration = getTotalDuration(allTracks, {originalReleasesOnly: true}); + + return data; + }, + + generate(data, relations, {html, language}) { + const {sections: sec} = relations; + + const addAccentsToEntry = ({ + rerelease, + entry, + otherArtistLinks, + contributionDescriptions, + }) => { + if (rerelease) { + return language.$('artistPage.creditList.entry.rerelease', {entry}); + } + + const options = {entry}; + const parts = ['artistPage.creditList.entry']; + + if (otherArtistLinks) { + parts.push('withArtists'); + options.artists = language.formatConjunctionList(otherArtistLinks); + } + + if (contributionDescriptions) { + parts.push('withContribution'); + options.contribution = language.formatUnitList(contributionDescriptions); + } + + if (parts.length === 1) { + return entry; + } + + return language.formatString(parts.join('.'), options); + }; + + const addAccentsToAlbumLink = ({ + albumLink, + date, + duration, + entries, + }) => { + const options = {album: albumLink}; + const parts = ['artistPage.creditList.album']; + + if (date) { + parts.push('withDate'); + options.date = language.formatDate(new Date(date)); + } + + if (duration) { + parts.push('withDuration'); + options.duration = language.formatDuration(duration, { + approximate: entries.length > 1, + }); + } + + return language.formatString(parts.join('.'), options); + }; + + return relations.layout + .slots({ + title: data.name, + headingMode: 'sticky', + + mainClasses: ['long-content'], + mainContent: [ + sec.contextNotes && [ + html.tag('p', language.$('releaseInfo.note')), + html.tag('blockquote', + sec.contextNotes.content), + ], + + sec.visit && + html.tag('p', + language.$('releaseInfo.visitOn', { + links: language.formatDisjunctionList(sec.visit.externalLinks), + })), + + sec.artworks?.artistGalleryLink && + html.tag('p', + language.$('artistPage.viewArtGallery', { + link: sec.artworks.artistGalleryLink.slots({ + content: language.$('artistPage.viewArtGallery.link'), + }), + })), + + (sec.tracks || sec.artworsk || sec.flashes || sec.commentary) && + html.tag('p', + language.$('misc.jumpTo.withLinks', { + links: language.formatUnitList( + [ + sec.tracks && + html.tag('a', + {href: '#tracks'}, + language.$('artistPage.trackList.title')), + + sec.artworks && + html.tag('a', + {href: '#art'}, + language.$('artistPage.artList.title')), + + sec.flashes && + html.tag('a', + {href: '#flashes'}, + language.$('artistPage.flashList.title')), + + sec.commentary && + html.tag('a', + {href: '#commentary'}, + language.$('artistPage.commentaryList.title')), + ].filter(Boolean)), + })), + + sec.tracks && [ + sec.tracks.heading + .slots({ + tag: 'h2', + id: 'tracks', + title: language.$('artistPage.trackList.title'), + }), + + data.totalDuration > 0 && + html.tag('p', + language.$('artistPage.contributedDurationLine', { + artist: data.name, + duration: + language.formatDuration(data.totalDuration, { + approximate: data.totalTrackCount > 1, + unit: true, + }), + })), + + sec.tracks.groupInfo && + html.tag('p', + language.$('artistPage.musicGroupsLine', { + groups: + language.formatUnitList( + sec.tracks.groupInfo.map(({groupLink, count, duration}) => + (duration + ? language.$('artistPage.groupsLine.item.withDuration', { + group: groupLink, + duration: language.formatDuration(duration, {approximate: count > 1}), + }) + : language.$('artistPage.groupsLine.item.withCount', { + group: groupLink, + count: language.countContributions(count), + })))), + })), + + html.tag('dl', + sec.tracks.chunks.map(({albumLink, date, duration, entries}) => [ + html.tag('dt', + addAccentsToAlbumLink({albumLink, date, duration, entries})), + + html.tag('dd', + html.tag('ul', + entries + .map(({trackLink, duration, ...properties}) => ({ + entry: + (duration + ? language.$('artistPage.creditList.entry.track.withDuration', { + track: trackLink, + duration: language.formatDuration(duration), + }) + : language.$('artistPage.creditList.entry.track', { + track: trackLink, + })), + ...properties, + })) + .map(addAccentsToEntry) + .map(entry => html.tag('li', entry)))), + ])), + ], + + sec.artworks && [ + sec.artworks.heading + .slots({ + tag: 'h2', + id: 'art', + title: language.$('artistPage.artList.title'), + }), + + sec.artworks.artistGalleryLink && + html.tag('p', + language.$('artistPage.viewArtGallery.orBrowseList', { + link: sec.artworks.artistGalleryLink.slots({ + content: language.$('artistPage.viewArtGallery.link'), + }), + })), + + sec.artworks.groupInfo && + html.tag('p', + language.$('artistPage.artGroupsLine', { + groups: + language.formatUnitList( + sec.artworks.groupInfo.map(({groupLink, count}) => + language.$('artistPage.groupsLine.item.withCount', { + group: groupLink, + count: + language.countContributions(count), + }))), + })), + + html.tag('dl', + sec.artworks.chunks.map(({albumLink, date, entries}) => [ + html.tag('dt', + addAccentsToAlbumLink({albumLink, date, entries})), + + html.tag('dd', + html.tag('ul', + entries + .map(({kind, trackLink, ...properties}) => ({ + entry: + (kind === 'trackCover' + ? language.$('artistPage.creditList.entry.track', { + track: trackLink, + }) + : html.tag('i', + language.$('artistPage.creditList.entry.album.' + { + albumWallpaper: 'wallpaperArt', + albumBanner: 'bannerArt', + albumCover: 'coverArt', + }[kind]))), + ...properties, + })) + .map(addAccentsToEntry) + .map(entry => html.tag('li', entry)))), + ])), + ], + + sec.commentary && [ + sec.commentary.heading + .slots({ + tag: 'h2', + id: 'commentary', + title: language.$('artistPage.commentaryList.title'), + }), + + html.tag('dl', + sec.commentary.chunks.map(({albumLink, entries}) => [ + html.tag('dt', + language.$('artistPage.creditList.album', { + album: albumLink, + })), + + html.tag('dd', + html.tag('ul', + entries + .map(({kind, trackLink}) => + (kind === 'trackCommentary' + ? language.$('artistPage.creditList.entry.track', { + track: trackLink, + }) + : html.tag('i', + language.$('artistPage.creditList.entry.album.commentary')))) + .map(entry => html.tag('li', entry)))), + ])), + ], + ], + + navLinkStyle: 'hierarchical', + navLinks: + relations.artistNavLinks + .slots({ + showExtraLinks: true, + }) + .content, + }); + }, +}; + +/* + +export function write(artist, {wikiData}) { + const {groupData, wikiInfo} = wikiData; + + let flashes, flashListChunks; + if (wikiInfo.enableFlashesAndGames) { + flashes = sortFlashesChronologically(artist.flashesAsContributor.slice()); + flashListChunks = chunkByProperties( + flashes.map((flash) => ({ + act: flash.act, + flash, + date: flash.date, + // Manual artists/contrib properties here, 8ecause we don't + // want to show the full list of other contri8utors inline. + // (It can often 8e very, very large!) + artists: [], + contrib: flash.contributorContribs.find(({who}) => who === artist), + })), + ['act'] + ).map(({act, chunk}) => ({ + act, + chunk, + dateFirst: chunk[0].date, + dateLast: chunk[chunk.length - 1].date, + })); + } + + const unbound_serializeArtistsAndContrib = + (key, {serializeContribs, serializeLink}) => + (thing) => { + const {artists, contrib} = getArtistsAndContrib(thing, key); + const ret = {}; + ret.link = serializeLink(thing); + if (contrib.what) ret.contribution = contrib.what; + if (!empty(artists)) ret.otherArtists = serializeContribs(artists); + return ret; + }; + + const unbound_serializeTrackListChunks = (chunks, {serializeLink}) => + chunks.map(({date, album, chunk, duration}) => ({ + album: serializeLink(album), + date, + duration, + tracks: chunk.map(({track}) => ({ + link: serializeLink(track), + duration: track.duration, + })), + })); + + const jumpTo = { + tracks: !empty(allTracks), + art: !empty(artThingsAll), + flashes: wikiInfo.enableFlashesAndGames && !empty(flashes), + commentary: !empty(commentaryThings), + }; + + const showJumpTo = Object.values(jumpTo).includes(true); + + const data = { + type: 'data', + path: ['artist', artist.directory], + data: ({serializeContribs, serializeLink}) => { + const serializeArtistsAndContrib = bindOpts(unbound_serializeArtistsAndContrib, { + serializeContribs, + serializeLink, + }); + + const serializeTrackListChunks = bindOpts(unbound_serializeTrackListChunks, { + serializeLink, + }); + + return { + albums: { + asCoverArtist: artist.albumsAsCoverArtist + .map(serializeArtistsAndContrib('coverArtistContribs')), + asWallpaperArtist: artist.albumsAsWallpaperArtist + .map(serializeArtistsAndContrib('wallpaperArtistContribs')), + asBannerArtist: artist.albumsAsBannerArtis + .map(serializeArtistsAndContrib('bannerArtistContribs')), + }, + flashes: wikiInfo.enableFlashesAndGames + ? { + asContributor: artist.flashesAsContributor + .map(flash => getArtistsAndContrib(flash, 'contributorContribs')) + .map(({contrib, thing: flash}) => ({ + link: serializeLink(flash), + contribution: contrib.what, + })), + } + : null, + tracks: { + asArtist: artist.tracksAsArtist + .map(serializeArtistsAndContrib('artistContribs')), + asContributor: artist.tracksAsContributo + .map(serializeArtistsAndContrib('contributorContribs')), + chunked: serializeTrackListChunks(trackListChunks), + }, + }; + }, + }; + + const infoPage = { + type: 'page', + path: ['artist', artist.directory], + page: ({ + fancifyURL, + generateInfoGalleryLinks, + getArtistAvatar, + getArtistString, + html, + link, + language, + transformMultiline, + }) => { + const generateTrackList = bindOpts(unbound_generateTrackList, { + getArtistString, + html, + language, + link, + }); + + return { + title: language.$('artistPage.title', {artist: name}), + + cover: artist.hasAvatar && { + src: getArtistAvatar(artist), + alt: language.$('misc.alt.artistAvatar'), + }, + + main: { + headingMode: 'sticky', + + content: [ + ...html.fragment( + wikiInfo.enableFlashesAndGames && + !empty(flashes) && [ + html.tag('h2', + {id: 'flashes', class: ['content-heading']}, + language.$('artistPage.flashList.title')), + + html.tag('dl', + flashListChunks.flatMap(({ + act, + chunk, + dateFirst, + dateLast, + }) => [ + html.tag('dt', + language.$('artistPage.creditList.flashAct.withDateRange', { + act: link.flash(chunk[0].flash, { + text: act.name, + }), + dateRange: language.formatDateRange( + dateFirst, + dateLast + ), + })), + + html.tag('dd', + html.tag('ul', + chunk + .map(({flash, ...props}) => ({ + ...props, + entry: language.$('artistPage.creditList.entry.flash', { + flash: link.flash(flash), + }), + })) + .map(opts => generateEntryAccents({ + getArtistString, + language, + ...opts, + })) + .map(row => html.tag('li', row)))), + ])), + ]), + ], + }, + }; + }, + }; + + const artThingsGallery = sortAlbumsTracksChronologically( + [ + ...(artist.albumsAsCoverArtist ?? []), + ...(artist.tracksAsCoverArtist ?? []), + ], + {latestFirst: true, getDate: (o) => o.coverArtDate}); + + const galleryPage = hasGallery && { + type: 'page', + path: ['artistGallery', artist.directory], + page: ({ + generateInfoGalleryLinks, + getAlbumCover, + getGridHTML, + getTrackCover, + html, + link, + language, + }) => ({ + title: language.$('artistGalleryPage.title', {artist: name}), + + main: { + classes: ['top-index'], + headingMode: 'static', + + content: [ + html.tag('p', + {class: 'quick-info'}, + language.$('artistGalleryPage.infoLine', { + coverArts: language.countCoverArts(artThingsGallery.length, { + unit: true, + }), + })), + + html.tag('div', + {class: 'grid-listing'}, + getGridHTML({ + entries: artThingsGallery.map((item) => ({item})), + srcFn: (thing) => + thing.album + ? getTrackCover(thing) + : getAlbumCover(thing), + linkFn: (thing, opts) => + thing.album + ? link.track(thing, opts) + : link.album(thing, opts), + })), + ], + }, + }), + }; + + return [data, infoPage, galleryPage].filter(Boolean); +} + +*/ |