diff options
| author | (quasar) nebula <qznebula@protonmail.com> | 2026-01-26 13:11:03 -0400 |
|---|---|---|
| committer | (quasar) nebula <qznebula@protonmail.com> | 2026-01-26 13:46:48 -0400 |
| commit | ca4e9b3fd53f91e1cd95c8aa20496177ec39d669 (patch) | |
| tree | 41010ff4455d44c2e791a9bf9ebadae11596afe7 /src/data/things/Artist.js | |
| parent | c4a2bd0e7b29abc201d40b7cdae7815a508f8681 (diff) | |
infra: rename singleton-export thing modules
Diffstat (limited to 'src/data/things/Artist.js')
| -rw-r--r-- | src/data/things/Artist.js | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/src/data/things/Artist.js b/src/data/things/Artist.js new file mode 100644 index 00000000..85bdc006 --- /dev/null +++ b/src/data/things/Artist.js @@ -0,0 +1,371 @@ +const ARTIST_DATA_FILE = 'artists.yaml'; + +import {inspect} from 'node:util'; + +import CacheableObject from '#cacheable-object'; +import {colors} from '#cli'; +import {input, V} from '#composite'; +import Thing from '#thing'; +import {parseArtistAliases, parseArtwork} from '#yaml'; + +import { + sortAlbumsTracksChronologically, + sortArtworksChronologically, + sortAlphabetically, + sortContributionsChronologically, +} from '#sort'; + +import {exitWithoutDependency, exposeConstant, exposeDependency} + from '#composite/control-flow'; +import {withFilteredList, withPropertyFromList} from '#composite/data'; +import {withContributionListSums} from '#composite/wiki-data'; + +import { + constitutibleArtwork, + contentString, + directory, + fileExtension, + flag, + name, + reverseReferenceList, + soupyFind, + soupyReverse, + thing, + thingList, + urls, +} from '#composite/wiki-properties'; + +export class Artist extends Thing { + static [Thing.referenceType] = 'artist'; + static [Thing.wikiData] = 'artistData'; + + static [Thing.constitutibleProperties] = [ + 'avatarArtwork', // from inline fields + ]; + + static [Thing.getPropertyDescriptors] = ({Contribution}) => ({ + // Update & expose + + name: name(V('Unnamed Artist')), + directory: directory(), + urls: urls(), + + contextNotes: contentString(), + + hasAvatar: flag(V(false)), + avatarFileExtension: fileExtension(V('jpg')), + + avatarArtwork: [ + exitWithoutDependency('hasAvatar', { + value: input.value(null), + mode: input.value('falsy'), + }), + + constitutibleArtwork.fromYAMLFieldSpec + .call(this, 'Avatar Artwork'), + ], + + isAlias: flag(V(false)), + artistAliases: thingList(V(Artist)), + aliasedArtist: thing(V(Artist)), + + // Update only + + find: soupyFind(), + reverse: soupyReverse(), + + // Expose only + + isArtist: exposeConstant(V(true)), + + mockSimpleContribution: { + flags: {expose: true}, + expose: { + dependencies: ['directory', '_find'], + compute: ({directory, _find: find}) => + Object.assign(new Contribution, { + artist: 'artist:' + directory, + + // These nulls have no effect, they're only included + // here for clarity. + date: null, + thing: null, + annotation: null, + artistProperty: null, + thingProperty: null, + + find, + }), + }, + }, + + trackArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('trackArtistContributionsBy'), + }), + + trackContributorContributions: reverseReferenceList({ + reverse: soupyReverse.input('trackContributorContributionsBy'), + }), + + trackCoverArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('trackCoverArtistContributionsBy'), + }), + + tracksAsCommentator: reverseReferenceList({ + reverse: soupyReverse.input('tracksWithCommentaryBy'), + }), + + albumArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('albumArtistContributionsBy'), + }), + + albumTrackArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('albumTrackArtistContributionsBy'), + }), + + albumCoverArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('albumCoverArtistContributionsBy'), + }), + + albumWallpaperArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('albumWallpaperArtistContributionsBy'), + }), + + albumBannerArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('albumBannerArtistContributionsBy'), + }), + + albumsAsCommentator: reverseReferenceList({ + reverse: soupyReverse.input('albumsWithCommentaryBy'), + }), + + flashContributorContributions: reverseReferenceList({ + reverse: soupyReverse.input('flashContributorContributionsBy'), + }), + + flashesAsCommentator: reverseReferenceList({ + reverse: soupyReverse.input('flashesWithCommentaryBy'), + }), + + closelyLinkedGroups: reverseReferenceList({ + reverse: soupyReverse.input('groupsCloselyLinkedTo'), + }), + + musicContributions: [ + { + dependencies: [ + 'trackArtistContributions', + 'trackContributorContributions', + ], + + compute: (continuation, { + trackArtistContributions, + trackContributorContributions, + }) => continuation({ + ['#contributions']: [ + ...trackArtistContributions, + ...trackContributorContributions, + ], + }), + }, + + { + dependencies: ['#contributions'], + compute: ({'#contributions': contributions}) => + sortContributionsChronologically( + contributions, + sortAlbumsTracksChronologically), + }, + ], + + artworkContributions: [ + { + dependencies: [ + 'trackCoverArtistContributions', + 'albumCoverArtistContributions', + 'albumWallpaperArtistContributions', + 'albumBannerArtistContributions', + ], + + compute: (continuation, { + trackCoverArtistContributions, + albumCoverArtistContributions, + albumWallpaperArtistContributions, + albumBannerArtistContributions, + }) => continuation({ + ['#contributions']: [ + ...trackCoverArtistContributions, + ...albumCoverArtistContributions, + ...albumWallpaperArtistContributions, + ...albumBannerArtistContributions, + ], + }), + }, + + { + dependencies: ['#contributions'], + compute: ({'#contributions': contributions}) => + sortContributionsChronologically( + contributions, + sortArtworksChronologically), + }, + ], + + musicVideoArtistContributions: reverseReferenceList({ + reverse: soupyReverse.input('musicVideoArtistContributionsBy'), + }), + + musicVideoContributorContributions: reverseReferenceList({ + reverse: soupyReverse.input('musicVideoContributorContributionsBy'), + }), + + totalDuration: [ + withPropertyFromList('musicContributions', V('thing')), + withPropertyFromList('#musicContributions.thing', V('isMainRelease')), + + withFilteredList('musicContributions', '#musicContributions.thing.isMainRelease') + .outputs({'#filteredList': '#mainReleaseContributions'}), + + withContributionListSums('#mainReleaseContributions'), + exposeDependency('#contributionListDuration'), + ], + }); + + static [Thing.getSerializeDescriptors] = ({ + serialize: S, + }) => ({ + name: S.id, + directory: S.id, + urls: S.id, + contextNotes: S.id, + + hasAvatar: S.id, + avatarFileExtension: S.id, + + tracksAsCommentator: S.toRefs, + albumsAsCommentator: S.toRefs, + }); + + 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 alias of originalArtist.artistAliases) { + if (alias === artist) break; + if (alias.directory === artist.directory) 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, + thingProperty: 'avatarArtwork', + fileExtensionFromThingProperty: 'avatarFileExtension', + }), + }, + + 'Has Avatar': {property: 'hasAvatar'}, + 'Avatar File Extension': {property: 'avatarFileExtension'}, + + 'Aliases': { + property: 'artistAliases', + transform: parseArtistAliases, + }, + + '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, + + 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 { + aliasedArtist = CacheableObject.getUpdateValue(this, 'aliasedArtist'); + } + + parts.push(` ${colors.yellow(`[of ${aliasedArtist}]`)}`); + } + + return parts.join(''); + } + + getOwnArtworkPath(artwork) { + return [ + 'media.artistAvatar', + this.directory, + artwork.fileExtension, + ]; + } +} |