« 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/composite/things
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/composite/things')
-rw-r--r--src/data/composite/things/album/index.js2
-rw-r--r--src/data/composite/things/album/withHasCoverArt.js64
-rw-r--r--src/data/composite/things/album/withTrackSections.js127
-rw-r--r--src/data/composite/things/album/withTracks.js46
-rw-r--r--src/data/composite/things/art-tag/index.js2
-rw-r--r--src/data/composite/things/art-tag/withAllDescendantArtTags.js44
-rw-r--r--src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js46
-rw-r--r--src/data/composite/things/artist/artistTotalDuration.js69
-rw-r--r--src/data/composite/things/artist/index.js1
-rw-r--r--src/data/composite/things/artwork/index.js1
-rw-r--r--src/data/composite/things/artwork/withDate.js41
-rw-r--r--src/data/composite/things/contribution/index.js7
-rw-r--r--src/data/composite/things/contribution/inheritFromContributionPresets.js61
-rw-r--r--src/data/composite/things/contribution/thingPropertyMatches.js46
-rw-r--r--src/data/composite/things/contribution/thingReferenceTypeMatches.js66
-rw-r--r--src/data/composite/things/contribution/withContainingReverseContributionList.js80
-rw-r--r--src/data/composite/things/contribution/withContributionArtist.js26
-rw-r--r--src/data/composite/things/contribution/withContributionContext.js45
-rw-r--r--src/data/composite/things/contribution/withMatchingContributionPresets.js70
-rw-r--r--src/data/composite/things/flash-act/withFlashSide.js6
-rw-r--r--src/data/composite/things/flash/withFlashAct.js6
-rw-r--r--src/data/composite/things/track-section/index.js3
-rw-r--r--src/data/composite/things/track-section/withAlbum.js20
-rw-r--r--src/data/composite/things/track-section/withContinueCountingFrom.js25
-rw-r--r--src/data/composite/things/track-section/withStartCountingFrom.js64
-rw-r--r--src/data/composite/things/track/index.js16
-rw-r--r--src/data/composite/things/track/inferredAdditionalNameList.js67
-rw-r--r--src/data/composite/things/track/inheritContributionListFromMainRelease.js44
-rw-r--r--src/data/composite/things/track/inheritFromMainRelease.js41
-rw-r--r--src/data/composite/things/track/inheritFromOriginalRelease.js50
-rw-r--r--src/data/composite/things/track/sharedAdditionalNameList.js38
-rw-r--r--src/data/composite/things/track/trackReverseReferenceList.js38
-rw-r--r--src/data/composite/things/track/withAlbum.js22
-rw-r--r--src/data/composite/things/track/withAllReleases.js47
-rw-r--r--src/data/composite/things/track/withAlwaysReferenceByDirectory.js49
-rw-r--r--src/data/composite/things/track/withContainingTrackSection.js36
-rw-r--r--src/data/composite/things/track/withCoverArtistContribs.js73
-rw-r--r--src/data/composite/things/track/withDate.js34
-rw-r--r--src/data/composite/things/track/withDirectorySuffix.js36
-rw-r--r--src/data/composite/things/track/withHasUniqueCoverArt.js77
-rw-r--r--src/data/composite/things/track/withMainRelease.js (renamed from src/data/composite/things/track/withOriginalRelease.js)40
-rw-r--r--src/data/composite/things/track/withOtherReleases.js29
-rw-r--r--src/data/composite/things/track/withPropertyFromAlbum.js18
-rw-r--r--src/data/composite/things/track/withPropertyFromMainRelease.js86
-rw-r--r--src/data/composite/things/track/withSuffixDirectoryFromAlbum.js53
-rw-r--r--src/data/composite/things/track/withTrackArtDate.js60
-rw-r--r--src/data/composite/things/track/withTrackNumber.js50
47 files changed, 1476 insertions, 496 deletions
diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js
index 8139f10e..dfc6864f 100644
--- a/src/data/composite/things/album/index.js
+++ b/src/data/composite/things/album/index.js
@@ -1,2 +1,2 @@
+export {default as withHasCoverArt} from './withHasCoverArt.js';
 export {default as withTracks} from './withTracks.js';
-export {default as withTrackSections} from './withTrackSections.js';
diff --git a/src/data/composite/things/album/withHasCoverArt.js b/src/data/composite/things/album/withHasCoverArt.js
new file mode 100644
index 00000000..fd3f2894
--- /dev/null
+++ b/src/data/composite/things/album/withHasCoverArt.js
@@ -0,0 +1,64 @@
+// TODO: This shouldn't be coded as an Album-specific thing,
+// or even really to do with cover artworks in particular, either.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
+import {fillMissingListItems, withFlattenedList, withPropertyFromList}
+  from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: 'withHasCoverArt',
+
+  outputs: ['#hasCoverArt'],
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: 'coverArtistContribs',
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: ['#availability'],
+      compute: (continuation, {
+        ['#availability']: availability,
+      }) =>
+        (availability
+          ? continuation.raiseOutput({
+              ['#hasCoverArt']: true,
+            })
+          : continuation()),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: 'coverArtworks',
+      mode: input.value('empty'),
+      output: input.value({'#hasCoverArt': false}),
+    }),
+
+    withPropertyFromList({
+      list: 'coverArtworks',
+      property: input.value('artistContribs'),
+      internal: input.value(true),
+    }),
+
+    // Since we're getting the update value for each artwork's artistContribs,
+    // it may not be set at all, and in that case won't be exposing as [].
+    fillMissingListItems({
+      list: '#coverArtworks.artistContribs',
+      fill: input.value([]),
+    }),
+
+    withFlattenedList({
+      list: '#coverArtworks.artistContribs',
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#flattenedList',
+      mode: input.value('empty'),
+    }).outputs({
+      '#availability': '#hasCoverArt',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/album/withTrackSections.js b/src/data/composite/things/album/withTrackSections.js
deleted file mode 100644
index 0a1ebebc..00000000
--- a/src/data/composite/things/album/withTrackSections.js
+++ /dev/null
@@ -1,127 +0,0 @@
-import {input, templateCompositeFrom} from '#composite';
-import find from '#find';
-import {empty, filterMultipleArrays, stitchArrays} from '#sugar';
-import {isTrackSectionList} from '#validators';
-
-import {exitWithoutDependency, exitWithoutUpdateValue}
-  from '#composite/control-flow';
-import {withResolvedReferenceList} from '#composite/wiki-data';
-
-import {
-  fillMissingListItems,
-  withFlattenedList,
-  withPropertiesFromList,
-  withUnflattenedList,
-} from '#composite/data';
-
-export default templateCompositeFrom({
-  annotation: `withTrackSections`,
-
-  outputs: ['#trackSections'],
-
-  steps: () => [
-    exitWithoutDependency({
-      dependency: 'ownTrackData',
-      value: input.value([]),
-    }),
-
-    exitWithoutUpdateValue({
-      mode: input.value('empty'),
-      value: input.value([]),
-    }),
-
-    // TODO: input.updateValue description down here is a kludge.
-    withPropertiesFromList({
-      list: input.updateValue({
-        validate: isTrackSectionList,
-      }),
-      prefix: input.value('#sections'),
-      properties: input.value([
-        'tracks',
-        'dateOriginallyReleased',
-        'isDefaultTrackSection',
-        'name',
-        'color',
-      ]),
-    }),
-
-    fillMissingListItems({
-      list: '#sections.tracks',
-      fill: input.value([]),
-    }),
-
-    fillMissingListItems({
-      list: '#sections.isDefaultTrackSection',
-      fill: input.value(false),
-    }),
-
-    fillMissingListItems({
-      list: '#sections.name',
-      fill: input.value('Unnamed Track Section'),
-    }),
-
-    fillMissingListItems({
-      list: '#sections.color',
-      fill: input.dependency('color'),
-    }),
-
-    withFlattenedList({
-      list: '#sections.tracks',
-    }).outputs({
-      ['#flattenedList']: '#trackRefs',
-      ['#flattenedIndices']: '#sections.startIndex',
-    }),
-
-    withResolvedReferenceList({
-      list: '#trackRefs',
-      data: 'ownTrackData',
-      notFoundMode: input.value('null'),
-      find: input.value(find.track),
-    }).outputs({
-      ['#resolvedReferenceList']: '#tracks',
-    }),
-
-    withUnflattenedList({
-      list: '#tracks',
-      indices: '#sections.startIndex',
-    }).outputs({
-      ['#unflattenedList']: '#sections.tracks',
-    }),
-
-    {
-      dependencies: [
-        '#sections.tracks',
-        '#sections.name',
-        '#sections.color',
-        '#sections.dateOriginallyReleased',
-        '#sections.isDefaultTrackSection',
-        '#sections.startIndex',
-      ],
-
-      compute: (continuation, {
-        '#sections.tracks': tracks,
-        '#sections.name': name,
-        '#sections.color': color,
-        '#sections.dateOriginallyReleased': dateOriginallyReleased,
-        '#sections.isDefaultTrackSection': isDefaultTrackSection,
-        '#sections.startIndex': startIndex,
-      }) => {
-        filterMultipleArrays(
-          tracks, name, color, dateOriginallyReleased, isDefaultTrackSection, startIndex,
-          tracks => !empty(tracks));
-
-        return continuation({
-          ['#trackSections']:
-            stitchArrays({
-              tracks,
-              name,
-              color,
-              dateOriginallyReleased,
-              isDefaultTrackSection,
-              startIndex,
-            }),
-        });
-      },
-    },
-  ],
-});
diff --git a/src/data/composite/things/album/withTracks.js b/src/data/composite/things/album/withTracks.js
index fff3d5ae..835ee570 100644
--- a/src/data/composite/things/album/withTracks.js
+++ b/src/data/composite/things/album/withTracks.js
@@ -1,9 +1,8 @@
 import {input, templateCompositeFrom} from '#composite';
-import find from '#find';
 
-import {exitWithoutDependency, raiseOutputWithoutDependency}
-  from '#composite/control-flow';
-import {withResolvedReferenceList} from '#composite/wiki-data';
+import {withFlattenedList, withPropertyFromList} from '#composite/data';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
 
 export default templateCompositeFrom({
   annotation: `withTracks`,
@@ -11,41 +10,20 @@ export default templateCompositeFrom({
   outputs: ['#tracks'],
 
   steps: () => [
-    exitWithoutDependency({
-      dependency: 'ownTrackData',
-      value: input.value([]),
-    }),
-
     raiseOutputWithoutDependency({
       dependency: 'trackSections',
-      mode: input.value('empty'),
-      output: input.value({
-        ['#tracks']: [],
-      }),
+      output: input.value({'#tracks': []}),
     }),
 
-    {
-      dependencies: ['trackSections'],
-      compute: (continuation, {trackSections}) =>
-        continuation({
-          '#trackRefs': trackSections
-            .flatMap(section => section.tracks ?? []),
-        }),
-    },
-
-    withResolvedReferenceList({
-      list: '#trackRefs',
-      data: 'ownTrackData',
-      find: input.value(find.track),
+    withPropertyFromList({
+      list: 'trackSections',
+      property: input.value('tracks'),
     }),
 
-    {
-      dependencies: ['#resolvedReferenceList'],
-      compute: (continuation, {
-        ['#resolvedReferenceList']: resolvedReferenceList,
-      }) => continuation({
-        ['#tracks']: resolvedReferenceList,
-      })
-    },
+    withFlattenedList({
+      list: '#trackSections.tracks',
+    }).outputs({
+      ['#flattenedList']: '#tracks',
+    }),
   ],
 });
diff --git a/src/data/composite/things/art-tag/index.js b/src/data/composite/things/art-tag/index.js
new file mode 100644
index 00000000..bbd38293
--- /dev/null
+++ b/src/data/composite/things/art-tag/index.js
@@ -0,0 +1,2 @@
+export {default as withAllDescendantArtTags} from './withAllDescendantArtTags.js';
+export {default as withAncestorArtTagBaobabTree} from './withAncestorArtTagBaobabTree.js';
diff --git a/src/data/composite/things/art-tag/withAllDescendantArtTags.js b/src/data/composite/things/art-tag/withAllDescendantArtTags.js
new file mode 100644
index 00000000..795f96cd
--- /dev/null
+++ b/src/data/composite/things/art-tag/withAllDescendantArtTags.js
@@ -0,0 +1,44 @@
+// Gets all the art tags which descend from this one - that means its own direct
+// descendants, but also all the direct and indirect desceands of each of those!
+// The results aren't specially sorted, but they won't contain any duplicates
+// (for example if two descendant tags both route deeper to end up including
+// some of the same tags).
+
+import {input, templateCompositeFrom} from '#composite';
+import {unique} from '#sugar';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withResolvedReferenceList} from '#composite/wiki-data';
+import {soupyFind} from '#composite/wiki-properties';
+
+export default templateCompositeFrom({
+  annotation: `withAllDescendantArtTags`,
+
+  outputs: ['#allDescendantArtTags'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'directDescendantArtTags',
+      mode: input.value('empty'),
+      output: input.value({'#allDescendantArtTags': []})
+    }),
+
+    withResolvedReferenceList({
+      list: 'directDescendantArtTags',
+      find: soupyFind.input('artTag'),
+    }),
+
+    {
+      dependencies: ['#resolvedReferenceList'],
+      compute: (continuation, {
+        ['#resolvedReferenceList']: directDescendantArtTags,
+      }) => continuation({
+        ['#allDescendantArtTags']:
+          unique([
+            ...directDescendantArtTags,
+            ...directDescendantArtTags.flatMap(artTag => artTag.allDescendantArtTags),
+          ]),
+      }),
+    },
+  ],
+})
diff --git a/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js b/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js
new file mode 100644
index 00000000..e084a42b
--- /dev/null
+++ b/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js
@@ -0,0 +1,46 @@
+// Gets all the art tags which are ancestors of this one as a "baobab tree" -
+// what you'd typically think of as roots are all up in the air! Since this
+// really is backwards from the way that the art tag tree is written in data,
+// chances are pretty good that there will be many of the exact same "leaf"
+// nodes - art tags which don't themselves have any ancestors. In the actual
+// data structure, each node is a Map, with keys for each ancestor and values
+// for each ancestor's own baobab (thus a branching structure, just like normal
+// trees in this regard).
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withReverseReferenceList} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
+
+export default templateCompositeFrom({
+  annotation: `withAncestorArtTagBaobabTree`,
+
+  outputs: ['#ancestorArtTagBaobabTree'],
+
+  steps: () => [
+    withReverseReferenceList({
+      reverse: soupyReverse.input('artTagsWhichDirectlyAncestor'),
+    }).outputs({
+      ['#reverseReferenceList']: '#directAncestorArtTags',
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#directAncestorArtTags',
+      mode: input.value('empty'),
+      output: input.value({'#ancestorArtTagBaobabTree': new Map()}),
+    }),
+
+    {
+      dependencies: ['#directAncestorArtTags'],
+      compute: (continuation, {
+        ['#directAncestorArtTags']: directAncestorArtTags,
+      }) => continuation({
+        ['#ancestorArtTagBaobabTree']:
+          new Map(
+            directAncestorArtTags
+              .map(artTag => [artTag, artTag.ancestorArtTagBaobabTree])),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/artist/artistTotalDuration.js b/src/data/composite/things/artist/artistTotalDuration.js
new file mode 100644
index 00000000..b8a205fe
--- /dev/null
+++ b/src/data/composite/things/artist/artistTotalDuration.js
@@ -0,0 +1,69 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {exposeDependency} from '#composite/control-flow';
+import {withFilteredList, withPropertyFromList} from '#composite/data';
+import {withContributionListSums, withReverseReferenceList}
+  from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
+
+export default templateCompositeFrom({
+  annotation: `artistTotalDuration`,
+
+  compose: false,
+
+  steps: () => [
+    withReverseReferenceList({
+      reverse: soupyReverse.input('trackArtistContributionsBy'),
+    }).outputs({
+      '#reverseReferenceList': '#contributionsAsArtist',
+    }),
+
+    withReverseReferenceList({
+      reverse: soupyReverse.input('trackContributorContributionsBy'),
+    }).outputs({
+      '#reverseReferenceList': '#contributionsAsContributor',
+    }),
+
+    {
+      dependencies: [
+        '#contributionsAsArtist',
+        '#contributionsAsContributor',
+      ],
+
+      compute: (continuation, {
+        ['#contributionsAsArtist']: artistContribs,
+        ['#contributionsAsContributor']: contributorContribs,
+      }) => continuation({
+        ['#allContributions']: [
+          ...artistContribs,
+          ...contributorContribs,
+        ],
+      }),
+    },
+
+    withPropertyFromList({
+      list: '#allContributions',
+      property: input.value('thing'),
+    }),
+
+    withPropertyFromList({
+      list: '#allContributions.thing',
+      property: input.value('isMainRelease'),
+    }),
+
+    withFilteredList({
+      list: '#allContributions',
+      filter: '#allContributions.thing.isMainRelease',
+    }).outputs({
+      '#filteredList': '#mainReleaseContributions',
+    }),
+
+    withContributionListSums({
+      list: '#mainReleaseContributions',
+    }),
+
+    exposeDependency({
+      dependency: '#contributionListDuration',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/artist/index.js b/src/data/composite/things/artist/index.js
new file mode 100644
index 00000000..55514c71
--- /dev/null
+++ b/src/data/composite/things/artist/index.js
@@ -0,0 +1 @@
+export {default as artistTotalDuration} from './artistTotalDuration.js';
diff --git a/src/data/composite/things/artwork/index.js b/src/data/composite/things/artwork/index.js
new file mode 100644
index 00000000..b92bff72
--- /dev/null
+++ b/src/data/composite/things/artwork/index.js
@@ -0,0 +1 @@
+export {default as withDate} from './withDate.js';
diff --git a/src/data/composite/things/artwork/withDate.js b/src/data/composite/things/artwork/withDate.js
new file mode 100644
index 00000000..5e05b814
--- /dev/null
+++ b/src/data/composite/things/artwork/withDate.js
@@ -0,0 +1,41 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `withDate`,
+
+  inputs: {
+    from: input({
+      defaultDependency: 'date',
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#date'],
+
+  steps: () => [
+    {
+      dependencies: [input('from')],
+      compute: (continuation, {
+        [input('from')]: date,
+      }) =>
+        (date
+          ? continuation.raiseOutput({'#date': date})
+          : continuation()),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: 'dateFromThingProperty',
+      output: input.value({'#date': null}),
+    }),
+
+    withPropertyFromObject({
+      object: 'thing',
+      property: 'dateFromThingProperty',
+    }).outputs({
+      ['#value']: '#date',
+    }),
+  ],
+})
diff --git a/src/data/composite/things/contribution/index.js b/src/data/composite/things/contribution/index.js
new file mode 100644
index 00000000..9b22be2e
--- /dev/null
+++ b/src/data/composite/things/contribution/index.js
@@ -0,0 +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/inheritFromContributionPresets.js b/src/data/composite/things/contribution/inheritFromContributionPresets.js
new file mode 100644
index 00000000..a74e6db3
--- /dev/null
+++ b/src/data/composite/things/contribution/inheritFromContributionPresets.js
@@ -0,0 +1,61 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromList} from '#composite/data';
+
+import withMatchingContributionPresets
+  from './withMatchingContributionPresets.js';
+
+export default templateCompositeFrom({
+  annotation: `inheritFromContributionPresets`,
+
+  inputs: {
+    property: input({type: 'string'}),
+  },
+
+  steps: () => [
+    withMatchingContributionPresets().outputs({
+      '#matchingContributionPresets': '#presets',
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#presets',
+      mode: input.value('empty'),
+    }),
+
+    withPropertyFromList({
+      list: '#presets',
+      property: input('property'),
+    }),
+
+    {
+      dependencies: ['#values'],
+
+      compute: (continuation, {
+        ['#values']: values,
+      }) => continuation({
+        ['#index']:
+          values.findIndex(value =>
+            value !== undefined &&
+            value !== null),
+      }),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: '#index',
+      mode: input.value('index'),
+    }),
+
+    {
+      dependencies: ['#values', '#index'],
+
+      compute: (continuation, {
+        ['#values']: values,
+        ['#index']: index,
+      }) => continuation({
+        ['#value']:
+          values[index],
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/contribution/thingPropertyMatches.js b/src/data/composite/things/contribution/thingPropertyMatches.js
new file mode 100644
index 00000000..1e9019b8
--- /dev/null
+++ b/src/data/composite/things/contribution/thingPropertyMatches.js
@@ -0,0 +1,46 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `thingPropertyMatches`,
+
+  compose: false,
+
+  inputs: {
+    value: input({type: 'string'}),
+  },
+
+  steps: () => [
+    {
+      dependencies: ['thing', 'thingProperty'],
+
+      compute: (continuation, {thing, thingProperty}) =>
+        continuation({
+          ['#thingProperty']:
+            (thing.constructor[Symbol.for('Thing.referenceType')] === 'artwork'
+              ? thing.artistContribsFromThingProperty
+              : thingProperty),
+        }),
+    },
+
+    exitWithoutDependency({
+      dependency: '#thingProperty',
+      value: input.value(false),
+    }),
+
+    {
+      dependencies: [
+        '#thingProperty',
+        input('value'),
+      ],
+
+      compute: ({
+        ['#thingProperty']: thingProperty,
+        [input('value')]: value,
+      }) =>
+        thingProperty === value,
+    },
+  ],
+});
diff --git a/src/data/composite/things/contribution/thingReferenceTypeMatches.js b/src/data/composite/things/contribution/thingReferenceTypeMatches.js
new file mode 100644
index 00000000..4042e78f
--- /dev/null
+++ b/src/data/composite/things/contribution/thingReferenceTypeMatches.js
@@ -0,0 +1,66 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `thingReferenceTypeMatches`,
+
+  compose: false,
+
+  inputs: {
+    value: input({type: 'string'}),
+  },
+
+  steps: () => [
+    exitWithoutDependency({
+      dependency: 'thing',
+      value: input.value(false),
+    }),
+
+    withPropertyFromObject({
+      object: 'thing',
+      property: input.value('constructor'),
+    }),
+
+    {
+      dependencies: [
+        '#thing.constructor',
+        input('value'),
+      ],
+
+      compute: (continuation, {
+        ['#thing.constructor']: constructor,
+        [input('value')]: value,
+      }) =>
+        (constructor[Symbol.for('Thing.referenceType')] === value
+          ? continuation.exit(true)
+       : constructor[Symbol.for('Thing.referenceType')] === 'artwork'
+          ? continuation()
+          : continuation.exit(false)),
+    },
+
+    withPropertyFromObject({
+      object: 'thing',
+      property: input.value('thing'),
+    }),
+
+    withPropertyFromObject({
+      object: '#thing.thing',
+      property: input.value('constructor'),
+    }),
+
+    {
+      dependencies: [
+        '#thing.thing.constructor',
+        input('value'),
+      ],
+
+      compute: ({
+        ['#thing.thing.constructor']: constructor,
+        [input('value')]: value,
+      }) =>
+        constructor[Symbol.for('Thing.referenceType')] === value,
+    },
+  ],
+});
diff --git a/src/data/composite/things/contribution/withContainingReverseContributionList.js b/src/data/composite/things/contribution/withContainingReverseContributionList.js
new file mode 100644
index 00000000..175d6cbb
--- /dev/null
+++ b/src/data/composite/things/contribution/withContainingReverseContributionList.js
@@ -0,0 +1,80 @@
+// Get the artist's contribution list containing this property. Although that
+// list literally includes both dated and dateless contributions, here, if the
+// current contribution is dateless, the list is filtered to only include
+// dateless contributions from the same immediately nearby context.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck}
+  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']: '#list',
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: 'date',
+    }).outputs({
+      ['#availability']: '#hasDate',
+    }),
+
+    {
+      dependencies: ['#hasDate', '#list'],
+      compute: (continuation, {
+        ['#hasDate']: hasDate,
+        ['#list']: list,
+      }) =>
+        (hasDate
+          ? continuation.raiseOutput({
+              ['#containingReverseContributionList']:
+                list.filter(contrib => contrib.date),
+            })
+          : continuation({
+              ['#list']:
+                list.filter(contrib => !contrib.date),
+            })),
+    },
+
+    {
+      dependencies: ['#list', 'thing'],
+      compute: (continuation, {
+        ['#list']: list,
+        ['thing']: thing,
+      }) => continuation({
+        ['#containingReverseContributionList']:
+          (thing.album
+            ? list.filter(contrib => contrib.thing.album === thing.album)
+            : list),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/contribution/withContributionArtist.js b/src/data/composite/things/contribution/withContributionArtist.js
new file mode 100644
index 00000000..5f81c716
--- /dev/null
+++ b/src/data/composite/things/contribution/withContributionArtist.js
@@ -0,0 +1,26 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {withResolvedReference} from '#composite/wiki-data';
+import {soupyFind} from '#composite/wiki-properties';
+
+export default templateCompositeFrom({
+  annotation: `withContributionArtist`,
+
+  inputs: {
+    ref: input({
+      type: 'string',
+      defaultDependency: 'artist',
+    }),
+  },
+
+  outputs: ['#artist'],
+
+  steps: () => [
+    withResolvedReference({
+      ref: input('ref'),
+      find: soupyFind.input('artist'),
+    }).outputs({
+      '#resolvedReference': '#artist',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/contribution/withContributionContext.js b/src/data/composite/things/contribution/withContributionContext.js
new file mode 100644
index 00000000..3c1c31c0
--- /dev/null
+++ b/src/data/composite/things/contribution/withContributionContext.js
@@ -0,0 +1,45 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+export default templateCompositeFrom({
+  annotation: `withContributionContext`,
+
+  outputs: [
+    '#contributionTarget',
+    '#contributionProperty',
+  ],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'thing',
+      output: input.value({
+        '#contributionTarget': null,
+        '#contributionProperty': null,
+      }),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: 'thingProperty',
+      output: input.value({
+        '#contributionTarget': null,
+        '#contributionProperty': null,
+      }),
+    }),
+
+    {
+      dependencies: ['thing', 'thingProperty'],
+
+      compute: (continuation, {
+        ['thing']: thing,
+        ['thingProperty']: thingProperty,
+      }) => continuation({
+        ['#contributionTarget']:
+          thing.constructor[Symbol.for('Thing.referenceType')],
+
+        ['#contributionProperty']:
+          thingProperty,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/contribution/withMatchingContributionPresets.js b/src/data/composite/things/contribution/withMatchingContributionPresets.js
new file mode 100644
index 00000000..09454164
--- /dev/null
+++ b/src/data/composite/things/contribution/withMatchingContributionPresets.js
@@ -0,0 +1,70 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+import withContributionContext from './withContributionContext.js';
+
+export default templateCompositeFrom({
+  annotation: `withMatchingContributionPresets`,
+
+  outputs: ['#matchingContributionPresets'],
+
+  steps: () => [
+    withPropertyFromObject({
+      object: 'thing',
+      property: input.value('wikiInfo'),
+      internal: input.value(true),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#thing.wikiInfo',
+      output: input.value({
+        '#matchingContributionPresets': null,
+      }),
+    }),
+
+    withPropertyFromObject({
+      object: '#thing.wikiInfo',
+      property: input.value('contributionPresets'),
+    }).outputs({
+      '#thing.wikiInfo.contributionPresets': '#contributionPresets',
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#contributionPresets',
+      mode: input.value('empty'),
+      output: input.value({
+        '#matchingContributionPresets': [],
+      }),
+    }),
+
+    withContributionContext(),
+
+    {
+      dependencies: [
+        '#contributionPresets',
+        '#contributionTarget',
+        '#contributionProperty',
+        'annotation',
+      ],
+
+      compute: (continuation, {
+        ['#contributionPresets']: presets,
+        ['#contributionTarget']: target,
+        ['#contributionProperty']: property,
+        ['annotation']: annotation,
+      }) => continuation({
+        ['#matchingContributionPresets']:
+          presets
+            .filter(preset =>
+              preset.context[0] === target &&
+              preset.context.slice(1).includes(property) &&
+              // For now, only match if the annotation is a complete match.
+              // Partial matches (e.g. because the contribution includes "two"
+              // annotations, separated by commas) don't count.
+              preset.annotation === annotation),
+      })
+    },
+  ],
+});
diff --git a/src/data/composite/things/flash-act/withFlashSide.js b/src/data/composite/things/flash-act/withFlashSide.js
index 64daa1fb..e09f06e6 100644
--- a/src/data/composite/things/flash-act/withFlashSide.js
+++ b/src/data/composite/things/flash-act/withFlashSide.js
@@ -2,9 +2,10 @@
 // If there's no side whose list of flash acts includes this act, the output
 // dependency will be null.
 
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `withFlashSide`,
@@ -13,8 +14,7 @@ export default templateCompositeFrom({
 
   steps: () => [
     withUniqueReferencingThing({
-      data: 'flashSideData',
-      list: input.value('acts'),
+      reverse: soupyReverse.input('flashSidesWhoseActsInclude'),
     }).outputs({
       ['#uniqueReferencingThing']: '#flashSide',
     }),
diff --git a/src/data/composite/things/flash/withFlashAct.js b/src/data/composite/things/flash/withFlashAct.js
index 652b8bfb..87922aff 100644
--- a/src/data/composite/things/flash/withFlashAct.js
+++ b/src/data/composite/things/flash/withFlashAct.js
@@ -2,9 +2,10 @@
 // If there's no flash whose list of flashes includes this flash, the output
 // dependency will be null.
 
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `withFlashAct`,
@@ -13,8 +14,7 @@ export default templateCompositeFrom({
 
   steps: () => [
     withUniqueReferencingThing({
-      data: 'flashActData',
-      list: input.value('flashes'),
+      reverse: soupyReverse.input('flashActsWhoseFlashesInclude'),
     }).outputs({
       ['#uniqueReferencingThing']: '#flashAct',
     }),
diff --git a/src/data/composite/things/track-section/index.js b/src/data/composite/things/track-section/index.js
new file mode 100644
index 00000000..f11a2ab5
--- /dev/null
+++ b/src/data/composite/things/track-section/index.js
@@ -0,0 +1,3 @@
+export {default as withAlbum} from './withAlbum.js';
+export {default as withContinueCountingFrom} from './withContinueCountingFrom.js';
+export {default as withStartCountingFrom} from './withStartCountingFrom.js';
diff --git a/src/data/composite/things/track-section/withAlbum.js b/src/data/composite/things/track-section/withAlbum.js
new file mode 100644
index 00000000..e257062e
--- /dev/null
+++ b/src/data/composite/things/track-section/withAlbum.js
@@ -0,0 +1,20 @@
+// Gets the track section's album.
+
+import {templateCompositeFrom} from '#composite';
+
+import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
+
+export default templateCompositeFrom({
+  annotation: `withAlbum`,
+
+  outputs: ['#album'],
+
+  steps: () => [
+    withUniqueReferencingThing({
+      reverse: soupyReverse.input('albumsWhoseTrackSectionsInclude'),
+    }).outputs({
+      ['#uniqueReferencingThing']: '#album',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/track-section/withContinueCountingFrom.js b/src/data/composite/things/track-section/withContinueCountingFrom.js
new file mode 100644
index 00000000..e034b7a5
--- /dev/null
+++ b/src/data/composite/things/track-section/withContinueCountingFrom.js
@@ -0,0 +1,25 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import withStartCountingFrom from './withStartCountingFrom.js';
+
+export default templateCompositeFrom({
+  annotation: `withContinueCountingFrom`,
+
+  outputs: ['#continueCountingFrom'],
+
+  steps: () => [
+    withStartCountingFrom(),
+
+    {
+      dependencies: ['#startCountingFrom', 'tracks'],
+      compute: (continuation, {
+        ['#startCountingFrom']: startCountingFrom,
+        ['tracks']: tracks,
+      }) => continuation({
+        ['#continueCountingFrom']:
+          startCountingFrom +
+          tracks.length,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track-section/withStartCountingFrom.js b/src/data/composite/things/track-section/withStartCountingFrom.js
new file mode 100644
index 00000000..ef345327
--- /dev/null
+++ b/src/data/composite/things/track-section/withStartCountingFrom.js
@@ -0,0 +1,64 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withNearbyItemFromList, withPropertyFromObject} from '#composite/data';
+
+import withAlbum from './withAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: `withStartCountingFrom`,
+
+  inputs: {
+    from: input({
+      type: 'number',
+      defaultDependency: 'startCountingFrom',
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#startCountingFrom'],
+
+  steps: () => [
+    {
+      dependencies: [input('from')],
+      compute: (continuation, {
+        [input('from')]: from,
+      }) =>
+        (from === null
+          ? continuation()
+          : continuation.raiseOutput({'#startCountingFrom': from})),
+    },
+
+    withAlbum(),
+
+    raiseOutputWithoutDependency({
+      dependency: '#album',
+      output: input.value({'#startCountingFrom': 1}),
+    }),
+
+    withPropertyFromObject({
+      object: '#album',
+      property: input.value('trackSections'),
+    }),
+
+    withNearbyItemFromList({
+      list: '#album.trackSections',
+      item: input.myself(),
+      offset: input.value(-1),
+    }).outputs({
+      '#nearbyItem': '#previousTrackSection',
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#previousTrackSection',
+      output: input.value({'#startCountingFrom': 1}),
+    }),
+
+    withPropertyFromObject({
+      object: '#previousTrackSection',
+      property: input.value('continueCountingFrom'),
+    }).outputs({
+      '#previousTrackSection.continueCountingFrom': '#startCountingFrom',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js
index cc723a24..e789e736 100644
--- a/src/data/composite/things/track/index.js
+++ b/src/data/composite/things/track/index.js
@@ -1,11 +1,17 @@
 export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js';
-export {default as inferredAdditionalNameList} from './inferredAdditionalNameList.js';
-export {default as inheritFromOriginalRelease} from './inheritFromOriginalRelease.js';
-export {default as sharedAdditionalNameList} from './sharedAdditionalNameList.js';
-export {default as trackReverseReferenceList} from './trackReverseReferenceList.js';
-export {default as withAlbum} from './withAlbum.js';
+export {default as inheritContributionListFromMainRelease} from './inheritContributionListFromMainRelease.js';
+export {default as inheritFromMainRelease} from './inheritFromMainRelease.js';
+export {default as withAllReleases} from './withAllReleases.js';
 export {default as withAlwaysReferenceByDirectory} from './withAlwaysReferenceByDirectory.js';
 export {default as withContainingTrackSection} from './withContainingTrackSection.js';
+export {default as withCoverArtistContribs} from './withCoverArtistContribs.js';
+export {default as withDate} from './withDate.js';
+export {default as withDirectorySuffix} from './withDirectorySuffix.js';
 export {default as withHasUniqueCoverArt} from './withHasUniqueCoverArt.js';
+export {default as withMainRelease} from './withMainRelease.js';
 export {default as withOtherReleases} from './withOtherReleases.js';
 export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js';
+export {default as withPropertyFromMainRelease} from './withPropertyFromMainRelease.js';
+export {default as withSuffixDirectoryFromAlbum} from './withSuffixDirectoryFromAlbum.js';
+export {default as withTrackArtDate} from './withTrackArtDate.js';
+export {default as withTrackNumber} from './withTrackNumber.js';
diff --git a/src/data/composite/things/track/inferredAdditionalNameList.js b/src/data/composite/things/track/inferredAdditionalNameList.js
deleted file mode 100644
index 58e8d2a1..00000000
--- a/src/data/composite/things/track/inferredAdditionalNameList.js
+++ /dev/null
@@ -1,67 +0,0 @@
-// Infers additional name entries from other releases that were titled
-// differently; the corresponding releases are stored in eacn entry's "from"
-// array, which will include multiple items, if more than one other release
-// shares the same name differing from this one's.
-
-import {input, templateCompositeFrom} from '#composite';
-import {chunkByProperties} from '#sugar';
-
-import {exitWithoutDependency} from '#composite/control-flow';
-import {withFilteredList, withPropertyFromList} from '#composite/data';
-import {withThingsSortedAlphabetically} from '#composite/wiki-data';
-
-import withOtherReleases from './withOtherReleases.js';
-
-export default templateCompositeFrom({
-  annotation: `inferredAdditionalNameList`,
-
-  compose: false,
-
-  steps: () => [
-    withOtherReleases(),
-
-    exitWithoutDependency({
-      dependency: '#otherReleases',
-      mode: input.value('empty'),
-      value: input.value([]),
-    }),
-
-    withPropertyFromList({
-      list: '#otherReleases',
-      property: input.value('name'),
-    }),
-
-    {
-      dependencies: ['#otherReleases.name', 'name'],
-      compute: (continuation, {
-        ['#otherReleases.name']: releaseNames,
-        ['name']: ownName,
-      }) => continuation({
-        ['#nameFilter']:
-          releaseNames.map(name => name !== ownName),
-      }),
-    },
-
-    withFilteredList({
-      list: '#otherReleases',
-      filter: '#nameFilter',
-    }).outputs({
-      '#filteredList': '#differentlyNamedReleases',
-    }),
-
-    withThingsSortedAlphabetically({
-      things: '#differentlyNamedReleases',
-    }).outputs({
-      '#sortedThings': '#differentlyNamedReleases',
-    }),
-
-    {
-      dependencies: ['#differentlyNamedReleases'],
-      compute: ({
-        ['#differentlyNamedReleases']: releases,
-      }) =>
-        chunkByProperties(releases, ['name'])
-          .map(({name, chunk}) => ({name, from: chunk})),
-    },
-  ],
-});
diff --git a/src/data/composite/things/track/inheritContributionListFromMainRelease.js b/src/data/composite/things/track/inheritContributionListFromMainRelease.js
new file mode 100644
index 00000000..89252feb
--- /dev/null
+++ b/src/data/composite/things/track/inheritContributionListFromMainRelease.js
@@ -0,0 +1,44 @@
+// Like inheritFromMainRelease, but tuned for contributions.
+// Recontextualizes contributions for this track.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {exposeDependency, raiseOutputWithoutDependency}
+  from '#composite/control-flow';
+import {withRecontextualizedContributionList, withRedatedContributionList}
+  from '#composite/wiki-data';
+
+import withDate from './withDate.js';
+import withPropertyFromMainRelease
+  from './withPropertyFromMainRelease.js';
+
+export default templateCompositeFrom({
+  annotation: `inheritContributionListFromMainRelease`,
+
+  steps: () => [
+    withPropertyFromMainRelease({
+      property: input.thisProperty(),
+      notFoundValue: input.value([]),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#isSecondaryRelease',
+      mode: input.value('falsy'),
+    }),
+
+    withRecontextualizedContributionList({
+      list: '#mainReleaseValue',
+    }),
+
+    withDate(),
+
+    withRedatedContributionList({
+      list: '#mainReleaseValue',
+      date: '#date',
+    }),
+
+    exposeDependency({
+      dependency: '#mainReleaseValue',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/track/inheritFromMainRelease.js b/src/data/composite/things/track/inheritFromMainRelease.js
new file mode 100644
index 00000000..b1cbb65e
--- /dev/null
+++ b/src/data/composite/things/track/inheritFromMainRelease.js
@@ -0,0 +1,41 @@
+// Early exits with the value for the same property as specified on the
+// main release, if this track is a secondary release, and otherwise continues
+// without providing any further dependencies.
+//
+// Like withMainRelease, this will early exit (with notFoundValue) if the
+// main release is specified by reference and that reference doesn't
+// resolve to anything.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {exposeDependency, raiseOutputWithoutDependency}
+  from '#composite/control-flow';
+
+import withPropertyFromMainRelease
+  from './withPropertyFromMainRelease.js';
+
+export default templateCompositeFrom({
+  annotation: `inheritFromMainRelease`,
+
+  inputs: {
+    notFoundValue: input({
+      defaultValue: null,
+    }),
+  },
+
+  steps: () => [
+    withPropertyFromMainRelease({
+      property: input.thisProperty(),
+      notFoundValue: input('notFoundValue'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#isSecondaryRelease',
+      mode: input.value('falsy'),
+    }),
+
+    exposeDependency({
+      dependency: '#mainReleaseValue',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/track/inheritFromOriginalRelease.js b/src/data/composite/things/track/inheritFromOriginalRelease.js
deleted file mode 100644
index 27ed1387..00000000
--- a/src/data/composite/things/track/inheritFromOriginalRelease.js
+++ /dev/null
@@ -1,50 +0,0 @@
-// Early exits with a value inherited from the original release, if
-// this track is a rerelease, and otherwise continues with no further
-// dependencies provided. If allowOverride is true, then the continuation
-// will also be called if the original release exposed the requested
-// property as null.
-//
-// Like withOriginalRelease, this will early exit (with notFoundValue) if the
-// original release is specified by reference and that reference doesn't
-// resolve to anything.
-
-import {input, templateCompositeFrom} from '#composite';
-
-import withOriginalRelease from './withOriginalRelease.js';
-
-export default templateCompositeFrom({
-  annotation: `inheritFromOriginalRelease`,
-
-  inputs: {
-    property: input({type: 'string'}),
-    allowOverride: input({type: 'boolean', defaultValue: false}),
-    notFoundValue: input({defaultValue: null}),
-  },
-
-  steps: () => [
-    withOriginalRelease({
-      notFoundValue: input('notFoundValue'),
-    }),
-
-    {
-      dependencies: [
-        '#originalRelease',
-        input('property'),
-        input('allowOverride'),
-      ],
-
-      compute: (continuation, {
-        ['#originalRelease']: originalRelease,
-        [input('property')]: originalProperty,
-        [input('allowOverride')]: allowOverride,
-      }) => {
-        if (!originalRelease) return continuation();
-
-        const value = originalRelease[originalProperty];
-        if (allowOverride && value === null) return continuation();
-
-        return continuation.exit(value);
-      },
-    },
-  ],
-});
diff --git a/src/data/composite/things/track/sharedAdditionalNameList.js b/src/data/composite/things/track/sharedAdditionalNameList.js
deleted file mode 100644
index 1806ec80..00000000
--- a/src/data/composite/things/track/sharedAdditionalNameList.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Compiles additional names directly provided by other releases.
-
-import {input, templateCompositeFrom} from '#composite';
-
-import {exitWithoutDependency, exposeDependency}
-  from '#composite/control-flow';
-import {withFlattenedList, withPropertyFromList} from '#composite/data';
-
-import withOtherReleases from './withOtherReleases.js';
-
-export default templateCompositeFrom({
-  annotation: `sharedAdditionalNameList`,
-
-  compose: false,
-
-  steps: () => [
-    withOtherReleases(),
-
-    exitWithoutDependency({
-      dependency: '#otherReleases',
-      mode: input.value('empty'),
-      value: input.value([]),
-    }),
-
-    withPropertyFromList({
-      list: '#otherReleases',
-      property: input.value('additionalNames'),
-    }),
-
-    withFlattenedList({
-      list: '#otherReleases.additionalNames',
-    }),
-
-    exposeDependency({
-      dependency: '#flattenedList',
-    }),
-  ],
-});
diff --git a/src/data/composite/things/track/trackReverseReferenceList.js b/src/data/composite/things/track/trackReverseReferenceList.js
deleted file mode 100644
index 44940ae7..00000000
--- a/src/data/composite/things/track/trackReverseReferenceList.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Like a normal reverse reference list ("objects which reference this object
-// under a specified property"), only excluding rereleases from the possible
-// outputs. While it's useful to travel from a rerelease to the tracks it
-// references, rereleases aren't generally relevant from the perspective of
-// the tracks *being* referenced. Apart from hiding rereleases from lists on
-// the site, it also excludes keeps them from relational data processing, such
-// as on the "Tracks - by Times Referenced" listing page.
-
-import {input, templateCompositeFrom} from '#composite';
-import {withReverseReferenceList} from '#composite/wiki-data';
-
-export default templateCompositeFrom({
-  annotation: `trackReverseReferenceList`,
-
-  compose: false,
-
-  inputs: {
-    list: input({type: 'string'}),
-  },
-
-  steps: () => [
-    withReverseReferenceList({
-      data: 'trackData',
-      list: input('list'),
-    }),
-
-    {
-      flags: {expose: true},
-      expose: {
-        dependencies: ['#reverseReferenceList'],
-        compute: ({
-          ['#reverseReferenceList']: reverseReferenceList,
-        }) =>
-          reverseReferenceList.filter(track => !track.originalReleaseTrack),
-      },
-    },
-  ],
-});
diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js
deleted file mode 100644
index 03b840d4..00000000
--- a/src/data/composite/things/track/withAlbum.js
+++ /dev/null
@@ -1,22 +0,0 @@
-// Gets the track's album. This will early exit if albumData is missing.
-// If there's no album whose list of tracks includes this track, the output
-// dependency will be null.
-
-import {input, templateCompositeFrom} from '#composite';
-
-import {withUniqueReferencingThing} from '#composite/wiki-data';
-
-export default templateCompositeFrom({
-  annotation: `withAlbum`,
-
-  outputs: ['#album'],
-
-  steps: () => [
-    withUniqueReferencingThing({
-      data: 'albumData',
-      list: input.value('tracks'),
-    }).outputs({
-      ['#uniqueReferencingThing']: '#album',
-    }),
-  ],
-});
diff --git a/src/data/composite/things/track/withAllReleases.js b/src/data/composite/things/track/withAllReleases.js
new file mode 100644
index 00000000..b93bf753
--- /dev/null
+++ b/src/data/composite/things/track/withAllReleases.js
@@ -0,0 +1,47 @@
+// Gets all releases of the current track. All items of the outputs are
+// distinct Track objects; one track is the main release; all else are
+// secondary releases of that main release; and one item, which may be
+// the main release or one of the secondary releases, is the current
+// track. The results are sorted by date, and it is possible that the
+// main release is not actually the earliest/first.
+
+import {input, templateCompositeFrom} from '#composite';
+import {sortByDate} from '#sort';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+import withMainRelease from './withMainRelease.js';
+
+export default templateCompositeFrom({
+  annotation: `withAllReleases`,
+
+  outputs: ['#allReleases'],
+
+  steps: () => [
+    withMainRelease({
+      selfIfMain: input.value(true),
+      notFoundValue: input.value([]),
+    }),
+
+    // We don't talk about bruno no no
+    // Yes, this can perform a normal access equivalent to
+    // `this.secondaryReleases` from within a data composition.
+    // Oooooooooooooooooooooooooooooooooooooooooooooooo
+    withPropertyFromObject({
+      object: '#mainRelease',
+      property: input.value('secondaryReleases'),
+    }),
+
+    {
+      dependencies: ['#mainRelease', '#mainRelease.secondaryReleases'],
+      compute: (continuation, {
+        ['#mainRelease']: mainRelease,
+        ['#mainRelease.secondaryReleases']: secondaryReleases,
+      }) => continuation({
+        ['#allReleases']:
+          sortByDate([mainRelease, ...secondaryReleases]),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
index fac8e213..60faeaf4 100644
--- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
+++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
@@ -7,10 +7,17 @@ import {input, templateCompositeFrom} from '#composite';
 import find from '#find';
 import {isBoolean} from '#validators';
 
-import {exitWithoutDependency, exposeUpdateValueOrContinue}
-  from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
 import {withResolvedReference} from '#composite/wiki-data';
+import {soupyFind} from '#composite/wiki-properties';
+
+import {
+  exitWithoutDependency,
+  exposeDependencyOrContinue,
+  exposeUpdateValueOrContinue,
+} from '#composite/control-flow';
+
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
 
 export default templateCompositeFrom({
   annotation: `withAlwaysReferenceByDirectory`,
@@ -22,9 +29,20 @@ export default templateCompositeFrom({
       validate: input.value(isBoolean),
     }),
 
+    withPropertyFromAlbum({
+      property: input.value('alwaysReferenceTracksByDirectory'),
+    }),
+
+    // Falsy mode means this exposes true if the album's property is true,
+    // but continues if the property is false (which is also the default).
+    exposeDependencyOrContinue({
+      dependency: '#album.alwaysReferenceTracksByDirectory',
+      mode: input.value('falsy'),
+    }),
+
     // Remaining code is for defaulting to true if this track is a rerelease of
     // another with the same name, so everything further depends on access to
-    // trackData as well as originalReleaseTrack.
+    // trackData as well as mainReleaseTrack.
 
     exitWithoutDependency({
       dependency: 'trackData',
@@ -33,45 +51,46 @@ export default templateCompositeFrom({
     }),
 
     exitWithoutDependency({
-      dependency: 'originalReleaseTrack',
+      dependency: 'mainReleaseTrack',
       value: input.value(false),
     }),
 
-    // It's necessary to use the custom trackOriginalReleasesOnly find function
+    // It's necessary to use the custom trackMainReleasesOnly find function
     // here, so as to avoid recursion issues - the find.track() function depends
     // on accessing each track's alwaysReferenceByDirectory, which means it'll
     // hit *this track* - and thus this step - and end up recursing infinitely.
-    // By definition, find.trackOriginalReleasesOnly excludes tracks which have
-    // an originalReleaseTrack update value set, which means even though it does
+    // By definition, find.trackMainReleasesOnly excludes tracks which have
+    // an mainReleaseTrack update value set, which means even though it does
     // still access each of tracks' `alwaysReferenceByDirectory` property, it
     // won't access that of *this* track - it will never proceed past the
     // `exitWithoutDependency` step directly above, so there's no opportunity
     // for recursion.
     withResolvedReference({
-      ref: 'originalReleaseTrack',
+      ref: 'mainReleaseTrack',
       data: 'trackData',
-      find: input.value(find.trackOriginalReleasesOnly),
+      find: input.value(find.trackMainReleasesOnly),
     }).outputs({
-      '#resolvedReference': '#originalRelease',
+      '#resolvedReference': '#mainRelease',
     }),
 
     exitWithoutDependency({
-      dependency: '#originalRelease',
+      dependency: '#mainRelease',
       value: input.value(false),
     }),
 
     withPropertyFromObject({
-      object: '#originalRelease',
+      object: '#mainRelease',
       property: input.value('name'),
     }),
 
     {
-      dependencies: ['name', '#originalRelease.name'],
+      dependencies: ['name', '#mainRelease.name'],
       compute: (continuation, {
         name,
-        ['#originalRelease.name']: originalName,
+        ['#mainRelease.name']: mainReleaseName,
       }) => continuation({
-        ['#alwaysReferenceByDirectory']: name === originalName,
+        ['#alwaysReferenceByDirectory']:
+          name === mainReleaseName,
       }),
     },
   ],
diff --git a/src/data/composite/things/track/withContainingTrackSection.js b/src/data/composite/things/track/withContainingTrackSection.js
index eaac14de..3d4d081e 100644
--- a/src/data/composite/things/track/withContainingTrackSection.js
+++ b/src/data/composite/things/track/withContainingTrackSection.js
@@ -1,11 +1,9 @@
 // Gets the track section containing this track from its album's track list.
 
-import {input, templateCompositeFrom} from '#composite';
-import {is} from '#validators';
+import {templateCompositeFrom} from '#composite';
 
-import {raiseOutputWithoutDependency} from '#composite/control-flow';
-
-import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+import {withUniqueReferencingThing} from '#composite/wiki-data';
+import {soupyReverse} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
   annotation: `withContainingTrackSection`,
@@ -13,30 +11,10 @@ export default templateCompositeFrom({
   outputs: ['#trackSection'],
 
   steps: () => [
-    withPropertyFromAlbum({
-      property: input.value('trackSections'),
-    }),
-
-    raiseOutputWithoutDependency({
-      dependency: '#album.trackSections',
-      output: input.value({'#trackSection': null}),
+    withUniqueReferencingThing({
+      reverse: soupyReverse.input('trackSectionsWhichInclude'),
+    }).outputs({
+      ['#uniqueReferencingThing']: '#trackSection',
     }),
-
-    {
-      dependencies: [
-        input.myself(),
-        '#album.trackSections',
-      ],
-
-      compute: (continuation, {
-        [input.myself()]: track,
-        [input('notFoundMode')]: notFoundMode,
-        ['#album.trackSections']: trackSections,
-      }) => continuation({
-        ['#trackSection']:
-          trackSections.find(({tracks}) => tracks.includes(track))
-            ?? null,
-      }),
-    },
   ],
 });
diff --git a/src/data/composite/things/track/withCoverArtistContribs.js b/src/data/composite/things/track/withCoverArtistContribs.js
new file mode 100644
index 00000000..9057cfeb
--- /dev/null
+++ b/src/data/composite/things/track/withCoverArtistContribs.js
@@ -0,0 +1,73 @@
+import {input, templateCompositeFrom} from '#composite';
+import {isContributionList} from '#validators';
+
+import {exposeDependencyOrContinue} from '#composite/control-flow';
+
+import {
+  withRecontextualizedContributionList,
+  withRedatedContributionList,
+  withResolvedContribs,
+} from '#composite/wiki-data';
+
+import exitWithoutUniqueCoverArt from './exitWithoutUniqueCoverArt.js';
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+import withTrackArtDate from './withTrackArtDate.js';
+
+export default templateCompositeFrom({
+  annotation: `withCoverArtistContribs`,
+
+  inputs: {
+    from: input({
+      defaultDependency: 'coverArtistContribs',
+      validate: isContributionList,
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#coverArtistContribs'],
+
+  steps: () => [
+    exitWithoutUniqueCoverArt({
+      value: input.value([]),
+    }),
+
+    withTrackArtDate(),
+
+    withResolvedContribs({
+      from: input('from'),
+      thingProperty: input.value('coverArtistContribs'),
+      artistProperty: input.value('trackCoverArtistContributions'),
+      date: '#trackArtDate',
+    }).outputs({
+      '#resolvedContribs': '#coverArtistContribs',
+    }),
+
+    exposeDependencyOrContinue({
+      dependency: '#coverArtistContribs',
+      mode: input.value('empty'),
+    }),
+
+    withPropertyFromAlbum({
+      property: input.value('trackCoverArtistContribs'),
+    }),
+
+    withRecontextualizedContributionList({
+      list: '#album.trackCoverArtistContribs',
+      artistProperty: input.value('trackCoverArtistContributions'),
+    }),
+
+    withRedatedContributionList({
+      list: '#album.trackCoverArtistContribs',
+      date: '#trackArtDate',
+    }),
+
+    {
+      dependencies: ['#album.trackCoverArtistContribs'],
+      compute: (continuation, {
+        ['#album.trackCoverArtistContribs']: coverArtistContribs,
+      }) => continuation({
+        ['#coverArtistContribs']: coverArtistContribs,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withDate.js b/src/data/composite/things/track/withDate.js
new file mode 100644
index 00000000..b5a770e9
--- /dev/null
+++ b/src/data/composite/things/track/withDate.js
@@ -0,0 +1,34 @@
+// Gets the track's own date. This is either its dateFirstReleased property
+// or, if unset, the album's date.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: `withDate`,
+
+  outputs: ['#date'],
+
+  steps: () => [
+    {
+      dependencies: ['dateFirstReleased'],
+      compute: (continuation, {dateFirstReleased}) =>
+        (dateFirstReleased
+          ? continuation.raiseOutput({'#date': dateFirstReleased})
+          : continuation()),
+    },
+
+    withPropertyFromAlbum({
+      property: input.value('date'),
+    }),
+
+    {
+      dependencies: ['#album.date'],
+      compute: (continuation, {['#album.date']: albumDate}) =>
+        (albumDate
+          ? continuation.raiseOutput({'#date': albumDate})
+          : continuation.raiseOutput({'#date': null})),
+    },
+  ],
+})
diff --git a/src/data/composite/things/track/withDirectorySuffix.js b/src/data/composite/things/track/withDirectorySuffix.js
new file mode 100644
index 00000000..c063e158
--- /dev/null
+++ b/src/data/composite/things/track/withDirectorySuffix.js
@@ -0,0 +1,36 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+import withSuffixDirectoryFromAlbum from './withSuffixDirectoryFromAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: `withDirectorySuffix`,
+
+  outputs: ['#directorySuffix'],
+
+  steps: () => [
+    withSuffixDirectoryFromAlbum(),
+
+    raiseOutputWithoutDependency({
+      dependency: '#suffixDirectoryFromAlbum',
+      mode: input.value('falsy'),
+      output: input.value({['#directorySuffix']: null}),
+    }),
+
+    withPropertyFromAlbum({
+      property: input.value('directorySuffix'),
+    }),
+
+    {
+      dependencies: ['#album.directorySuffix'],
+      compute: (continuation, {
+        ['#album.directorySuffix']: directorySuffix,
+      }) => continuation({
+        ['#directorySuffix']:
+          directorySuffix,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js
index 96078d5f..85d3b92a 100644
--- a/src/data/composite/things/track/withHasUniqueCoverArt.js
+++ b/src/data/composite/things/track/withHasUniqueCoverArt.js
@@ -5,11 +5,18 @@
 // or a placeholder. (This property is named hasUniqueCoverArt instead of
 // the usual hasCoverArt to emphasize that it does not inherit from the
 // album.)
+//
+// withHasUniqueCoverArt is based only around the presence of *specified*
+// cover artist contributions, not whether the references to artists on those
+// contributions actually resolve to anything. It completely evades interacting
+// with find/replace.
 
 import {input, templateCompositeFrom} from '#composite';
-import {empty} from '#sugar';
 
-import {withResolvedContribs} from '#composite/wiki-data';
+import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
+import {fillMissingListItems, withFlattenedList, withPropertyFromList}
+  from '#composite/data';
 
 import withPropertyFromAlbum from './withPropertyFromAlbum.js';
 
@@ -29,33 +36,73 @@ export default templateCompositeFrom({
           : continuation()),
     },
 
-    withResolvedContribs({from: 'coverArtistContribs'}),
+    withResultOfAvailabilityCheck({
+      from: 'coverArtistContribs',
+      mode: input.value('empty'),
+    }),
 
     {
-      dependencies: ['#resolvedContribs'],
+      dependencies: ['#availability'],
       compute: (continuation, {
-        ['#resolvedContribs']: contribsFromTrack,
+        ['#availability']: availability,
       }) =>
-        (empty(contribsFromTrack)
-          ? continuation()
-          : continuation.raiseOutput({
+        (availability
+          ? continuation.raiseOutput({
               ['#hasUniqueCoverArt']: true,
-            })),
+            })
+          : continuation()),
     },
 
     withPropertyFromAlbum({
       property: input.value('trackCoverArtistContribs'),
+      internal: input.value(true),
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#album.trackCoverArtistContribs',
+      mode: input.value('empty'),
     }),
 
     {
-      dependencies: ['#album.trackCoverArtistContribs'],
+      dependencies: ['#availability'],
       compute: (continuation, {
-        ['#album.trackCoverArtistContribs']: contribsFromAlbum,
+        ['#availability']: availability,
       }) =>
-        continuation.raiseOutput({
-          ['#hasUniqueCoverArt']:
-            !empty(contribsFromAlbum),
-        }),
+        (availability
+          ? continuation.raiseOutput({
+              ['#hasUniqueCoverArt']: true,
+            })
+          : continuation()),
     },
+
+    raiseOutputWithoutDependency({
+      dependency: 'trackArtworks',
+      mode: input.value('empty'),
+      output: input.value({'#hasUniqueCoverArt': false}),
+    }),
+
+    withPropertyFromList({
+      list: 'trackArtworks',
+      property: input.value('artistContribs'),
+      internal: input.value(true),
+    }),
+
+    // Since we're getting the update value for each artwork's artistContribs,
+    // it may not be set at all, and in that case won't be exposing as [].
+    fillMissingListItems({
+      list: '#trackArtworks.artistContribs',
+      fill: input.value([]),
+    }),
+
+    withFlattenedList({
+      list: '#trackArtworks.artistContribs',
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#flattenedList',
+      mode: input.value('empty'),
+    }).outputs({
+      '#availability': '#hasUniqueCoverArt',
+    }),
   ],
 });
diff --git a/src/data/composite/things/track/withOriginalRelease.js b/src/data/composite/things/track/withMainRelease.js
index c7f49657..3a91edae 100644
--- a/src/data/composite/things/track/withOriginalRelease.js
+++ b/src/data/composite/things/track/withMainRelease.js
@@ -1,62 +1,54 @@
-// Just includes the original release of this track as a dependency.
-// If this track isn't a rerelease, then it'll provide null, unless the
-// {selfIfOriginal} option is set, in which case it'll provide this track
-// itself. This will early exit (with notFoundValue) if the original release
+// Just includes the main release of this track as a dependency.
+// If this track isn't a secondary release, then it'll provide null, unless
+// the {selfIfMain} option is set, in which case it'll provide this track
+// itself. This will early exit (with notFoundValue) if the main release
 // is specified by reference and that reference doesn't resolve to anything.
 
 import {input, templateCompositeFrom} from '#composite';
-import find from '#find';
-import {validateWikiData} from '#validators';
 
 import {exitWithoutDependency, withResultOfAvailabilityCheck}
   from '#composite/control-flow';
 import {withResolvedReference} from '#composite/wiki-data';
+import {soupyFind} from '#composite/wiki-properties';
 
 export default templateCompositeFrom({
-  annotation: `withOriginalRelease`,
+  annotation: `withMainRelease`,
 
   inputs: {
-    selfIfOriginal: input({type: 'boolean', defaultValue: false}),
-
-    data: input({
-      validate: validateWikiData({referenceType: 'track'}),
-      defaultDependency: 'trackData',
-    }),
-
+    selfIfMain: input({type: 'boolean', defaultValue: false}),
     notFoundValue: input({defaultValue: null}),
   },
 
-  outputs: ['#originalRelease'],
+  outputs: ['#mainRelease'],
 
   steps: () => [
     withResultOfAvailabilityCheck({
-      from: 'originalReleaseTrack',
+      from: 'mainReleaseTrack',
     }),
 
     {
       dependencies: [
         input.myself(),
-        input('selfIfOriginal'),
+        input('selfIfMain'),
         '#availability',
       ],
 
       compute: (continuation, {
         [input.myself()]: track,
-        [input('selfIfOriginal')]: selfIfOriginal,
+        [input('selfIfMain')]: selfIfMain,
         '#availability': availability,
       }) =>
         (availability
           ? continuation()
           : continuation.raiseOutput({
-              ['#originalRelease']:
-                (selfIfOriginal ? track : null),
+              ['#mainRelease']:
+                (selfIfMain ? track : null),
             })),
     },
 
     withResolvedReference({
-      ref: 'originalReleaseTrack',
-      data: input('data'),
-      find: input.value(find.track),
+      ref: 'mainReleaseTrack',
+      find: soupyFind.input('track'),
     }),
 
     exitWithoutDependency({
@@ -71,7 +63,7 @@ export default templateCompositeFrom({
         ['#resolvedReference']: resolvedReference,
       }) =>
         continuation({
-          ['#originalRelease']: resolvedReference,
+          ['#mainRelease']: resolvedReference,
         }),
     },
   ],
diff --git a/src/data/composite/things/track/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js
index f8c1c3f0..0639742f 100644
--- a/src/data/composite/things/track/withOtherReleases.js
+++ b/src/data/composite/things/track/withOtherReleases.js
@@ -1,8 +1,12 @@
+// Gets all releases of the current track *except* this track itself;
+// in other words, all other releases of the current track.
+
 import {input, templateCompositeFrom} from '#composite';
 
 import {exitWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
 
-import withOriginalRelease from './withOriginalRelease.js';
+import withAllReleases from './withAllReleases.js';
 
 export default templateCompositeFrom({
   annotation: `withOtherReleases`,
@@ -10,31 +14,16 @@ export default templateCompositeFrom({
   outputs: ['#otherReleases'],
 
   steps: () => [
-    exitWithoutDependency({
-      dependency: 'trackData',
-      mode: input.value('empty'),
-    }),
-
-    withOriginalRelease({
-      selfIfOriginal: input.value(true),
-      notFoundValue: input.value([]),
-    }),
+    withAllReleases(),
 
     {
-      dependencies: [input.myself(), '#originalRelease', 'trackData'],
+      dependencies: [input.myself(), '#allReleases'],
       compute: (continuation, {
         [input.myself()]: thisTrack,
-        ['#originalRelease']: originalRelease,
-        trackData,
+        ['#allReleases']: allReleases,
       }) => continuation({
         ['#otherReleases']:
-          (originalRelease === thisTrack
-            ? []
-            : [originalRelease])
-            .concat(trackData.filter(track =>
-              track !== originalRelease &&
-              track !== thisTrack &&
-              track.originalReleaseTrack === originalRelease)),
+          allReleases.filter(track => track !== thisTrack),
       }),
     },
   ],
diff --git a/src/data/composite/things/track/withPropertyFromAlbum.js b/src/data/composite/things/track/withPropertyFromAlbum.js
index d41390fa..a203c2e7 100644
--- a/src/data/composite/things/track/withPropertyFromAlbum.js
+++ b/src/data/composite/things/track/withPropertyFromAlbum.js
@@ -2,17 +2,15 @@
 // property name prefixed with '#album.' (by default).
 
 import {input, templateCompositeFrom} from '#composite';
-import {is} from '#validators';
 
 import {withPropertyFromObject} from '#composite/data';
 
-import withAlbum from './withAlbum.js';
-
 export default templateCompositeFrom({
   annotation: `withPropertyFromAlbum`,
 
   inputs: {
     property: input.staticValue({type: 'string'}),
+    internal: input({type: 'boolean', defaultValue: false}),
   },
 
   outputs: ({
@@ -20,11 +18,21 @@ export default templateCompositeFrom({
   }) => ['#album.' + property],
 
   steps: () => [
-    withAlbum(),
+    // XXX: This is a ridiculous hack considering `defaultValue` above.
+    // If we were certain what was up, we'd just get around to fixing it LOL
+    {
+      dependencies: [input('internal')],
+      compute: (continuation, {
+        [input('internal')]: internal,
+      }) => continuation({
+        ['#internal']: internal ?? false,
+      }),
+    },
 
     withPropertyFromObject({
-      object: '#album',
+      object: 'album',
       property: input('property'),
+      internal: '#internal',
     }),
 
     {
diff --git a/src/data/composite/things/track/withPropertyFromMainRelease.js b/src/data/composite/things/track/withPropertyFromMainRelease.js
new file mode 100644
index 00000000..393a4c63
--- /dev/null
+++ b/src/data/composite/things/track/withPropertyFromMainRelease.js
@@ -0,0 +1,86 @@
+// Provides a value inherited from the main release, if applicable, and a
+// flag indicating if this track is a secondary release or not.
+//
+// Like withMainRelease, this will early exit (with notFoundValue) if the
+// main release is specified by reference and that reference doesn't
+// resolve to anything.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+import withMainRelease from './withMainRelease.js';
+
+export default templateCompositeFrom({
+  annotation: `inheritFromMainRelease`,
+
+  inputs: {
+    property: input({type: 'string'}),
+
+    notFoundValue: input({
+      defaultValue: null,
+    }),
+  },
+
+  outputs: ({
+    [input.staticValue('property')]: property,
+  }) =>
+    ['#isSecondaryRelease'].concat(
+      (property
+        ? ['#mainRelease.' + property]
+        : ['#mainReleaseValue'])),
+
+  steps: () => [
+    withMainRelease({
+      notFoundValue: input('notFoundValue'),
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#mainRelease',
+    }),
+
+    {
+      dependencies: [
+        '#availability',
+        input.staticValue('property'),
+      ],
+
+      compute: (continuation, {
+        ['#availability']: availability,
+        [input.staticValue('property')]: property,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.raiseOutput(
+              Object.assign(
+                {'#isSecondaryRelease': false},
+                (property
+                  ? {['#mainRelease.' + property]: null}
+                  : {'#mainReleaseValue': null})))),
+    },
+
+    withPropertyFromObject({
+      object: '#mainRelease',
+      property: input('property'),
+    }),
+
+    {
+      dependencies: [
+        '#value',
+        input.staticValue('property'),
+      ],
+
+      compute: (continuation, {
+        ['#value']: value,
+        [input.staticValue('property')]: property,
+      }) =>
+        continuation.raiseOutput(
+          Object.assign(
+            {'#isSecondaryRelease': true},
+            (property
+              ? {['#mainRelease.' + property]: value}
+              : {'#mainReleaseValue': value}))),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
new file mode 100644
index 00000000..7159a3f4
--- /dev/null
+++ b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
@@ -0,0 +1,53 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: `withSuffixDirectoryFromAlbum`,
+
+  inputs: {
+    flagValue: input({
+      defaultDependency: 'suffixDirectoryFromAlbum',
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#suffixDirectoryFromAlbum'],
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: 'suffixDirectoryFromAlbum',
+    }),
+
+    {
+      dependencies: [
+        '#availability',
+        'suffixDirectoryFromAlbum'
+      ],
+
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['suffixDirectoryFromAlbum']: flagValue,
+      }) =>
+        (availability
+          ? continuation.raiseOutput({['#suffixDirectoryFromAlbum']: flagValue})
+          : continuation()),
+    },
+
+    withPropertyFromAlbum({
+      property: input.value('suffixTrackDirectories'),
+    }),
+
+    {
+      dependencies: ['#album.suffixTrackDirectories'],
+      compute: (continuation, {
+        ['#album.suffixTrackDirectories']: suffixTrackDirectories,
+      }) => continuation({
+        ['#suffixDirectoryFromAlbum']:
+          suffixTrackDirectories,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withTrackArtDate.js b/src/data/composite/things/track/withTrackArtDate.js
new file mode 100644
index 00000000..9b7b61c7
--- /dev/null
+++ b/src/data/composite/things/track/withTrackArtDate.js
@@ -0,0 +1,60 @@
+import {input, templateCompositeFrom} from '#composite';
+import {isDate} from '#validators';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+import withDate from './withDate.js';
+import withHasUniqueCoverArt from './withHasUniqueCoverArt.js';
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: `withTrackArtDate`,
+
+  inputs: {
+    from: input({
+      validate: isDate,
+      defaultDependency: 'coverArtDate',
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#trackArtDate'],
+
+  steps: () => [
+    withHasUniqueCoverArt(),
+
+    raiseOutputWithoutDependency({
+      dependency: '#hasUniqueCoverArt',
+      mode: input.value('falsy'),
+      output: input.value({'#trackArtDate': null}),
+    }),
+
+    {
+      dependencies: [input('from')],
+      compute: (continuation, {
+        [input('from')]: from,
+      }) =>
+        (from
+          ? continuation.raiseOutput({'#trackArtDate': from})
+          : continuation()),
+    },
+
+    withPropertyFromAlbum({
+      property: input.value('trackArtDate'),
+    }),
+
+    {
+      dependencies: ['#album.trackArtDate'],
+      compute: (continuation, {
+        ['#album.trackArtDate']: albumTrackArtDate,
+      }) =>
+        (albumTrackArtDate
+          ? continuation.raiseOutput({'#trackArtDate': albumTrackArtDate})
+          : continuation()),
+    },
+
+    withDate().outputs({
+      '#date': '#trackArtDate',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/track/withTrackNumber.js b/src/data/composite/things/track/withTrackNumber.js
new file mode 100644
index 00000000..61428e8c
--- /dev/null
+++ b/src/data/composite/things/track/withTrackNumber.js
@@ -0,0 +1,50 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withIndexInList, withPropertiesFromObject} from '#composite/data';
+
+import withContainingTrackSection from './withContainingTrackSection.js';
+
+export default templateCompositeFrom({
+  annotation: `withTrackNumber`,
+
+  outputs: ['#trackNumber'],
+
+  steps: () => [
+    withContainingTrackSection(),
+
+    // Zero is the fallback, not one, but in most albums the first track
+    // (and its intended output by this composition) will be one.
+    raiseOutputWithoutDependency({
+      dependency: '#trackSection',
+      output: input.value({'#trackNumber': 0}),
+    }),
+
+    withPropertiesFromObject({
+      object: '#trackSection',
+      properties: input.value(['tracks', 'startCountingFrom']),
+    }),
+
+    withIndexInList({
+      list: '#trackSection.tracks',
+      item: input.myself(),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#index',
+      output: input.value({'#trackNumber': 0}),
+    }),
+
+    {
+      dependencies: ['#trackSection.startCountingFrom', '#index'],
+      compute: (continuation, {
+        ['#trackSection.startCountingFrom']: startCountingFrom,
+        ['#index']: index,
+      }) => continuation({
+        ['#trackNumber']:
+          startCountingFrom +
+          index,
+      }),
+    },
+  ],
+});