« 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/artist.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/things/artist.js')
-rw-r--r--src/data/things/artist.js443
1 files changed, 336 insertions, 107 deletions
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 303f33f..841d652 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -1,121 +1,230 @@
-import Thing from './thing.js';
-
-import find from '../../util/find.js';
+export const ARTIST_DATA_FILE = 'artists.yaml';
+
+import {inspect} from 'node:util';
+
+import CacheableObject from '#cacheable-object';
+import {colors} from '#cli';
+import {input} from '#composite';
+import find from '#find';
+import {sortAlphabetically} from '#sort';
+import {stitchArrays, unique} from '#sugar';
+import Thing from '#thing';
+import {isName, validateArrayItems} from '#validators';
+import {getKebabCase} from '#wiki-data';
+
+import {withReverseContributionList} from '#composite/wiki-data';
+
+import {
+  contentString,
+  directory,
+  fileExtension,
+  flag,
+  name,
+  reverseContributionList,
+  reverseReferenceList,
+  singleReference,
+  urls,
+  wikiData,
+} from '#composite/wiki-properties';
 
 export class Artist extends Thing {
   static [Thing.referenceType] = 'artist';
+  static [Thing.wikiDataArray] = 'artistData';
 
-  static [Thing.getPropertyDescriptors] = ({
-    Album,
-    Flash,
-    Track,
-
-    validators: {
-      isName,
-      validateArrayItems,
-    },
-  }) => ({
+  static [Thing.getPropertyDescriptors] = ({Album, Flash, Track}) => ({
     // Update & expose
 
-    name: Thing.common.name('Unnamed Artist'),
-    directory: Thing.common.directory(),
-    urls: Thing.common.urls(),
-    contextNotes: Thing.common.simpleString(),
+    name: name('Unnamed Artist'),
+    directory: directory(),
+    urls: urls(),
+
+    contextNotes: contentString(),
 
-    hasAvatar: Thing.common.flag(false),
-    avatarFileExtension: Thing.common.fileExtension('jpg'),
+    hasAvatar: flag(false),
+    avatarFileExtension: fileExtension('jpg'),
 
     aliasNames: {
       flags: {update: true, expose: true},
-      update: {
-        validate: validateArrayItems(isName),
-      },
+      update: {validate: validateArrayItems(isName)},
+      expose: {transform: (names) => names ?? []},
     },
 
-    isAlias: Thing.common.flag(),
-    aliasedArtistRef: Thing.common.singleReference(Artist),
+    isAlias: flag(),
 
-    // Update only
-
-    albumData: Thing.common.wikiData(Album),
-    artistData: Thing.common.wikiData(Artist),
-    flashData: Thing.common.wikiData(Flash),
-    trackData: Thing.common.wikiData(Track),
+    aliasedArtist: singleReference({
+      class: input.value(Artist),
+      find: input.value(find.artist),
+      data: 'artistData',
+    }),
 
-    // Expose only
+    // Update only
 
-    aliasedArtist: {
-      flags: {expose: true},
+    albumData: wikiData({
+      class: input.value(Album),
+    }),
 
-      expose: {
-        dependencies: ['artistData', 'aliasedArtistRef'],
-        compute: ({artistData, aliasedArtistRef}) =>
-          aliasedArtistRef && artistData
-            ? find.artist(aliasedArtistRef, artistData, {mode: 'quiet'})
-            : null,
-      },
-    },
+    artistData: wikiData({
+      class: input.value(Artist),
+    }),
 
-    tracksAsArtist:
-      Artist.filterByContrib('trackData', 'artistContribs'),
-    tracksAsContributor:
-      Artist.filterByContrib('trackData', 'contributorContribs'),
-    tracksAsCoverArtist:
-      Artist.filterByContrib('trackData', 'coverArtistContribs'),
-
-    tracksAsAny: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['trackData'],
-
-        compute: ({trackData, [Artist.instance]: artist}) =>
-          trackData?.filter((track) =>
-            [
-              ...track.artistContribs,
-              ...track.contributorContribs,
-              ...track.coverArtistContribs,
-            ].some(({who}) => who === artist)) ?? [],
-      },
-    },
+    flashData: wikiData({
+      class: input.value(Flash),
+    }),
 
-    tracksAsCommentator: {
-      flags: {expose: true},
+    trackData: wikiData({
+      class: input.value(Track),
+    }),
 
-      expose: {
-        dependencies: ['trackData'],
+    // Expose only
 
-        compute: ({trackData, [Artist.instance]: artist}) =>
-          trackData?.filter(({commentatorArtists}) =>
-            commentatorArtists.includes(artist)) ?? [],
+    tracksAsArtist: reverseContributionList({
+      data: 'trackData',
+      list: input.value('artistContribs'),
+    }),
+
+    tracksAsContributor: reverseContributionList({
+      data: 'trackData',
+      list: input.value('contributorContribs'),
+    }),
+
+    tracksAsCoverArtist: reverseContributionList({
+      data: 'trackData',
+      list: input.value('coverArtistContribs'),
+    }),
+
+    tracksAsAny: [
+      withReverseContributionList({
+        data: 'trackData',
+        list: input.value('artistContribs'),
+      }).outputs({
+        '#reverseContributionList': '#tracksAsArtist',
+      }),
+
+      withReverseContributionList({
+        data: 'trackData',
+        list: input.value('contributorContribs'),
+      }).outputs({
+        '#reverseContributionList': '#tracksAsContributor',
+      }),
+
+      withReverseContributionList({
+        data: 'trackData',
+        list: input.value('coverArtistContribs'),
+      }).outputs({
+        '#reverseContributionList': '#tracksAsCoverArtist',
+      }),
+
+      {
+        dependencies: [
+          '#tracksAsArtist',
+          '#tracksAsContributor',
+          '#tracksAsCoverArtist',
+        ],
+
+        compute: ({
+          ['#tracksAsArtist']: tracksAsArtist,
+          ['#tracksAsContributor']: tracksAsContributor,
+          ['#tracksAsCoverArtist']: tracksAsCoverArtist,
+        }) =>
+          unique([
+            ...tracksAsArtist,
+            ...tracksAsContributor,
+            ...tracksAsCoverArtist,
+          ]),
       },
-    },
-
-    albumsAsAlbumArtist:
-      Artist.filterByContrib('albumData', 'artistContribs'),
-    albumsAsCoverArtist:
-      Artist.filterByContrib('albumData', 'coverArtistContribs'),
-    albumsAsWallpaperArtist:
-      Artist.filterByContrib('albumData', 'wallpaperArtistContribs'),
-    albumsAsBannerArtist:
-      Artist.filterByContrib('albumData', 'bannerArtistContribs'),
-
-    albumsAsCommentator: {
-      flags: {expose: true},
-
-      expose: {
-        dependencies: ['albumData'],
-
-        compute: ({albumData, [Artist.instance]: artist}) =>
-          albumData?.filter(({commentatorArtists}) =>
-            commentatorArtists.includes(artist)) ?? [],
+    ],
+
+    tracksAsCommentator: reverseReferenceList({
+      data: 'trackData',
+      list: input.value('commentatorArtists'),
+    }),
+
+    albumsAsAlbumArtist: reverseContributionList({
+      data: 'albumData',
+      list: input.value('artistContribs'),
+    }),
+
+    albumsAsCoverArtist: reverseContributionList({
+      data: 'albumData',
+      list: input.value('coverArtistContribs'),
+    }),
+
+    albumsAsWallpaperArtist: reverseContributionList({
+      data: 'albumData',
+      list: input.value('wallpaperArtistContribs'),
+    }),
+
+    albumsAsBannerArtist: reverseContributionList({
+      data: 'albumData',
+      list: input.value('bannerArtistContribs'),
+    }),
+
+    albumsAsAny: [
+      withReverseContributionList({
+        data: 'albumData',
+        list: input.value('artistContribs'),
+      }).outputs({
+        '#reverseContributionList': '#albumsAsArtist',
+      }),
+
+      withReverseContributionList({
+        data: 'albumData',
+        list: input.value('coverArtistContribs'),
+      }).outputs({
+        '#reverseContributionList': '#albumsAsCoverArtist',
+      }),
+
+      withReverseContributionList({
+        data: 'albumData',
+        list: input.value('wallpaperArtistContribs'),
+      }).outputs({
+        '#reverseContributionList': '#albumsAsWallpaperArtist',
+      }),
+
+      withReverseContributionList({
+        data: 'albumData',
+        list: input.value('bannerArtistContribs'),
+      }).outputs({
+        '#reverseContributionList': '#albumsAsBannerArtist',
+      }),
+
+      {
+        dependencies: [
+          '#albumsAsArtist',
+          '#albumsAsCoverArtist',
+          '#albumsAsWallpaperArtist',
+          '#albumsAsBannerArtist',
+        ],
+
+        compute: ({
+          ['#albumsAsArtist']: albumsAsArtist,
+          ['#albumsAsCoverArtist']: albumsAsCoverArtist,
+          ['#albumsAsWallpaperArtist']: albumsAsWallpaperArtist,
+          ['#albumsAsBannerArtist']: albumsAsBannerArtist,
+        }) =>
+          unique([
+            ...albumsAsArtist,
+            ...albumsAsCoverArtist,
+            ...albumsAsWallpaperArtist,
+            ...albumsAsBannerArtist,
+          ]),
       },
-    },
-
-    flashesAsContributor: Artist.filterByContrib(
-      'flashData',
-      'contributorContribs'
-    ),
+    ],
+
+    albumsAsCommentator: reverseReferenceList({
+      data: 'albumData',
+      list: input.value('commentatorArtists'),
+    }),
+
+    flashesAsContributor: reverseContributionList({
+      data: 'flashData',
+      list: input.value('contributorContribs'),
+    }),
+
+    flashesAsCommentator: reverseReferenceList({
+      data: 'flashData',
+      list: input.value('commentatorArtists'),
+    }),
   });
 
   static [Thing.getSerializeDescriptors] = ({
@@ -145,19 +254,139 @@ export class Artist extends Thing {
     flashesAsContributor: S.toRefs,
   });
 
-  static filterByContrib = (thingDataProperty, contribsProperty) => ({
-    flags: {expose: true},
+  static [Thing.findSpecs] = {
+    artist: {
+      referenceTypes: ['artist', 'artist-gallery'],
+      bindTo: 'artistData',
+
+      include: artist => !artist.isAlias,
+    },
+
+    artistAlias: {
+      referenceTypes: ['artist', 'artist-gallery'],
+      bindTo: 'artistData',
+
+      include: artist => artist.isAlias,
+
+      getMatchableDirectories(artist) {
+        const originalArtist = artist.aliasedArtist;
+
+        // Aliases never match by the same directory as the original.
+        if (artist.directory === originalArtist.directory) {
+          return [];
+        }
+
+        // Aliases never match by the same directory as some *previous* alias
+        // in the original's alias list. This is honestly a bit awkward, but it
+        // avoids artist aliases conflicting with each other when checking for
+        // duplicate directories.
+        for (const aliasName of originalArtist.aliasNames) {
+          // These are trouble. We should be accessing aliases' directories
+          // directly, but artists currently don't expose a reverse reference
+          // list for aliases. (This is pending a cleanup of "reverse reference"
+          // behavior in general.) It doesn't actually cause problems *here*
+          // because alias directories are computed from their names 100% of the
+          // time, but that *is* an assumption this code makes.
+          if (aliasName === artist.name) continue;
+          if (artist.directory === getKebabCase(aliasName)) {
+            return [];
+          }
+        }
+
+        // And, aliases never return just a blank string. This part is pretty
+        // spooky because it doesn't handle two differently named aliases, on
+        // different artists, who have names that are similar *apart* from a
+        // character that's shortened. But that's also so fundamentally scary
+        // that we can't support it properly with existing code, anyway - we
+        // would need to be able to specifically set a directory *on an alias,*
+        // which currently can't be done in YAML data files.
+        if (artist.directory === '') {
+          return [];
+        }
+
+        return [artist.directory];
+      },
+    },
+  };
+
+  static [Thing.yamlDocumentSpec] = {
+    fields: {
+      'Artist': {property: 'name'},
+      'Directory': {property: 'directory'},
+      'URLs': {property: 'urls'},
+      'Context Notes': {property: 'contextNotes'},
+
+      'Has Avatar': {property: 'hasAvatar'},
+      'Avatar File Extension': {property: 'avatarFileExtension'},
+
+      'Aliases': {property: 'aliasNames'},
+
+      'Dead URLs': {ignore: true},
+
+      'Review Points': {ignore: true},
+    },
+  };
+
+  static [Thing.getYamlLoadingSpec] = ({
+    documentModes: {allInOne},
+    thingConstructors: {Artist},
+  }) => ({
+    title: `Process artists file`,
+    file: ARTIST_DATA_FILE,
+
+    documentMode: allInOne,
+    documentThing: Artist,
 
-    expose: {
-      dependencies: [thingDataProperty],
+    save(results) {
+      const artists = results;
 
-      compute: ({
-        [thingDataProperty]: thingData,
-        [Artist.instance]: artist
-      }) =>
-        thingData?.filter(thing =>
-          thing[contribsProperty]
-            .some(contrib => contrib.who === artist)) ?? [],
+      const artistRefs =
+        artists.map(artist => Thing.getReference(artist));
+
+      const artistAliasNames =
+        artists.map(artist => artist.aliasNames);
+
+      const artistAliases =
+        stitchArrays({
+          originalArtistRef: artistRefs,
+          aliasNames: artistAliasNames,
+        }).flatMap(({originalArtistRef, aliasNames}) =>
+            aliasNames.map(name => {
+              const alias = new Artist();
+              alias.name = name;
+              alias.isAlias = true;
+              alias.aliasedArtist = originalArtistRef;
+              return alias;
+            }));
+
+      const artistData = [...artists, ...artistAliases];
+
+      return {artistData};
+    },
+
+    sort({artistData}) {
+      sortAlphabetically(artistData);
     },
   });
+
+  [inspect.custom]() {
+    const parts = [];
+
+    parts.push(Thing.prototype[inspect.custom].apply(this));
+
+    if (CacheableObject.getUpdateValue(this, 'isAlias')) {
+      parts.unshift(`${colors.yellow('[alias]')} `);
+
+      let aliasedArtist;
+      try {
+        aliasedArtist = this.aliasedArtist.name;
+      } catch (_error) {
+        aliasedArtist = CacheableObject.getUpdateValue(this, 'aliasedArtist');
+      }
+
+      parts.push(` ${colors.yellow(`[of ${aliasedArtist}]`)}`);
+    }
+
+    return parts.join('');
+  }
 }