« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/composite/data/excludeFromList.js5
-rw-r--r--src/data/composite/data/fillMissingListItems.js5
-rw-r--r--src/data/composite/data/index.js30
-rw-r--r--src/data/composite/data/withFilteredList.js6
-rw-r--r--src/data/composite/data/withFlattenedList.js6
-rw-r--r--src/data/composite/data/withIndexInList.js38
-rw-r--r--src/data/composite/data/withMappedList.js6
-rw-r--r--src/data/composite/data/withNearbyItemFromList.js73
-rw-r--r--src/data/composite/data/withPropertiesFromList.js6
-rw-r--r--src/data/composite/data/withPropertyFromList.js6
-rw-r--r--src/data/composite/data/withSortedList.js6
-rw-r--r--src/data/composite/data/withUnflattenedList.js6
-rw-r--r--src/data/composite/things/contribution/index.js1
-rw-r--r--src/data/composite/things/contribution/withContainingReverseContributionList.js40
-rw-r--r--src/data/composite/things/contribution/withContributionArtist.js7
-rw-r--r--src/data/composite/wiki-data/withRecontextualizedContributionList.js28
-rw-r--r--src/data/composite/wiki-data/withResolvedContribs.js8
-rw-r--r--src/data/composite/wiki-properties/contributionList.js8
-rw-r--r--src/data/things/album.js8
-rw-r--r--src/data/things/contribution.js48
-rw-r--r--src/data/things/flash.js1
-rw-r--r--src/data/things/language.js31
-rw-r--r--src/data/things/track.js5
23 files changed, 306 insertions, 72 deletions
diff --git a/src/data/composite/data/excludeFromList.js b/src/data/composite/data/excludeFromList.js
index d798dcdc..2a3e818e 100644
--- a/src/data/composite/data/excludeFromList.js
+++ b/src/data/composite/data/excludeFromList.js
@@ -5,11 +5,6 @@
 // See also:
 //  - fillMissingListItems
 //
-// More list utilities:
-//  - withFilteredList, withMappedList, withSortedList
-//  - withFlattenedList, withUnflattenedList
-//  - withPropertyFromList, withPropertiesFromList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 import {empty} from '#sugar';
diff --git a/src/data/composite/data/fillMissingListItems.js b/src/data/composite/data/fillMissingListItems.js
index 4f818a79..356b1119 100644
--- a/src/data/composite/data/fillMissingListItems.js
+++ b/src/data/composite/data/fillMissingListItems.js
@@ -4,11 +4,6 @@
 // See also:
 //  - excludeFromList
 //
-// More list utilities:
-//  - withFilteredList, withMappedList, withSortedList
-//  - withFlattenedList, withUnflattenedList
-//  - withPropertyFromList, withPropertiesFromList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 
diff --git a/src/data/composite/data/index.js b/src/data/composite/data/index.js
index 0a47c43c..c80bb350 100644
--- a/src/data/composite/data/index.js
+++ b/src/data/composite/data/index.js
@@ -3,16 +3,32 @@
 // Entries here may depend on entries in #composite/control-flow.
 //
 
+// Utilities which act on generic objects
+
+export {default as withPropertiesFromObject} from './withPropertiesFromObject.js';
+export {default as withPropertyFromObject} from './withPropertyFromObject.js';
+
+// Utilities which act on generic lists
+
 export {default as excludeFromList} from './excludeFromList.js';
+
 export {default as fillMissingListItems} from './fillMissingListItems.js';
+export {default as withUniqueItemsOnly} from './withUniqueItemsOnly.js';
+
 export {default as withFilteredList} from './withFilteredList.js';
-export {default as withFlattenedList} from './withFlattenedList.js';
 export {default as withMappedList} from './withMappedList.js';
-export {default as withPropertiesFromList} from './withPropertiesFromList.js';
-export {default as withPropertiesFromObject} from './withPropertiesFromObject.js';
-export {default as withPropertyFromList} from './withPropertyFromList.js';
-export {default as withPropertyFromObject} from './withPropertyFromObject.js';
 export {default as withSortedList} from './withSortedList.js';
-export {default as withSum} from './withSum.js';
+
+export {default as withPropertyFromList} from './withPropertyFromList.js';
+export {default as withPropertiesFromList} from './withPropertiesFromList.js';
+
+export {default as withFlattenedList} from './withFlattenedList.js';
 export {default as withUnflattenedList} from './withUnflattenedList.js';
-export {default as withUniqueItemsOnly} from './withUniqueItemsOnly.js';
+
+export {default as withIndexInList} from './withIndexInList.js';
+export {default as withNearbyItemFromList} from './withNearbyItemFromList.js';
+
+// Utilities which act on slightly more particular data forms
+// (probably, containers of particular kinds of values)
+
+export {default as withSum} from './withSum.js';
diff --git a/src/data/composite/data/withFilteredList.js b/src/data/composite/data/withFilteredList.js
index 82e56903..60fe66f4 100644
--- a/src/data/composite/data/withFilteredList.js
+++ b/src/data/composite/data/withFilteredList.js
@@ -16,12 +16,6 @@
 //  - withMappedList
 //  - withSortedList
 //
-// More list utilities:
-//  - excludeFromList
-//  - fillMissingListItems
-//  - withFlattenedList, withUnflattenedList
-//  - withPropertyFromList, withPropertiesFromList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 
diff --git a/src/data/composite/data/withFlattenedList.js b/src/data/composite/data/withFlattenedList.js
index edfa3403..31b1a742 100644
--- a/src/data/composite/data/withFlattenedList.js
+++ b/src/data/composite/data/withFlattenedList.js
@@ -5,12 +5,6 @@
 // See also:
 //  - withUnflattenedList
 //
-// More list utilities:
-//  - excludeFromList
-//  - fillMissingListItems
-//  - withFilteredList, withMappedList, withSortedList
-//  - withPropertyFromList, withPropertiesFromList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 
diff --git a/src/data/composite/data/withIndexInList.js b/src/data/composite/data/withIndexInList.js
new file mode 100644
index 00000000..b1af2033
--- /dev/null
+++ b/src/data/composite/data/withIndexInList.js
@@ -0,0 +1,38 @@
+// Gets the index of the provided item in the provided list. Note that this
+// will output -1 if the item is not found, and this may be detected using
+// any availability check with type: 'index'. If the list includes the item
+// twice, the output index will be of the first match.
+//
+// Both the list and item must be provided.
+//
+// See also:
+//  - withNearbyItemFromList
+//  - exitWithoutDependency
+//  - raiseOutputWithoutDependency
+//
+
+import {input, templateCompositeFrom} from '#composite';
+
+export default templateCompositeFrom({
+  annotation: `withIndexInList`,
+
+  inputs: {
+    list: input({acceptsNull: false, type: 'array'}),
+    item: input({acceptsNull: false}),
+  },
+
+  outputs: ['#index'],
+
+  steps: () => [
+    {
+      dependencies: [input('list'), input('item')],
+      compute: (continuation, {
+        [input('list')]: list,
+        [input('item')]: item,
+      }) => continuation({
+        ['#index']:
+          list.indexOf(item),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/data/withMappedList.js b/src/data/composite/data/withMappedList.js
index e0a700b2..0bc63a92 100644
--- a/src/data/composite/data/withMappedList.js
+++ b/src/data/composite/data/withMappedList.js
@@ -5,12 +5,6 @@
 //  - withFilteredList
 //  - withSortedList
 //
-// More list utilities:
-//  - excludeFromList
-//  - fillMissingListItems
-//  - withFlattenedList, withUnflattenedList
-//  - withPropertyFromList, withPropertiesFromList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 
diff --git a/src/data/composite/data/withNearbyItemFromList.js b/src/data/composite/data/withNearbyItemFromList.js
new file mode 100644
index 00000000..83a8cc21
--- /dev/null
+++ b/src/data/composite/data/withNearbyItemFromList.js
@@ -0,0 +1,73 @@
+// Gets a nearby (typically adjacent) item in a list, meaning the item which is
+// placed at a particular offset compared to the provided item. This is null if
+// the provided list doesn't include the provided item at all, and also if the
+// offset would read past either end of the list - except if configured:
+//
+//  - If the 'wrap' input is provided (as true), the offset will loop around
+//    and continue from the opposing end.
+//
+//  - If the 'valuePastEdge' input is provided, that value will be output
+//    instead of null.
+//
+// Both the list and item must be provided.
+//
+// See also:
+//  - withIndexInList
+//
+
+import {input, templateCompositeFrom} from '#composite';
+import {atOffset} from '#sugar';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+import withIndexInList from './withIndexInList.js';
+
+export default templateCompositeFrom({
+  annotation: `withNearbyItemFromList`,
+
+  inputs: {
+    list: input({acceptsNull: false, type: 'array'}),
+    item: input({acceptsNull: false}),
+
+    offset: input({type: 'number'}),
+    wrap: input({type: 'boolean', defaultValue: false}),
+  },
+
+  outputs: ['#nearbyItem'],
+
+  steps: () => [
+    withIndexInList({
+      list: input('list'),
+      item: input('item'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#index',
+      mode: input.value('index'),
+
+      output: input.value({
+        ['#nearbyItem']:
+          null,
+      }),
+    }),
+
+    {
+      dependencies: [
+        input('list'),
+        input('offset'),
+        input('wrap'),
+        '#index',
+      ],
+
+      compute: (continuation, {
+        [input('list')]: list,
+        [input('offset')]: offset,
+        [input('wrap')]: wrap,
+        ['#index']: index,
+      }) => continuation({
+        ['#nearbyItem']:
+          atOffset(list, index, offset, {wrap}),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/data/withPropertiesFromList.js b/src/data/composite/data/withPropertiesFromList.js
index 08907bab..fb4134bc 100644
--- a/src/data/composite/data/withPropertiesFromList.js
+++ b/src/data/composite/data/withPropertiesFromList.js
@@ -8,12 +8,6 @@
 //  - withPropertiesFromObject
 //  - withPropertyFromList
 //
-// More list utilities:
-//  - excludeFromList
-//  - fillMissingListItems
-//  - withFilteredList, withMappedList, withSortedList
-//  - withFlattenedList, withUnflattenedList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 import {isString, validateArrayItems} from '#validators';
diff --git a/src/data/composite/data/withPropertyFromList.js b/src/data/composite/data/withPropertyFromList.js
index a2c66d77..65ebf77b 100644
--- a/src/data/composite/data/withPropertyFromList.js
+++ b/src/data/composite/data/withPropertyFromList.js
@@ -9,12 +9,6 @@
 //  - withPropertiesFromList
 //  - withPropertyFromObject
 //
-// More list utilities:
-//  - excludeFromList
-//  - fillMissingListItems
-//  - withFilteredList, withMappedList, withSortedList
-//  - withFlattenedList, withUnflattenedList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 
diff --git a/src/data/composite/data/withSortedList.js b/src/data/composite/data/withSortedList.js
index dd810786..a7d21768 100644
--- a/src/data/composite/data/withSortedList.js
+++ b/src/data/composite/data/withSortedList.js
@@ -27,12 +27,6 @@
 //  - withFilteredList
 //  - withMappedList
 //
-// More list utilities:
-//  - excludeFromList
-//  - fillMissingListItems
-//  - withFlattenedList, withUnflattenedList
-//  - withPropertyFromList, withPropertiesFromList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 
diff --git a/src/data/composite/data/withUnflattenedList.js b/src/data/composite/data/withUnflattenedList.js
index 39a666dc..820d628a 100644
--- a/src/data/composite/data/withUnflattenedList.js
+++ b/src/data/composite/data/withUnflattenedList.js
@@ -7,12 +7,6 @@
 // See also:
 //  - withFlattenedList
 //
-// More list utilities:
-//  - excludeFromList
-//  - fillMissingListItems
-//  - withFilteredList, withMappedList, withSortedList
-//  - withPropertyFromList, withPropertiesFromList
-//
 
 import {input, templateCompositeFrom} from '#composite';
 import {isWholeNumber, validateArrayItems} from '#validators';
diff --git a/src/data/composite/things/contribution/index.js b/src/data/composite/things/contribution/index.js
index 2c812644..9b22be2e 100644
--- a/src/data/composite/things/contribution/index.js
+++ b/src/data/composite/things/contribution/index.js
@@ -1,6 +1,7 @@
 export {default as inheritFromContributionPresets} from './inheritFromContributionPresets.js';
 export {default as thingPropertyMatches} from './thingPropertyMatches.js';
 export {default as thingReferenceTypeMatches} from './thingReferenceTypeMatches.js';
+export {default as withContainingReverseContributionList} from './withContainingReverseContributionList.js';
 export {default as withContributionArtist} from './withContributionArtist.js';
 export {default as withContributionContext} from './withContributionContext.js';
 export {default as withMatchingContributionPresets} from './withMatchingContributionPresets.js';
diff --git a/src/data/composite/things/contribution/withContainingReverseContributionList.js b/src/data/composite/things/contribution/withContainingReverseContributionList.js
new file mode 100644
index 00000000..56704c8b
--- /dev/null
+++ b/src/data/composite/things/contribution/withContainingReverseContributionList.js
@@ -0,0 +1,40 @@
+// Get the artist's contribution list containing this property.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+import withContributionArtist from './withContributionArtist.js';
+
+export default templateCompositeFrom({
+  annotation: `withContainingReverseContributionList`,
+
+  inputs: {
+    artistProperty: input({
+      defaultDependency: 'artistProperty',
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#containingReverseContributionList'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: input('artistProperty'),
+      output: input.value({
+        ['#containingReverseContributionList']:
+          null,
+      }),
+    }),
+
+    withContributionArtist(),
+
+    withPropertyFromObject({
+      object: '#artist',
+      property: input('artistProperty'),
+    }).outputs({
+      ['#value']: '#containingReverseContributionList',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/contribution/withContributionArtist.js b/src/data/composite/things/contribution/withContributionArtist.js
index 9e588936..5a611c1a 100644
--- a/src/data/composite/things/contribution/withContributionArtist.js
+++ b/src/data/composite/things/contribution/withContributionArtist.js
@@ -5,10 +5,13 @@ import {withPropertyFromObject} from '#composite/data';
 import {withResolvedReference} from '#composite/wiki-data';
 
 export default templateCompositeFrom({
-  annotation: `withOwnContributionArtist`,
+  annotation: `withContributionArtist`,
 
   inputs: {
-    ref: input({type: 'string'}),
+    ref: input({
+      type: 'string',
+      defaultDependency: 'artist',
+    }),
   },
 
   outputs: ['#artist'],
diff --git a/src/data/composite/wiki-data/withRecontextualizedContributionList.js b/src/data/composite/wiki-data/withRecontextualizedContributionList.js
index 06c997b5..d2401eac 100644
--- a/src/data/composite/wiki-data/withRecontextualizedContributionList.js
+++ b/src/data/composite/wiki-data/withRecontextualizedContributionList.js
@@ -1,12 +1,14 @@
 // Clones all the contributions in a list, with thing and thingProperty both
 // updated to match the current thing. Overwrites the provided dependency.
-// Doesn't do anything if the provided dependency is null.
+// Optionally updates artistProperty as well. Doesn't do anything if
+// the provided dependency is null.
 //
 // See also:
 //  - withRedatedContributionList
 //
 
 import {input, templateCompositeFrom} from '#composite';
+import {isStringNonEmpty} from '#validators';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 import {withClonedThings} from '#composite/wiki-data';
@@ -19,6 +21,11 @@ export default templateCompositeFrom({
       type: 'array',
       acceptsNull: true,
     }),
+
+    artistProperty: input({
+      validate: isStringNonEmpty,
+      defaultValue: null,
+    }),
   },
 
   outputs: ({
@@ -47,16 +54,25 @@ export default templateCompositeFrom({
     },
 
     {
-      dependencies: [input.myself(), input.thisProperty()],
+      dependencies: [
+        input.myself(),
+        input.thisProperty(),
+        input('artistProperty'),
+      ],
 
       compute: (continuation, {
         [input.myself()]: myself,
         [input.thisProperty()]: thisProperty,
+        [input('artistProperty')]: artistProperty,
       }) => continuation({
-        ['#assignment']: {
-          thing: myself,
-          thingProperty: thisProperty,
-        },
+        ['#assignment']:
+          Object.assign(
+            {thing: myself},
+            {thingProperty: thisProperty},
+
+            (artistProperty
+              ? {artistProperty}
+              : {})),
       }),
     },
 
diff --git a/src/data/composite/wiki-data/withResolvedContribs.js b/src/data/composite/wiki-data/withResolvedContribs.js
index 23b91691..b5d7255b 100644
--- a/src/data/composite/wiki-data/withResolvedContribs.js
+++ b/src/data/composite/wiki-data/withResolvedContribs.js
@@ -36,6 +36,11 @@ export default templateCompositeFrom({
       validate: isStringNonEmpty,
       defaultValue: null,
     }),
+
+    artistProperty: input({
+      validate: isStringNonEmpty,
+      defaultValue: null,
+    }),
   },
 
   outputs: ['#resolvedContribs'],
@@ -103,12 +108,14 @@ export default templateCompositeFrom({
       dependencies: [
         '#details',
         '#thingProperty',
+        input('artistProperty'),
         input.myself(),
       ],
 
       compute: (continuation, {
         ['#details']: details,
         ['#thingProperty']: thingProperty,
+        [input('artistProperty')]: artistProperty,
         [input.myself()]: myself,
       }) => continuation({
         ['#contributions']:
@@ -119,6 +126,7 @@ export default templateCompositeFrom({
               ...details,
               thing: myself,
               thingProperty: thingProperty,
+              artistProperty: artistProperty,
             });
 
             return contrib;
diff --git a/src/data/composite/wiki-properties/contributionList.js b/src/data/composite/wiki-properties/contributionList.js
index a0e6e52b..d9a6b417 100644
--- a/src/data/composite/wiki-properties/contributionList.js
+++ b/src/data/composite/wiki-properties/contributionList.js
@@ -15,7 +15,7 @@
 //
 
 import {input, templateCompositeFrom} from '#composite';
-import {isContributionList, isDate} from '#validators';
+import {isContributionList, isDate, isStringNonEmpty} from '#validators';
 
 import {exposeConstant, exposeDependencyOrContinue} from '#composite/control-flow';
 import {withResolvedContribs} from '#composite/wiki-data';
@@ -30,6 +30,11 @@ export default templateCompositeFrom({
       validate: isDate,
       acceptsNull: true,
     }),
+
+    artistProperty: input({
+      validate: isStringNonEmpty,
+      defaultValue: null,
+    }),
   },
 
   update: {validate: isContributionList},
@@ -38,6 +43,7 @@ export default templateCompositeFrom({
     withResolvedContribs({
       from: input.updateValue(),
       thingProperty: input.thisProperty(),
+      artistProperty: input('artistProperty'),
       date: input('date'),
     }),
 
diff --git a/src/data/things/album.js b/src/data/things/album.js
index ae5226ba..a0021946 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -142,6 +142,7 @@ export class Album extends Thing {
 
     artistContribs: contributionList({
       date: 'date',
+      artistProperty: input.value('albumArtistContributions'),
     }),
 
     coverArtistContribs: [
@@ -151,6 +152,7 @@ export class Album extends Thing {
 
       contributionList({
         date: '#coverArtDate',
+        artistProperty: input.value('albumCoverArtistContributions'),
       }),
     ],
 
@@ -158,6 +160,10 @@ export class Album extends Thing {
       // May be null, indicating cover art was added for tracks on the date
       // each track specifies, or else the track's own release date.
       date: 'trackArtDate',
+
+      // This is the "correct" value, but it gets overwritten - with the same
+      // value - regardless.
+      artistProperty: input.value('trackCoverArtistContributions'),
     }),
 
     wallpaperArtistContribs: [
@@ -167,6 +173,7 @@ export class Album extends Thing {
 
       contributionList({
         date: '#coverArtDate',
+        artistProperty: input.value('albumWallpaperArtistContributions'),
       }),
     ],
 
@@ -177,6 +184,7 @@ export class Album extends Thing {
 
       contributionList({
         date: '#coverArtDate',
+        artistProperty: input.value('albumBannerArtistContributions'),
       }),
     ],
 
diff --git a/src/data/things/contribution.js b/src/data/things/contribution.js
index 9d6a9711..79acf1e1 100644
--- a/src/data/things/contribution.js
+++ b/src/data/things/contribution.js
@@ -8,7 +8,7 @@ import Thing from '#thing';
 import {isStringNonEmpty, isThing, validateReference} from '#validators';
 
 import {exitWithoutDependency, exposeDependency} from '#composite/control-flow';
-import {withPropertyFromObject} from '#composite/data';
+import {withNearbyItemFromList, withPropertyFromObject} from '#composite/data';
 import {withResolvedReference} from '#composite/wiki-data';
 import {flag, simpleDate} from '#composite/wiki-properties';
 
@@ -16,6 +16,7 @@ import {
   inheritFromContributionPresets,
   thingPropertyMatches,
   thingReferenceTypeMatches,
+  withContainingReverseContributionList,
   withContributionArtist,
   withContributionContext,
   withMatchingContributionPresets,
@@ -35,6 +36,11 @@ export class Contribution extends Thing {
       update: {validate: isStringNonEmpty},
     },
 
+    artistProperty: {
+      flags: {update: true, expose: true},
+      update: {validate: isStringNonEmpty},
+    },
+
     date: simpleDate(),
 
     artist: [
@@ -155,6 +161,46 @@ export class Contribution extends Thing {
     isForFlash: thingReferenceTypeMatches({
       value: input.value('flash'),
     }),
+
+    previousBySameArtist: [
+      withContainingReverseContributionList().outputs({
+        '#containingReverseContributionList': '#list',
+      }),
+
+      exitWithoutDependency({
+        dependency: '#list',
+      }),
+
+      withNearbyItemFromList({
+        list: '#list',
+        item: input.myself(),
+        offset: input.value(-1),
+      }),
+
+      exposeDependency({
+        dependency: '#nearbyItem',
+      }),
+    ],
+
+    nextBySameArtist: [
+      withContainingReverseContributionList().outputs({
+        '#containingReverseContributionList': '#list',
+      }),
+
+      exitWithoutDependency({
+        dependency: '#list',
+      }),
+
+      withNearbyItemFromList({
+        list: '#list',
+        item: input.myself(),
+        offset: input.value(+1),
+      }),
+
+      exposeDependency({
+        dependency: '#nearbyItem',
+      }),
+    ],
   });
 
   [inspect.custom](depth, options, inspect) {
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 7d37ed81..89e59fe7 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -100,6 +100,7 @@ export class Flash extends Thing {
 
     contributorContribs: contributionList({
       date: 'date',
+      artistProperty: input.value('flashContributorContributions'),
     }),
 
     featuredTracks: referenceList({
diff --git a/src/data/things/language.js b/src/data/things/language.js
index f20927a4..88f16ecb 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -208,9 +208,7 @@ export class Language extends Thing {
       args.at(-1) !== null;
 
     const key =
-      (hasOptions ? args.slice(0, -1) : args)
-        .filter(Boolean)
-        .join('.');
+      this.#joinKeyParts(hasOptions ? args.slice(0, -1) : args);
 
     const options =
       (hasOptions
@@ -843,6 +841,33 @@ export class Language extends Thing {
       return this.formatString('count.fileSize.bytes', {bytes});
     }
   }
+
+  // Utility function to quickly provide a useful string key
+  // (generally a prefix) to stuff nested beneath it.
+  encapsulate(...args) {
+    const fn =
+      (typeof args.at(-1) === 'function'
+        ? args.at(-1)
+        : null);
+
+    const parts =
+      (fn
+        ? args.slice(0, -1)
+        : args);
+
+    const capsule =
+      this.#joinKeyParts(parts);
+
+    if (fn) {
+      return fn(capsule);
+    } else {
+      return capsule;
+    }
+  }
+
+  #joinKeyParts(parts) {
+    return parts.filter(Boolean).join('.');
+  }
 }
 
 const countHelper = (stringKey, optionName = stringKey) =>
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 28a784f5..4aaf364c 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -203,6 +203,7 @@ export class Track extends Thing {
       withResolvedContribs({
         from: input.updateValue({validate: isContributionList}),
         thingProperty: input.thisProperty(),
+        artistProperty: input.value('trackArtistContributions'),
         date: '#date',
       }).outputs({
         '#resolvedContribs': '#artistContribs',
@@ -219,6 +220,7 @@ export class Track extends Thing {
 
       withRecontextualizedContributionList({
         list: '#album.artistContribs',
+        artistProperty: input.value('trackArtistContributions'),
       }),
 
       withRedatedContributionList({
@@ -236,6 +238,7 @@ export class Track extends Thing {
 
       contributionList({
         date: '#date',
+        artistProperty: input.value('trackContributorContributions'),
       }),
     ],
 
@@ -254,6 +257,7 @@ export class Track extends Thing {
       withResolvedContribs({
         from: input.updateValue({validate: isContributionList}),
         thingProperty: input.thisProperty(),
+        artistProperty: input.value('trackCoverArtistContributions'),
         date: '#trackArtDate',
       }).outputs({
         '#resolvedContribs': '#coverArtistContribs',
@@ -270,6 +274,7 @@ export class Track extends Thing {
 
       withRecontextualizedContributionList({
         list: '#album.trackCoverArtistContribs',
+        artistProperty: input.value('trackCoverArtistContributions'),
       }),
 
       withRedatedContributionList({