« 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/things/track.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/things/track.js')
-rw-r--r--src/data/things/track.js1089
1 files changed, 808 insertions, 281 deletions
diff --git a/src/data/things/track.js b/src/data/things/track.js
index 557ba2a7..ab7511a8 100644
--- a/src/data/things/track.js
+++ b/src/data/things/track.js
@@ -2,10 +2,21 @@ import {inspect} from 'node:util';
 
 import CacheableObject from '#cacheable-object';
 import {colors} from '#cli';
-import {input} from '#composite';
+import {input, V} from '#composite';
+import {onlyItem} from '#sugar';
+import {sortByDate} from '#sort';
 import Thing from '#thing';
-import {isBoolean, isColor, isContributionList, isDate, isFileExtension}
-  from '#validators';
+import {getKebabCase} from '#wiki-data';
+
+import {
+  isBoolean,
+  isColor,
+  isContentString,
+  isContributionList,
+  isDate,
+  isFileExtension,
+  validateReference,
+} from '#validators';
 
 import {
   parseAdditionalFiles,
@@ -15,26 +26,41 @@ import {
   parseCommentary,
   parseContributors,
   parseCreditingSources,
+  parseReferencingSources,
   parseDate,
   parseDimensions,
   parseDuration,
   parseLyrics,
 } from '#yaml';
 
-import {withPropertyFromObject} from '#composite/data';
-
 import {
+  exitWithoutDependency,
+  exitWithoutUpdateValue,
   exposeConstant,
   exposeDependency,
   exposeDependencyOrContinue,
   exposeUpdateValueOrContinue,
   exposeWhetherDependencyAvailable,
+  withAvailabilityFilter,
+  withResultOfAvailabilityCheck,
 } from '#composite/control-flow';
 
 import {
+  fillMissingListItems,
+  withFilteredList,
+  withFlattenedList,
+  withIndexInList,
+  withMappedList,
+  withPropertiesFromObject,
+  withPropertyFromList,
+  withPropertyFromObject,
+} from '#composite/data';
+
+import {
   withRecontextualizedContributionList,
   withRedatedContributionList,
   withResolvedContribs,
+  withResolvedReference,
 } from '#composite/wiki-data';
 
 import {
@@ -52,7 +78,6 @@ import {
   reverseReferenceList,
   simpleDate,
   simpleString,
-  singleReference,
   soupyFind,
   soupyReverse,
   thing,
@@ -62,26 +87,22 @@ import {
 } from '#composite/wiki-properties';
 
 import {
-  exitWithoutUniqueCoverArt,
   inheritContributionListFromMainRelease,
   inheritFromMainRelease,
-  withAllReleases,
-  withAlwaysReferenceByDirectory,
-  withContainingTrackSection,
-  withCoverArtistContribs,
-  withDate,
-  withDirectorySuffix,
-  withHasUniqueCoverArt,
-  withMainRelease,
-  withOtherReleases,
-  withPropertyFromAlbum,
-  withSuffixDirectoryFromAlbum,
-  withTrackArtDate,
-  withTrackNumber,
 } from '#composite/things/track';
 
 export class Track extends Thing {
   static [Thing.referenceType] = 'track';
+  static [Thing.wikiData] = 'trackData';
+
+  static [Thing.constitutibleProperties] = [
+    // Contributions currently aren't being observed for constitution.
+    // 'artistContribs', // from main release or album
+    // 'contributorContribs', // from main release
+    // 'coverArtistContribs', // from main release
+
+    'trackArtworks', // from inline fields
+  ];
 
   static [Thing.getPropertyDescriptors] = ({
     AdditionalFile,
@@ -91,265 +112,370 @@ export class Track extends Thing {
     Artwork,
     CommentaryEntry,
     CreditingSourcesEntry,
-    Flash,
     LyricsEntry,
+    ReferencingSourcesEntry,
     TrackSection,
     WikiInfo,
   }) => ({
-    // Update & expose
-
-    name: name('Unnamed Track'),
+    // > Update & expose - Internal relationships
 
-    directory: [
-      withDirectorySuffix(),
+    album: thing(V(Album)),
+    trackSection: thing(V(TrackSection)),
 
-      directory({
-        suffix: '#directorySuffix',
-      }),
-    ],
+    // > Update & expose - Identifying metadata
 
-    suffixDirectoryFromAlbum: [
-      {
-        dependencies: [
-          input.updateValue({validate: isBoolean}),
-        ],
+    name: name(V('Unnamed Track')),
+    nameText: contentString(),
 
-        compute: (continuation, {
-          [input.updateValue()]: value,
-        }) => continuation({
-          ['#flagValue']: value ?? false,
-        }),
-      },
+    directory: directory({
+      suffix: 'directorySuffix',
+    }),
 
-      withSuffixDirectoryFromAlbum({
-        flagValue: '#flagValue',
+    suffixDirectoryFromAlbum: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isBoolean),
       }),
 
-      exposeDependency({
-        dependency: '#suffixDirectoryFromAlbum',
-      })
+      withPropertyFromObject('trackSection', V('suffixTrackDirectories')),
+      exposeDependency('#trackSection.suffixTrackDirectories'),
     ],
 
-    album: thing({
-      class: input.value(Album),
-    }),
-
-    additionalNames: thingList({
-      class: input.value(AdditionalName),
-    }),
-
-    bandcampTrackIdentifier: simpleString(),
-    bandcampArtworkIdentifier: simpleString(),
-
-    duration: duration(),
-    urls: urls(),
-    dateFirstReleased: simpleDate(),
-
-    color: [
+    // 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.
+    alwaysReferenceByDirectory: [
       exposeUpdateValueOrContinue({
-        validate: input.value(isColor),
+        validate: input.value(isBoolean),
       }),
 
-      withContainingTrackSection(),
+      withPropertyFromObject('album', V('alwaysReferenceTracksByDirectory')),
 
-      withPropertyFromObject({
-        object: '#trackSection',
-        property: input.value('color'),
+      // 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'),
       }),
 
-      exposeDependencyOrContinue({dependency: '#trackSection.color'}),
-
-      withPropertyFromAlbum({
-        property: input.value('color'),
-      }),
+      exitWithoutDependency('_mainRelease', V(false)),
+      exitWithoutDependency('mainReleaseTrack', V(false)),
 
-      exposeDependency({dependency: '#album.color'}),
-    ],
+      withPropertyFromObject('mainReleaseTrack', V('name')),
 
-    alwaysReferenceByDirectory: [
-      withAlwaysReferenceByDirectory(),
-      exposeDependency({dependency: '#alwaysReferenceByDirectory'}),
+      {
+        dependencies: ['name', '#mainReleaseTrack.name'],
+        compute: ({
+          ['name']: name,
+          ['#mainReleaseTrack.name']: mainReleaseName,
+        }) =>
+          getKebabCase(name) ===
+          getKebabCase(mainReleaseName),
+      },
     ],
 
-    // Disables presenting the track as though it has its own unique artwork.
-    // This flag should only be used in select circumstances, i.e. to override
-    // an album's trackCoverArtists. This flag supercedes that property, as well
-    // as the track's own coverArtists.
-    disableUniqueCoverArt: flag(),
-
-    // File extension for track's corresponding media file. This represents the
-    // track's unique cover artwork, if any, and does not inherit the extension
-    // of the album's main artwork. It does inherit trackCoverArtFileExtension,
-    // if present on the album.
-    coverArtFileExtension: [
-      exitWithoutUniqueCoverArt(),
-
-      exposeUpdateValueOrContinue({
-        validate: input.value(isFileExtension),
+    // 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: [
+      exitWithoutUpdateValue({
+        validate: input.value(
+          validateReference(['album', 'track'])),
       }),
 
-      withPropertyFromAlbum({
-        property: input.value('trackCoverArtFileExtension'),
-      }),
+      {
+        dependencies: ['name'],
+        transform: (ref, continuation, {name: ownName}) =>
+          (ref === 'same name single'
+            ? continuation(ref, {
+                ['#albumOrTrackReference']: null,
+                ['#sameNameSingleReference']: ownName,
+              })
+            : continuation(ref, {
+                ['#albumOrTrackReference']: ref,
+                ['#sameNameSingleReference']: null,
+              })),
+      },
 
-      exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}),
+      withResolvedReference({
+        ref: '#albumOrTrackReference',
+        find: soupyFind.input('trackMainReleasesOnly'),
+      }).outputs({
+        '#resolvedReference': '#matchingTrack',
+      }),
 
-      exposeConstant({
-        value: input.value('jpg'),
+      withResolvedReference({
+        ref: '#albumOrTrackReference',
+        find: soupyFind.input('album'),
+      }).outputs({
+        '#resolvedReference': '#matchingAlbum',
       }),
-    ],
 
-    coverArtDate: [
-      withTrackArtDate({
-        from: input.updateValue({
-          validate: isDate,
+      withResolvedReference({
+        ref: '#sameNameSingleReference',
+        find: soupyFind.input('albumSinglesOnly'),
+        findOptions: input.value({
+          fuzz: {
+            capitalization: true,
+            kebab: true,
+          },
         }),
+      }).outputs({
+        '#resolvedReference': '#sameNameSingle',
       }),
 
-      exposeDependency({dependency: '#trackArtDate'}),
-    ],
+      exposeDependencyOrContinue('#sameNameSingle'),
 
-    coverArtDimensions: [
-      exitWithoutUniqueCoverArt(),
+      {
+        dependencies: [
+          '#matchingTrack',
+          '#matchingAlbum',
+        ],
 
-      exposeUpdateValueOrContinue(),
+        compute: (continuation, {
+          ['#matchingTrack']: matchingTrack,
+          ['#matchingAlbum']: matchingAlbum,
+        }) =>
+          (matchingTrack && matchingAlbum
+            ? continuation()
+         : matchingTrack ?? matchingAlbum
+            ? matchingTrack ?? matchingAlbum
+            : null),
+      },
 
-      withPropertyFromAlbum({
-        property: input.value('trackDimensions'),
-      }),
+      withPropertyFromObject( '#matchingAlbum', V('tracks')),
 
-      exposeDependencyOrContinue({dependency: '#album.trackDimensions'}),
+      {
+        dependencies: [
+          '#matchingAlbum.tracks',
+          '#matchingTrack',
+        ],
 
-      dimensions(),
+        compute: ({
+          ['#matchingAlbum.tracks']: matchingAlbumTracks,
+          ['#matchingTrack']: matchingTrack,
+        }) =>
+          (matchingAlbumTracks.includes(matchingTrack)
+            ? matchingTrack
+            : null),
+      },
     ],
 
-    commentary: thingList({
-      class: input.value(CommentaryEntry),
-    }),
+    bandcampTrackIdentifier: simpleString(),
+    bandcampArtworkIdentifier: simpleString(),
 
-    creditSources: thingList({
-      class: input.value(CreditingSourcesEntry),
-    }),
+    additionalNames: thingList(V(AdditionalName)),
 
-    lyrics: [
-      // TODO: Inherited lyrics are literally the same objects, so of course
-      // their .thing properties aren't going to point back to this one, and
-      // certainly couldn't be recontextualized...
-      inheritFromMainRelease(),
+    dateFirstReleased: simpleDate(),
 
-      thingList({
-        class: input.value(LyricsEntry),
+    // > Update & expose - Credits and contributors
+
+    artistText: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isContentString),
       }),
-    ],
 
-    additionalFiles: thingList({
-      class: input.value(AdditionalFile),
-    }),
+      withPropertyFromObject('album', V('trackArtistText')),
+      exposeDependency('#album.trackArtistText'),
+    ],
 
-    sheetMusicFiles: thingList({
-      class: input.value(AdditionalFile),
-    }),
+    artistTextInLists: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isContentString),
+      }),
 
-    midiProjectFiles: thingList({
-      class: input.value(AdditionalFile),
-    }),
+      exposeDependencyOrContinue('_artistText'),
 
-    mainReleaseTrack: singleReference({
-      class: input.value(Track),
-      find: soupyFind.input('track'),
-    }),
+      withPropertyFromObject('album', V('trackArtistText')),
+      exposeDependency('#album.trackArtistText'),
+    ],
 
     artistContribs: [
-      inheritContributionListFromMainRelease(),
-
-      withDate(),
-
       withResolvedContribs({
         from: input.updateValue({validate: isContributionList}),
+        date: 'date',
         thingProperty: input.thisProperty(),
         artistProperty: input.value('trackArtistContributions'),
-        date: '#date',
       }).outputs({
         '#resolvedContribs': '#artistContribs',
       }),
 
-      exposeDependencyOrContinue({
-        dependency: '#artistContribs',
-        mode: input.value('empty'),
-      }),
+      exposeDependencyOrContinue('#artistContribs', V('empty')),
 
-      withPropertyFromAlbum({
-        property: input.value('artistContribs'),
-      }),
+      // Specifically inherit artist contributions later than artist contribs.
+      // Secondary releases' artists may differ from the main release.
+      inheritContributionListFromMainRelease(),
+
+      withPropertyFromObject('album', V('trackArtistContribs')),
 
       withRecontextualizedContributionList({
-        list: '#album.artistContribs',
+        list: '#album.trackArtistContribs',
         artistProperty: input.value('trackArtistContributions'),
       }),
 
       withRedatedContributionList({
-        list: '#album.artistContribs',
-        date: '#date',
+        list: '#album.trackArtistContribs',
+        date: 'date',
       }),
 
-      exposeDependency({dependency: '#album.artistContribs'}),
+      exposeDependency('#album.trackArtistContribs'),
     ],
 
     contributorContribs: [
       inheritContributionListFromMainRelease(),
 
-      withDate(),
-
       contributionList({
-        date: '#date',
+        date: 'date',
         artistProperty: input.value('trackContributorContributions'),
       }),
     ],
 
-    coverArtistContribs: [
-      withCoverArtistContribs({
-        from: input.updateValue({
-          validate: isContributionList,
-        }),
+    // > Update & expose - General configuration
+
+    countInArtistTotals: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isBoolean),
       }),
 
-      exposeDependency({dependency: '#coverArtistContribs'}),
+      withPropertyFromObject('trackSection', V('countTracksInArtistTotals')),
+      exposeDependency('#trackSection.countTracksInArtistTotals'),
     ],
 
-    referencedTracks: [
-      inheritFromMainRelease({
-        notFoundValue: input.value([]),
-      }),
+    disableUniqueCoverArt: flag(V(false)),
+    disableDate: flag(V(false)),
 
-      referenceList({
-        class: input.value(Track),
-        find: soupyFind.input('track'),
+    // > Update & expose - General metadata
+
+    duration: duration(),
+
+    color: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isColor),
       }),
+
+      withPropertyFromObject('trackSection', V('color')),
+      exposeDependencyOrContinue('#trackSection.color'),
+
+      withPropertyFromObject('album', V('color')),
+      exposeDependency('#album.color'),
     ],
 
-    sampledTracks: [
-      inheritFromMainRelease({
-        notFoundValue: input.value([]),
+    needsLyrics: [
+      exposeUpdateValueOrContinue({
+        mode: input.value('falsy'),
+        validate: input.value(isBoolean),
       }),
 
-      referenceList({
-        class: input.value(Track),
-        find: soupyFind.input('track'),
+      exitWithoutDependency('_lyrics', {
+        value: input.value(false),
+        mode: input.value('empty'),
       }),
+
+      withPropertyFromList('_lyrics', V('helpNeeded')),
+
+      {
+        dependencies: ['#lyrics.helpNeeded'],
+        compute: ({
+          ['#lyrics.helpNeeded']: helpNeeded,
+        }) =>
+          helpNeeded.includes(true)
+      },
     ],
 
+    urls: urls(),
+
+    // > Update & expose - Artworks
+
     trackArtworks: [
-      exitWithoutUniqueCoverArt({
+      exitWithoutDependency('hasUniqueCoverArt', {
         value: input.value([]),
+        mode: input.value('falsy'),
       }),
 
       constitutibleArtworkList.fromYAMLFieldSpec
         .call(this, 'Track Artwork'),
     ],
 
+    coverArtistContribs: [
+      exitWithoutDependency('hasUniqueCoverArt', {
+        value: input.value([]),
+        mode: input.value('falsy'),
+      }),
+
+      withResolvedContribs({
+        from: input.updateValue({validate: isContributionList}),
+        date: 'coverArtDate',
+        thingProperty: input.value('coverArtistContribs'),
+        artistProperty: input.value('trackCoverArtistContributions'),
+      }),
+
+      exposeDependencyOrContinue('#resolvedContribs', V('empty')),
+
+      withPropertyFromObject('album', V('trackCoverArtistContribs')),
+
+      withRecontextualizedContributionList({
+        list: '#album.trackCoverArtistContribs',
+        artistProperty: input.value('trackCoverArtistContributions'),
+      }),
+
+      withRedatedContributionList({
+        list: '#album.trackCoverArtistContribs',
+        date: 'coverArtDate',
+      }),
+
+      exposeDependency('#album.trackCoverArtistContribs'),
+    ],
+
+    coverArtDate: [
+      exitWithoutDependency('hasUniqueCoverArt', {
+        value: input.value(null),
+        mode: input.value('falsy'),
+      }),
+
+      exposeUpdateValueOrContinue({
+        validate: input.value(isDate),
+      }),
+
+      withPropertyFromObject('album', V('trackArtDate')),
+      exposeDependencyOrContinue('#album.trackArtDate'),
+
+      exposeDependency('date'),
+    ],
+
+    coverArtFileExtension: [
+      exitWithoutDependency('hasUniqueCoverArt', {
+        value: input.value(null),
+        mode: input.value('falsy'),
+      }),
+
+      exposeUpdateValueOrContinue({
+        validate: input.value(isFileExtension),
+      }),
+
+      withPropertyFromObject('album', V('trackCoverArtFileExtension')),
+      exposeDependencyOrContinue('#album.trackCoverArtFileExtension'),
+
+      exposeConstant(V('jpg')),
+    ],
+
+    coverArtDimensions: [
+      exitWithoutDependency('hasUniqueCoverArt', {
+        value: input.value(null),
+        mode: input.value('falsy'),
+      }),
+
+      exposeUpdateValueOrContinue(),
+
+      withPropertyFromObject('album', V('trackDimensions')),
+      exposeDependencyOrContinue('#album.trackDimensions'),
+
+      dimensions(),
+    ],
+
     artTags: [
-      exitWithoutUniqueCoverArt({
+      exitWithoutDependency('hasUniqueCoverArt', {
         value: input.value([]),
+        mode: input.value('falsy'),
       }),
 
       referenceList({
@@ -359,67 +485,341 @@ export class Track extends Thing {
     ],
 
     referencedArtworks: [
-      exitWithoutUniqueCoverArt({
+      exitWithoutDependency('hasUniqueCoverArt', {
         value: input.value([]),
+        mode: input.value('falsy'),
       }),
 
       referencedArtworkList(),
     ],
 
-    // Update only
+    // > Update & expose - Referenced tracks
+
+    previousProductionTracks: [
+      inheritFromMainRelease(),
+
+      referenceList({
+        class: input.value(Track),
+        find: soupyFind.input('trackMainReleasesOnly'),
+      }),
+    ],
+
+    referencedTracks: [
+      inheritFromMainRelease(),
+
+      referenceList({
+        class: input.value(Track),
+        find: soupyFind.input('trackMainReleasesOnly'),
+      }),
+    ],
+
+    sampledTracks: [
+      inheritFromMainRelease(),
+
+      referenceList({
+        class: input.value(Track),
+        find: soupyFind.input('trackMainReleasesOnly'),
+      }),
+    ],
+
+    // > Update & expose - Additional files
+
+    additionalFiles: thingList(V(AdditionalFile)),
+    sheetMusicFiles: thingList(V(AdditionalFile)),
+    midiProjectFiles: thingList(V(AdditionalFile)),
+
+    // > Update & expose - Content entries
+
+    lyrics: [
+      // TODO: Inherited lyrics are literally the same objects, so of course
+      // their .thing properties aren't going to point back to this one, and
+      // certainly couldn't be recontextualized...
+      inheritFromMainRelease(),
+
+      thingList(V(LyricsEntry)),
+    ],
+
+    commentary: thingList(V(CommentaryEntry)),
+    creditingSources: thingList(V(CreditingSourcesEntry)),
+    referencingSources: thingList(V(ReferencingSourcesEntry)),
+
+    // > Update only
 
     find: soupyFind(),
     reverse: soupyReverse(),
 
     // used for referencedArtworkList (mixedFind)
-    artworkData: wikiData({
-      class: input.value(Artwork),
-    }),
-
-    // used for withAlwaysReferenceByDirectory (for some reason)
-    trackData: wikiData({
-      class: input.value(Track),
-    }),
+    artworkData: wikiData(V(Artwork)),
 
     // used for withMatchingContributionPresets (indirectly by Contribution)
-    wikiInfo: thing({
-      class: input.value(WikiInfo),
-    }),
+    wikiInfo: thing(V(WikiInfo)),
 
-    // Expose only
+    // > Expose only
+
+    isTrack: exposeConstant(V(true)),
 
     commentatorArtists: commentatorArtists(),
 
+    directorySuffix: [
+      exitWithoutDependency('suffixDirectoryFromAlbum', {
+        value: input.value(null),
+        mode: input.value('falsy'),
+      }),
+
+      withPropertyFromObject('trackSection', V('directorySuffix')),
+      exposeDependency('#trackSection.directorySuffix'),
+    ],
+
     date: [
-      withDate(),
-      exposeDependency({dependency: '#date'}),
+      {
+        dependencies: ['disableDate'],
+        compute: (continuation, {disableDate}) =>
+          (disableDate
+            ? null
+            : continuation()),
+      },
+
+      exposeDependencyOrContinue('dateFirstReleased'),
+
+      withPropertyFromObject('album', V('date')),
+      exposeDependency('#album.date'),
     ],
 
     trackNumber: [
-      withTrackNumber(),
-      exposeDependency({dependency: '#trackNumber'}),
+      // Zero is the fallback, not one, but in most albums the first track
+      // (and its intended output by this composition) will be one.
+
+      exitWithoutDependency('trackSection', V(0)),
+      withPropertiesFromObject('trackSection', V(['tracks', 'startCountingFrom'])),
+
+      withIndexInList('#trackSection.tracks', input.myself()),
+      exitWithoutDependency('#index', V(0), V('index')),
+
+      {
+        dependencies: ['#trackSection.startCountingFrom', '#index'],
+        compute: ({
+          ['#trackSection.startCountingFrom']: startCountingFrom,
+          ['#index']: index,
+        }) => startCountingFrom + index,
+      },
     ],
 
+    // Whether or not the track has "unique" cover artwork - a cover which is
+    // specifically associated with this track in particular, rather than with
+    // the track's album as a whole. This is typically used to select between
+    // displaying the track artwork and a fallback, such as the album artwork
+    // or a placeholder. (This property is named hasUniqueCoverArt instead of
+    // the usual hasCoverArt to emphasize that it does not inherit from the
+    // album.)
+    //
+    // hasUniqueCoverArt is based only around the presence of *specified*
+    // cover artist contributions, not whether the references to artists on those
+    // contributions actually resolve to anything. It completely evades interacting
+    // with find/replace.
     hasUniqueCoverArt: [
-      withHasUniqueCoverArt(),
-      exposeDependency({dependency: '#hasUniqueCoverArt'}),
-    ],
+      {
+        dependencies: ['disableUniqueCoverArt'],
+        compute: (continuation, {disableUniqueCoverArt}) =>
+          (disableUniqueCoverArt
+            ? false
+            : continuation()),
+      },
+
+      withResultOfAvailabilityCheck({
+        from: '_coverArtistContribs',
+        mode: input.value('empty'),
+      }),
+
+      {
+        dependencies: ['#availability'],
+        compute: (continuation, {
+          ['#availability']: availability,
+        }) =>
+          (availability
+            ? true
+            : continuation()),
+      },
+
+      withPropertyFromObject('album', {
+        property: input.value('trackCoverArtistContribs'),
+        internal: input.value(true),
+      }),
 
-    isMainRelease: [
-      withMainRelease(),
+      withResultOfAvailabilityCheck({
+        from: '#album.trackCoverArtistContribs',
+        mode: input.value('empty'),
+      }),
+
+      {
+        dependencies: ['#availability'],
+        compute: (continuation, {
+          ['#availability']: availability,
+        }) =>
+          (availability
+            ? true
+            : continuation()),
+      },
+
+      exitWithoutDependency('_trackArtworks', {
+        value: input.value(false),
+        mode: input.value('empty'),
+      }),
+
+      withPropertyFromList('_trackArtworks', {
+        property: input.value('artistContribs'),
+        internal: input.value(true),
+      }),
+
+      // Since we're getting the update value for each artwork's artistContribs,
+      // it may not be set at all, and in that case won't be exposing as [].
+      fillMissingListItems('#trackArtworks.artistContribs', V([])),
+
+      withFlattenedList('#trackArtworks.artistContribs'),
 
       exposeWhetherDependencyAvailable({
-        dependency: '#mainRelease',
-        negate: input.value(true),
+        dependency: '#flattenedList',
+        mode: input.value('empty'),
       }),
     ],
 
-    isSecondaryRelease: [
-      withMainRelease(),
+    isMainRelease:
+      exposeWhetherDependencyAvailable({
+        dependency: 'mainReleaseTrack',
+        negate: input.value(true),
+      }),
 
+    isSecondaryRelease:
       exposeWhetherDependencyAvailable({
-        dependency: '#mainRelease',
+        dependency: 'mainReleaseTrack',
       }),
+
+    mainReleaseTrack: [
+      exitWithoutDependency('mainRelease'),
+
+      withPropertyFromObject('mainRelease', V('isTrack')),
+
+      {
+        dependencies: ['mainRelease', '#mainRelease.isTrack'],
+        compute: (continuation, {
+          ['mainRelease']: mainRelease,
+          ['#mainRelease.isTrack']: mainReleaseIsTrack,
+        }) =>
+          (mainReleaseIsTrack
+            ? 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('mainRelease', V('tracks')),
+
+      withPropertyFromList('#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('#mainRelease.tracks', '#availabilityFilter')
+        .outputs({'#filteredList': '#mainRelease.tracks'}),
+
+      withPropertyFromList('#mainRelease.tracks', V('name')),
+
+      withPropertyFromList('#mainRelease.tracks', {
+        property: input.value('directory'),
+        internal: input.value(true),
+      }),
+
+      withMappedList('#mainRelease.tracks.name', '#mapItsNameLikeName')
+        .outputs({'#mappedList': '#filterItsNameLikeName'}),
+
+      withMappedList('#mainRelease.tracks.directory', '#mapItsDirectoryLikeDirectory')
+        .outputs({'#mappedList': '#filterItsDirectoryLikeDirectory'}),
+
+      withMappedList('#mainRelease.tracks.name', '#mapItsNameLikeDirectory')
+        .outputs({'#mappedList': '#filterItsNameLikeDirectory'}),
+
+      withMappedList('#mainRelease.tracks.directory', '#mapItsDirectoryLikeName')
+        .outputs({'#mappedList': '#filterItsDirectoryLikeName'}),
+
+      withFilteredList('#mainRelease.tracks', '#filterItsNameLikeName')
+        .outputs({'#filteredList': '#matchingItsNameLikeName'}),
+
+      withFilteredList('#mainRelease.tracks', '#filterItsDirectoryLikeDirectory')
+        .outputs({'#filteredList': '#matchingItsDirectoryLikeDirectory'}),
+
+      withFilteredList('#mainRelease.tracks', '#filterItsNameLikeDirectory')
+        .outputs({'#filteredList': '#matchingItsNameLikeDirectory'}),
+
+      withFilteredList('#mainRelease.tracks', '#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: ({
+          ['#mainReleaseTrack']: mainReleaseTrack,
+          [input.myself()]: thisTrack,
+        }) =>
+          (mainReleaseTrack === thisTrack
+            ? null
+            : mainReleaseTrack),
+      },
     ],
 
     // Only has any value for main releases, because secondary releases
@@ -429,15 +829,70 @@ export class Track extends Thing {
     }),
 
     allReleases: [
-      withAllReleases(),
-      exposeDependency({dependency: '#allReleases'}),
+      {
+        dependencies: [
+          'mainReleaseTrack',
+          'secondaryReleases',
+          input.myself(),
+        ],
+
+        compute: (continuation, {
+          mainReleaseTrack,
+          secondaryReleases,
+          [input.myself()]: thisTrack,
+        }) =>
+          (mainReleaseTrack
+            ? continuation({
+                ['#mainReleaseTrack']: mainReleaseTrack,
+                ['#secondaryReleaseTracks']: mainReleaseTrack.secondaryReleases,
+              })
+            : continuation({
+                ['#mainReleaseTrack']: thisTrack,
+                ['#secondaryReleaseTracks']: secondaryReleases,
+              })),
+      },
+
+      {
+        dependencies: [
+          '#mainReleaseTrack',
+          '#secondaryReleaseTracks',
+        ],
+
+        compute: ({
+          ['#mainReleaseTrack']: mainReleaseTrack,
+          ['#secondaryReleaseTracks']: secondaryReleaseTracks,
+        }) =>
+          sortByDate([mainReleaseTrack, ...secondaryReleaseTracks]),
+      },
     ],
 
     otherReleases: [
-      withOtherReleases(),
-      exposeDependency({dependency: '#otherReleases'}),
+      {
+        dependencies: [input.myself(), 'allReleases'],
+        compute: ({
+          [input.myself()]: thisTrack,
+          ['allReleases']: allReleases,
+        }) =>
+          allReleases.filter(track => track !== thisTrack),
+      },
+    ],
+
+    commentaryFromMainRelease: [
+      exitWithoutDependency('mainReleaseTrack', V([])),
+
+      withPropertyFromObject('mainReleaseTrack', V('commentary')),
+      exposeDependency('#mainReleaseTrack.commentary'),
+    ],
+
+    groups: [
+      withPropertyFromObject('album', V('groups')),
+      exposeDependency('#album.groups'),
     ],
 
+    followingProductionTracks: reverseReferenceList({
+      reverse: soupyReverse.input('tracksWhichAreFollowingProductionsOf'),
+    }),
+
     referencedByTracks: reverseReferenceList({
       reverse: soupyReverse.input('tracksWhichReference'),
     }),
@@ -453,14 +908,14 @@ export class Track extends Thing {
 
   static [Thing.yamlDocumentSpec] = {
     fields: {
+      // Identifying metadata
+
       'Track': {property: 'name'},
+      'Track Text': {property: 'nameText'},
       'Directory': {property: 'directory'},
       'Suffix Directory': {property: 'suffixDirectoryFromAlbum'},
-
-      'Additional Names': {
-        property: 'additionalNames',
-        transform: parseAdditionalNames,
-      },
+      'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'},
+      'Main Release': {property: 'mainRelease'},
 
       'Bandcamp Track ID': {
         property: 'bandcampTrackIdentifier',
@@ -472,17 +927,86 @@ export class Track extends Thing {
         transform: String,
       },
 
+      'Additional Names': {
+        property: 'additionalNames',
+        transform: parseAdditionalNames,
+      },
+
+      'Date First Released': {
+        property: 'dateFirstReleased',
+        transform: parseDate,
+      },
+
+      // Credits and contributors
+
+      'Artist Text': {property: 'artistText'},
+      'Artist Text In Lists': {property: 'artistTextInLists'},
+
+      'Artists': {
+        property: 'artistContribs',
+        transform: parseContributors,
+      },
+
+      'Contributors': {
+        property: 'contributorContribs',
+        transform: parseContributors,
+      },
+
+      // General configuration
+
+      'Count In Artist Totals': {property: 'countInArtistTotals'},
+
+      'Has Cover Art': {
+        property: 'disableUniqueCoverArt',
+        transform: value =>
+          (typeof value === 'boolean'
+            ? !value
+            : value),
+      },
+
+      'Has Date': {
+        property: 'disableDate',
+        transform: value =>
+          (typeof value === 'boolean'
+            ? !value
+            : value),
+      },
+
+      // General metadata
+
       'Duration': {
         property: 'duration',
         transform: parseDuration,
       },
 
       'Color': {property: 'color'},
+
+      'Needs Lyrics': {
+        property: 'needsLyrics',
+      },
+
       'URLs': {property: 'urls'},
 
-      'Date First Released': {
-        property: 'dateFirstReleased',
-        transform: parseDate,
+      // Artworks
+
+      'Track Artwork': {
+        property: 'trackArtworks',
+        transform:
+          parseArtwork({
+            thingProperty: 'trackArtworks',
+            dimensionsFromThingProperty: 'coverArtDimensions',
+            fileExtensionFromThingProperty: 'coverArtFileExtension',
+            dateFromThingProperty: 'coverArtDate',
+            artTagsFromThingProperty: 'artTags',
+            referencedArtworksFromThingProperty: 'referencedArtworks',
+            artistContribsFromThingProperty: 'coverArtistContribs',
+            artistContribsArtistProperty: 'trackCoverArtistContributions',
+          }),
+      },
+
+      'Cover Artists': {
+        property: 'coverArtistContribs',
+        transform: parseContributors,
       },
 
       'Cover Art Date': {
@@ -497,30 +1021,20 @@ export class Track extends Thing {
         transform: parseDimensions,
       },
 
-      'Has Cover Art': {
-        property: 'disableUniqueCoverArt',
-        transform: value =>
-          (typeof value === 'boolean'
-            ? !value
-            : value),
-      },
-
-      'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'},
+      'Art Tags': {property: 'artTags'},
 
-      'Lyrics': {
-        property: 'lyrics',
-        transform: parseLyrics,
+      'Referenced Artworks': {
+        property: 'referencedArtworks',
+        transform: parseAnnotatedReferences,
       },
 
-      'Commentary': {
-        property: 'commentary',
-        transform: parseCommentary,
-      },
+      // Referenced tracks
 
-      'Credit Sources': {
-        property: 'creditSources',
-        transform: parseCreditingSources,
-      },
+      'Previous Productions': {property: 'previousProductionTracks'},
+      'Referenced Tracks': {property: 'referencedTracks'},
+      'Sampled Tracks': {property: 'sampledTracks'},
+
+      // Additional files
 
       'Additional Files': {
         property: 'additionalFiles',
@@ -537,54 +1051,41 @@ export class Track extends Thing {
         transform: parseAdditionalFiles,
       },
 
-      'Main Release': {property: 'mainReleaseTrack'},
-      'Referenced Tracks': {property: 'referencedTracks'},
-      'Sampled Tracks': {property: 'sampledTracks'},
+      // Content entries
 
-      'Referenced Artworks': {
-        property: 'referencedArtworks',
-        transform: parseAnnotatedReferences,
-      },
-
-      'Franchises': {ignore: true},
-      'Inherit Franchises': {ignore: true},
-
-      'Artists': {
-        property: 'artistContribs',
-        transform: parseContributors,
+      'Lyrics': {
+        property: 'lyrics',
+        transform: parseLyrics,
       },
 
-      'Contributors': {
-        property: 'contributorContribs',
-        transform: parseContributors,
+      'Commentary': {
+        property: 'commentary',
+        transform: parseCommentary,
       },
 
-      'Cover Artists': {
-        property: 'coverArtistContribs',
-        transform: parseContributors,
+      'Crediting Sources': {
+        property: 'creditingSources',
+        transform: parseCreditingSources,
       },
 
-      'Track Artwork': {
-        property: 'trackArtworks',
-        transform:
-          parseArtwork({
-            thingProperty: 'trackArtworks',
-            dimensionsFromThingProperty: 'coverArtDimensions',
-            fileExtensionFromThingProperty: 'coverArtFileExtension',
-            dateFromThingProperty: 'coverArtDate',
-            artTagsFromThingProperty: 'artTags',
-            referencedArtworksFromThingProperty: 'referencedArtworks',
-            artistContribsFromThingProperty: 'coverArtistContribs',
-            artistContribsArtistProperty: 'trackCoverArtistContributions',
-          }),
+      'Referencing Sources': {
+        property: 'referencingSources',
+        transform: parseReferencingSources,
       },
 
-      'Art Tags': {property: 'artTags'},
+      // Shenanigans
 
+      'Franchises': {ignore: true},
+      'Inherit Franchises': {ignore: true},
       'Review Points': {ignore: true},
     },
 
     invalidFieldCombinations: [
+      {message: `Secondary releases never count in artist totals`, fields: [
+        'Main Release',
+        'Count In Artist Totals',
+      ]},
+
       {message: `Secondary releases inherit references from the main one`, fields: [
         'Main Release',
         'Referenced Tracks',
@@ -595,11 +1096,6 @@ export class Track extends Thing {
         'Sampled Tracks',
       ]},
 
-      {message: `Secondary releases inherit artists from the main one`, fields: [
-        'Main Release',
-        'Artists',
-      ]},
-
       {message: `Secondary releases inherit contributors from the main one`, fields: [
         'Main Release',
         'Contributors',
@@ -641,7 +1137,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`),
@@ -741,6 +1237,13 @@ export class Track extends Thing {
       referencing: track => track.isSecondaryRelease ? [track] : [],
       referenced: track => [track.mainReleaseTrack],
     },
+
+    tracksWhichAreFollowingProductionsOf: {
+      bindTo: 'trackData',
+
+      referencing: track => track.isMainRelease ? [track] : [],
+      referenced: track => track.previousProductionTracks,
+    },
   };
 
   // Track YAML loading is handled in album.js.
@@ -771,12 +1274,36 @@ export class Track extends Thing {
     ];
   }
 
+  countOwnContributionInContributionTotals(_contrib) {
+    if (!this.countInArtistTotals) {
+      return false;
+    }
+
+    if (this.isSecondaryRelease) {
+      return false;
+    }
+
+    return true;
+  }
+
+  countOwnContributionInDurationTotals(_contrib) {
+    if (!this.countInArtistTotals) {
+      return false;
+    }
+
+    if (this.isSecondaryRelease) {
+      return false;
+    }
+
+    return true;
+  }
+
   [inspect.custom](depth) {
     const parts = [];
 
     parts.push(Thing.prototype[inspect.custom].apply(this));
 
-    if (CacheableObject.getUpdateValue(this, 'mainReleaseTrack')) {
+    if (CacheableObject.getUpdateValue(this, 'mainRelease')) {
       parts.unshift(`${colors.yellow('[secrelease]')} `);
     }