« get me outta code hell

data: Track.mainRelease, "Main Release: <album or track>" - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2025-10-02 20:35:34 -0300
committer(quasar) nebula <qznebula@protonmail.com>2025-10-02 20:35:34 -0300
commitca412ca52b1998fe715951309d3e0546560c2c58 (patch)
tree4629d6b9f2e96cee427a0a641fa4472bbb6f78f6 /src
parentbfc4a8cb5deb69d3692837f8ede95089a01bef44 (diff)
data: Track.mainRelease, "Main Release: <album or track>"
Diffstat (limited to 'src')
-rw-r--r--src/data/checks.js53
-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.js18
-rw-r--r--src/data/composite/things/track/withAlwaysReferenceByDirectory.js98
-rw-r--r--src/data/composite/things/track/withMainRelease.js90
-rw-r--r--src/data/composite/things/track/withMainReleaseTrack.js214
-rw-r--r--src/data/composite/things/track/withPropertyFromMainRelease.js8
-rw-r--r--src/data/composite/wiki-data/withResolvedReference.js2
-rw-r--r--src/data/composite/wiki-properties/singleReference.js20
-rw-r--r--src/data/things/track.js64
-rw-r--r--src/data/yaml.js1
12 files changed, 457 insertions, 183 deletions
diff --git a/src/data/checks.js b/src/data/checks.js
index 5eba593b..edfd7e5b 100644
--- a/src/data/checks.js
+++ b/src/data/checks.js
@@ -237,7 +237,7 @@ export function filterReferenceErrors(wikiData, {
       sampledTracks: '_trackMainReleasesOnly',
       artTags: '_artTag',
       referencedArtworks: '_artwork',
-      mainReleaseTrack: '_trackMainReleasesOnly',
+      mainRelease: '_mainRelease',
       commentary: '_content',
       creditingSources: '_content',
       referencingSources: '_content',
@@ -346,6 +346,55 @@ export function filterReferenceErrors(wikiData, {
                 };
                 break;
 
+              case '_mainRelease':
+                findFn = ref => {
+                  // Mocking what's going on in `withMainRelease`.
+
+                  let track, trackError;
+                  let album, albumError;
+
+                  try {
+                    track = boundFind.trackMainReleasesOnly(ref);
+                  } catch (caughtError) {
+                    trackError = new Error(
+                      `Didn't match a track`, {cause: caughtError});
+                  }
+
+                  try {
+                    album = boundFind.album(ref);
+                  } catch (caughtError) {
+                    albumError = new Error(
+                      `Didn't match an album`, {cause: caughtError});
+                  }
+
+                  if (track && album) {
+                    if (album.tracks.includes(track)) {
+                      return track;
+                    } else {
+                      throw new Error(
+                        `Unrelated album and track matched for reference "${ref}". Please resolve:\n` +
+                        `- ${inspect(track)}\n` +
+                        `- ${inspect(album)}\n` +
+                        `Returning null for this reference.`);
+                    }
+                  }
+
+                  if (track ?? album) {
+                    return track ?? album;
+                  }
+
+                  const aggregateCause =
+                    new AggregateError([albumError, trackError]);
+
+                  aggregateCause[Symbol.for('hsmusic.aggregate.translucent')] = true;
+
+                  throw new Error(`Trouble matching "${ref}"`, {
+                    cause: aggregateCause,
+                  });
+                }
+
+                break;
+
               case '_trackArtwork':
                 findFn = ref => boundFind.track(ref.reference);
                 break;
@@ -353,7 +402,7 @@ export function filterReferenceErrors(wikiData, {
               case '_trackMainReleasesOnly':
                 findFn = trackRef => {
                   const track = boundFind.track(trackRef);
-                  const mainRef = track && CacheableObject.getUpdateValue(track, 'mainReleaseTrack');
+                  const mainRef = track && CacheableObject.getUpdateValue(track, 'mainRelease');
 
                   if (mainRef) {
                     // It's possible for the main release to not actually exist, in this case.
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 891db102..bd54384f 100644
--- a/src/data/composite/things/track/withAllReleases.js
+++ b/src/data/composite/things/track/withAllReleases.js
@@ -10,7 +10,7 @@ import {sortByDate} from '#sort';
 
 import {withPropertyFromObject} from '#composite/data';
 
-import withMainRelease from './withMainRelease.js';
+import withMainReleaseTrack from './withMainReleaseTrack.js';
 
 export default templateCompositeFrom({
   annotation: `withAllReleases`,
@@ -18,7 +18,7 @@ export default templateCompositeFrom({
   outputs: ['#allReleases'],
 
   steps: () => [
-    withMainRelease({
+    withMainReleaseTrack({
       selfIfMain: input.value(true),
       notFoundValue: input.value([]),
     }),
@@ -28,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 c6545ca9..00000000
--- a/src/data/composite/things/track/withAlwaysReferenceByDirectory.js
+++ /dev/null
@@ -1,98 +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 {getKebabCase} from '#wiki-data';
-
-import {withPropertyFromObject} from '#composite/data';
-import {withResolvedReference} from '#composite/wiki-data';
-
-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']:
-          getKebabCase(name) ===
-          getKebabCase(mainReleaseName),
-      }),
-    },
-  ],
-});
diff --git a/src/data/composite/things/track/withMainRelease.js b/src/data/composite/things/track/withMainRelease.js
index 3a91edae..b1f427eb 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,78 @@ 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}),
+    }),
+
+    withResolvedReference({
+      ref: input('from'),
+      find: soupyFind.input('trackMainReleasesOnly'),
+    }).outputs({
+      '#resolvedReference': '#matchingTrack',
+    }),
+
+    withResolvedReference({
+      ref: input('from'),
+      find: soupyFind.input('album'),
+    }).outputs({
+      '#resolvedReference': '#matchingAlbum',
     }),
 
     {
       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..fa678161
--- /dev/null
+++ b/src/data/composite/things/track/withMainReleaseTrack.js
@@ -0,0 +1,214 @@
+// 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 {getKebabCase} from '#wiki-data';
+
+import {exitWithoutDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
+
+import {
+  withFilteredList,
+  withMappedList,
+  withPropertyFromList,
+  withPropertyFromObject,
+} from '#composite/data';
+
+import withMainRelease from './withMainRelease.js';
+
+function onlyItem(array) {
+  if (array.length === 1) {
+    return array[0];
+  } else {
+    return null;
+  }
+}
+
+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('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,
+      }),
+    },
+  ],
+});
diff --git a/src/data/composite/things/track/withPropertyFromMainRelease.js b/src/data/composite/things/track/withPropertyFromMainRelease.js
index 393a4c63..3e1b6d19 100644
--- a/src/data/composite/things/track/withPropertyFromMainRelease.js
+++ b/src/data/composite/things/track/withPropertyFromMainRelease.js
@@ -10,7 +10,7 @@ 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`,
@@ -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/wiki-data/withResolvedReference.js b/src/data/composite/wiki-data/withResolvedReference.js
index 6f422194..0786353e 100644
--- a/src/data/composite/wiki-data/withResolvedReference.js
+++ b/src/data/composite/wiki-data/withResolvedReference.js
@@ -17,7 +17,7 @@ export default templateCompositeFrom({
   inputs: {
     ref: input({type: 'string', acceptsNull: true}),
 
-    data: inputWikiData({allowMixedTypes: false}),
+    data: inputWikiData({allowMixedTypes: true}),
     find: inputSoupyFind(),
   },
 
diff --git a/src/data/composite/wiki-properties/singleReference.js b/src/data/composite/wiki-properties/singleReference.js
index f532ebbe..2435dd2d 100644
--- a/src/data/composite/wiki-properties/singleReference.js
+++ b/src/data/composite/wiki-properties/singleReference.js
@@ -8,31 +8,31 @@
 //
 
 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 {referenceListInputDescriptions, referenceListUpdateDescription}
+  from './helpers/reference-list-helpers.js';
+
 export default templateCompositeFrom({
   annotation: `singleReference`,
 
   compose: false,
 
   inputs: {
-    class: input.staticValue({validate: isThingClass}),
+    ...referenceListInputDescriptions(),
 
+    data: inputWikiData({allowMixedTypes: true}),
     find: inputSoupyFind(),
-    data: inputWikiData({allowMixedTypes: false}),
   },
 
-  update: ({
-    [input.staticValue('class')]: thingClass,
-  }) => ({
-    validate:
-      validateReference(
-        thingClass[Symbol.for('Thing.referenceType')]),
-  }),
+  update:
+    referenceListUpdateDescription({
+      validateReferenceList: validateReference,
+    }),
 
   steps: () => [
     withResolvedReference({
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 87eca2e9..110769e0 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -12,6 +12,7 @@ import {
   isContributionList,
   isDate,
   isFileExtension,
+  validateReference,
 } from '#validators';
 
 import {
@@ -60,7 +61,6 @@ import {
   reverseReferenceList,
   simpleDate,
   simpleString,
-  singleReference,
   soupyFind,
   soupyReverse,
   thing,
@@ -70,17 +70,18 @@ import {
 } from '#composite/wiki-properties';
 
 import {
+  alwaysReferenceByDirectory,
   exitWithoutUniqueCoverArt,
   inheritContributionListFromMainRelease,
   inheritFromMainRelease,
   withAllReleases,
-  withAlwaysReferenceByDirectory,
   withContainingTrackSection,
   withCoverArtistContribs,
   withDate,
   withDirectorySuffix,
   withHasUniqueCoverArt,
   withMainRelease,
+  withMainReleaseTrack,
   withOtherReleases,
   withPropertyFromAlbum,
   withSuffixDirectoryFromAlbum,
@@ -143,15 +144,23 @@ export class Track extends Thing {
       })
     ],
 
-    alwaysReferenceByDirectory: [
-      withAlwaysReferenceByDirectory(),
-      exposeDependency({dependency: '#alwaysReferenceByDirectory'}),
-    ],
+    alwaysReferenceByDirectory: alwaysReferenceByDirectory(),
 
-    mainReleaseTrack: singleReference({
-      class: input.value(Track),
-      find: soupyFind.input('track'),
-    }),
+    // Album or track. The exposed value is really just what's provided here,
+    // whether or not a matching track is found on a provided album, for
+    // example. When presenting or processing, read `mainReleaseTrack`.
+    mainRelease: [
+      withMainRelease({
+        from: input.updateValue({
+          validate:
+            validateReference(['album', 'track']),
+        }),
+      }),
+
+      exposeDependency({
+        dependency: '#mainRelease',
+      }),
+    ],
 
     bandcampTrackIdentifier: simpleString(),
     bandcampArtworkIdentifier: simpleString(),
@@ -453,11 +462,6 @@ export class Track extends Thing {
       class: input.value(Artwork),
     }),
 
-    // used for withAlwaysReferenceByDirectory (for some reason)
-    trackData: wikiData({
-      class: input.value(Track),
-    }),
-
     // used for withMatchingContributionPresets (indirectly by Contribution)
     wikiInfo: thing({
       class: input.value(WikiInfo),
@@ -489,19 +493,27 @@ export class Track extends Thing {
     ],
 
     isMainRelease: [
-      withMainRelease(),
+      withMainReleaseTrack(),
 
       exposeWhetherDependencyAvailable({
-        dependency: '#mainRelease',
+        dependency: '#mainReleaseTrack',
         negate: input.value(true),
       }),
     ],
 
     isSecondaryRelease: [
-      withMainRelease(),
+      withMainReleaseTrack(),
 
       exposeWhetherDependencyAvailable({
-        dependency: '#mainRelease',
+        dependency: '#mainReleaseTrack',
+      }),
+    ],
+
+    mainReleaseTrack: [
+      withMainReleaseTrack(),
+
+      exposeDependency({
+        dependency: '#mainReleaseTrack',
       }),
     ],
 
@@ -522,20 +534,20 @@ export class Track extends Thing {
     ],
 
     commentaryFromMainRelease: [
-      withMainRelease(),
+      withMainReleaseTrack(),
 
       exitWithoutDependency({
-        dependency: '#mainRelease',
+        dependency: '#mainReleaseTrack',
         value: input.value([]),
       }),
 
       withPropertyFromObject({
-        object: '#mainRelease',
+        object: '#mainReleaseTrack',
         property: input.value('commentary'),
       }),
 
       exposeDependency({
-        dependency: '#mainRelease.commentary',
+        dependency: '#mainReleaseTrack.commentary',
       }),
     ],
 
@@ -570,7 +582,7 @@ export class Track extends Thing {
       'Directory': {property: 'directory'},
       'Suffix Directory': {property: 'suffixDirectoryFromAlbum'},
       'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'},
-      'Main Release': {property: 'mainReleaseTrack'},
+      'Main Release': {property: 'mainRelease'},
 
       'Bandcamp Track ID': {
         property: 'bandcampTrackIdentifier',
@@ -797,7 +809,7 @@ export class Track extends Thing {
       bindTo: 'trackData',
 
       include: track =>
-        !CacheableObject.getUpdateValue(track, 'mainReleaseTrack'),
+        !CacheableObject.getUpdateValue(track, 'mainRelease'),
 
       // It's still necessary to check alwaysReferenceByDirectory here, since
       // it may be set manually (with `Always Reference By Directory: true`),
@@ -956,7 +968,7 @@ export class Track extends Thing {
 
     parts.push(Thing.prototype[inspect.custom].apply(this));
 
-    if (CacheableObject.getUpdateValue(this, 'mainReleaseTrack')) {
+    if (CacheableObject.getUpdateValue(this, 'mainRelease')) {
       parts.unshift(`${colors.yellow('[secrelease]')} `);
     }
 
diff --git a/src/data/yaml.js b/src/data/yaml.js
index abe3bde2..719a4d93 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -1695,7 +1695,6 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) {
 
     ['trackData', [
       'artworkData',
-      'trackData',
       'wikiInfo',
     ]],