diff options
| author | (quasar) nebula <qznebula@protonmail.com> | 2026-04-14 20:26:02 -0300 |
|---|---|---|
| committer | (quasar) nebula <qznebula@protonmail.com> | 2026-04-14 20:26:02 -0300 |
| commit | 2336e252d25536d3678119ff070189e666b98927 (patch) | |
| tree | 788b9c689b8c7ac4324af092c73e95f20b5abd6f /src | |
| parent | ed38f9529084cdd3ff6cdfb56148fd9a99c259b2 (diff) | |
content, data: generateArtistInfoPageMusicVideosChunkedList
Diffstat (limited to 'src')
| -rw-r--r-- | src/common-util/wiki-data.js | 49 | ||||
| -rw-r--r-- | src/content/dependencies/generateArtistInfoPage.js | 36 | ||||
| -rw-r--r-- | src/content/dependencies/generateArtistInfoPageMusicVideosChunk.js | 58 | ||||
| -rw-r--r-- | src/content/dependencies/generateArtistInfoPageMusicVideosChunkItem.js | 118 | ||||
| -rw-r--r-- | src/content/dependencies/generateArtistInfoPageMusicVideosChunkedList.js | 66 | ||||
| -rw-r--r-- | src/content/dependencies/generateArtistInfoPageTracksChunkItem.js | 48 | ||||
| -rw-r--r-- | src/data/composite/wiki-data/helpers/withResolvedReverse.js | 2 | ||||
| -rw-r--r-- | src/data/things/Artist.js | 33 | ||||
| -rw-r--r-- | src/data/things/MusicVideo.js | 26 | ||||
| -rw-r--r-- | src/reverse.js | 7 | ||||
| -rw-r--r-- | src/strings-default.yaml | 181 |
11 files changed, 562 insertions, 62 deletions
diff --git a/src/common-util/wiki-data.js b/src/common-util/wiki-data.js index 54f8b7ed..14ae8e96 100644 --- a/src/common-util/wiki-data.js +++ b/src/common-util/wiki-data.js @@ -280,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/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/generateArtistInfoPageTracksChunkItem.js b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js index 69d8eebd..22a4a228 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkItem.js @@ -1,5 +1,6 @@ import {sortAlbumsTracksChronologically} from '#sort'; import {empty} from '#sugar'; +import {selectRepresentativeArtistContributorContribs} from '#wiki-data'; export default { query(artist, contribs, chunkContribs) { @@ -15,51 +16,8 @@ export default { chunkContribs.flat() .some(contrib => +contrib.date !== +query.track.album.date); - 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.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 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/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/MusicVideo.js b/src/data/things/MusicVideo.js index 8e4e2d6d..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() { 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 e8bda92f..c27d45ae 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -1457,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, @@ -1471,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: @@ -1500,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: @@ -1512,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 |