« 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.js365
1 files changed, 254 insertions, 111 deletions
diff --git a/src/data/things/artist.js b/src/data/things/artist.js
index 303f33f3..87e1c563 100644
--- a/src/data/things/artist.js
+++ b/src/data/things/artist.js
@@ -1,121 +1,131 @@
-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 {sortAlphabetically} from '#sort';
+import {stitchArrays} from '#sugar';
+import Thing from '#thing';
+import {isName, validateArrayItems} from '#validators';
+import {getKebabCase} from '#wiki-data';
+import {parseArtwork} from '#yaml';
+
+import {exitWithoutDependency} from '#composite/control-flow';
+
+import {
+  constitutibleArtwork,
+  contentString,
+  directory,
+  fileExtension,
+  flag,
+  name,
+  reverseReferenceList,
+  singleReference,
+  soupyFind,
+  soupyReverse,
+  urls,
+  wikiData,
+} from '#composite/wiki-properties';
+
+import {artistTotalDuration} from '#composite/things/artist';
 
 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, Group, 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: flag(false),
+    avatarFileExtension: fileExtension('jpg'),
 
-    hasAvatar: Thing.common.flag(false),
-    avatarFileExtension: Thing.common.fileExtension('jpg'),
+    avatarArtwork: [
+      exitWithoutDependency({
+        dependency: 'hasAvatar',
+        value: input.value(null),
+      }),
+
+      constitutibleArtwork.fromYAMLFieldSpec
+        .call(this, 'Avatar Artwork'),
+    ],
 
     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(),
+
+    aliasedArtist: singleReference({
+      class: input.value(Artist),
+      find: soupyFind.input('artist'),
+    }),
 
     // Update only
 
-    albumData: Thing.common.wikiData(Album),
-    artistData: Thing.common.wikiData(Artist),
-    flashData: Thing.common.wikiData(Flash),
-    trackData: Thing.common.wikiData(Track),
+    find: soupyFind(),
+    reverse: soupyReverse(),
 
     // Expose only
 
-    aliasedArtist: {
-      flags: {expose: true},
+    trackArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('trackArtistContributionsBy'),
+    }),
 
-      expose: {
-        dependencies: ['artistData', 'aliasedArtistRef'],
-        compute: ({artistData, aliasedArtistRef}) =>
-          aliasedArtistRef && artistData
-            ? find.artist(aliasedArtistRef, artistData, {mode: 'quiet'})
-            : null,
-      },
-    },
+    trackContributorContributions: reverseReferenceList({
+      reverse: soupyReverse.input('trackContributorContributionsBy'),
+    }),
 
-    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)) ?? [],
-      },
-    },
+    trackCoverArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('trackCoverArtistContributionsBy'),
+    }),
 
-    tracksAsCommentator: {
-      flags: {expose: true},
+    tracksAsCommentator: reverseReferenceList({
+      reverse: soupyReverse.input('tracksWithCommentaryBy'),
+    }),
 
-      expose: {
-        dependencies: ['trackData'],
+    albumArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('albumArtistContributionsBy'),
+    }),
 
-        compute: ({trackData, [Artist.instance]: artist}) =>
-          trackData?.filter(({commentatorArtists}) =>
-            commentatorArtists.includes(artist)) ?? [],
-      },
-    },
+    albumCoverArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('albumCoverArtistContributionsBy'),
+    }),
 
-    albumsAsAlbumArtist:
-      Artist.filterByContrib('albumData', 'artistContribs'),
-    albumsAsCoverArtist:
-      Artist.filterByContrib('albumData', 'coverArtistContribs'),
-    albumsAsWallpaperArtist:
-      Artist.filterByContrib('albumData', 'wallpaperArtistContribs'),
-    albumsAsBannerArtist:
-      Artist.filterByContrib('albumData', 'bannerArtistContribs'),
+    albumWallpaperArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('albumWallpaperArtistContributionsBy'),
+    }),
 
-    albumsAsCommentator: {
-      flags: {expose: true},
+    albumBannerArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('albumBannerArtistContributionsBy'),
+    }),
 
-      expose: {
-        dependencies: ['albumData'],
+    albumsAsCommentator: reverseReferenceList({
+      reverse: soupyReverse.input('albumsWithCommentaryBy'),
+    }),
 
-        compute: ({albumData, [Artist.instance]: artist}) =>
-          albumData?.filter(({commentatorArtists}) =>
-            commentatorArtists.includes(artist)) ?? [],
-      },
-    },
+    flashContributorContributions: reverseReferenceList({
+      reverse: soupyReverse.input('flashContributorContributionsBy'),
+    }),
+
+    flashesAsCommentator: reverseReferenceList({
+      reverse: soupyReverse.input('flashesWithCommentaryBy'),
+    }),
+
+    closelyLinkedGroups: reverseReferenceList({
+      reverse: soupyReverse.input('groupsCloselyLinkedTo'),
+    }),
 
-    flashesAsContributor: Artist.filterByContrib(
-      'flashData',
-      'contributorContribs'
-    ),
+    totalDuration: artistTotalDuration(),
   });
 
   static [Thing.getSerializeDescriptors] = ({
@@ -131,33 +141,166 @@ export class Artist extends Thing {
 
     aliasNames: S.id,
 
-    tracksAsArtist: S.toRefs,
-    tracksAsContributor: S.toRefs,
-    tracksAsCoverArtist: S.toRefs,
     tracksAsCommentator: S.toRefs,
-
-    albumsAsAlbumArtist: S.toRefs,
-    albumsAsCoverArtist: S.toRefs,
-    albumsAsWallpaperArtist: S.toRefs,
-    albumsAsBannerArtist: S.toRefs,
     albumsAsCommentator: S.toRefs,
-
-    flashesAsContributor: S.toRefs,
   });
 
-  static filterByContrib = (thingDataProperty, contribsProperty) => ({
-    flags: {expose: true},
+  static [Thing.findSpecs] = {
+    artist: {
+      referenceTypes: ['artist', 'artist-gallery'],
+      bindTo: 'artistData',
 
-    expose: {
-      dependencies: [thingDataProperty],
+      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'},
+
+      // note: doesn't really work as an independent field yet
+      'Avatar Artwork': {
+        property: 'avatarArtwork',
+        transform:
+          parseArtwork({
+            single: true,
+            fileExtensionFromThingProperty: 'avatarFileExtension',
+          }),
+      },
+
+      'Has Avatar': {property: 'hasAvatar'},
+      'Avatar File Extension': {property: 'avatarFileExtension'},
+
+      'Aliases': {property: 'aliasNames'},
+
+      'Dead URLs': {ignore: true},
+
+      'Review Points': {ignore: true},
+    },
+  };
 
-      compute: ({
-        [thingDataProperty]: thingData,
-        [Artist.instance]: artist
-      }) =>
-        thingData?.filter(thing =>
-          thing[contribsProperty]
-            .some(contrib => contrib.who === artist)) ?? [],
+  static [Thing.getYamlLoadingSpec] = ({
+    documentModes: {allInOne},
+    thingConstructors: {Artist},
+  }) => ({
+    title: `Process artists file`,
+    file: ARTIST_DATA_FILE,
+
+    documentMode: allInOne,
+    documentThing: Artist,
+
+    save(results) {
+      const artists = results;
+
+      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];
+
+      const artworkData =
+        artistData
+          .filter(artist => artist.hasAvatar)
+          .map(artist => artist.avatarArtwork);
+
+      return {artistData, artworkData};
+    },
+
+    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('');
+  }
+
+  getOwnArtworkPath(artwork) {
+    return [
+      'media.artistAvatar',
+      this.directory,
+      artwork.fileExtension,
+    ];
+  }
 }