« 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.js1
-rw-r--r--src/data/composite/things/album/withCoverArtDate.js50
-rw-r--r--src/data/composite/things/artwork/index.js7
-rw-r--r--src/data/composite/things/artwork/withArtTags.js99
-rw-r--r--src/data/composite/things/artwork/withAttachedArtwork.js43
-rw-r--r--src/data/composite/things/artwork/withContainingArtworkList.js46
-rw-r--r--src/data/composite/things/artwork/withContentWarningArtTags.js27
-rw-r--r--src/data/composite/things/artwork/withContribsFromAttachedArtwork.js27
-rw-r--r--src/data/composite/things/artwork/withDate.js41
-rw-r--r--src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js65
-rw-r--r--src/data/composite/things/content/contentArtists.js40
-rw-r--r--src/data/composite/things/content/hasAnnotationPart.js25
-rw-r--r--src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js61
-rw-r--r--src/data/composite/things/content/index.js7
-rw-r--r--src/data/composite/things/content/withAnnotationParts.js103
-rw-r--r--src/data/composite/things/content/withHasAnnotationPart.js43
-rw-r--r--src/data/composite/things/content/withSourceText.js53
-rw-r--r--src/data/composite/things/content/withSourceURLs.js62
-rw-r--r--src/data/composite/things/content/withWebArchiveDate.js41
-rw-r--r--src/data/composite/things/contribution/index.js2
-rw-r--r--src/data/composite/things/contribution/thingPropertyMatches.js33
-rw-r--r--src/data/composite/things/contribution/thingReferenceTypeMatches.js39
-rw-r--r--src/data/composite/things/contribution/withContainingReverseContributionList.js46
-rw-r--r--src/data/composite/things/track-section/index.js2
-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.js3
-rw-r--r--src/data/composite/things/track/trackAdditionalNameList.js38
-rw-r--r--src/data/composite/things/track/withAlbum.js22
-rw-r--r--src/data/composite/things/track/withAllReleases.js1
-rw-r--r--src/data/composite/things/track/withAlwaysReferenceByDirectory.js16
-rw-r--r--src/data/composite/things/track/withCoverArtistContribs.js73
-rw-r--r--src/data/composite/things/track/withHasUniqueCoverArt.js76
-rw-r--r--src/data/composite/things/track/withOtherReleases.js3
-rw-r--r--src/data/composite/things/track/withPropertyFromAlbum.js17
-rw-r--r--src/data/composite/things/track/withTrackArtDate.js24
-rw-r--r--src/data/composite/things/track/withTrackNumber.js50
37 files changed, 1178 insertions, 197 deletions
diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js
index 8b5098f0..de1d37c3 100644
--- a/src/data/composite/things/album/index.js
+++ b/src/data/composite/things/album/index.js
@@ -1 +1,2 @@
+export {default as withCoverArtDate} from './withCoverArtDate.js';
 export {default as withTracks} from './withTracks.js';
diff --git a/src/data/composite/things/album/withCoverArtDate.js b/src/data/composite/things/album/withCoverArtDate.js
new file mode 100644
index 00000000..978f566a
--- /dev/null
+++ b/src/data/composite/things/album/withCoverArtDate.js
@@ -0,0 +1,50 @@
+import {input, templateCompositeFrom} from '#composite';
+import {isDate} from '#validators';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withHasArtwork} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withCoverArtDate`,
+
+  inputs: {
+    from: input({
+      validate: isDate,
+      defaultDependency: 'coverArtDate',
+      acceptsNull: true,
+    }),
+  },
+
+  outputs: ['#coverArtDate'],
+
+  steps: () => [
+    withHasArtwork({
+      contribs: 'coverArtistContribs',
+      artworks: 'coverArtworks',
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#hasArtwork',
+      mode: input.value('falsy'),
+      output: input.value({'#coverArtDate': null}),
+    }),
+
+    {
+      dependencies: [input('from')],
+      compute: (continuation, {
+        [input('from')]: from,
+      }) =>
+        (from
+          ? continuation.raiseOutput({'#coverArtDate': from})
+          : continuation()),
+    },
+
+    {
+      dependencies: ['date'],
+      compute: (continuation, {date}) =>
+        (date
+          ? continuation({'#coverArtDate': date})
+          : continuation({'#coverArtDate': null})),
+    },
+  ],
+});
diff --git a/src/data/composite/things/artwork/index.js b/src/data/composite/things/artwork/index.js
new file mode 100644
index 00000000..b5e5e167
--- /dev/null
+++ b/src/data/composite/things/artwork/index.js
@@ -0,0 +1,7 @@
+export {default as withArtTags} from './withArtTags.js';
+export {default as withAttachedArtwork} from './withAttachedArtwork.js';
+export {default as withContainingArtworkList} from './withContainingArtworkList.js';
+export {default as withContentWarningArtTags} from './withContentWarningArtTags.js';
+export {default as withContribsFromAttachedArtwork} from './withContribsFromAttachedArtwork.js';
+export {default as withDate} from './withDate.js';
+export {default as withPropertyFromAttachedArtwork} from './withPropertyFromAttachedArtwork.js';
diff --git a/src/data/composite/things/artwork/withArtTags.js b/src/data/composite/things/artwork/withArtTags.js
new file mode 100644
index 00000000..1fed3c31
--- /dev/null
+++ b/src/data/composite/things/artwork/withArtTags.js
@@ -0,0 +1,99 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+import {withResolvedReferenceList} from '#composite/wiki-data';
+import {soupyFind} from '#composite/wiki-properties';
+
+import withPropertyFromAttachedArtwork
+  from './withPropertyFromAttachedArtwork.js';
+
+export default templateCompositeFrom({
+  annotation: `withArtTags`,
+
+  inputs: {
+    from: input({
+      type: 'array',
+      acceptsNull: true,
+      defaultDependency: 'artTags',
+    }),
+  },
+
+  outputs: ['#artTags'],
+
+  steps: () => [
+    withResolvedReferenceList({
+      list: input('from'),
+      find: soupyFind.input('artTag'),
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#resolvedReferenceList',
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: ['#availability', '#resolvedReferenceList'],
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['#resolvedReferenceList']: resolvedReferenceList,
+      }) =>
+        (availability
+          ? continuation.raiseOutput({
+              '#artTags': resolvedReferenceList,
+            })
+          : continuation()),
+    },
+
+    withPropertyFromAttachedArtwork({
+      property: input.value('artTags'),
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#attachedArtwork.artTags',
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: ['#availability', '#attachedArtwork.artTags'],
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['#attachedArtwork.artTags']: attachedArtworkArtTags,
+      }) =>
+        (availability
+          ? continuation.raiseOutput({
+              '#artTags': attachedArtworkArtTags,
+            })
+          : continuation()),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: 'artTagsFromThingProperty',
+      output: input.value({'#artTags': []}),
+    }),
+
+    withPropertyFromObject({
+      object: 'thing',
+      property: 'artTagsFromThingProperty',
+    }).outputs({
+      ['#value']: '#thing.artTags',
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#thing.artTags',
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: ['#availability', '#thing.artTags'],
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['#thing.artTags']: thingArtTags,
+      }) =>
+        (availability
+          ? continuation({'#artTags': thingArtTags})
+          : continuation({'#artTags': []})),
+    },
+  ],
+});
diff --git a/src/data/composite/things/artwork/withAttachedArtwork.js b/src/data/composite/things/artwork/withAttachedArtwork.js
new file mode 100644
index 00000000..d7c0d87b
--- /dev/null
+++ b/src/data/composite/things/artwork/withAttachedArtwork.js
@@ -0,0 +1,43 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {flipFilter, raiseOutputWithoutDependency}
+  from '#composite/control-flow';
+import {withNearbyItemFromList, withPropertyFromList} from '#composite/data';
+
+import withContainingArtworkList from './withContainingArtworkList.js';
+
+export default templateCompositeFrom({
+  annotaion: `withContribsFromMainArtwork`,
+
+  outputs: ['#attachedArtwork'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'attachAbove',
+      mode: input.value('falsy'),
+      output: input.value({'#attachedArtwork': null}),
+    }),
+
+    withContainingArtworkList(),
+
+    withPropertyFromList({
+      list: '#containingArtworkList',
+      property: input.value('attachAbove'),
+    }),
+
+    flipFilter({
+      filter: '#containingArtworkList.attachAbove',
+    }).outputs({
+      '#containingArtworkList.attachAbove': '#filterNotAttached',
+    }),
+
+    withNearbyItemFromList({
+      list: '#containingArtworkList',
+      item: input.myself(),
+      offset: input.value(-1),
+      filter: '#filterNotAttached',
+    }).outputs({
+      '#nearbyItem': '#attachedArtwork',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/artwork/withContainingArtworkList.js b/src/data/composite/things/artwork/withContainingArtworkList.js
new file mode 100644
index 00000000..9c928ffd
--- /dev/null
+++ b/src/data/composite/things/artwork/withContainingArtworkList.js
@@ -0,0 +1,46 @@
+// Gets the list of artworks which contains this one, which is functionally
+// equivalent to `this.thing[this.thingProperty]`. If the exposed value is not
+// a list at all (i.e. the property holds a single artwork), this composition
+// outputs null.
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+export default templateCompositeFrom({
+  annotation: `withContainingArtworkList`,
+
+  outputs: ['#containingArtworkList'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'thing',
+      output: input.value({'#containingArtworkList': null}),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: 'thingProperty',
+      output: input.value({'#containingArtworkList': null}),
+    }),
+
+    withPropertyFromObject({
+      object: 'thing',
+      property: 'thingProperty',
+    }).outputs({
+      '#value': '#containingValue',
+    }),
+
+    {
+      dependencies: ['#containingValue'],
+      compute: (continuation, {
+        ['#containingValue']: containingValue,
+      }) => continuation({
+        ['#containingArtworkList']:
+          (Array.isArray(containingValue)
+            ? containingValue
+            : null),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/artwork/withContentWarningArtTags.js b/src/data/composite/things/artwork/withContentWarningArtTags.js
new file mode 100644
index 00000000..4c07e837
--- /dev/null
+++ b/src/data/composite/things/artwork/withContentWarningArtTags.js
@@ -0,0 +1,27 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {withFilteredList, withPropertyFromList} from '#composite/data';
+
+import withArtTags from './withArtTags.js';
+
+export default templateCompositeFrom({
+  annotation: `withContentWarningArtTags`,
+
+  outputs: ['#contentWarningArtTags'],
+
+  steps: () => [
+    withArtTags(),
+
+    withPropertyFromList({
+      list: '#artTags',
+      property: input.value('isContentWarning'),
+    }),
+
+    withFilteredList({
+      list: '#artTags',
+      filter: '#artTags.isContentWarning',
+    }).outputs({
+      '#filteredList': '#contentWarningArtTags',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
new file mode 100644
index 00000000..e9425c95
--- /dev/null
+++ b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
@@ -0,0 +1,27 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withRecontextualizedContributionList} from '#composite/wiki-data';
+
+import withPropertyFromAttachedArtwork from './withPropertyFromAttachedArtwork.js';
+
+export default templateCompositeFrom({
+  annotaion: `withContribsFromAttachedArtwork`,
+
+  outputs: ['#attachedArtwork.artistContribs'],
+
+  steps: () => [
+    withPropertyFromAttachedArtwork({
+      property: input.value('artistContribs'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#attachedArtwork.artistContribs',
+      output: input.value({'#attachedArtwork.artistContribs': null}),
+    }),
+
+    withRecontextualizedContributionList({
+      list: '#attachedArtwork.artistContribs',
+    }),
+  ],
+});
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/artwork/withPropertyFromAttachedArtwork.js b/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js
new file mode 100644
index 00000000..a2f954b9
--- /dev/null
+++ b/src/data/composite/things/artwork/withPropertyFromAttachedArtwork.js
@@ -0,0 +1,65 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+
+import withAttachedArtwork from './withAttachedArtwork.js';
+
+function getOutputName({
+  [input.staticValue('property')]: property,
+}) {
+  if (property) {
+    return `#attachedArtwork.${property}`;
+  } else {
+    return '#value';
+  }
+}
+
+export default templateCompositeFrom({
+  annotation: `withPropertyFromAttachedArtwork`,
+
+  inputs: {
+    property: input({type: 'string'}),
+  },
+
+  outputs: inputs => [getOutputName(inputs)],
+
+  steps: () => [
+    {
+      dependencies: [input.staticValue('property')],
+      compute: (continuation, inputs) =>
+        continuation({'#output': getOutputName(inputs)}),
+    },
+
+    withAttachedArtwork(),
+
+    withResultOfAvailabilityCheck({
+      from: '#attachedArtwork',
+    }),
+
+    {
+      dependencies: ['#availability', '#output'],
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['#output']: output,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.raiseOutput({[output]: null})),
+    },
+
+    withPropertyFromObject({
+      object: '#attachedArtwork',
+      property: input('property'),
+    }),
+
+    {
+      dependencies: ['#value', '#output'],
+      compute: (continuation, {
+        ['#value']: value,
+        ['#output']: output,
+      }) =>
+        continuation.raiseOutput({[output]: value}),
+    },
+  ],
+});
diff --git a/src/data/composite/things/content/contentArtists.js b/src/data/composite/things/content/contentArtists.js
new file mode 100644
index 00000000..8d5db5a5
--- /dev/null
+++ b/src/data/composite/things/content/contentArtists.js
@@ -0,0 +1,40 @@
+import {input, templateCompositeFrom} from '#composite';
+import {validateReferenceList} from '#validators';
+
+import {exitWithoutDependency, exposeDependency}
+  from '#composite/control-flow';
+import {withResolvedReferenceList} from '#composite/wiki-data';
+import {soupyFind} from '#composite/wiki-properties';
+
+import withExpressedOrImplicitArtistReferences
+  from './helpers/withExpressedOrImplicitArtistReferences.js';
+
+export default templateCompositeFrom({
+  annotation: `contentArtists`,
+
+  compose: false,
+
+  update: {
+    validate: validateReferenceList('artist'),
+  },
+
+  steps: () => [
+    withExpressedOrImplicitArtistReferences({
+      from: input.updateValue(),
+    }),
+
+    exitWithoutDependency({
+      dependency: '#artistReferences',
+      value: input.value([]),
+    }),
+
+    withResolvedReferenceList({
+      list: '#artistReferences',
+      find: soupyFind.input('artist'),
+    }),
+
+    exposeDependency({
+      dependency: '#resolvedReferenceList',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/content/hasAnnotationPart.js b/src/data/composite/things/content/hasAnnotationPart.js
new file mode 100644
index 00000000..83d175e3
--- /dev/null
+++ b/src/data/composite/things/content/hasAnnotationPart.js
@@ -0,0 +1,25 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {exposeDependency} from '#composite/control-flow';
+
+import withHasAnnotationPart from './withHasAnnotationPart.js';
+
+export default templateCompositeFrom({
+  annotation: `hasAnnotationPart`,
+
+  compose: false,
+
+  inputs: {
+    part: input({type: 'string'}),
+  },
+
+  steps: () => [
+    withHasAnnotationPart({
+      part: input('part'),
+    }),
+
+    exposeDependency({
+      dependency: '#hasAnnotationPart',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js
new file mode 100644
index 00000000..69da8c75
--- /dev/null
+++ b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js
@@ -0,0 +1,61 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withFilteredList, withMappedList} from '#composite/data';
+import {withContentNodes} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withExpressedOrImplicitArtistReferences`,
+
+  inputs: {
+    from: input({type: 'array', acceptsNull: true}),
+  },
+
+  outputs: ['#artistReferences'],
+
+  steps: () => [
+    {
+      dependencies: [input('from')],
+      compute: (continuation, {
+        [input('from')]: expressedArtistReferences,
+      }) =>
+        (expressedArtistReferences
+          ? continuation.raiseOutput({'#artistReferences': expressedArtistReferences})
+          : continuation()),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: 'artistText',
+      output: input.value({'#artistReferences': null}),
+    }),
+
+    withContentNodes({
+      from: 'artistText',
+    }),
+
+    withMappedList({
+      list: '#contentNodes',
+      map: input.value(node =>
+        node.type === 'tag' &&
+        node.data.replacerKey?.data === 'artist'),
+    }).outputs({
+      '#mappedList': '#artistTagFilter',
+    }),
+
+    withFilteredList({
+      list: '#contentNodes',
+      filter: '#artistTagFilter',
+    }).outputs({
+      '#filteredList': '#artistTags',
+    }),
+
+    withMappedList({
+      list: '#artistTags',
+      map: input.value(node =>
+        'artist:' +
+        node.data.replacerValue[0].data),
+    }).outputs({
+      '#mappedList': '#artistReferences',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/content/index.js b/src/data/composite/things/content/index.js
new file mode 100644
index 00000000..4176337d
--- /dev/null
+++ b/src/data/composite/things/content/index.js
@@ -0,0 +1,7 @@
+export {default as contentArtists} from './contentArtists.js';
+export {default as hasAnnotationPart} from './hasAnnotationPart.js';
+export {default as withAnnotationParts} from './withAnnotationParts.js';
+export {default as withHasAnnotationPart} from './withHasAnnotationPart.js';
+export {default as withSourceText} from './withSourceText.js';
+export {default as withSourceURLs} from './withSourceURLs.js';
+export {default as withWebArchiveDate} from './withWebArchiveDate.js';
diff --git a/src/data/composite/things/content/withAnnotationParts.js b/src/data/composite/things/content/withAnnotationParts.js
new file mode 100644
index 00000000..0c5a0294
--- /dev/null
+++ b/src/data/composite/things/content/withAnnotationParts.js
@@ -0,0 +1,103 @@
+import {input, templateCompositeFrom} from '#composite';
+import {empty, transposeArrays} from '#sugar';
+import {is} from '#validators';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromList} from '#composite/data';
+import {splitContentNodesAround, withContentNodes} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withAnnotationParts`,
+
+  inputs: {
+    mode: input({
+      validate: is('strings', 'nodes'),
+    }),
+  },
+
+  outputs: ['#annotationParts'],
+
+  steps: () => [
+    raiseOutputWithoutDependency({
+      dependency: 'annotation',
+      output: input.value({'#annotationParts': []}),
+    }),
+
+    withContentNodes({
+      from: 'annotation',
+    }),
+
+    splitContentNodesAround({
+      nodes: '#contentNodes',
+      around: input.value(/, */g),
+    }),
+
+    {
+      dependencies: ['#contentNodeLists'],
+      compute: (continuation, {
+        ['#contentNodeLists']: nodeLists,
+      }) => continuation({
+        ['#contentNodeLists']:
+          nodeLists.filter(list => !empty(list)),
+      }),
+    },
+
+    {
+      dependencies: ['#contentNodeLists', input('mode')],
+      compute: (continuation, {
+        ['#contentNodeLists']: nodeLists,
+        [input('mode')]: mode,
+      }) =>
+        (mode === 'nodes'
+          ? continuation.raiseOutput({'#annotationParts': nodeLists})
+          : continuation()),
+    },
+
+    {
+      dependencies: ['#contentNodeLists'],
+
+      compute: (continuation, {
+        ['#contentNodeLists']: nodeLists,
+      }) => continuation({
+        ['#firstNodes']:
+          nodeLists.map(list => list.at(0)),
+
+        ['#lastNodes']:
+          nodeLists.map(list => list.at(-1)),
+      }),
+    },
+
+    withPropertyFromList({
+      list: '#firstNodes',
+      property: input.value('i'),
+    }).outputs({
+      '#firstNodes.i': '#startIndices',
+    }),
+
+    withPropertyFromList({
+      list: '#lastNodes',
+      property: input.value('iEnd'),
+    }).outputs({
+      '#lastNodes.iEnd': '#endIndices',
+    }),
+
+    {
+      dependencies: [
+        'annotation',
+        '#startIndices',
+        '#endIndices',
+      ],
+
+      compute: (continuation, {
+        ['annotation']: annotation,
+        ['#startIndices']: startIndices,
+        ['#endIndices']: endIndices,
+      }) => continuation({
+        ['#annotationParts']:
+          transposeArrays([startIndices, endIndices])
+            .map(([start, end]) =>
+              annotation.slice(start, end)),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/content/withHasAnnotationPart.js b/src/data/composite/things/content/withHasAnnotationPart.js
new file mode 100644
index 00000000..4af554f3
--- /dev/null
+++ b/src/data/composite/things/content/withHasAnnotationPart.js
@@ -0,0 +1,43 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+import withAnnotationParts from './withAnnotationParts.js';
+
+export default templateCompositeFrom({
+  annotation: `withHasAnnotationPart`,
+
+  inputs: {
+    part: input({type: 'string'}),
+  },
+
+  outputs: ['#hasAnnotationPart'],
+
+  steps: () => [
+    withAnnotationParts({
+      mode: input.value('strings'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#annotationParts',
+      output: input.value({'#hasAnnotationPart': false}),
+    }),
+
+    {
+      dependencies: [
+        input('part'),
+        '#annotationParts',
+      ],
+
+      compute: (continuation, {
+        [input('part')]: search,
+        ['#annotationParts']: parts,
+      }) => continuation({
+        ['#hasAnnotationPart']:
+          parts.some(part =>
+            part.toLowerCase() ===
+            search.toLowerCase()),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/content/withSourceText.js b/src/data/composite/things/content/withSourceText.js
new file mode 100644
index 00000000..292306b7
--- /dev/null
+++ b/src/data/composite/things/content/withSourceText.js
@@ -0,0 +1,53 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+import withAnnotationParts from './withAnnotationParts.js';
+
+export default templateCompositeFrom({
+  annotation: `withSourceText`,
+
+  outputs: ['#sourceText'],
+
+  steps: () => [
+    withAnnotationParts({
+      mode: input.value('nodes'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#annotationParts',
+      output: input.value({'#sourceText': null}),
+    }),
+
+    {
+      dependencies: ['#annotationParts'],
+      compute: (continuation, {
+        ['#annotationParts']: annotationParts,
+      }) => continuation({
+        ['#firstPartWithExternalLink']:
+          annotationParts
+            .find(nodes => nodes
+              .some(node => node.type === 'external-link')) ??
+          null,
+      }),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: '#firstPartWithExternalLink',
+      output: input.value({'#sourceText': null}),
+    }),
+
+    {
+      dependencies: ['annotation', '#firstPartWithExternalLink'],
+      compute: (continuation, {
+        ['annotation']: annotation,
+        ['#firstPartWithExternalLink']: nodes,
+      }) => continuation({
+        ['#sourceText']:
+          annotation.slice(
+            nodes.at(0).i,
+            nodes.at(-1).iEnd),
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/content/withSourceURLs.js b/src/data/composite/things/content/withSourceURLs.js
new file mode 100644
index 00000000..f85ff9ea
--- /dev/null
+++ b/src/data/composite/things/content/withSourceURLs.js
@@ -0,0 +1,62 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withFilteredList, withMappedList} from '#composite/data';
+
+import withAnnotationParts from './withAnnotationParts.js';
+
+export default templateCompositeFrom({
+  annotation: `withSourceURLs`,
+
+  outputs: ['#sourceURLs'],
+
+  steps: () => [
+    withAnnotationParts({
+      mode: input.value('nodes'),
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#annotationParts',
+      output: input.value({'#sourceURLs': []}),
+    }),
+
+    {
+      dependencies: ['#annotationParts'],
+      compute: (continuation, {
+        ['#annotationParts']: annotationParts,
+      }) => continuation({
+        ['#firstPartWithExternalLink']:
+          annotationParts
+            .find(nodes => nodes
+              .some(node => node.type === 'external-link')) ??
+          null,
+      }),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: '#firstPartWithExternalLink',
+      output: input.value({'#sourceURLs': []}),
+    }),
+
+    withMappedList({
+      list: '#firstPartWithExternalLink',
+      map: input.value(node => node.type === 'external-link'),
+    }).outputs({
+      '#mappedList': '#externalLinkFilter',
+    }),
+
+    withFilteredList({
+      list: '#firstPartWithExternalLink',
+      filter: '#externalLinkFilter',
+    }).outputs({
+      '#filteredList': '#externalLinks',
+    }),
+
+    withMappedList({
+      list: '#externalLinks',
+      map: input.value(node => node.data.href),
+    }).outputs({
+      '#mappedList': '#sourceURLs',
+    }),
+  ],
+});
diff --git a/src/data/composite/things/content/withWebArchiveDate.js b/src/data/composite/things/content/withWebArchiveDate.js
new file mode 100644
index 00000000..3aaa4f64
--- /dev/null
+++ b/src/data/composite/things/content/withWebArchiveDate.js
@@ -0,0 +1,41 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+
+export default templateCompositeFrom({
+  annotation: `withWebArchiveDate`,
+
+  outputs: ['#webArchiveDate'],
+
+  steps: () => [
+    {
+      dependencies: ['annotation'],
+
+      compute: (continuation, {annotation}) =>
+        continuation({
+          ['#dateText']:
+            annotation
+              ?.match(/https?:\/\/web.archive.org\/web\/([0-9]{8,8})[0-9]*\//)
+              ?.[1] ??
+            null,
+        }),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: '#dateText',
+      output: input.value({['#webArchiveDate']: null}),
+    }),
+
+    {
+      dependencies: ['#dateText'],
+      compute: (continuation, {['#dateText']: dateText}) =>
+        continuation({
+          ['#webArchiveDate']:
+            new Date(
+              dateText.slice(0, 4) + '/' +
+              dateText.slice(4, 6) + '/' +
+              dateText.slice(6, 8)),
+        }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/contribution/index.js b/src/data/composite/things/contribution/index.js
index 9b22be2e..31d86b8b 100644
--- a/src/data/composite/things/contribution/index.js
+++ b/src/data/composite/things/contribution/index.js
@@ -1,6 +1,4 @@
 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';
diff --git a/src/data/composite/things/contribution/thingPropertyMatches.js b/src/data/composite/things/contribution/thingPropertyMatches.js
deleted file mode 100644
index 4a37f2cf..00000000
--- a/src/data/composite/things/contribution/thingPropertyMatches.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import {input, templateCompositeFrom} from '#composite';
-
-import {exitWithoutDependency} from '#composite/control-flow';
-
-export default templateCompositeFrom({
-  annotation: `thingPropertyMatches`,
-
-  compose: false,
-
-  inputs: {
-    value: input({type: 'string'}),
-  },
-
-  steps: () => [
-    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
deleted file mode 100644
index 2ee811af..00000000
--- a/src/data/composite/things/contribution/thingReferenceTypeMatches.js
+++ /dev/null
@@ -1,39 +0,0 @@
-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: ({
-        ['#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
index 56704c8b..175d6cbb 100644
--- a/src/data/composite/things/contribution/withContainingReverseContributionList.js
+++ b/src/data/composite/things/contribution/withContainingReverseContributionList.js
@@ -1,8 +1,12 @@
-// Get the artist's contribution list containing this property.
+// 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} from '#composite/control-flow';
+import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
 
 import withContributionArtist from './withContributionArtist.js';
@@ -34,7 +38,43 @@ export default templateCompositeFrom({
       object: '#artist',
       property: input('artistProperty'),
     }).outputs({
-      ['#value']: '#containingReverseContributionList',
+      ['#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/track-section/index.js b/src/data/composite/things/track-section/index.js
index 3202ed49..f11a2ab5 100644
--- a/src/data/composite/things/track-section/index.js
+++ b/src/data/composite/things/track-section/index.js
@@ -1 +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/withContinueCountingFrom.js b/src/data/composite/things/track-section/withContinueCountingFrom.js
new file mode 100644
index 00000000..0ca52b6c
--- /dev/null
+++ b/src/data/composite/things/track-section/withContinueCountingFrom.js
@@ -0,0 +1,25 @@
+import {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 beb8c6ab..e789e736 100644
--- a/src/data/composite/things/track/index.js
+++ b/src/data/composite/things/track/index.js
@@ -1,10 +1,10 @@
 export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.js';
 export {default as inheritContributionListFromMainRelease} from './inheritContributionListFromMainRelease.js';
 export {default as inheritFromMainRelease} from './inheritFromMainRelease.js';
-export {default as withAlbum} from './withAlbum.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';
@@ -14,3 +14,4 @@ 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/trackAdditionalNameList.js b/src/data/composite/things/track/trackAdditionalNameList.js
deleted file mode 100644
index 65a2263d..00000000
--- a/src/data/composite/things/track/trackAdditionalNameList.js
+++ /dev/null
@@ -1,38 +0,0 @@
-// Compiles additional names from various sources.
-
-import {input, templateCompositeFrom} from '#composite';
-import {isAdditionalNameList} from '#validators';
-
-import withInferredAdditionalNames from './withInferredAdditionalNames.js';
-import withSharedAdditionalNames from './withSharedAdditionalNames.js';
-
-export default templateCompositeFrom({
-  annotation: `trackAdditionalNameList`,
-
-  compose: false,
-
-  update: {validate: isAdditionalNameList},
-
-  steps: () => [
-    withInferredAdditionalNames(),
-    withSharedAdditionalNames(),
-
-    {
-      dependencies: [
-        '#inferredAdditionalNames',
-        '#sharedAdditionalNames',
-        input.updateValue(),
-      ],
-
-      compute: ({
-        ['#inferredAdditionalNames']: inferredAdditionalNames,
-        ['#sharedAdditionalNames']: sharedAdditionalNames,
-        [input.updateValue()]: providedAdditionalNames,
-      }) => [
-        ...providedAdditionalNames ?? [],
-        ...sharedAdditionalNames,
-        ...inferredAdditionalNames,
-      ],
-    },
-  ],
-});
diff --git a/src/data/composite/things/track/withAlbum.js b/src/data/composite/things/track/withAlbum.js
deleted file mode 100644
index 4c55e1f4..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 {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('albumsWhoseTracksInclude'),
-    }).outputs({
-      ['#uniqueReferencingThing']: '#album',
-    }),
-  ],
-});
diff --git a/src/data/composite/things/track/withAllReleases.js b/src/data/composite/things/track/withAllReleases.js
index b93bf753..891db102 100644
--- a/src/data/composite/things/track/withAllReleases.js
+++ b/src/data/composite/things/track/withAllReleases.js
@@ -8,7 +8,6 @@
 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';
diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
index aebcf793..87edf21e 100644
--- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
+++ b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
@@ -9,7 +9,6 @@ import {isBoolean} from '#validators';
 
 import {withPropertyFromObject} from '#composite/data';
 import {withResolvedReference} from '#composite/wiki-data';
-import {soupyFind} from '#composite/wiki-properties';
 
 import {
   exitWithoutDependency,
@@ -17,6 +16,8 @@ import {
   exposeUpdateValueOrContinue,
 } from '#composite/control-flow';
 
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+
 export default templateCompositeFrom({
   annotation: `withAlwaysReferenceByDirectory`,
 
@@ -27,18 +28,7 @@ export default templateCompositeFrom({
       validate: input.value(isBoolean),
     }),
 
-    // withAlwaysReferenceByDirectory is sort of a fragile area - we can't
-    // find the track's album the normal way because albums' track lists
-    // recurse back into alwaysReferenceByDirectory!
-    withResolvedReference({
-      ref: 'dataSourceAlbum',
-      find: soupyFind.input('album'),
-    }).outputs({
-      '#resolvedReference': '#album',
-    }),
-
-    withPropertyFromObject({
-      object: '#album',
+    withPropertyFromAlbum({
       property: input.value('alwaysReferenceTracksByDirectory'),
     }),
 
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/withHasUniqueCoverArt.js b/src/data/composite/things/track/withHasUniqueCoverArt.js
index f7e65f25..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,36 +36,73 @@ export default templateCompositeFrom({
           : continuation()),
     },
 
-    withResolvedContribs({
+    withResultOfAvailabilityCheck({
       from: 'coverArtistContribs',
-      date: input.value(null),
+      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/withOtherReleases.js b/src/data/composite/things/track/withOtherReleases.js
index 0639742f..bb3e8983 100644
--- a/src/data/composite/things/track/withOtherReleases.js
+++ b/src/data/composite/things/track/withOtherReleases.js
@@ -3,9 +3,6 @@
 
 import {input, templateCompositeFrom} from '#composite';
 
-import {exitWithoutDependency} from '#composite/control-flow';
-import {withPropertyFromObject} from '#composite/data';
-
 import withAllReleases from './withAllReleases.js';
 
 export default templateCompositeFrom({
diff --git a/src/data/composite/things/track/withPropertyFromAlbum.js b/src/data/composite/things/track/withPropertyFromAlbum.js
index e9c5b56e..a203c2e7 100644
--- a/src/data/composite/things/track/withPropertyFromAlbum.js
+++ b/src/data/composite/things/track/withPropertyFromAlbum.js
@@ -5,13 +5,12 @@ import {input, templateCompositeFrom} from '#composite';
 
 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: ({
@@ -19,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/withTrackArtDate.js b/src/data/composite/things/track/withTrackArtDate.js
index e2c4d8bc..9b7b61c7 100644
--- a/src/data/composite/things/track/withTrackArtDate.js
+++ b/src/data/composite/things/track/withTrackArtDate.js
@@ -1,11 +1,3 @@
-// Gets the date of cover art release. This represents only the track's own
-// unique cover artwork, if any.
-//
-// If the 'fallback' option is false (the default), this will only output
-// the track's own coverArtDate or its album's trackArtDate. If 'fallback'
-// is set, and neither of these is available, it'll output the track's own
-// date instead.
-
 import {input, templateCompositeFrom} from '#composite';
 import {isDate} from '#validators';
 
@@ -24,11 +16,6 @@ export default templateCompositeFrom({
       defaultDependency: 'coverArtDate',
       acceptsNull: true,
     }),
-
-    fallback: input({
-      type: 'boolean',
-      defaultValue: false,
-    }),
   },
 
   outputs: ['#trackArtDate'],
@@ -57,20 +44,13 @@ export default templateCompositeFrom({
     }),
 
     {
-      dependencies: [
-        '#album.trackArtDate',
-        input('fallback'),
-      ],
-
+      dependencies: ['#album.trackArtDate'],
       compute: (continuation, {
         ['#album.trackArtDate']: albumTrackArtDate,
-        [input('fallback')]: fallback,
       }) =>
         (albumTrackArtDate
           ? continuation.raiseOutput({'#trackArtDate': albumTrackArtDate})
-       : fallback
-          ? continuation()
-          : continuation.raiseOutput({'#trackArtDate': null})),
+          : continuation()),
     },
 
     withDate().outputs({
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,
+      }),
+    },
+  ],
+});