« 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
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/composite')
-rw-r--r--src/data/composite/data/withLengthOfList.js1
-rw-r--r--src/data/composite/things/album/index.js2
-rw-r--r--src/data/composite/things/album/withCoverArtDate.js (renamed from src/data/composite/wiki-data/withCoverArtDate.js)13
-rw-r--r--src/data/composite/things/artwork/index.js2
-rw-r--r--src/data/composite/things/artwork/withArtTags.js99
-rw-r--r--src/data/composite/things/artwork/withContentWarningArtTags.js27
-rw-r--r--src/data/composite/things/artwork/withContribsFromAttachedArtwork.js1
-rw-r--r--src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js1
-rw-r--r--src/data/composite/things/content/withAnnotationParts.js13
-rw-r--r--src/data/composite/things/content/withSourceText.js1
-rw-r--r--src/data/composite/things/content/withSourceURLs.js1
-rw-r--r--src/data/composite/things/contribution/index.js2
-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/language/index.js1
-rw-r--r--src/data/composite/things/language/withStrings.js111
-rw-r--r--src/data/composite/things/track-section/withContinueCountingFrom.js2
-rw-r--r--src/data/composite/things/track/alwaysReferenceByDirectory.js69
-rw-r--r--src/data/composite/things/track/index.js3
-rw-r--r--src/data/composite/things/track/withAllReleases.js19
-rw-r--r--src/data/composite/things/track/withAlwaysReferenceByDirectory.js97
-rw-r--r--src/data/composite/things/track/withDate.js8
-rw-r--r--src/data/composite/things/track/withDirectorySuffix.js22
-rw-r--r--src/data/composite/things/track/withMainRelease.js133
-rw-r--r--src/data/composite/things/track/withMainReleaseTrack.js248
-rw-r--r--src/data/composite/things/track/withOtherReleases.js3
-rw-r--r--src/data/composite/things/track/withPropertyFromMainRelease.js10
-rw-r--r--src/data/composite/things/track/withSuffixDirectoryFromAlbum.js20
-rw-r--r--src/data/composite/wiki-data/exitWithoutArtwork.js45
-rw-r--r--src/data/composite/wiki-data/index.js4
-rw-r--r--src/data/composite/wiki-data/inputFindOptions.js5
-rw-r--r--src/data/composite/wiki-data/withConstitutedArtwork.js1
-rw-r--r--src/data/composite/wiki-data/withHasArtwork.js (renamed from src/data/composite/things/album/withHasCoverArt.js)59
-rw-r--r--src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js3
-rw-r--r--src/data/composite/wiki-data/withResolvedReference.js22
-rw-r--r--src/data/composite/wiki-data/withResolvedReferenceList.js21
-rw-r--r--src/data/composite/wiki-properties/annotatedReferenceList.js11
-rw-r--r--src/data/composite/wiki-properties/canonicalBase.js16
-rw-r--r--src/data/composite/wiki-properties/index.js1
-rw-r--r--src/data/composite/wiki-properties/referenceList.js11
-rw-r--r--src/data/composite/wiki-properties/referencedArtworkList.js1
-rw-r--r--src/data/composite/wiki-properties/singleReference.js31
42 files changed, 913 insertions, 339 deletions
diff --git a/src/data/composite/data/withLengthOfList.js b/src/data/composite/data/withLengthOfList.js
index efc3ecae..e67aa887 100644
--- a/src/data/composite/data/withLengthOfList.js
+++ b/src/data/composite/data/withLengthOfList.js
@@ -1,5 +1,4 @@
 import {input, templateCompositeFrom} from '#composite';
-import {stitchArrays} from '#sugar';
 
 function getOutputName({
   [input.staticDependency('list')]: list,
diff --git a/src/data/composite/things/album/index.js b/src/data/composite/things/album/index.js
index dfc6864f..de1d37c3 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 withCoverArtDate} from './withCoverArtDate.js';
 export {default as withTracks} from './withTracks.js';
diff --git a/src/data/composite/wiki-data/withCoverArtDate.js b/src/data/composite/things/album/withCoverArtDate.js
index a114d5ff..978f566a 100644
--- a/src/data/composite/wiki-data/withCoverArtDate.js
+++ b/src/data/composite/things/album/withCoverArtDate.js
@@ -2,8 +2,7 @@ import {input, templateCompositeFrom} from '#composite';
 import {isDate} from '#validators';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
-
-import withResolvedContribs from './withResolvedContribs.js';
+import {withHasArtwork} from '#composite/wiki-data';
 
 export default templateCompositeFrom({
   annotation: `withCoverArtDate`,
@@ -19,14 +18,14 @@ export default templateCompositeFrom({
   outputs: ['#coverArtDate'],
 
   steps: () => [
-    withResolvedContribs({
-      from: 'coverArtistContribs',
-      date: input.value(null),
+    withHasArtwork({
+      contribs: 'coverArtistContribs',
+      artworks: 'coverArtworks',
     }),
 
     raiseOutputWithoutDependency({
-      dependency: '#resolvedContribs',
-      mode: input.value('empty'),
+      dependency: '#hasArtwork',
+      mode: input.value('falsy'),
       output: input.value({'#coverArtDate': null}),
     }),
 
diff --git a/src/data/composite/things/artwork/index.js b/src/data/composite/things/artwork/index.js
index 3693c10f..b5e5e167 100644
--- a/src/data/composite/things/artwork/index.js
+++ b/src/data/composite/things/artwork/index.js
@@ -1,5 +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/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
index 36abb3fe..e9425c95 100644
--- a/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
+++ b/src/data/composite/things/artwork/withContribsFromAttachedArtwork.js
@@ -1,7 +1,6 @@
 import {input, templateCompositeFrom} from '#composite';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
-import {withPropertyFromObject} from '#composite/data';
 import {withRecontextualizedContributionList} from '#composite/wiki-data';
 
 import withPropertyFromAttachedArtwork from './withPropertyFromAttachedArtwork.js';
diff --git a/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js
index 62799d43..69da8c75 100644
--- a/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js
+++ b/src/data/composite/things/content/helpers/withExpressedOrImplicitArtistReferences.js
@@ -52,6 +52,7 @@ export default templateCompositeFrom({
     withMappedList({
       list: '#artistTags',
       map: input.value(node =>
+        'artist:' +
         node.data.replacerValue[0].data),
     }).outputs({
       '#mappedList': '#artistReferences',
diff --git a/src/data/composite/things/content/withAnnotationParts.js b/src/data/composite/things/content/withAnnotationParts.js
index 5eb8e3d5..0c5a0294 100644
--- a/src/data/composite/things/content/withAnnotationParts.js
+++ b/src/data/composite/things/content/withAnnotationParts.js
@@ -1,6 +1,5 @@
 import {input, templateCompositeFrom} from '#composite';
-import {parseContentNodes} from '#replacer';
-import {transposeArrays} from '#sugar';
+import {empty, transposeArrays} from '#sugar';
 import {is} from '#validators';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
@@ -34,6 +33,16 @@ export default templateCompositeFrom({
     }),
 
     {
+      dependencies: ['#contentNodeLists'],
+      compute: (continuation, {
+        ['#contentNodeLists']: nodeLists,
+      }) => continuation({
+        ['#contentNodeLists']:
+          nodeLists.filter(list => !empty(list)),
+      }),
+    },
+
+    {
       dependencies: ['#contentNodeLists', input('mode')],
       compute: (continuation, {
         ['#contentNodeLists']: nodeLists,
diff --git a/src/data/composite/things/content/withSourceText.js b/src/data/composite/things/content/withSourceText.js
index d310e8ea..292306b7 100644
--- a/src/data/composite/things/content/withSourceText.js
+++ b/src/data/composite/things/content/withSourceText.js
@@ -1,5 +1,4 @@
 import {input, templateCompositeFrom} from '#composite';
-import {parseContentNodes} from '#replacer';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 
diff --git a/src/data/composite/things/content/withSourceURLs.js b/src/data/composite/things/content/withSourceURLs.js
index f1e8dbc0..f85ff9ea 100644
--- a/src/data/composite/things/content/withSourceURLs.js
+++ b/src/data/composite/things/content/withSourceURLs.js
@@ -1,5 +1,4 @@
 import {input, templateCompositeFrom} from '#composite';
-import {parseContentNodes} from '#replacer';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 import {withFilteredList, withMappedList} from '#composite/data';
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 1e9019b8..00000000
--- a/src/data/composite/things/contribution/thingPropertyMatches.js
+++ /dev/null
@@ -1,46 +0,0 @@
-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
deleted file mode 100644
index 4042e78f..00000000
--- a/src/data/composite/things/contribution/thingReferenceTypeMatches.js
+++ /dev/null
@@ -1,66 +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: (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/language/index.js b/src/data/composite/things/language/index.js
new file mode 100644
index 00000000..f22cdaf6
--- /dev/null
+++ b/src/data/composite/things/language/index.js
@@ -0,0 +1 @@
+export {default as withStrings} from './withStrings.js';
diff --git a/src/data/composite/things/language/withStrings.js b/src/data/composite/things/language/withStrings.js
new file mode 100644
index 00000000..3b8d46b3
--- /dev/null
+++ b/src/data/composite/things/language/withStrings.js
@@ -0,0 +1,111 @@
+import {logWarn} from '#cli';
+import {input, templateCompositeFrom} from '#composite';
+import {empty, withEntries} from '#sugar';
+import {languageOptionRegex} from '#wiki-data';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+
+export default templateCompositeFrom({
+  annotation: `withStrings`,
+
+  inputs: {
+    from: input({defaultDependency: 'strings'}),
+  },
+
+  outputs: ['#strings'],
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input('from'),
+    }).outputs({
+      '#availability': '#stringsAvailability',
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: 'inheritedStrings',
+    }).outputs({
+      '#availability': '#inheritedStringsAvailability',
+    }),
+
+    {
+      dependencies: [
+        '#stringsAvailability',
+        '#inheritedStringsAvailability',
+      ],
+
+      compute: (continuation, {
+        ['#stringsAvailability']: stringsAvailability,
+        ['#inheritedStringsAvailability']: inheritedStringsAvailability,
+      }) =>
+        (stringsAvailability || inheritedStringsAvailability
+          ? continuation()
+          : continuation.raiseOutput({'#strings': null})),
+    },
+
+    {
+      dependencies: [input('from'), '#inheritedStringsAvailability'],
+      compute: (continuation, {
+        [input('from')]: strings,
+        ['#inheritedStringsAvailability']: inheritedStringsAvailability,
+      }) =>
+        (inheritedStringsAvailability
+          ? continuation()
+          : continuation.raiseOutput({'#strings': strings})),
+    },
+
+    {
+      dependencies: ['inheritedStrings', '#stringsAvailability'],
+      compute: (continuation, {
+        ['inheritedStrings']: inheritedStrings,
+        ['#stringsAvailability']: stringsAvailability,
+      }) =>
+        (stringsAvailability
+          ? continuation()
+          : continuation.raiseOutput({'#strings': inheritedStrings})),
+    },
+
+    {
+      dependencies: [input('from'), 'inheritedStrings', 'code'],
+      compute(continuation, {
+        [input('from')]: strings,
+        ['inheritedStrings']: inheritedStrings,
+        ['code']: code,
+      }) {
+        const validStrings = {
+          ...inheritedStrings,
+          ...strings,
+        };
+
+        const optionsFromTemplate = template =>
+          Array.from(template.matchAll(languageOptionRegex))
+            .map(({groups}) => groups.name);
+
+        for (const [key, providedTemplate] of Object.entries(strings)) {
+          const inheritedTemplate = inheritedStrings[key];
+          if (!inheritedTemplate) continue;
+
+          const providedOptions = optionsFromTemplate(providedTemplate);
+          const inheritedOptions = optionsFromTemplate(inheritedTemplate);
+
+          const missingOptionNames =
+            inheritedOptions.filter(name => !providedOptions.includes(name));
+
+          const misplacedOptionNames =
+            providedOptions.filter(name => !inheritedOptions.includes(name));
+
+          if (!empty(missingOptionNames) || !empty(misplacedOptionNames)) {
+            logWarn`Not using ${code ?? '(no code)'} string ${key}:`;
+            if (!empty(missingOptionNames))
+              logWarn`- Missing options: ${missingOptionNames.join(', ')}`;
+            if (!empty(misplacedOptionNames))
+              logWarn`- Unexpected options: ${misplacedOptionNames.join(', ')}`;
+
+            validStrings[key] = inheritedStrings[key];
+          }
+        }
+
+        return continuation({'#strings': validStrings});
+      },
+    },
+  ],
+});
diff --git a/src/data/composite/things/track-section/withContinueCountingFrom.js b/src/data/composite/things/track-section/withContinueCountingFrom.js
index e034b7a5..0ca52b6c 100644
--- a/src/data/composite/things/track-section/withContinueCountingFrom.js
+++ b/src/data/composite/things/track-section/withContinueCountingFrom.js
@@ -1,4 +1,4 @@
-import {input, templateCompositeFrom} from '#composite';
+import {templateCompositeFrom} from '#composite';
 
 import withStartCountingFrom from './withStartCountingFrom.js';
 
diff --git a/src/data/composite/things/track/alwaysReferenceByDirectory.js b/src/data/composite/things/track/alwaysReferenceByDirectory.js
new file mode 100644
index 00000000..a342d38b
--- /dev/null
+++ b/src/data/composite/things/track/alwaysReferenceByDirectory.js
@@ -0,0 +1,69 @@
+// Controls how find.track works - it'll never be matched by a reference
+// just to the track's name, which means you don't have to always reference
+// some *other* (much more commonly referenced) track by directory instead
+// of more naturally by name.
+
+import {input, templateCompositeFrom} from '#composite';
+import {isBoolean} from '#validators';
+import {getKebabCase} from '#wiki-data';
+
+import {withPropertyFromObject} from '#composite/data';
+
+import {
+  exitWithoutDependency,
+  exposeDependencyOrContinue,
+  exposeUpdateValueOrContinue,
+} from '#composite/control-flow';
+
+import withMainReleaseTrack from './withMainReleaseTrack.js';
+import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+
+export default templateCompositeFrom({
+  annotation: `alwaysReferenceByDirectory`,
+
+  compose: false,
+
+  steps: () => [
+    exposeUpdateValueOrContinue({
+      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'),
+    }),
+
+    exitWithoutDependency({
+      dependency: 'mainRelease',
+      value: input.value(false),
+    }),
+
+    withMainReleaseTrack(),
+
+    exitWithoutDependency({
+      dependency: '#mainReleaseTrack',
+      value: input.value(false),
+    }),
+
+    withPropertyFromObject({
+      object: '#mainReleaseTrack',
+      property: input.value('name'),
+    }),
+
+    {
+      dependencies: ['name', '#mainReleaseTrack.name'],
+      compute: ({
+        ['name']: name,
+        ['#mainReleaseTrack.name']: mainReleaseName,
+      }) =>
+        getKebabCase(name) ===
+        getKebabCase(mainReleaseName),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/index.js b/src/data/composite/things/track/index.js
index e789e736..1c203cd9 100644
--- a/src/data/composite/things/track/index.js
+++ b/src/data/composite/things/track/index.js
@@ -1,14 +1,15 @@
+export {default as alwaysReferenceByDirectory} from './alwaysReferenceByDirectory.js';
 export {default as exitWithoutUniqueCoverArt} from './exitWithoutUniqueCoverArt.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 withMainReleaseTrack} from './withMainReleaseTrack.js';
 export {default as withOtherReleases} from './withOtherReleases.js';
 export {default as withPropertyFromAlbum} from './withPropertyFromAlbum.js';
 export {default as withPropertyFromMainRelease} from './withPropertyFromMainRelease.js';
diff --git a/src/data/composite/things/track/withAllReleases.js b/src/data/composite/things/track/withAllReleases.js
index b93bf753..bd54384f 100644
--- a/src/data/composite/things/track/withAllReleases.js
+++ b/src/data/composite/things/track/withAllReleases.js
@@ -8,10 +8,9 @@
 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';
+import withMainReleaseTrack from './withMainReleaseTrack.js';
 
 export default templateCompositeFrom({
   annotation: `withAllReleases`,
@@ -19,7 +18,7 @@ export default templateCompositeFrom({
   outputs: ['#allReleases'],
 
   steps: () => [
-    withMainRelease({
+    withMainReleaseTrack({
       selfIfMain: input.value(true),
       notFoundValue: input.value([]),
     }),
@@ -29,18 +28,22 @@ export default templateCompositeFrom({
     // `this.secondaryReleases` from within a data composition.
     // Oooooooooooooooooooooooooooooooooooooooooooooooo
     withPropertyFromObject({
-      object: '#mainRelease',
+      object: '#mainReleaseTrack',
       property: input.value('secondaryReleases'),
     }),
 
     {
-      dependencies: ['#mainRelease', '#mainRelease.secondaryReleases'],
+      dependencies: [
+        '#mainReleaseTrack',
+        '#mainReleaseTrack.secondaryReleases',
+      ],
+
       compute: (continuation, {
-        ['#mainRelease']: mainRelease,
-        ['#mainRelease.secondaryReleases']: secondaryReleases,
+        ['#mainReleaseTrack']: mainReleaseTrack,
+        ['#mainReleaseTrack.secondaryReleases']: secondaryReleases,
       }) => continuation({
         ['#allReleases']:
-          sortByDate([mainRelease, ...secondaryReleases]),
+          sortByDate([mainReleaseTrack, ...secondaryReleases]),
       }),
     },
   ],
diff --git a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js b/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
deleted file mode 100644
index 60faeaf4..00000000
--- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
+++ /dev/null
@@ -1,97 +0,0 @@
-// Controls how find.track works - it'll never be matched by a reference
-// just to the track's name, which means you don't have to always reference
-// some *other* (much more commonly referenced) track by directory instead
-// of more naturally by name.
-
-import {input, templateCompositeFrom} from '#composite';
-import find from '#find';
-import {isBoolean} from '#validators';
-
-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`,
-
-  outputs: ['#alwaysReferenceByDirectory'],
-
-  steps: () => [
-    exposeUpdateValueOrContinue({
-      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 mainReleaseTrack.
-
-    exitWithoutDependency({
-      dependency: 'trackData',
-      mode: input.value('empty'),
-      value: input.value(false),
-    }),
-
-    exitWithoutDependency({
-      dependency: 'mainReleaseTrack',
-      value: input.value(false),
-    }),
-
-    // 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.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: 'mainReleaseTrack',
-      data: 'trackData',
-      find: input.value(find.trackMainReleasesOnly),
-    }).outputs({
-      '#resolvedReference': '#mainRelease',
-    }),
-
-    exitWithoutDependency({
-      dependency: '#mainRelease',
-      value: input.value(false),
-    }),
-
-    withPropertyFromObject({
-      object: '#mainRelease',
-      property: input.value('name'),
-    }),
-
-    {
-      dependencies: ['name', '#mainRelease.name'],
-      compute: (continuation, {
-        name,
-        ['#mainRelease.name']: mainReleaseName,
-      }) => continuation({
-        ['#alwaysReferenceByDirectory']:
-          name === mainReleaseName,
-      }),
-    },
-  ],
-});
diff --git a/src/data/composite/things/track/withDate.js b/src/data/composite/things/track/withDate.js
index b5a770e9..1851c0d2 100644
--- a/src/data/composite/things/track/withDate.js
+++ b/src/data/composite/things/track/withDate.js
@@ -12,6 +12,14 @@ export default templateCompositeFrom({
 
   steps: () => [
     {
+      dependencies: ['disableDate'],
+      compute: (continuation, {disableDate}) =>
+        (disableDate
+          ? continuation.raiseOutput({'#date': null})
+          : continuation()),
+    },
+
+    {
       dependencies: ['dateFirstReleased'],
       compute: (continuation, {dateFirstReleased}) =>
         (dateFirstReleased
diff --git a/src/data/composite/things/track/withDirectorySuffix.js b/src/data/composite/things/track/withDirectorySuffix.js
index c063e158..c3651491 100644
--- a/src/data/composite/things/track/withDirectorySuffix.js
+++ b/src/data/composite/things/track/withDirectorySuffix.js
@@ -1,8 +1,9 @@
 import {input, templateCompositeFrom} from '#composite';
 
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
 
-import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+import withContainingTrackSection from './withContainingTrackSection.js';
 import withSuffixDirectoryFromAlbum from './withSuffixDirectoryFromAlbum.js';
 
 export default templateCompositeFrom({
@@ -16,21 +17,16 @@ export default templateCompositeFrom({
     raiseOutputWithoutDependency({
       dependency: '#suffixDirectoryFromAlbum',
       mode: input.value('falsy'),
-      output: input.value({['#directorySuffix']: null}),
+      output: input.value({'#directorySuffix': null}),
     }),
 
-    withPropertyFromAlbum({
+    withContainingTrackSection(),
+
+    withPropertyFromObject({
+      object: '#trackSection',
       property: input.value('directorySuffix'),
+    }).outputs({
+      '#trackSection.directorySuffix': '#directorySuffix',
     }),
-
-    {
-      dependencies: ['#album.directorySuffix'],
-      compute: (continuation, {
-        ['#album.directorySuffix']: directorySuffix,
-      }) => continuation({
-        ['#directorySuffix']:
-          directorySuffix,
-      }),
-    },
   ],
 });
diff --git a/src/data/composite/things/track/withMainRelease.js b/src/data/composite/things/track/withMainRelease.js
index 3a91edae..67a312ae 100644
--- a/src/data/composite/things/track/withMainRelease.js
+++ b/src/data/composite/things/track/withMainRelease.js
@@ -1,13 +1,15 @@
-// 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.
+// Resolves this track's `mainRelease` reference, using weird-ass atypical
+// machinery that operates on soupyFind and does not operate on findMixed,
+// let alone a prim and proper standalone find spec.
+//
+// Raises null only if there is no `mainRelease` reference provided at all.
+// This will early exit (with notFoundValue) if the reference doesn't resolve.
+//
 
 import {input, templateCompositeFrom} from '#composite';
 
-import {exitWithoutDependency, withResultOfAvailabilityCheck}
-  from '#composite/control-flow';
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
 import {withResolvedReference} from '#composite/wiki-data';
 import {soupyFind} from '#composite/wiki-properties';
 
@@ -15,56 +17,121 @@ export default templateCompositeFrom({
   annotation: `withMainRelease`,
 
   inputs: {
-    selfIfMain: input({type: 'boolean', defaultValue: false}),
+    from: input({
+      defaultDependency: 'mainRelease',
+      acceptsNull: true,
+    }),
+
     notFoundValue: input({defaultValue: null}),
   },
 
   outputs: ['#mainRelease'],
 
   steps: () => [
-    withResultOfAvailabilityCheck({
-      from: 'mainReleaseTrack',
+    raiseOutputWithoutDependency({
+      dependency: input('from'),
+      output: input.value({'#mainRelease': null}),
     }),
 
     {
+      dependencies: [input('from'), 'name'],
+      compute: (continuation, {
+        [input('from')]: ref,
+        ['name']: ownName,
+      }) =>
+        (ref === 'same name single'
+          ? continuation({
+              ['#albumOrTrackReference']: null,
+              ['#sameNameSingleReference']: ownName,
+            })
+          : continuation({
+              ['#albumOrTrackReference']: ref,
+              ['#sameNameSingleReference']: null,
+            })),
+    },
+
+    withResolvedReference({
+      ref: '#albumOrTrackReference',
+      find: soupyFind.input('trackMainReleasesOnly'),
+    }).outputs({
+      '#resolvedReference': '#matchingTrack',
+    }),
+
+    withResolvedReference({
+      ref: '#albumOrTrackReference',
+      find: soupyFind.input('album'),
+    }).outputs({
+      '#resolvedReference': '#matchingAlbum',
+    }),
+
+    withResolvedReference({
+      ref: '#sameNameSingleReference',
+      find: soupyFind.input('albumSinglesOnly'),
+      findOptions: input.value({
+        fuzz: {
+          capitalization: true,
+          kebab: true,
+        },
+      }),
+    }).outputs({
+      '#resolvedReference': '#sameNameSingle',
+    }),
+
+    {
+      dependencies: ['#sameNameSingle'],
+      compute: (continuation, {
+        ['#sameNameSingle']: sameNameSingle,
+      }) =>
+        (sameNameSingle
+          ? continuation.raiseOutput({
+              ['#mainRelease']:
+                sameNameSingle,
+            })
+          : continuation()),
+    },
+
+    {
       dependencies: [
-        input.myself(),
-        input('selfIfMain'),
-        '#availability',
+        '#matchingTrack',
+        '#matchingAlbum',
+        input('notFoundValue'),
       ],
 
       compute: (continuation, {
-        [input.myself()]: track,
-        [input('selfIfMain')]: selfIfMain,
-        '#availability': availability,
+        ['#matchingTrack']: matchingTrack,
+        ['#matchingAlbum']: matchingAlbum,
+        [input('notFoundValue')]: notFoundValue,
       }) =>
-        (availability
+        (matchingTrack && matchingAlbum
           ? continuation()
-          : continuation.raiseOutput({
+       : matchingTrack ?? matchingAlbum
+          ? continuation.raiseOutput({
               ['#mainRelease']:
-                (selfIfMain ? track : null),
-            })),
+                matchingTrack ?? matchingAlbum,
+            })
+          : continuation.exit(notFoundValue)),
     },
 
-    withResolvedReference({
-      ref: 'mainReleaseTrack',
-      find: soupyFind.input('track'),
-    }),
-
-    exitWithoutDependency({
-      dependency: '#resolvedReference',
-      value: input('notFoundValue'),
+    withPropertyFromObject({
+      object: '#matchingAlbum',
+      property: input.value('tracks'),
     }),
 
     {
-      dependencies: ['#resolvedReference'],
+      dependencies: [
+        '#matchingAlbum.tracks',
+        '#matchingTrack',
+        input('notFoundValue'),
+      ],
 
       compute: (continuation, {
-        ['#resolvedReference']: resolvedReference,
+        ['#matchingAlbum.tracks']: matchingAlbumTracks,
+        ['#matchingTrack']: matchingTrack,
+        [input('notFoundValue')]: notFoundValue,
       }) =>
-        continuation({
-          ['#mainRelease']: resolvedReference,
-        }),
+        (matchingAlbumTracks.includes(matchingTrack)
+          ? continuation.raiseOutput({'#mainRelease': matchingTrack})
+          : continuation.exit(notFoundValue)),
     },
   ],
 });
diff --git a/src/data/composite/things/track/withMainReleaseTrack.js b/src/data/composite/things/track/withMainReleaseTrack.js
new file mode 100644
index 00000000..6371e895
--- /dev/null
+++ b/src/data/composite/things/track/withMainReleaseTrack.js
@@ -0,0 +1,248 @@
+// Just provides 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 {onlyItem} from '#sugar';
+import {getKebabCase} from '#wiki-data';
+
+import {
+  exitWithoutDependency,
+  withAvailabilityFilter,
+  withResultOfAvailabilityCheck,
+} from '#composite/control-flow';
+
+import {
+  withFilteredList,
+  withMappedList,
+  withPropertyFromList,
+  withPropertyFromObject,
+} from '#composite/data';
+
+import withMainRelease from './withMainRelease.js';
+
+export default templateCompositeFrom({
+  annotation: `withMainReleaseTrack`,
+
+  inputs: {
+    selfIfMain: input({type: 'boolean', defaultValue: false}),
+    notFoundValue: input({defaultValue: null}),
+  },
+
+  outputs: ['#mainReleaseTrack'],
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: 'mainRelease',
+    }),
+
+    {
+      dependencies: [
+        input.myself(),
+        input('selfIfMain'),
+        '#availability',
+      ],
+
+      compute: (continuation, {
+        [input.myself()]: track,
+        [input('selfIfMain')]: selfIfMain,
+        '#availability': availability,
+      }) =>
+        (availability
+          ? continuation()
+          : continuation.raiseOutput({
+              ['#mainReleaseTrack']:
+                (selfIfMain ? track : null),
+            })),
+    },
+
+    withMainRelease(),
+
+    exitWithoutDependency({
+      dependency: '#mainRelease',
+      value: input('notFoundValue'),
+    }),
+
+    withPropertyFromObject({
+      object: '#mainRelease',
+      property: input.value('isTrack'),
+    }),
+
+    {
+      dependencies: ['#mainRelease', '#mainRelease.isTrack'],
+
+      compute: (continuation, {
+        ['#mainRelease']: mainRelease,
+        ['#mainRelease.isTrack']: mainReleaseIsTrack,
+      }) =>
+        (mainReleaseIsTrack
+          ? continuation.raiseOutput({
+              ['#mainReleaseTrack']: mainRelease,
+            })
+          : continuation()),
+    },
+
+    {
+      dependencies: ['name', 'directory'],
+      compute: (continuation, {
+        ['name']: ownName,
+        ['directory']: ownDirectory,
+      }) => {
+        const ownNameKebabed = getKebabCase(ownName);
+
+        return continuation({
+          ['#mapItsNameLikeName']:
+            name => getKebabCase(name) === ownNameKebabed,
+
+          ['#mapItsDirectoryLikeDirectory']:
+            (ownDirectory
+              ? directory => directory === ownDirectory
+              : () => false),
+
+          ['#mapItsNameLikeDirectory']:
+            (ownDirectory
+              ? name => getKebabCase(name) === ownDirectory
+              : () => false),
+
+          ['#mapItsDirectoryLikeName']:
+            directory => directory === ownNameKebabed,
+        });
+      },
+    },
+
+    withPropertyFromObject({
+      object: '#mainRelease',
+      property: input.value('tracks'),
+    }),
+
+    withPropertyFromList({
+      list: '#mainRelease.tracks',
+      property: input.value('mainRelease'),
+      internal: input.value(true),
+    }),
+
+    withAvailabilityFilter({
+      from: '#mainRelease.tracks.mainRelease',
+    }),
+
+    withMappedList({
+      list: '#availabilityFilter',
+      map: input.value(item => !item),
+    }).outputs({
+      '#mappedList': '#availabilityFilter',
+    }),
+
+    withFilteredList({
+      list: '#mainRelease.tracks',
+      filter: '#availabilityFilter',
+    }).outputs({
+      '#filteredList': '#mainRelease.tracks',
+    }),
+
+    withPropertyFromList({
+      list: '#mainRelease.tracks',
+      property: input.value('name'),
+    }),
+
+    withPropertyFromList({
+      list: '#mainRelease.tracks',
+      property: input.value('directory'),
+      internal: input.value(true),
+    }),
+
+    withMappedList({
+      list: '#mainRelease.tracks.name',
+      map: '#mapItsNameLikeName',
+    }).outputs({
+      '#mappedList': '#filterItsNameLikeName',
+    }),
+
+    withMappedList({
+      list: '#mainRelease.tracks.directory',
+      map: '#mapItsDirectoryLikeDirectory',
+    }).outputs({
+      '#mappedList': '#filterItsDirectoryLikeDirectory',
+    }),
+
+    withMappedList({
+      list: '#mainRelease.tracks.name',
+      map: '#mapItsNameLikeDirectory',
+    }).outputs({
+      '#mappedList': '#filterItsNameLikeDirectory',
+    }),
+
+    withMappedList({
+      list: '#mainRelease.tracks.directory',
+      map: '#mapItsDirectoryLikeName',
+    }).outputs({
+      '#mappedList': '#filterItsDirectoryLikeName',
+    }),
+
+    withFilteredList({
+      list: '#mainRelease.tracks',
+      filter: '#filterItsNameLikeName',
+    }).outputs({
+      '#filteredList': '#matchingItsNameLikeName',
+    }),
+
+    withFilteredList({
+      list: '#mainRelease.tracks',
+      filter: '#filterItsDirectoryLikeDirectory',
+    }).outputs({
+      '#filteredList': '#matchingItsDirectoryLikeDirectory',
+    }),
+
+    withFilteredList({
+      list: '#mainRelease.tracks',
+      filter: '#filterItsNameLikeDirectory',
+    }).outputs({
+      '#filteredList': '#matchingItsNameLikeDirectory',
+    }),
+
+    withFilteredList({
+      list: '#mainRelease.tracks',
+      filter: '#filterItsDirectoryLikeName',
+    }).outputs({
+      '#filteredList': '#matchingItsDirectoryLikeName',
+    }),
+
+    {
+      dependencies: [
+        '#matchingItsNameLikeName',
+        '#matchingItsDirectoryLikeDirectory',
+        '#matchingItsNameLikeDirectory',
+        '#matchingItsDirectoryLikeName',
+      ],
+
+      compute: (continuation, {
+        ['#matchingItsNameLikeName']:           NLN,
+        ['#matchingItsDirectoryLikeDirectory']: DLD,
+        ['#matchingItsNameLikeDirectory']:      NLD,
+        ['#matchingItsDirectoryLikeName']:      DLN,
+      }) => continuation({
+        ['#mainReleaseTrack']:
+          onlyItem(DLD) ??
+          onlyItem(NLN) ??
+          onlyItem(DLN) ??
+          onlyItem(NLD) ??
+          null,
+      }),
+    },
+
+    {
+      dependencies: ['#mainReleaseTrack', input.myself()],
+
+      compute: (continuation, {
+        ['#mainReleaseTrack']: mainReleaseTrack,
+        [input.myself()]: thisTrack,
+      }) => continuation({
+        ['#mainReleaseTrack']:
+          (mainReleaseTrack === thisTrack
+            ? null
+            : mainReleaseTrack),
+      }),
+    },
+  ],
+});
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/withPropertyFromMainRelease.js b/src/data/composite/things/track/withPropertyFromMainRelease.js
index 393a4c63..c6f65653 100644
--- a/src/data/composite/things/track/withPropertyFromMainRelease.js
+++ b/src/data/composite/things/track/withPropertyFromMainRelease.js
@@ -10,10 +10,10 @@ import {input, templateCompositeFrom} from '#composite';
 import {withResultOfAvailabilityCheck} from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
 
-import withMainRelease from './withMainRelease.js';
+import withMainReleaseTrack from './withMainReleaseTrack.js';
 
 export default templateCompositeFrom({
-  annotation: `inheritFromMainRelease`,
+  annotation: `withPropertyFromMainRelease`,
 
   inputs: {
     property: input({type: 'string'}),
@@ -32,12 +32,12 @@ export default templateCompositeFrom({
         : ['#mainReleaseValue'])),
 
   steps: () => [
-    withMainRelease({
+    withMainReleaseTrack({
       notFoundValue: input('notFoundValue'),
     }),
 
     withResultOfAvailabilityCheck({
-      from: '#mainRelease',
+      from: '#mainReleaseTrack',
     }),
 
     {
@@ -61,7 +61,7 @@ export default templateCompositeFrom({
     },
 
     withPropertyFromObject({
-      object: '#mainRelease',
+      object: '#mainReleaseTrack',
       property: input('property'),
     }),
 
diff --git a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
index 7159a3f4..30c777b6 100644
--- a/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
+++ b/src/data/composite/things/track/withSuffixDirectoryFromAlbum.js
@@ -1,8 +1,9 @@
 import {input, templateCompositeFrom} from '#composite';
 
 import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
 
-import withPropertyFromAlbum from './withPropertyFromAlbum.js';
+import withContainingTrackSection from './withContainingTrackSection.js';
 
 export default templateCompositeFrom({
   annotation: `withSuffixDirectoryFromAlbum`,
@@ -36,18 +37,13 @@ export default templateCompositeFrom({
           : continuation()),
     },
 
-    withPropertyFromAlbum({
+    withContainingTrackSection(),
+
+    withPropertyFromObject({
+      object: '#trackSection',
       property: input.value('suffixTrackDirectories'),
+    }).outputs({
+      '#trackSection.suffixTrackDirectories': '#suffixDirectoryFromAlbum',
     }),
-
-    {
-      dependencies: ['#album.suffixTrackDirectories'],
-      compute: (continuation, {
-        ['#album.suffixTrackDirectories']: suffixTrackDirectories,
-      }) => continuation({
-        ['#suffixDirectoryFromAlbum']:
-          suffixTrackDirectories,
-      }),
-    },
   ],
 });
diff --git a/src/data/composite/wiki-data/exitWithoutArtwork.js b/src/data/composite/wiki-data/exitWithoutArtwork.js
new file mode 100644
index 00000000..8e799fda
--- /dev/null
+++ b/src/data/composite/wiki-data/exitWithoutArtwork.js
@@ -0,0 +1,45 @@
+import {input, templateCompositeFrom} from '#composite';
+import {isContributionList, isThing, strictArrayOf} from '#validators';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+
+import withHasArtwork from './withHasArtwork.js';
+
+export default templateCompositeFrom({
+  annotation: `exitWithoutArtwork`,
+
+  inputs: {
+    contribs: input({
+      validate: isContributionList,
+      defaultValue: null,
+    }),
+
+    artwork: input({
+      validate: isThing,
+      defaultValue: null,
+    }),
+
+    artworks: input({
+      validate: strictArrayOf(isThing),
+      defaultValue: null,
+    }),
+
+    value: input({
+      defaultValue: null,
+    }),
+  },
+
+  steps: () => [
+    withHasArtwork({
+      contribs: input('contribs'),
+      artwork: input('artwork'),
+      artworks: input('artworks'),
+    }),
+
+    exitWithoutDependency({
+      dependency: '#hasArtwork',
+      mode: input.value('falsy'),
+      value: input('value'),
+    }),
+  ],
+});
diff --git a/src/data/composite/wiki-data/index.js b/src/data/composite/wiki-data/index.js
index 38afc2ac..d70d7c56 100644
--- a/src/data/composite/wiki-data/index.js
+++ b/src/data/composite/wiki-data/index.js
@@ -5,8 +5,10 @@
 //
 
 export {default as exitWithoutContribs} from './exitWithoutContribs.js';
+export {default as exitWithoutArtwork} from './exitWithoutArtwork.js';
 export {default as gobbleSoupyFind} from './gobbleSoupyFind.js';
 export {default as gobbleSoupyReverse} from './gobbleSoupyReverse.js';
+export {default as inputFindOptions} from './inputFindOptions.js';
 export {default as inputNotFoundMode} from './inputNotFoundMode.js';
 export {default as inputSoupyFind} from './inputSoupyFind.js';
 export {default as inputSoupyReverse} from './inputSoupyReverse.js';
@@ -16,8 +18,8 @@ export {default as withClonedThings} from './withClonedThings.js';
 export {default as withConstitutedArtwork} from './withConstitutedArtwork.js';
 export {default as withContentNodes} from './withContentNodes.js';
 export {default as withContributionListSums} from './withContributionListSums.js';
-export {default as withCoverArtDate} from './withCoverArtDate.js';
 export {default as withDirectory} from './withDirectory.js';
+export {default as withHasArtwork} from './withHasArtwork.js';
 export {default as withRecontextualizedContributionList} from './withRecontextualizedContributionList.js';
 export {default as withRedatedContributionList} from './withRedatedContributionList.js';
 export {default as withResolvedAnnotatedReferenceList} from './withResolvedAnnotatedReferenceList.js';
diff --git a/src/data/composite/wiki-data/inputFindOptions.js b/src/data/composite/wiki-data/inputFindOptions.js
new file mode 100644
index 00000000..07ed4bce
--- /dev/null
+++ b/src/data/composite/wiki-data/inputFindOptions.js
@@ -0,0 +1,5 @@
+import {input} from '#composite';
+
+export default function inputFindOptions() {
+  return input({type: 'object', defaultValue: null});
+}
diff --git a/src/data/composite/wiki-data/withConstitutedArtwork.js b/src/data/composite/wiki-data/withConstitutedArtwork.js
index 6187d55b..28d719e2 100644
--- a/src/data/composite/wiki-data/withConstitutedArtwork.js
+++ b/src/data/composite/wiki-data/withConstitutedArtwork.js
@@ -1,6 +1,5 @@
 import {input, templateCompositeFrom} from '#composite';
 import thingConstructors from '#things';
-import {isContributionList} from '#validators';
 
 export default templateCompositeFrom({
   annotation: `withConstitutedArtwork`,
diff --git a/src/data/composite/things/album/withHasCoverArt.js b/src/data/composite/wiki-data/withHasArtwork.js
index fd3f2894..9c22f439 100644
--- a/src/data/composite/things/album/withHasCoverArt.js
+++ b/src/data/composite/wiki-data/withHasArtwork.js
@@ -1,7 +1,5 @@
-// 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 {isContributionList, isThing, strictArrayOf} from '#validators';
 
 import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck}
   from '#composite/control-flow';
@@ -9,13 +7,30 @@ import {fillMissingListItems, withFlattenedList, withPropertyFromList}
   from '#composite/data';
 
 export default templateCompositeFrom({
-  annotation: 'withHasCoverArt',
+  annotation: 'withHasArtwork',
+
+  inputs: {
+    contribs: input({
+      validate: isContributionList,
+      defaultValue: null,
+    }),
+
+    artwork: input({
+      validate: isThing,
+      defaultValue: null,
+    }),
+
+    artworks: input({
+      validate: strictArrayOf(isThing),
+      defaultValue: null,
+    }),
+  },
 
-  outputs: ['#hasCoverArt'],
+  outputs: ['#hasArtwork'],
 
   steps: () => [
     withResultOfAvailabilityCheck({
-      from: 'coverArtistContribs',
+      from: input('contribs'),
       mode: input.value('empty'),
     }),
 
@@ -26,19 +41,37 @@ export default templateCompositeFrom({
       }) =>
         (availability
           ? continuation.raiseOutput({
-              ['#hasCoverArt']: true,
+              ['#hasArtwork']: true,
             })
           : continuation()),
     },
 
+    {
+      dependencies: [input('artwork'), input('artworks')],
+      compute: (continuation, {
+        [input('artwork')]: artwork,
+        [input('artworks')]: artworks,
+      }) =>
+        continuation({
+          ['#artworks']:
+            (artwork && artworks
+              ? [artwork, ...artworks]
+           : artwork
+              ? [artwork]
+           : artworks
+              ? artworks
+              : []),
+        }),
+    },
+
     raiseOutputWithoutDependency({
-      dependency: 'coverArtworks',
+      dependency: '#artworks',
       mode: input.value('empty'),
-      output: input.value({'#hasCoverArt': false}),
+      output: input.value({'#hasArtwork': false}),
     }),
 
     withPropertyFromList({
-      list: 'coverArtworks',
+      list: '#artworks',
       property: input.value('artistContribs'),
       internal: input.value(true),
     }),
@@ -46,19 +79,19 @@ export default templateCompositeFrom({
     // 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',
+      list: '#artworks.artistContribs',
       fill: input.value([]),
     }),
 
     withFlattenedList({
-      list: '#coverArtworks.artistContribs',
+      list: '#artworks.artistContribs',
     }),
 
     withResultOfAvailabilityCheck({
       from: '#flattenedList',
       mode: input.value('empty'),
     }).outputs({
-      '#availability': '#hasCoverArt',
+      '#availability': '#hasArtwork',
     }),
   ],
 });
diff --git a/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js
index 9cc52f29..670dc422 100644
--- a/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js
+++ b/src/data/composite/wiki-data/withResolvedAnnotatedReferenceList.js
@@ -7,6 +7,7 @@ import {withPropertyFromList} from '#composite/data';
 import {raiseOutputWithoutDependency, withAvailabilityFilter}
   from '#composite/control-flow';
 
+import inputFindOptions from './inputFindOptions.js';
 import inputSoupyFind from './inputSoupyFind.js';
 import inputNotFoundMode from './inputNotFoundMode.js';
 import inputWikiData from './inputWikiData.js';
@@ -28,6 +29,7 @@ export default templateCompositeFrom({
 
     data: inputWikiData({allowMixedTypes: true}),
     find: inputSoupyFind(),
+    findOptions: inputFindOptions(),
 
     notFoundMode: inputNotFoundMode(),
   },
@@ -61,6 +63,7 @@ export default templateCompositeFrom({
       list: '#references',
       data: input('data'),
       find: input('find'),
+      findOptions: input('findOptions'),
       notFoundMode: input.value('null'),
     }),
 
diff --git a/src/data/composite/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js
index 6f422194..d9a05367 100644
--- a/src/data/composite/wiki-data/withResolvedReference.js
+++ b/src/data/composite/wiki-data/withResolvedReference.js
@@ -8,6 +8,7 @@ import {input, templateCompositeFrom} from '#composite';
 import {raiseOutputWithoutDependency} from '#composite/control-flow';
 
 import gobbleSoupyFind from './gobbleSoupyFind.js';
+import inputFindOptions from './inputFindOptions.js';
 import inputSoupyFind from './inputSoupyFind.js';
 import inputWikiData from './inputWikiData.js';
 
@@ -17,8 +18,9 @@ export default templateCompositeFrom({
   inputs: {
     ref: input({type: 'string', acceptsNull: true}),
 
-    data: inputWikiData({allowMixedTypes: false}),
+    data: inputWikiData({allowMixedTypes: true}),
     find: inputSoupyFind(),
+    findOptions: inputFindOptions(),
   },
 
   outputs: ['#resolvedReference'],
@@ -36,21 +38,35 @@ export default templateCompositeFrom({
     }),
 
     {
+      dependencies: [input('findOptions')],
+      compute: (continuation, {
+        [input('findOptions')]: findOptions,
+      }) => continuation({
+        ['#findOptions']:
+          (findOptions
+            ? {...findOptions, mode: 'quiet'}
+            : {mode: 'quiet'}),
+      }),
+    },
+
+    {
       dependencies: [
         input('ref'),
         input('data'),
         '#find',
+        '#findOptions',
       ],
 
       compute: (continuation, {
         [input('ref')]: ref,
         [input('data')]: data,
         ['#find']: findFunction,
+        ['#findOptions']: findOptions,
       }) => continuation({
         ['#resolvedReference']:
           (data
-            ? findFunction(ref, data, {mode: 'quiet'}) ?? null
-            : findFunction(ref, {mode: 'quiet'}) ?? null),
+            ? findFunction(ref, data, findOptions) ?? null
+            : findFunction(ref, findOptions) ?? null),
       }),
     },
   ],
diff --git a/src/data/composite/wiki-data/withResolvedReferenceList.js b/src/data/composite/wiki-data/withResolvedReferenceList.js
index 9dc960dd..14ce6919 100644
--- a/src/data/composite/wiki-data/withResolvedReferenceList.js
+++ b/src/data/composite/wiki-data/withResolvedReferenceList.js
@@ -11,6 +11,7 @@ import {raiseOutputWithoutDependency, withAvailabilityFilter}
 import {withMappedList} from '#composite/data';
 
 import gobbleSoupyFind from './gobbleSoupyFind.js';
+import inputFindOptions from './inputFindOptions.js';
 import inputNotFoundMode from './inputNotFoundMode.js';
 import inputSoupyFind from './inputSoupyFind.js';
 import inputWikiData from './inputWikiData.js';
@@ -27,6 +28,7 @@ export default templateCompositeFrom({
 
     data: inputWikiData({allowMixedTypes: true}),
     find: inputSoupyFind(),
+    findOptions: inputFindOptions(),
 
     notFoundMode: inputNotFoundMode(),
   },
@@ -47,15 +49,28 @@ export default templateCompositeFrom({
     }),
 
     {
-      dependencies: [input('data'), '#find'],
+      dependencies: [input('findOptions')],
+      compute: (continuation, {
+        [input('findOptions')]: findOptions,
+      }) => continuation({
+        ['#findOptions']:
+          (findOptions
+            ? {...findOptions, mode: 'quiet'}
+            : {mode: 'quiet'}),
+      }),
+    },
+
+    {
+      dependencies: [input('data'), '#find', '#findOptions'],
       compute: (continuation, {
         [input('data')]: data,
         ['#find']: findFunction,
+        ['#findOptions']: findOptions,
       }) => continuation({
         ['#map']:
           (data
-            ? ref => findFunction(ref, data, {mode: 'quiet'})
-            : ref => findFunction(ref, {mode: 'quiet'})),
+            ? ref => findFunction(ref, data, findOptions)
+            : ref => findFunction(ref, findOptions)),
       }),
     },
 
diff --git a/src/data/composite/wiki-properties/annotatedReferenceList.js b/src/data/composite/wiki-properties/annotatedReferenceList.js
index 8e6c96a1..aea0f22c 100644
--- a/src/data/composite/wiki-properties/annotatedReferenceList.js
+++ b/src/data/composite/wiki-properties/annotatedReferenceList.js
@@ -9,8 +9,13 @@ import {
 } from '#validators';
 
 import {exposeDependency} from '#composite/control-flow';
-import {inputSoupyFind, inputWikiData, withResolvedAnnotatedReferenceList}
-  from '#composite/wiki-data';
+
+import {
+  inputFindOptions,
+  inputSoupyFind,
+  inputWikiData,
+  withResolvedAnnotatedReferenceList,
+} from '#composite/wiki-data';
 
 import {referenceListInputDescriptions, referenceListUpdateDescription}
   from './helpers/reference-list-helpers.js';
@@ -25,6 +30,7 @@ export default templateCompositeFrom({
 
     data: inputWikiData({allowMixedTypes: true}),
     find: inputSoupyFind(),
+    findOptions: inputFindOptions(),
 
     reference: input.staticValue({type: 'string', defaultValue: 'reference'}),
     annotation: input.staticValue({type: 'string', defaultValue: 'annotation'}),
@@ -57,6 +63,7 @@ export default templateCompositeFrom({
 
       data: input('data'),
       find: input('find'),
+      findOptions: input('findOptions'),
     }),
 
     exposeDependency({dependency: '#resolvedAnnotatedReferenceList'}),
diff --git a/src/data/composite/wiki-properties/canonicalBase.js b/src/data/composite/wiki-properties/canonicalBase.js
new file mode 100644
index 00000000..81740d6c
--- /dev/null
+++ b/src/data/composite/wiki-properties/canonicalBase.js
@@ -0,0 +1,16 @@
+import {isURL} from '#validators';
+
+export default function() {
+  return {
+    flags: {update: true, expose: true},
+    update: {validate: isURL},
+    expose: {
+      transform: (value) =>
+        (value === null
+          ? null
+       : value.endsWith('/')
+          ? value
+          : value + '/'),
+    },
+  };
+}
diff --git a/src/data/composite/wiki-properties/index.js b/src/data/composite/wiki-properties/index.js
index e8f109d3..57a2b8f2 100644
--- a/src/data/composite/wiki-properties/index.js
+++ b/src/data/composite/wiki-properties/index.js
@@ -4,6 +4,7 @@
 // #composite/data, and #composite/wiki-data.
 
 export {default as annotatedReferenceList} from './annotatedReferenceList.js';
+export {default as canonicalBase} from './canonicalBase.js';
 export {default as color} from './color.js';
 export {default as commentatorArtists} from './commentatorArtists.js';
 export {default as constitutibleArtwork} from './constitutibleArtwork.js';
diff --git a/src/data/composite/wiki-properties/referenceList.js b/src/data/composite/wiki-properties/referenceList.js
index 4f8207b5..663349ee 100644
--- a/src/data/composite/wiki-properties/referenceList.js
+++ b/src/data/composite/wiki-properties/referenceList.js
@@ -11,8 +11,13 @@ import {input, templateCompositeFrom} from '#composite';
 import {validateReferenceList} from '#validators';
 
 import {exposeDependency} from '#composite/control-flow';
-import {inputSoupyFind, inputWikiData, withResolvedReferenceList}
-  from '#composite/wiki-data';
+
+import {
+  inputFindOptions,
+  inputSoupyFind,
+  inputWikiData,
+  withResolvedReferenceList,
+} from '#composite/wiki-data';
 
 import {referenceListInputDescriptions, referenceListUpdateDescription}
   from './helpers/reference-list-helpers.js';
@@ -27,6 +32,7 @@ export default templateCompositeFrom({
 
     data: inputWikiData({allowMixedTypes: true}),
     find: inputSoupyFind(),
+    findOptions: inputFindOptions(),
   },
 
   update:
@@ -39,6 +45,7 @@ export default templateCompositeFrom({
       list: input.updateValue(),
       data: input('data'),
       find: input('find'),
+      findOptions: input('findOptions'),
     }),
 
     exposeDependency({dependency: '#resolvedReferenceList'}),
diff --git a/src/data/composite/wiki-properties/referencedArtworkList.js b/src/data/composite/wiki-properties/referencedArtworkList.js
index 9ba2e393..4f243493 100644
--- a/src/data/composite/wiki-properties/referencedArtworkList.js
+++ b/src/data/composite/wiki-properties/referencedArtworkList.js
@@ -1,6 +1,5 @@
 import {input, templateCompositeFrom} from '#composite';
 import find from '#find';
-import {isDate} from '#validators';
 
 import annotatedReferenceList from './annotatedReferenceList.js';
 
diff --git a/src/data/composite/wiki-properties/singleReference.js b/src/data/composite/wiki-properties/singleReference.js
index f532ebbe..25b97907 100644
--- a/src/data/composite/wiki-properties/singleReference.js
+++ b/src/data/composite/wiki-properties/singleReference.js
@@ -8,11 +8,19 @@
 //
 
 import {input, templateCompositeFrom} from '#composite';
-import {isThingClass, validateReference} from '#validators';
+import {validateReference} from '#validators';
 
 import {exposeDependency} from '#composite/control-flow';
-import {inputSoupyFind, inputWikiData, withResolvedReference}
-  from '#composite/wiki-data';
+
+import {
+  inputFindOptions,
+  inputSoupyFind,
+  inputWikiData,
+  withResolvedReference,
+} from '#composite/wiki-data';
+
+import {referenceListInputDescriptions, referenceListUpdateDescription}
+  from './helpers/reference-list-helpers.js';
 
 export default templateCompositeFrom({
   annotation: `singleReference`,
@@ -20,25 +28,24 @@ export default templateCompositeFrom({
   compose: false,
 
   inputs: {
-    class: input.staticValue({validate: isThingClass}),
+    ...referenceListInputDescriptions(),
 
+    data: inputWikiData({allowMixedTypes: true}),
     find: inputSoupyFind(),
-    data: inputWikiData({allowMixedTypes: false}),
+    findOptions: inputFindOptions(),
   },
 
-  update: ({
-    [input.staticValue('class')]: thingClass,
-  }) => ({
-    validate:
-      validateReference(
-        thingClass[Symbol.for('Thing.referenceType')]),
-  }),
+  update:
+    referenceListUpdateDescription({
+      validateReferenceList: validateReference,
+    }),
 
   steps: () => [
     withResolvedReference({
       ref: input.updateValue(),
       data: input('data'),
       find: input('find'),
+      findOptions: input('findOptions'),
     }),
 
     exposeDependency({dependency: '#resolvedReference'}),