diff options
Diffstat (limited to 'src/data/things/artist.js')
-rw-r--r-- | src/data/things/artist.js | 365 |
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, + ]; + } } |