« 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--src/common-util/wiki-data.js49
-rw-r--r--src/content/dependencies/generateArtistInfoPage.js36
-rw-r--r--src/content/dependencies/generateArtistInfoPageMusicVideosChunk.js58
-rw-r--r--src/content/dependencies/generateArtistInfoPageMusicVideosChunkItem.js118
-rw-r--r--src/content/dependencies/generateArtistInfoPageMusicVideosChunkedList.js66
-rw-r--r--src/content/dependencies/generateArtistInfoPageTracksChunkItem.js48
-rw-r--r--src/data/composite/wiki-data/helpers/withResolvedReverse.js2
-rw-r--r--src/data/things/Artist.js33
-rw-r--r--src/data/things/MusicVideo.js26
-rw-r--r--src/reverse.js7
-rw-r--r--src/strings-default.yaml181
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