diff options
Diffstat (limited to 'src')
18 files changed, 813 insertions, 178 deletions
diff --git a/src/common-util/wiki-data.js b/src/common-util/wiki-data.js index 21e15725..14ae8e96 100644 --- a/src/common-util/wiki-data.js +++ b/src/common-util/wiki-data.js @@ -262,17 +262,14 @@ export function getArtistAvatar(artist, {to}) { // Used in multiple content functions for the artist info page, // because shared logic is torture oooooooooooooooo. export function chunkArtistTrackContributions(contributions) { - const date = contrib => contrib.date; - const album = contrib => (contrib.thing.isTrack ? contrib.thing.album : contrib.thing); return ( - // First chunk by (contribution) date and album. + // First chunk by (contribution) album. chunkByConditions(contributions, [ - (a, b) => +date(a) !== +date(b), (a, b) => album(a) !== album(b), ]).map(contribs => // Then, *within* the boundaries of the existing chunks, @@ -283,6 +280,55 @@ export function chunkArtistTrackContributions(contributions) { ]))); } +// Ditto. More shared logic for the artist page. +export function selectRepresentativeArtistContributorContribs(contribs) { + const creditedAsNormalArtist = + contribs + .some(contrib => + contrib.thingProperty === 'artistContribs' && + !contrib.isFeaturingCredit); + + const creditedAsContributor = + contribs + .some(contrib => contrib.thingProperty === 'contributorContribs'); + + const annotatedContribs = + contribs + .filter(contrib => !empty(contrib.annotationParts)); + + const annotatedArtistContribs = + annotatedContribs + .filter(contrib => contrib.thingProperty === 'artistContribs'); + + const annotatedContributorContribs = + annotatedContribs + .filter(contrib => contrib.thingProperty === 'contributorContribs'); + + // Don't display annotations associated with crediting in the + // Contributors field if the artist is also credited as an Artist + // *and* the Artist-field contribution is non-annotated. This is + // so that we don't misrepresent the artist - the contributor + // annotation tends to be for "secondary" and performance roles. + // For example, this avoids crediting Marcy Nabors on Renewed + // Return seemingly only for "bass clarinet" when they're also + // the one who composed and arranged Renewed Return! + if ( + creditedAsNormalArtist && + creditedAsContributor && + empty(annotatedArtistContribs) + ) { + return null; + } else if ( + !empty(annotatedArtistContribs) || + !empty(annotatedContributorContribs) + ) { + return [ + ...annotatedArtistContribs, + ...annotatedContributorContribs, + ]; + } +} + // Big-ass homepage row functions export function getNewAdditions(numAlbums, {albumData}) { diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index cf8ce994..29bc34e6 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -14,6 +14,11 @@ export default { ...artist.trackCoverArtistContributions, ], + musicVideoContributions: [ + ...artist.musicVideoArtistContributions, + ...artist.musicVideoContributorContributions, + ], + // Banners and wallpapers don't show up in the artist gallery page, only // cover art. hasGallery: @@ -79,6 +84,12 @@ export default { ? relation('linkArtistGallery', artist) : null), + musicVideosChunkedList: + relation('generateArtistInfoPageMusicVideosChunkedList', artist), + + musicVideosGroupInfo: + relation('generateArtistGroupContributionsInfo', query.musicVideoContributions), + flashesChunkedList: relation('generateArtistInfoPageFlashesChunkedList', artist), @@ -216,6 +227,11 @@ export default { {href: '#art'}, language.$(pageCapsule, 'artList.title')), + !html.isBlank(relations.musicVideosChunkedList) && + html.tag('a', + {href: '#music-videos'}, + language.$(pageCapsule, 'musicVideoList.title')), + !html.isBlank(relations.flashesChunkedList) && html.tag('a', {href: '#flashes'}, @@ -329,6 +345,26 @@ export default { relations.contentHeading.clone() .slots({ tag: 'h2', + attributes: {id: 'music-videos'}, + title: language.$(pageCapsule, 'musicVideoList.title'), + }), + + relations.musicVideosChunkedList.slots({ + groupInfo: + language.encapsulate(pageCapsule, 'groupContributions', capsule => + relations.musicVideosGroupInfo.slots({ + title: language.$(capsule, 'title.artworks'), + showBothColumns: false, + sort: 'count', + countUnit: 'artworks', + })), + }), + ]), + + html.tags([ + relations.contentHeading.clone() + .slots({ + tag: 'h2', attributes: {id: 'flashes'}, title: language.$(pageCapsule, 'flashList.title'), }), diff --git a/src/content/dependencies/generateArtistInfoPageChunk.js b/src/content/dependencies/generateArtistInfoPageChunk.js index 3fa46c61..e19030c9 100644 --- a/src/content/dependencies/generateArtistInfoPageChunk.js +++ b/src/content/dependencies/generateArtistInfoPageChunk.js @@ -18,30 +18,30 @@ export default { mutable: false, }, - dates: { - validate: v => v.sparseArrayOf(v.isDate), - }, + // Container and items, respectively. + date: {validate: v => v.isDate}, + dates: {validate: v => v.sparseArrayOf(v.isDate)}, duration: {validate: v => v.isDuration}, durationApproximate: {type: 'boolean'}, }, generate(slots, {html, language}) { - let earliestDate = null; - let latestDate = null; - let onlyDate = null; + let earliestItemDate = null; + let latestItemDate = null; + let onlyItemDate = null; if (!empty(slots.dates)) { - earliestDate = - slots.dates - .reduce((a, b) => a <= b ? a : b); + earliestItemDate = slots.dates[0]; + latestItemDate = slots.dates[1]; - latestDate = - slots.dates - .reduce((a, b) => a <= b ? b : a); + for (const date of slots.dates.slice(1)) { + if (date < earliestItemDate) earliestItemDate = date; + if (date > latestItemDate) latestItemDate = date; + } - if (+earliestDate === +latestDate) { - onlyDate = earliestDate; + if (+earliestItemDate === +latestItemDate) { + onlyItemDate = earliestItemDate; } } @@ -51,9 +51,16 @@ export default { const options = {album: slots.link}; const parts = ['artistPage.creditList.album']; - if (onlyDate) { + if (slots.date) { + parts.push('withDate'); + options.date = language.formatDate(slots.date); + } else if (onlyItemDate) { parts.push('withDate'); - options.date = language.formatDate(onlyDate); + options.date = language.formatDate(onlyItemDate); + } else if (earliestItemDate && latestItemDate) { + parts.push('withDateRange'); + options.dateRange = + language.formatDateRange(earliestItemDate, latestItemDate); } if (slots.duration) { @@ -72,13 +79,13 @@ export default { const options = {act: slots.link}; const parts = ['artistPage.creditList.flashAct']; - if (onlyDate) { + if (onlyItemDate) { parts.push('withDate'); - options.date = language.formatDate(onlyDate); - } else if (earliestDate && latestDate) { + options.date = language.formatDate(onlyItemDate); + } else if (earliestItemDate && latestItemDate) { parts.push('withDateRange'); options.dateRange = - language.formatDateRange(earliestDate, latestDate); + language.formatDateRange(earliestItemDate, latestItemDate); } accentedLink = language.formatString(...parts, options); diff --git a/src/content/dependencies/generateArtistInfoPageMusicVideosChunk.js b/src/content/dependencies/generateArtistInfoPageMusicVideosChunk.js new file mode 100644 index 00000000..6912d4d6 --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageMusicVideosChunk.js @@ -0,0 +1,58 @@ +export default { + relations: (relation, artist, album, contribs) => ({ + template: + relation('generateArtistInfoPageChunk'), + + albumLink: + relation('linkAlbum', album), + + albumArtistCredit: + relation('generateArtistCredit', album.artistContribs, []), + + items: + contribs.map(contribs => + relation('generateArtistInfoPageMusicVideosChunkItem', + artist, + contribs)), + }), + + data: (_artist, album, contribs) => ({ + albumDate: + album.date, + + contribDates: + contribs + .flat() + .map(contrib => contrib.date), + }), + + generate: (data, relations, {html, language}) => + relations.template.slots({ + mode: 'album', + + link: + language.encapsulate('artistPage.creditList.album', workingCapsule => { + const creditCapsule = workingCapsule + '.credit'; + const workingOptions = {album: relations.albumLink}; + + relations.albumArtistCredit.setSlots({ + normalStringKey: creditCapsule + '.by', + }); + + if (!html.isBlank(relations.albumArtistCredit)) { + workingCapsule += '.withCredit'; + workingOptions.credit = + html.tag('span', {class: 'by'}, + relations.albumArtistCredit); + } + + return language.$(workingCapsule, workingOptions); + }), + + date: data.albumDate, + dates: data.contribDates, + + list: + html.tag('ul', relations.items), + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageMusicVideosChunkItem.js b/src/content/dependencies/generateArtistInfoPageMusicVideosChunkItem.js new file mode 100644 index 00000000..8bae860d --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageMusicVideosChunkItem.js @@ -0,0 +1,118 @@ +import {empty} from '#sugar'; +import {selectRepresentativeArtistContributorContribs} from '#wiki-data'; + +export default { + query(_artist, contribs) { + const query = {}; + + query.musicVideo = contribs[0].thing; + + query.albumOrTrack = query.musicVideo.thing; + + query.album = + (query.albumOrTrack.isAlbum + ? query.albumOrTrack + : query.albumOrTrack.album); + + query.displayedContributions = + selectRepresentativeArtistContributorContribs(contribs); + + return query; + }, + + relations: (relation, query, artist, _contribs) => ({ + template: + relation('generateArtistInfoPageChunkItem'), + + trackLink: + (query.albumOrTrack.isTrack + ? relation('linkTrack', query.albumOrTrack) + : null), + + artistCredit: + relation('generateArtistCredit', + query.musicVideo.artistContribs, + (empty(query.album.artistContribs) + ? [artist.mockSimpleContribution] + : query.album.artistContribs)), + + externalLinks: + query.musicVideo.urls + .map(url => relation('linkExternal', url)), + }), + + data: (query, _artist, contribs) => ({ + date: contribs[0].date, + + for: + (query.albumOrTrack.isAlbum + ? 'album' + : 'track'), + + title: query.musicVideo.title, + label: query.musicVideo.label, + + contribAnnotationParts: + (query.displayedContributions + ? query.displayedContributions + .flatMap(contrib => contrib.annotationParts) + : null), + }), + + generate: (data, relations, {html, language}) => + relations.template.slots({ + annotation: + (data.contribAnnotationParts + ? language.formatUnitList(data.contribAnnotationParts) + : html.blank()), + + content: + language.encapsulate('artistPage.creditList.entry', entryCapsule => { + let workingCapsule = entryCapsule; + let workingOptions = {}; + + workingCapsule += '.' + data.for + '.musicVideo'; + + const musicVideoCapsule = workingCapsule; + + if (data.for === 'track') { + workingOptions.track = + relations.trackLink; + } + + if (data.date) { + workingCapsule += '.withDate'; + workingOptions.date = language.formatDate(data.date); + } + + relations.artistCredit.setSlots({ + normalStringKey: + musicVideoCapsule + '.credit' + + (data.title ? '.alongsideTitle' + : data.label ? '.alongsideLabel' + : ''), + }); + + if (!html.isBlank(relations.artistCredit)) { + workingCapsule += '.withCredit'; + workingOptions.credit = relations.artistCredit; + } + + if (data.title) { + workingCapsule += '.withTitle'; + workingOptions.title = language.sanitize(data.title); + } else if (data.label) { + workingCapsule += '.withLabel'; + workingOptions.label = language.sanitize(data.label); + } + + if (!empty(relations.externalLinks)) { + workingCapsule += '.withLinks'; + workingOptions.links = + language.formatUnitList(relations.externalLinks); + } + + return language.$(workingCapsule, workingOptions); + }), + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageMusicVideosChunkedList.js b/src/content/dependencies/generateArtistInfoPageMusicVideosChunkedList.js new file mode 100644 index 00000000..588fbbeb --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageMusicVideosChunkedList.js @@ -0,0 +1,66 @@ +import {chunkByConditions, stitchArrays} from '#sugar'; +import {sortAlbumsTracksChronologically, sortContributionsChronologically} + from '#sort'; + +export default { + query(artist) { + const query = {}; + + const allContributions = [ + ...artist.musicVideoArtistContributions, + ...artist.musicVideoContributorContributions, + ...artist.otherMusicVideoArtistContributionsToOwnAlbums, + ]; + + const getMusicVideo = contrib => + contrib.thing; + + const getAlbumOrTrack = contrib => + getMusicVideo(contrib).thing; + + sortContributionsChronologically( + allContributions, + sortAlbumsTracksChronologically, + {getThing: getAlbumOrTrack}); + + const getAlbum = contrib => + (getAlbumOrTrack(contrib).isTrack + ? getAlbumOrTrack(contrib).album + : getAlbumOrTrack(contrib)); + + query.contribs = + chunkByConditions(allContributions, [ + (a, b) => getAlbum(a) !== getAlbum(b), + ]).map(contribs => + chunkByConditions(contribs, [ + (a, b) => getMusicVideo(a) !== getMusicVideo(b), + ])); + + query.albums = + query.contribs + .map(contribs => contribs[0][0]) + .map(contrib => getAlbum(contrib)); + + return query; + }, + + relations: (relation, query, artist) => ({ + template: + relation('generateArtistInfoPageChunkedList'), + + chunks: + stitchArrays({ + album: query.albums, + contribs: query.contribs, + }).map(({album, contribs}) => + relation('generateArtistInfoPageMusicVideosChunk', + artist, + album, + contribs)), + }), + + generate: (relations) => + relations.template.slots({ + chunks: relations.chunks, + }), +}; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunk.js b/src/content/dependencies/generateArtistInfoPageTracksChunk.js index b3727756..7a7fc6a9 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunk.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunk.js @@ -61,7 +61,7 @@ export default { .filter(contribs => countTowardTrackTotals(contribs) === false), }), - relations: (relation, query, artist, album, _trackContribLists) => ({ + relations: (relation, query, artist, album, trackContribLists) => ({ template: relation('generateArtistInfoPageChunk'), @@ -82,13 +82,15 @@ export default { query.contribListsCountingTowardTotals.map(trackContribs => relation('generateArtistInfoPageTracksChunkItem', artist, - trackContribs)), + trackContribs, + trackContribLists)), itemsNotCountingTowardTotals: query.contribListsNotCountingTowardTotals.map(trackContribs => relation('generateArtistInfoPageTracksChunkItem', artist, - trackContribs)), + trackContribs, + trackContribLists)), }), data(artist, _query, album, trackContribLists) { @@ -97,7 +99,10 @@ export default { const contribs = trackContribLists.flat(); - data.dates = + data.albumDate = + album.date; + + data.contribDates = contribs .map(contrib => contrib.date); @@ -168,7 +173,9 @@ export default { return language.$(workingCapsule, workingOptions); }), - dates: data.dates, + date: data.albumDate, + dates: data.contribDates, + duration: data.duration, durationApproximate: data.durationApproximate, diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js index 3d6e274b..22a4a228 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js @@ -1,60 +1,23 @@ import {sortAlbumsTracksChronologically} from '#sort'; import {empty} from '#sugar'; +import {selectRepresentativeArtistContributorContribs} from '#wiki-data'; export default { - query(artist, contribs) { + query(artist, contribs, chunkContribs) { const query = {}; - // TODO: Very mysterious what to do if the set of contributions is, - // in total, associated with more than one thing. No design yet. query.track = contribs[0].thing; - const creditedAsNormalArtist = - contribs - .some(contrib => - contrib.thingProperty === 'artistContribs' && - !contrib.isFeaturingCredit); - - const creditedAsContributor = - contribs - .some(contrib => contrib.thingProperty === 'contributorContribs'); - - const annotatedContribs = - contribs - .filter(contrib => !empty(contrib.annotationParts)); - - const annotatedArtistContribs = - annotatedContribs - .filter(contrib => contrib.thingProperty === 'artistContribs'); - - const annotatedContributorContribs = - annotatedContribs - .filter(contrib => contrib.thingProperty === 'contributorContribs'); - - // Don't display annotations associated with crediting in the - // Contributors field if the artist is also credited as an Artist - // *and* the Artist-field contribution is non-annotated. This is - // so that we don't misrepresent the artist - the contributor - // annotation tends to be for "secondary" and performance roles. - // For example, this avoids crediting Marcy Nabors on Renewed - // Return seemingly only for "bass clarinet" when they're also - // the one who composed and arranged Renewed Return! - if ( - creditedAsNormalArtist && - creditedAsContributor && - empty(annotatedArtistContribs) - ) { - query.displayedContributions = null; - } else if ( - !empty(annotatedArtistContribs) || - !empty(annotatedContributorContribs) - ) { - query.displayedContributions = [ - ...annotatedArtistContribs, - ...annotatedContributorContribs, - ]; - } + query.date = + contribs[0].date; + + query.anyItemsExpresslyDated = + chunkContribs.flat() + .some(contrib => +contrib.date !== +query.track.album.date); + + query.displayedContributions = + selectRepresentativeArtistContributorContribs(contribs); // It's kinda awkward to perform this chronological sort here, // per track, rather than just reusing the one that's done to @@ -112,6 +75,11 @@ export default { }), data: (query) => ({ + date: + (query.anyItemsExpresslyDated + ? query.date + : null), + duration: query.track.duration, @@ -146,6 +114,7 @@ export default { relations.trackListItem.slots({ showArtists: 'auto', showDuration: slots.showDuration, + showDate: data.date, })), }), }), diff --git a/src/content/dependencies/generateTrackListItem.js b/src/content/dependencies/generateTrackListItem.js index c8c57534..383f0025 100644 --- a/src/content/dependencies/generateTrackListItem.js +++ b/src/content/dependencies/generateTrackListItem.js @@ -25,6 +25,9 @@ export default { }), data: (track, _contextContributions) => ({ + date: + track.date, + duration: track.duration ?? 0, @@ -47,6 +50,11 @@ export default { default: false, }, + showDate: { + validate: v => v.anyOf(v.isBoolean, v.isDate), + default: false, + }, + colorMode: { validate: v => v.is('none', 'track', 'line'), default: 'track', @@ -62,48 +70,83 @@ export default { language.encapsulate(itemCapsule, workingCapsule => { const workingOptions = {}; - workingOptions.track = - relations.trackLink - .slot('color', slots.colorMode === 'track'); + const accent = + language.encapsulate(itemCapsule, 'accent', accentCapsule => { + let workingCapsule = accentCapsule; + let workingOptions = {}; + let any = false; + + if (slots.showDate) { + any = true; + workingCapsule += '.withDate'; + workingOptions.date = + language.$(accentCapsule, 'date', { + date: + (slots.showDate === true + ? language.formatDate(data.date) + : language.formatDate(slots.showDate)), + }); + } + + if (slots.showDuration) { + any = true; + workingCapsule += '.withDuration'; + workingOptions.duration = + (data.trackHasDuration + ? language.$(accentCapsule, 'duration', { + duration: + language.formatDuration(data.duration), + }) + : relations.missingDuration); + } + + if (any) { + return language.$(workingCapsule, workingOptions); + } else { + return html.blank(); + } + }); - if (slots.showDuration) { - workingCapsule += '.withDuration'; - workingOptions.duration = - (data.trackHasDuration - ? language.$(itemCapsule, 'withDuration.duration', { - duration: - language.formatDuration(data.duration), - }) - : relations.missingDuration); + if (!html.isBlank(accent)) { + workingCapsule += '.withAccent'; + workingOptions.accent = accent; } - const chosenCredit = - (slots.showArtists === true - ? relations.acontextualCredit - : slots.showArtists === 'auto' - ? relations.contextualCredit - : null); - - if (chosenCredit) { - const artistCapsule = language.encapsulate(itemCapsule, 'withArtists'); - - chosenCredit.setSlots({ - normalStringKey: - artistCapsule + '.by', - - featuringStringKey: - artistCapsule + '.featuring', + workingOptions.track = + relations.trackLink + .slot('color', slots.colorMode === 'track'); - normalFeaturingStringKey: - artistCapsule + '.by.featuring', + const artists = + language.encapsulate(itemCapsule, 'artists', artistsCapsule => { + const chosenCredit = + (slots.showArtists === true + ? relations.acontextualCredit + : slots.showArtists === 'auto' + ? relations.contextualCredit + : null); + + if (!chosenCredit) { + return html.blank(); + } + + // This might still be blank, if the contextual credit is chosen + // and it matches its context credit. + return chosenCredit.slots({ + normalStringKey: + artistsCapsule + '.by', + + featuringStringKey: + artistsCapsule + '.featuring', + + normalFeaturingStringKey: + artistsCapsule + '.by.featuring', + }); }); - if (!html.isBlank(chosenCredit)) { - workingCapsule += '.withArtists'; - workingOptions.by = - html.tag('span', {class: 'by'}, - chosenCredit); - } + if (!html.isBlank(artists)) { + workingCapsule += '.withArtists'; + workingOptions.artists = + html.tag('span', {class: 'by'}, artists); } return language.$(workingCapsule, workingOptions); diff --git a/src/content/dependencies/generateTrackListMissingDuration.js b/src/content/dependencies/generateTrackListMissingDuration.js index 70db23c2..f3c5c6ce 100644 --- a/src/content/dependencies/generateTrackListMissingDuration.js +++ b/src/content/dependencies/generateTrackListMissingDuration.js @@ -8,27 +8,26 @@ export default { }), generate: (relations, {html, language}) => - language.encapsulate('trackList.item.withDuration', itemCapsule => - language.encapsulate(itemCapsule, 'duration', durationCapsule => - relations.textWithTooltip.slots({ - attributes: {class: 'missing-duration'}, - customInteractionCue: true, + language.encapsulate('trackList.item.accent.duration', capsule => + relations.textWithTooltip.slots({ + attributes: {class: 'missing-duration'}, + customInteractionCue: true, - text: - language.$(durationCapsule, { - duration: - html.tag('span', {class: 'text-with-tooltip-interaction-cue'}, - {tabindex: '0'}, + text: + language.$(capsule, { + duration: + html.tag('span', {class: 'text-with-tooltip-interaction-cue'}, + {tabindex: '0'}, - language.$(durationCapsule, 'missing')), - }), + language.$(capsule, 'missing')), + }), - tooltip: - relations.tooltip.slots({ - attributes: {class: 'missing-duration-tooltip'}, + tooltip: + relations.tooltip.slots({ + attributes: {class: 'missing-duration-tooltip'}, - content: - language.$(durationCapsule, 'missing.info'), - }), - }))), + content: + language.$(capsule, 'missing.info'), + }), + })), }; diff --git a/src/data/composite/wiki-data/helpers/withResolvedReverse.js b/src/data/composite/wiki-data/helpers/withResolvedReverse.js index 818f60b7..bad64925 100644 --- a/src/data/composite/wiki-data/helpers/withResolvedReverse.js +++ b/src/data/composite/wiki-data/helpers/withResolvedReverse.js @@ -5,7 +5,7 @@ import {input, templateCompositeFrom} from '#composite'; import inputWikiData from '../inputWikiData.js'; export default templateCompositeFrom({ - annotation: `withReverseReferenceList`, + annotation: `withResolvedReverse`, inputs: { data: inputWikiData({allowMixedTypes: true}), diff --git a/src/data/thing.js b/src/data/thing.js index 0a6e3be4..f5afe076 100644 --- a/src/data/thing.js +++ b/src/data/thing.js @@ -67,14 +67,7 @@ export default class Thing extends CacheableObject { name = colors.yellow(`couldn't get name`); } - let reference; - try { - if (this.directory) { - reference = colors.blue(Thing.getReference(this)); - } - } catch { - reference = colors.yellow(`couldn't get reference`); - } + let reference = Thing.inspectReference(this, {showConstructor: false}); return ( (name ? `${constructorName} ${name}` : `${constructorName}`) + @@ -127,6 +120,64 @@ export default class Thing extends CacheableObject { return `${thing.constructor[Thing.referenceType]}:${thing.directory}`; } + static inspectReference(thing, {showConstructor = true} = {}) { + const referenceType = + thing.constructor[Thing.referenceType] ?? + null; + + const constructorPart = + (showConstructor + ? `${thing.constructor.name} ` + : ``); + + let errored = false; + const tryToGet = property => { + try { + return thing[property] ?? null; + } catch { + errored = true; + return null; + } + }; + + const directoryPart = this.inspectDirectory(thing); + const directoryErrored = directoryPart === null; + + if (directoryPart && referenceType) { + return colors.blue(`${referenceType}:${directoryPart}`); + } else if (directoryPart) { + return constructorPart + `(${colors.blue(directoryPart)})`; + } else if (tryToGet('name')) { + return constructorPart + `(named ${inspect(thing.name)}`; + } else if (errored && directoryErrored) { + return constructorPart + `(${colors.yellow(`couldn't compute reference`)})`; + } else { + return constructorPart; + } + } + + static inspectDirectory(thing) { + let errored = false; + const tryToGet = property => { + try { + return thing[property] ?? null; + } catch { + errored = true; + return null; + } + }; + + if (tryToGet('directory')) { + return thing.directory; + } else if (tryToGet('unqualifiedDirectory')) { + return `…${thing.unqualifiedDirectory}`; + } else if (errored) { + return null; + } else { + return ''; + } + } + static extendDocumentSpec(thingClass, subspec) { const superspec = thingClass[Thing.yamlDocumentSpec]; diff --git a/src/data/things/Artist.js b/src/data/things/Artist.js index f518e31e..b82ef8bf 100644 --- a/src/data/things/Artist.js +++ b/src/data/things/Artist.js @@ -14,8 +14,10 @@ import { import {exitWithoutDependency, exposeConstant, exposeDependency} from '#composite/control-flow'; -import {withFilteredList, withPropertyFromList} from '#composite/data'; -import {withContributionListSums} from '#composite/wiki-data'; +import {withFilteredList, withMappedList, withPropertyFromList} + from '#composite/data'; +import {withContributionListSums, withReverseReferenceList} + from '#composite/wiki-data'; import { constitutibleArtwork, @@ -216,6 +218,33 @@ export class Artist extends Thing { reverse: soupyReverse.input('musicVideoContributorContributionsBy'), }), + otherMusicVideoArtistContributionsToOwnAlbums: [ + withReverseReferenceList({ + reverse: soupyReverse.input('musicVideoArtistContributionsToAlbumsBy'), + }).outputs({ + '#reverseReferenceList': '#allArtistContributions', + }), + + { + dependencies: [input.myself()], + compute: (continuation, { + [input.myself()]: myself, + }) => continuation({ + ['#isNotMyself']: artist => artist !== myself, + }), + }, + + withPropertyFromList('#allArtistContributions', V('artist')), + + withMappedList('#allArtistContributions.artist', '#isNotMyself') + .outputs({'#mappedList': '#differentArtistFilter'}), + + withFilteredList('#allArtistContributions', '#differentArtistFilter') + .outputs({'#filteredList': '#otherArtistContributions'}), + + exposeDependency('#otherArtistContributions'), + ], + totalDuration: [ withPropertyFromList('musicContributions', V('thing')), withPropertyFromList('#musicContributions.thing', V('isMainRelease')), diff --git a/src/data/things/Artwork.js b/src/data/things/Artwork.js index 7beb3567..d2bd31ba 100644 --- a/src/data/things/Artwork.js +++ b/src/data/things/Artwork.js @@ -422,7 +422,7 @@ export class Artwork extends Thing { parts.push(` for ${inspect(this.thing, newOptions)}`); } else { - parts.push(` for ${colors.blue(Thing.getReference(this.thing))}`); + parts.push(` for ${Thing.inspectReference(this.thing)}`); } } diff --git a/src/data/things/MusicVideo.js b/src/data/things/MusicVideo.js index 3a41caf5..77c8c619 100644 --- a/src/data/things/MusicVideo.js +++ b/src/data/things/MusicVideo.js @@ -167,6 +167,32 @@ export class MusicVideo extends Thing { musicVideoContributorContributionsBy: soupyReverse.contributionsBy('musicVideoData', 'contributorContribs'), + + musicVideoArtistContributionsToAlbumsBy: { + bindTo: 'musicVideoData', + + referencing: musicVideo => musicVideo.artistContribs, + + *referenced(musicVideoContrib) { + const musicVideo = musicVideoContrib.thing; + const trackOrAlbum = musicVideo.thing; + if (trackOrAlbum.isTrack) { + const albumArtists = + trackOrAlbum.album.artistContribs + .map(albumContrib => albumContrib.artist); + + for (const trackContrib of trackOrAlbum.artistContribs) { + if (albumArtists.includes(trackContrib.artist)) { + yield trackContrib.artist; + } + } + } else { + for (const albumContrib of trackOrAlbum.artistContribs) { + yield albumContrib.artist; + } + } + }, + }, }; get path() { @@ -179,7 +205,15 @@ export class MusicVideo extends Thing { [inspect.custom](depth, options, inspect) { const parts = []; - parts.push(Thing.prototype[inspect.custom].apply(this)); + parts.push(this.constructor.name); + + if (this.title) { + parts.push(` ${colors.green(`"${this.title}"`)}`); + } else if (this.label) { + parts.push(` (${colors.green(`"${this.label}"`)})`); + } else if (this.unqualifiedDirectory !== 'music-video') { + parts.push(` (${colors.blue(this.unqualifiedDirectory)})`); + } if (this.thing) { if (depth >= 0) { @@ -193,7 +227,7 @@ export class MusicVideo extends Thing { parts.push(` for ${inspect(this.thing, newOptions)}`); } else { - parts.push(` for ${colors.blue(Thing.getReference(this.thing))}`); + parts.push(` for ${colors.blue(Thing.inspectReference(this.thing))}`); } } diff --git a/src/data/things/contrib/Contribution.js b/src/data/things/contrib/Contribution.js index 4352b58a..305e6a14 100644 --- a/src/data/things/contrib/Contribution.js +++ b/src/data/things/contrib/Contribution.js @@ -314,8 +314,7 @@ export class Contribution extends Thing { } if (artist) { - artistRef = - colors.blue(Thing.getReference(artist)); + artistRef = Thing.inspectReference(artist); } } else { artistRef = @@ -326,7 +325,7 @@ export class Contribution extends Thing { accentParts.push(`by ${artistRef}`); } - if (this.thing) { + if (this.thing) toPart: { if (depth >= 0) { const newOptions = { ...options, @@ -336,10 +335,13 @@ export class Contribution extends Thing { : options.depth - 1), }; - accentParts.push(`to ${inspect(this.thing, newOptions)}`); - } else { - accentParts.push(`to ${colors.blue(Thing.getReference(this.thing))}`); + try { + accentParts.push(`to ${inspect(this.thing, newOptions)}`); + break toPart; + } catch {} } + + accentParts.push(`to ${Thing.inspectReference(this.thing)}`); } if (!empty(accentParts)) { diff --git a/src/reverse.js b/src/reverse.js index b4b225f0..7d7e3672 100644 --- a/src/reverse.js +++ b/src/reverse.js @@ -45,11 +45,12 @@ function reverseHelper(spec) { const interstitialReferencingThings = (spec.bindTo === 'wikiData' - ? spec.referencing(data) - : data.flatMap(thing => spec.referencing(thing))); + ? Array.from(spec.referencing(data)) + : data.flatMap(thing => Array.from(spec.referencing(thing)))); const referencedThings = - interstitialReferencingThings.map(thing => spec.referenced(thing)); + interstitialReferencingThings + .map(thing => Array.from(spec.referenced(thing))); const referencingThings = (spec.tidy diff --git a/src/strings-default.yaml b/src/strings-default.yaml index daca8347..c27d45ae 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -447,25 +447,35 @@ trackList: item: _: "{TRACK}" - withDuration: - _: >- - {DURATION} {TRACK} + withAccent: >- + {ACCENT} {TRACK} - duration: - _: "({DURATION})" - missing: "_:__" - missing.info: "no duration provided; treated as zero seconds long" + accent.withDate: >- + ({DATE}) - withArtists: - _: >- - {TRACK} {BY} + accent.withDate.withDuration: >- + ({DURATION}; {DATE}) + + accent.withDuration: >- + ({DURATION}) + + accent.date: "{DATE}" + + accent.duration: + _: "{DURATION}" + missing: "_:__" + missing.info: "no duration provided; treated as zero seconds long" + withArtists: >- + {TRACK} {ARTISTS} + + artists: by: "by {ARTISTS}" featuring: "feat. {ARTISTS}" by.featuring: "by {ARTISTS} feat. {FEATURING}" - withDuration.withArtists: >- - {DURATION} {TRACK} {BY} + withAccent.withArtists: >- + {ACCENT} {TRACK} {ARTISTS} rerelease: >- {TRACK} (rerelease) @@ -1447,6 +1457,88 @@ artistPage: track: "{TRACK}" + track.musicVideo: + _: >- + {TRACK} + + withLinks: >- + {TRACK}: {LINKS} + + withTitle: >- + {TRACK} — {TITLE} + + withTitle.withLinks: >- + {TRACK} — {TITLE}: {LINKS} + + withLabel: >- + {TRACK} — {LABEL} + + withLabel.withLinks: >- + {TRACK} — {LABEL}: {LINKS} + + withCredit: >- + {TRACK}, {CREDIT} + + withCredit.withLinks: >- + {TRACK}, {CREDIT}: {LINKS} + + withCredit.withTitle: >- + {TRACK}, {TITLE} {CREDIT} + + withCredit.withTitle.withLinks: >- + {TRACK}, {TITLE} {CREDIT}: {LINKS} + + withCredit.withLabel: >- + {TRACK}, {LABEL} {CREDIT} + + withCredit.withLabel.withLinks: >- + {TRACK}, {LABEL} {CREDIT}: {LINKS} + + withDate: >- + ({DATE}) {TRACK} + + withDate.withLinks: >- + ({DATE}) {TRACK}: {LINKS} + + withDate.withTitle: >- + ({DATE}) {TRACK} — {TITLE} + + withDate.withTitle.withLinks: >- + ({DATE}) {TRACK} — {TITLE}: {LINKS} + + withDate.withLabel: >- + ({DATE}) {TRACK} — {LABEL} + + withDate.withLabel.withLinks: >- + ({DATE}) {TRACK} — {LABEL}: {LINKS} + + withDate.withCredit: >- + ({DATE}) {TRACK}, {CREDIT} + + withDate.withCredit.withLinks: >- + ({DATE}) {TRACK}, {CREDIT}: {LINKS} + + withDate.withCredit.withTitle: >- + ({DATE}) {TRACK}, {TITLE} {CREDIT} + + withDate.withCredit.withTitle.withLinks: >- + ({DATE}) {TRACK}, {TITLE} {CREDIT}: {LINKS} + + withDate.withCredit.withLabel: >- + ({DATE}) {TRACK}, {LABEL} {CREDIT} + + withDate.withCredit.withLabel.withLinks: >- + ({DATE}) {TRACK}, {LABEL} {CREDIT}: {LINKS} + + credit: >- + video by {ARTISTS} + + credit.alongsideLabel: >- + by {ARTISTS} + + credit.alongsideTitle: >- + by {ARTISTS} + # album: # The artist info page doesn't display if the artist is # musically credited outright for the album as a whole, @@ -1461,6 +1553,88 @@ artistPage: bannerArt: "(banner art)" commentary: "(album commentary)" + musicVideo: + _: >- + (album music video) + + withLinks: >- + (album music video: {LINKS}) + + withTitle: >- + (for album: {TITLE}) + + withTitle.withLinks: >- + (for album: {TITLE} - {LINKS}) + + withLabel: >- + (for album: {LABEL}) + + withLabel.withLinks: >- + (for album: {LABEL} - {LINKS}) + + withCredit: >- + (album music video {CREDIT}) + + withCredit.withLinks: >- + (album music video {CREDIT} - {LINKS}) + + withCredit.withTitle: >- + (for album: {TITLE} {CREDIT}) + + withCredit.withTitle.withLinks: >- + (for album: {TITLE} {CREDIT} - {LINKS}) + + withCredit.withLabel: >- + (for album: {LABEL} {CREDIT}) + + withCredit.withLabel.withLinks: >- + (for album: {LABEL} {CREDIT} - {LINKS}) + + withDate: >- + ({DATE}: album music video) + + withDate.withLinks: >- + ({DATE}, album music video: {LINKS}) + + withDate.withTitle: >- + ({DATE}, for album: {TITLE}) + + withDate.withTitle.withLinks: >- + ({DATE}, for album: {TITLE} - {LINKS}) + + withDate.withLabel: >- + ({DATE}, for album: {LABEL}) + + withDate.withLabel.withLinks: >- + ({DATE}, for album: {LABEL} - {LINKS}) + + withDate.withCredit: >- + ({DATE}: album music video {CREDIT}) + + withDate.withCredit.withLinks: >- + ({DATE}, album music video {CREDIT} - {LINKS}) + + withDate.withCredit.withTitle: >- + ({DATE}, for album: {TITLE} {CREDIT}) + + withDate.withCredit.withTitle.withLinks: >- + ({DATE}, for album: {TITLE} {CREDIT} - {LINKS}) + + withDate.withCredit.withLabel: >- + ({DATE}, for album: {LABEL} {CREDIT}) + + withDate.withCredit.withLabel.withLinks: >- + ({DATE}, for album: {LABEL} {CREDIT} - {LINKS}) + + credit: >- + by {ARTISTS} + + credit.alongsideLabel: >- + by {ARTISTS} + + credit.alongsideTitle: >- + by {ARTISTS} + flash: "{FLASH}" artwork.accent: @@ -1490,6 +1664,7 @@ artistPage: title: music: "Contributed music to groups:" artworks: "Contributed artworks to groups:" + musicVideos: "Contributed to music videos in groups:" withSortButton: "{TITLE} ({SORT})" sorting: @@ -1502,17 +1677,11 @@ artistPage: countDurationAccent: "({COUNT} — {DURATION})" durationCountAccent: "({DURATION} — {COUNT})" - trackList: - title: "Tracks" - - artList: - title: "Artworks" - - flashList: - title: "Flashes" - - commentaryList: - title: "Commentary" + trackList.title: "Tracks" + artList.title: "Artworks" + musicVideoList.title: "Music Videos" + flashList.title: "Flashes" + commentaryList.title: "Commentary" # viewArtGallery: # This is shown twice on the page - once at almost the very top |