« 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/album.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/things/album.js')
-rw-r--r--src/data/things/album.js619
1 files changed, 477 insertions, 142 deletions
diff --git a/src/data/things/album.js b/src/data/things/album.js
index a0021946..5132b962 100644
--- a/src/data/things/album.js
+++ b/src/data/things/album.js
@@ -6,31 +6,38 @@ import {inspect} from 'node:util';
 import CacheableObject from '#cacheable-object';
 import {colors} from '#cli';
 import {input} from '#composite';
-import find from '#find';
 import {traverse} from '#node-utils';
 import {sortAlbumsTracksChronologically, sortChronologically} from '#sort';
-import {accumulateSum, empty} from '#sugar';
+import {empty} from '#sugar';
 import Thing from '#thing';
-import {isColor, isDate, validateWikiData} from '#validators';
-import {parseAdditionalFiles, parseContributors, parseDate, parseDimensions}
-  from '#yaml';
+import {isColor, isDate, isDirectory, isNumber} from '#validators';
+
+import {
+  parseAdditionalFiles,
+  parseAdditionalNames,
+  parseAnnotatedReferences,
+  parseArtwork,
+  parseCommentary,
+  parseContributors,
+  parseCreditingSources,
+  parseDate,
+  parseDimensions,
+  parseWallpaperParts,
+} from '#yaml';
 
 import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue}
   from '#composite/control-flow';
 import {withPropertyFromObject} from '#composite/data';
 
-import {
-  exitWithoutContribs,
-  withDirectory,
-  withResolvedReference,
-  withCoverArtDate,
-} from '#composite/wiki-data';
+import {exitWithoutContribs, withDirectory, withCoverArtDate}
+  from '#composite/wiki-data';
 
 import {
-  additionalFiles,
-  commentary,
   color,
   commentatorArtists,
+  constitutibleArtwork,
+  constitutibleArtworkList,
+  contentString,
   contribsPresent,
   contributionList,
   dimensions,
@@ -38,37 +45,66 @@ import {
   fileExtension,
   flag,
   name,
+  referencedArtworkList,
   referenceList,
   simpleDate,
   simpleString,
-  singleReference,
+  soupyFind,
+  soupyReverse,
   thing,
+  thingList,
   urls,
+  wallpaperParts,
   wikiData,
 } from '#composite/wiki-properties';
 
-import {withTracks} from '#composite/things/album';
-import {withAlbum} from '#composite/things/track-section';
+import {withHasCoverArt, withTracks} from '#composite/things/album';
+import {withAlbum, withContinueCountingFrom, withStartCountingFrom}
+  from '#composite/things/track-section';
 
 export class Album extends Thing {
   static [Thing.referenceType] = 'album';
 
   static [Thing.getPropertyDescriptors] = ({
+    AdditionalFile,
+    AdditionalName,
     ArtTag,
-    Artist,
+    Artwork,
+    CommentaryEntry,
+    CreditingSourcesEntry,
     Group,
-    Track,
     TrackSection,
     WikiInfo,
   }) => ({
     // Update & expose
 
     name: name('Unnamed Album'),
-    color: color(),
     directory: directory(),
-    urls: urls(),
 
+    directorySuffix: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(isDirectory),
+      }),
+
+      withDirectory(),
+
+      exposeDependency({
+        dependency: '#directory',
+      }),
+    ],
+
+    alwaysReferenceByDirectory: flag(false),
     alwaysReferenceTracksByDirectory: flag(false),
+    suffixTrackDirectories: flag(false),
+
+    countTracksInArtistTotals: flag(true),
+
+    color: color(),
+    urls: urls(),
+
+    additionalNames: thingList({
+      class: input.value(AdditionalName),
+    }),
 
     bandcampAlbumIdentifier: simpleString(),
     bandcampArtworkIdentifier: simpleString(),
@@ -78,13 +114,10 @@ export class Album extends Thing {
     dateAddedToWiki: simpleDate(),
 
     coverArtDate: [
-      // TODO: Why does this fall back, but Track.coverArtDate doesn't?
       withCoverArtDate({
         from: input.updateValue({
           validate: isDate,
         }),
-
-        fallback: input.value(true),
       }),
 
       exposeDependency({dependency: '#coverArtDate'}),
@@ -112,6 +145,15 @@ export class Album extends Thing {
       simpleString(),
     ],
 
+    wallpaperParts: [
+      exitWithoutContribs({
+        contribs: 'wallpaperArtistContribs',
+        value: input.value([]),
+      }),
+
+      wallpaperParts(),
+    ],
+
     bannerStyle: [
       exitWithoutContribs({contribs: 'bannerArtistContribs'}),
       simpleString(),
@@ -122,22 +164,66 @@ export class Album extends Thing {
       dimensions(),
     ],
 
+    trackDimensions: dimensions(),
+
     bannerDimensions: [
       exitWithoutContribs({contribs: 'bannerArtistContribs'}),
       dimensions(),
     ],
 
+    wallpaperArtwork: [
+      exitWithoutDependency({
+        dependency: 'wallpaperArtistContribs',
+        mode: input.value('empty'),
+        value: input.value(null),
+      }),
+
+      constitutibleArtwork.fromYAMLFieldSpec
+        .call(this, 'Wallpaper Artwork'),
+    ],
+
+    bannerArtwork: [
+      exitWithoutDependency({
+        dependency: 'bannerArtistContribs',
+        mode: input.value('empty'),
+        value: input.value(null),
+      }),
+
+      constitutibleArtwork.fromYAMLFieldSpec
+        .call(this, 'Banner Artwork'),
+    ],
+
+    coverArtworks: [
+      withHasCoverArt(),
+
+      exitWithoutDependency({
+        dependency: '#hasCoverArt',
+        mode: input.value('falsy'),
+        value: input.value([]),
+      }),
+
+      constitutibleArtworkList.fromYAMLFieldSpec
+        .call(this, 'Cover Artwork'),
+    ],
+
     hasTrackNumbers: flag(true),
     isListedOnHomepage: flag(true),
     isListedInGalleries: flag(true),
 
-    commentary: commentary(),
-    additionalFiles: additionalFiles(),
+    commentary: thingList({
+      class: input.value(CommentaryEntry),
+    }),
+
+    creditingSources: thingList({
+      class: input.value(CreditingSourcesEntry),
+    }),
 
-    trackSections: referenceList({
-      referenceType: input.value('unqualified-track-section'),
-      data: 'ownTrackSectionData',
-      find: input.value(find.unqualifiedTrackSection),
+    additionalFiles: thingList({
+      class: input.value(AdditionalFile),
+    }),
+
+    trackSections: thingList({
+      class: input.value(TrackSection),
     }),
 
     artistContribs: contributionList({
@@ -146,9 +232,7 @@ export class Album extends Thing {
     }),
 
     coverArtistContribs: [
-      withCoverArtDate({
-        fallback: input.value(true),
-      }),
+      withCoverArtDate(),
 
       contributionList({
         date: '#coverArtDate',
@@ -167,9 +251,7 @@ export class Album extends Thing {
     }),
 
     wallpaperArtistContribs: [
-      withCoverArtDate({
-        fallback: input.value(true),
-      }),
+      withCoverArtDate(),
 
       contributionList({
         date: '#coverArtDate',
@@ -178,9 +260,7 @@ export class Album extends Thing {
     ],
 
     bannerArtistContribs: [
-      withCoverArtDate({
-        fallback: input.value(true),
-      }),
+      withCoverArtDate(),
 
       contributionList({
         date: '#coverArtDate',
@@ -190,8 +270,7 @@ export class Album extends Thing {
 
     groups: referenceList({
       class: input.value(Group),
-      find: input.value(find.group),
-      data: 'groupData',
+      find: soupyFind.input('group'),
     }),
 
     artTags: [
@@ -202,29 +281,30 @@ export class Album extends Thing {
 
       referenceList({
         class: input.value(ArtTag),
-        find: input.value(find.artTag),
-        data: 'artTagData',
+        find: soupyFind.input('artTag'),
       }),
     ],
 
-    // Update only
+    referencedArtworks: [
+      exitWithoutContribs({
+        contribs: 'coverArtistContribs',
+        value: input.value([]),
+      }),
 
-    artistData: wikiData({
-      class: input.value(Artist),
-    }),
+      referencedArtworkList(),
+    ],
 
-    artTagData: wikiData({
-      class: input.value(ArtTag),
-    }),
+    // Update only
 
-    groupData: wikiData({
-      class: input.value(Group),
-    }),
+    find: soupyFind(),
+    reverse: soupyReverse(),
 
-    ownTrackSectionData: wikiData({
-      class: input.value(TrackSection),
+    // used for referencedArtworkList (mixedFind)
+    artworkData: wikiData({
+      class: input.value(Artwork),
     }),
 
+    // used for withMatchingContributionPresets (indirectly by Contribution)
     wikiInfo: thing({
       class: input.value(WikiInfo),
     }),
@@ -233,7 +313,11 @@ export class Album extends Thing {
 
     commentatorArtists: commentatorArtists(),
 
-    hasCoverArt: contribsPresent({contribs: 'coverArtistContribs'}),
+    hasCoverArt: [
+      withHasCoverArt(),
+      exposeDependency({dependency: '#hasCoverArt'}),
+    ],
+
     hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}),
     hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}),
 
@@ -285,20 +369,131 @@ export class Album extends Thing {
 
   static [Thing.findSpecs] = {
     album: {
-      referenceTypes: ['album', 'album-commentary', 'album-gallery'],
+      referenceTypes: [
+        'album',
+        'album-commentary',
+        'album-gallery',
+      ],
+
+      bindTo: 'albumData',
+
+      getMatchableNames: album =>
+        (album.alwaysReferenceByDirectory 
+          ? [] 
+          : [album.name]),
+    },
+
+    albumWithArtwork: {
+      referenceTypes: [
+        'album',
+        'album-referencing-artworks',
+        'album-referenced-artworks',
+      ],
+
       bindTo: 'albumData',
+
+      include: album =>
+        album.hasCoverArt,
+
+      getMatchableNames: album =>
+        (album.alwaysReferenceByDirectory 
+          ? [] 
+          : [album.name]),
+    },
+
+    albumPrimaryArtwork: {
+      [Thing.findThisThingOnly]: false,
+
+      referenceTypes: [
+        'album',
+        'album-referencing-artworks',
+        'album-referenced-artworks',
+      ],
+
+      bindTo: 'artworkData',
+
+      include: (artwork, {Artwork, Album}) =>
+        artwork instanceof Artwork &&
+        artwork.thing instanceof Album &&
+        artwork === artwork.thing.coverArtworks[0],
+
+      getMatchableNames: ({thing: album}) =>
+        (album.alwaysReferenceByDirectory
+          ? []
+          : [album.name]),
+
+      getMatchableDirectories: ({thing: album}) =>
+        [album.directory],
+    },
+  };
+
+  static [Thing.reverseSpecs] = {
+    albumsWhoseTracksInclude: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.tracks,
+    },
+
+    albumsWhoseTrackSectionsInclude: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.trackSections,
+    },
+
+    albumsWhoseArtworksFeature: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.artTags,
+    },
+
+    albumsWhoseGroupsInclude: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.groups,
+    },
+
+    albumArtistContributionsBy:
+      soupyReverse.contributionsBy('albumData', 'artistContribs'),
+
+    albumCoverArtistContributionsBy:
+      soupyReverse.artworkContributionsBy('albumData', 'coverArtworks'),
+
+    albumWallpaperArtistContributionsBy:
+      soupyReverse.artworkContributionsBy('albumData', 'wallpaperArtwork', {single: true}),
+
+    albumBannerArtistContributionsBy:
+      soupyReverse.artworkContributionsBy('albumData', 'bannerArtwork', {single: true}),
+
+    albumsWithCommentaryBy: {
+      bindTo: 'albumData',
+
+      referencing: album => [album],
+      referenced: album => album.commentatorArtists,
     },
   };
 
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Album': {property: 'name'},
+
       'Directory': {property: 'directory'},
+      'Directory Suffix': {property: 'directorySuffix'},
+      'Suffix Track Directories': {property: 'suffixTrackDirectories'},
 
+      'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'},
       'Always Reference Tracks By Directory': {
         property: 'alwaysReferenceTracksByDirectory',
       },
 
+      'Additional Names': {
+        property: 'additionalNames',
+        transform: parseAdditionalNames,
+      },
+
       'Bandcamp Album ID': {
         property: 'bandcampAlbumIdentifier',
         transform: String,
@@ -309,6 +504,8 @@ export class Album extends Thing {
         transform: String,
       },
 
+      'Count Tracks In Artist Totals': {property: 'countInArtistTotals'},
+
       'Date': {
         property: 'date',
         transform: parseDate,
@@ -321,6 +518,49 @@ export class Album extends Thing {
       'Listed on Homepage': {property: 'isListedOnHomepage'},
       'Listed in Galleries': {property: 'isListedInGalleries'},
 
+      'Cover Artwork': {
+        property: 'coverArtworks',
+        transform:
+          parseArtwork({
+            thingProperty: 'coverArtworks',
+            dimensionsFromThingProperty: 'coverArtDimensions',
+            fileExtensionFromThingProperty: 'coverArtFileExtension',
+            dateFromThingProperty: 'coverArtDate',
+            artistContribsFromThingProperty: 'coverArtistContribs',
+            artistContribsArtistProperty: 'albumCoverArtistContributions',
+            artTagsFromThingProperty: 'artTags',
+            referencedArtworksFromThingProperty: 'referencedArtworks',
+          }),
+      },
+
+      'Banner Artwork': {
+        property: 'bannerArtwork',
+        transform:
+          parseArtwork({
+            single: true,
+            thingProperty: 'bannerArtwork',
+            dimensionsFromThingProperty: 'bannerDimensions',
+            fileExtensionFromThingProperty: 'bannerFileExtension',
+            dateFromThingProperty: 'date',
+            artistContribsFromThingProperty: 'bannerArtistContribs',
+            artistContribsArtistProperty: 'albumBannerArtistContributions',
+          }),
+      },
+
+      'Wallpaper Artwork': {
+        property: 'wallpaperArtwork',
+        transform:
+          parseArtwork({
+            single: true,
+            thingProperty: 'wallpaperArtwork',
+            dimensionsFromThingProperty: null,
+            fileExtensionFromThingProperty: 'wallpaperFileExtension',
+            dateFromThingProperty: 'date',
+            artistContribsFromThingProperty: 'wallpaperArtistContribs',
+            artistContribsArtistProperty: 'albumWallpaperArtistContributions',
+          }),
+      },
+
       'Cover Art Date': {
         property: 'coverArtDate',
         transform: parseDate,
@@ -344,14 +584,25 @@ export class Album extends Thing {
         transform: parseDimensions,
       },
 
+      'Default Track Dimensions': {
+        property: 'trackDimensions',
+        transform: parseDimensions,
+      },
+
       'Wallpaper Artists': {
         property: 'wallpaperArtistContribs',
         transform: parseContributors,
       },
 
-      'Wallpaper Style': {property: 'wallpaperStyle'},
       'Wallpaper File Extension': {property: 'wallpaperFileExtension'},
 
+      'Wallpaper Style': {property: 'wallpaperStyle'},
+
+      'Wallpaper Parts': {
+        property: 'wallpaperParts',
+        transform: parseWallpaperParts,
+      },
+
       'Banner Artists': {
         property: 'bannerArtistContribs',
         transform: parseContributors,
@@ -365,13 +616,26 @@ export class Album extends Thing {
         transform: parseDimensions,
       },
 
-      'Commentary': {property: 'commentary'},
+      'Commentary': {
+        property: 'commentary',
+        transform: parseCommentary,
+      },
+
+      'Crediting Sources': {
+        property: 'creditingSources',
+        transform: parseCreditingSources,
+      },
 
       'Additional Files': {
         property: 'additionalFiles',
         transform: parseAdditionalFiles,
       },
 
+      'Referenced Artworks': {
+        property: 'referencedArtworks',
+        transform: parseAnnotatedReferences,
+      },
+
       'Franchises': {ignore: true},
 
       'Artists': {
@@ -394,11 +658,23 @@ export class Album extends Thing {
 
       'Review Points': {ignore: true},
     },
+
+    invalidFieldCombinations: [
+      {message: `Specify one wallpaper style or multiple wallpaper parts, not both`, fields: [
+        'Wallpaper Parts',
+        'Wallpaper Style',
+      ]},
+
+      {message: `Wallpaper file extensions are specified on asset, per part`, fields: [
+        'Wallpaper Parts',
+        'Wallpaper File Extension',
+      ]},
+    ],
   };
 
   static [Thing.getYamlLoadingSpec] = ({
     documentModes: {headerAndEntries},
-    thingConstructors: {Album, Track, TrackSectionHelper},
+    thingConstructors: {Album, Track},
   }) => ({
     title: `Process album files`,
 
@@ -420,6 +696,12 @@ export class Album extends Thing {
       const trackSectionData = [];
       const trackData = [];
 
+      const artworkData = [];
+      const commentaryData = [];
+      const creditingSourceData = [];
+      const referencingSourceData = [];
+      const lyricsData = [];
+
       for (const {header: album, entries} of results) {
         const trackSections = [];
 
@@ -431,8 +713,6 @@ export class Album extends Thing {
           isDefaultTrackSection: true,
         });
 
-        const albumRef = Thing.getReference(album);
-
         const closeCurrentTrackSection = () => {
           if (
             currentTrackSection.isDefaultTrackSection &&
@@ -442,15 +722,8 @@ export class Album extends Thing {
           }
 
           currentTrackSection.tracks =
-            currentTrackSectionTracks
-              .map(track => Thing.getReference(track));
-
-          currentTrackSection.ownTrackData =
             currentTrackSectionTracks;
 
-          currentTrackSection.ownAlbumData =
-            [album];
-
           trackSections.push(currentTrackSection);
           trackSectionData.push(currentTrackSection);
         };
@@ -466,23 +739,53 @@ export class Album extends Thing {
           currentTrackSectionTracks.push(entry);
           trackData.push(entry);
 
-          entry.dataSourceAlbum = albumRef;
+          // Set the track's album before accessing its list of artworks.
+          // The existence of its artwork objects may depend on access to
+          // its album's 'Default Track Cover Artists'.
+          entry.album = album;
+
+          artworkData.push(...entry.trackArtworks);
+          commentaryData.push(...entry.commentary);
+          creditingSourceData.push(...entry.creditingSources);
+          referencingSourceData.push(...entry.referencingSources);
+
+          // TODO: As exposed, Track.lyrics tries to inherit from the main
+          // release, which is impossible before the data's been linked.
+          // We just use the update value here. But it's icky!
+          lyricsData.push(...CacheableObject.getUpdateValue(entry, 'lyrics') ?? []);
         }
 
         closeCurrentTrackSection();
 
         albumData.push(album);
 
-        album.trackSections =
-          trackSections
-            .map(trackSection =>
-              `unqualified-track-section:` +
-              trackSection.unqualifiedDirectory);
+        artworkData.push(...album.coverArtworks);
+
+        if (album.bannerArtwork) {
+          artworkData.push(album.bannerArtwork);
+        }
+
+        if (album.wallpaperArtwork) {
+          artworkData.push(album.wallpaperArtwork);
+        }
+
+        commentaryData.push(...album.commentary);
+        creditingSourceData.push(...album.creditingSources);
 
-        album.ownTrackSectionData = trackSections;
+        album.trackSections = trackSections;
       }
 
-      return {albumData, trackSectionData, trackData};
+      return {
+        albumData,
+        trackSectionData,
+        trackData,
+
+        artworkData,
+        commentaryData,
+        creditingSourceData,
+        referencingSourceData,
+        lyricsData,
+      };
     },
 
     sort({albumData, trackData}) {
@@ -490,13 +793,65 @@ export class Album extends Thing {
       sortAlbumsTracksChronologically(trackData);
     },
   });
+
+  getOwnAdditionalFilePath(_file, filename) {
+    return [
+      'media.albumAdditionalFile',
+      this.directory,
+      filename,
+    ];
+  }
+
+  getOwnArtworkPath(artwork) {
+    if (artwork === this.bannerArtwork) {
+      return [
+        'media.albumBanner',
+        this.directory,
+        artwork.fileExtension,
+      ];
+    }
+
+    if (artwork === this.wallpaperArtwork) {
+      if (!empty(this.wallpaperParts)) {
+        return null;
+      }
+
+      return [
+        'media.albumWallpaper',
+        this.directory,
+        artwork.fileExtension,
+      ];
+    }
+
+    // TODO: using trackCover here is obviously, badly wrong
+    // but we ought to refactor banners and wallpapers similarly
+    // (i.e. depend on those intrinsic artwork paths rather than
+    // accessing media.{albumBanner,albumWallpaper} from content
+    // or other code directly)
+    return [
+      'media.trackCover',
+      this.directory,
+
+      (artwork.unqualifiedDirectory
+        ? 'cover-' + artwork.unqualifiedDirectory
+        : 'cover'),
+
+      artwork.fileExtension,
+    ];
+  }
+
+  // As of writing, albums don't even have a `duration` property...
+  // so this function will never be called... but the message stands...
+  countOwnContributionInDurationTotals(_contrib) {
+    return false;
+  }
 }
 
 export class TrackSection extends Thing {
   static [Thing.friendlyName] = `Track Section`;
   static [Thing.referenceType] = `track-section`;
 
-  static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({
+  static [Thing.getPropertyDescriptors] = ({Track}) => ({
     // Update & expose
 
     name: name('Unnamed Track Section'),
@@ -518,30 +873,32 @@ export class TrackSection extends Thing {
       exposeDependency({dependency: '#album.color'}),
     ],
 
+    startCountingFrom: [
+      withStartCountingFrom({
+        from: input.updateValue({validate: isNumber}),
+      }),
+
+      exposeDependency({dependency: '#startCountingFrom'}),
+    ],
+
     dateOriginallyReleased: simpleDate(),
 
     isDefaultTrackSection: flag(false),
 
+    description: contentString(),
+
     album: [
       withAlbum(),
       exposeDependency({dependency: '#album'}),
     ],
 
-    tracks: referenceList({
+    tracks: thingList({
       class: input.value(Track),
-      data: 'ownTrackData',
-      find: input.value(find.track),
     }),
 
     // Update only
 
-    ownAlbumData: wikiData({
-      class: input.value(Album),
-    }),
-
-    ownTrackData: wikiData({
-      class: input.value(Track),
-    }),
+    reverse: soupyReverse(),
 
     // Expose only
 
@@ -573,42 +930,10 @@ export class TrackSection extends Thing {
       },
     ],
 
-    startIndex: [
-      withAlbum(),
-
-      withPropertyFromObject({
-        object: '#album',
-        property: input.value('trackSections'),
-      }),
-
-      {
-        dependencies: ['#album.trackSections', input.myself()],
-        compute: (continuation, {
-          ['#album.trackSections']: trackSections,
-          [input.myself()]: myself,
-        }) => continuation({
-          ['#index']:
-            trackSections.indexOf(myself),
-        }),
-      },
-
-      exitWithoutDependency({
-        dependency: '#index',
-        mode: input.value('index'),
-        value: input.value(0),
-      }),
+    continueCountingFrom: [
+      withContinueCountingFrom(),
 
-      {
-        dependencies: ['#album.trackSections', '#index'],
-        compute: ({
-          ['#album.trackSections']: trackSections,
-          ['#index']: index,
-        }) =>
-          accumulateSum(
-            trackSections
-              .slice(0, index)
-              .map(section => section.tracks.length)),
-      },
+      exposeDependency({dependency: '#continueCountingFrom'}),
     ],
   });
 
@@ -626,15 +951,27 @@ export class TrackSection extends Thing {
     },
   };
 
+  static [Thing.reverseSpecs] = {
+    trackSectionsWhichInclude: {
+      bindTo: 'trackSectionData',
+
+      referencing: trackSection => [trackSection],
+      referenced: trackSection => trackSection.tracks,
+    },
+  };
+
   static [Thing.yamlDocumentSpec] = {
     fields: {
       'Section': {property: 'name'},
       'Color': {property: 'color'},
+      'Start Counting From': {property: 'startCountingFrom'},
 
       'Date Originally Released': {
         property: 'dateOriginallyReleased',
         transform: parseDate,
       },
+
+      'Description': {property: 'description'},
     },
   };
 
@@ -643,40 +980,38 @@ export class TrackSection extends Thing {
 
     parts.push(Thing.prototype[inspect.custom].apply(this));
 
-    if (depth >= 0) {
+    if (depth >= 0) showAlbum: {
       let album = null;
       try {
         album = this.album;
-      } catch {}
+      } catch {
+        break showAlbum;
+      }
 
       let first = null;
       try {
-        first = this.startIndex;
+        first = this.tracks.at(0).trackNumber;
       } catch {}
 
-      let length = null;
+      let last = null;
       try {
-        length = this.tracks.length;
+        last = this.tracks.at(-1).trackNumber;
       } catch {}
 
-      album ??= CacheableObject.getUpdateValue(this, 'ownAlbumData')?.[0];
-
-      if (album) {
-        const albumName = album.name;
-        const albumIndex = album.trackSections.indexOf(this);
+      const albumName = album.name;
+      const albumIndex = album.trackSections.indexOf(this);
 
-        const num =
-          (albumIndex === -1
-            ? 'indeterminate position'
-            : `#${albumIndex + 1}`);
+      const num =
+        (albumIndex === -1
+          ? 'indeterminate position'
+          : `#${albumIndex + 1}`);
 
-        const range =
-          (albumIndex >= 0 && first !== null && length !== null
-            ? `: ${first + 1}-${first + length + 1}`
-            : '');
+      const range =
+        (albumIndex >= 0 && first !== null && last !== null
+          ? `: ${first}-${last}`
+          : '');
 
-        parts.push(` (${colors.yellow(num + range)} in ${colors.green(albumName)})`);
-      }
+      parts.push(` (${colors.yellow(num + range)} in ${colors.green(`"${albumName}"`)})`);
     }
 
     return parts.join('');