diff options
Diffstat (limited to 'src/data/things/album.js')
-rw-r--r-- | src/data/things/album.js | 553 |
1 files changed, 442 insertions, 111 deletions
diff --git a/src/data/things/album.js b/src/data/things/album.js index e9f55b2c..4c85ddfa 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -3,29 +3,41 @@ export const DATA_ALBUM_DIRECTORY = 'album'; import * as path from 'node:path'; 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 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, + parseContributors, + parseDate, + parseDimensions, + parseWallpaperParts, +} from '#yaml'; import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; -import {exitWithoutContribs, withDirectory, withResolvedReference} + +import {exitWithoutContribs, withDirectory, withCoverArtDate} from '#composite/wiki-data'; import { additionalFiles, + additionalNameList, commentary, color, commentatorArtists, + constitutibleArtwork, + constitutibleArtworkList, + contentString, contribsPresent, contributionList, dimensions, @@ -33,35 +45,60 @@ import { fileExtension, flag, name, + referencedArtworkList, referenceList, + reverseReferenceList, 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] = ({ ArtTag, - Artist, + Artwork, 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), + + color: color(), + urls: urls(), + + additionalNames: additionalNameList(), bandcampAlbumIdentifier: simpleString(), bandcampArtworkIdentifier: simpleString(), @@ -71,13 +108,13 @@ export class Album extends Thing { dateAddedToWiki: simpleDate(), coverArtDate: [ - exitWithoutContribs({contribs: 'coverArtistContribs'}), - - exposeUpdateValueOrContinue({ - validate: input.value(isDate), + withCoverArtDate({ + from: input.updateValue({ + validate: isDate, + }), }), - exposeDependency({dependency: 'date'}), + exposeDependency({dependency: '#coverArtDate'}), ], coverArtFileExtension: [ @@ -102,6 +139,15 @@ export class Album extends Thing { simpleString(), ], + wallpaperParts: [ + exitWithoutContribs({ + contribs: 'wallpaperArtistContribs', + value: input.value([]), + }), + + wallpaperParts(), + ], + bannerStyle: [ exitWithoutContribs({contribs: 'bannerArtistContribs'}), simpleString(), @@ -112,34 +158,105 @@ 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(), + creditSources: commentary(), additionalFiles: additionalFiles(), - trackSections: referenceList({ - referenceType: input.value('unqualified-track-section'), - data: 'ownTrackSectionData', - find: input.value(find.unqualifiedTrackSection), + trackSections: thingList({ + class: input.value(TrackSection), }), - artistContribs: contributionList(), - coverArtistContribs: contributionList(), - trackCoverArtistContribs: contributionList(), - wallpaperArtistContribs: contributionList(), - bannerArtistContribs: contributionList(), + artistContribs: contributionList({ + date: 'date', + artistProperty: input.value('albumArtistContributions'), + }), + + coverArtistContribs: [ + withCoverArtDate(), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumCoverArtistContributions'), + }), + ], + + trackCoverArtistContribs: contributionList({ + // May be null, indicating cover art was added for tracks on the date + // each track specifies, or else the track's own release date. + date: 'trackArtDate', + + // This is the "correct" value, but it gets overwritten - with the same + // value - regardless. + artistProperty: input.value('trackCoverArtistContributions'), + }), + + wallpaperArtistContribs: [ + withCoverArtDate(), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumWallpaperArtistContributions'), + }), + ], + + bannerArtistContribs: [ + withCoverArtDate(), + + contributionList({ + date: '#coverArtDate', + artistProperty: input.value('albumBannerArtistContributions'), + }), + ], groups: referenceList({ class: input.value(Group), - find: input.value(find.group), - data: 'groupData', + find: soupyFind.input('group'), }), artTags: [ @@ -150,34 +267,43 @@ 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(), + + // used for referencedArtworkList (mixedFind) + artworkData: wikiData({ + class: input.value(Artwork), }), - ownTrackSectionData: wikiData({ - class: input.value(TrackSection), + // used for withMatchingContributionPresets (indirectly by Contribution) + wikiInfo: thing({ + class: input.value(WikiInfo), }), // Expose only commentatorArtists: commentatorArtists(), - hasCoverArt: contribsPresent({contribs: 'coverArtistContribs'}), + hasCoverArt: [ + withHasCoverArt(), + exposeDependency({dependency: '#hasCoverArt'}), + ], + hasWallpaperArt: contribsPresent({contribs: 'wallpaperArtistContribs'}), hasBannerArt: contribsPresent({contribs: 'bannerArtistContribs'}), @@ -229,20 +355,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, @@ -265,6 +502,46 @@ export class Album extends Thing { 'Listed on Homepage': {property: 'isListedOnHomepage'}, 'Listed in Galleries': {property: 'isListedInGalleries'}, + 'Cover Artwork': { + property: 'coverArtworks', + transform: + parseArtwork({ + dimensionsFromThingProperty: 'coverArtDimensions', + fileExtensionFromThingProperty: 'coverArtFileExtension', + dateFromThingProperty: 'coverArtDate', + artistContribsFromThingProperty: 'coverArtistContribs', + artistContribsArtistProperty: 'albumCoverArtistContributions', + artTagsFromThingProperty: 'artTags', + referencedArtworksFromThingProperty: 'referencedArtworks', + }), + }, + + 'Banner Artwork': { + property: 'bannerArtwork', + transform: + parseArtwork({ + single: true, + dimensionsFromThingProperty: 'bannerDimensions', + fileExtensionFromThingProperty: 'bannerFileExtension', + dateFromThingProperty: 'date', + artistContribsFromThingProperty: 'bannerArtistContribs', + artistContribsArtistProperty: 'albumBannerArtistContributions', + }), + }, + + 'Wallpaper Artwork': { + property: 'wallpaperArtwork', + transform: + parseArtwork({ + single: true, + dimensionsFromThingProperty: null, + fileExtensionFromThingProperty: 'wallpaperFileExtension', + dateFromThingProperty: 'date', + artistContribsFromThingProperty: 'wallpaperArtistContribs', + artistContribsArtistProperty: 'albumWallpaperArtistContributions', + }), + }, + 'Cover Art Date': { property: 'coverArtDate', transform: parseDate, @@ -288,6 +565,11 @@ export class Album extends Thing { transform: parseDimensions, }, + 'Default Track Dimensions': { + property: 'trackDimensions', + transform: parseDimensions, + }, + 'Wallpaper Artists': { property: 'wallpaperArtistContribs', transform: parseContributors, @@ -296,6 +578,11 @@ export class Album extends Thing { 'Wallpaper Style': {property: 'wallpaperStyle'}, 'Wallpaper File Extension': {property: 'wallpaperFileExtension'}, + 'Wallpaper Parts': { + property: 'wallpaperParts', + transform: parseWallpaperParts, + }, + 'Banner Artists': { property: 'bannerArtistContribs', transform: parseContributors, @@ -310,12 +597,18 @@ export class Album extends Thing { }, 'Commentary': {property: 'commentary'}, + 'Credit Sources': {property: 'creditSources'}, 'Additional Files': { property: 'additionalFiles', transform: parseAdditionalFiles, }, + 'Referenced Artworks': { + property: 'referencedArtworks', + transform: parseAnnotatedReferences, + }, + 'Franchises': {ignore: true}, 'Artists': { @@ -338,11 +631,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`, @@ -363,6 +668,7 @@ export class Album extends Thing { const albumData = []; const trackSectionData = []; const trackData = []; + const artworkData = []; for (const {header: album, entries} of results) { const trackSections = []; @@ -386,15 +692,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); }; @@ -410,23 +709,37 @@ 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); } 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); + } - album.ownTrackSectionData = trackSections; + album.trackSections = trackSections; } - return {albumData, trackSectionData, trackData}; + return { + albumData, + trackSectionData, + trackData, + artworkData, + }; }, sort({albumData, trackData}) { @@ -434,6 +747,44 @@ export class Album extends Thing { sortAlbumsTracksChronologically(trackData); }, }); + + 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, + ]; + } } export class TrackSection extends Thing { @@ -462,30 +813,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 @@ -517,42 +870,10 @@ export class TrackSection extends Thing { }, ], - startIndex: [ - withAlbum(), + continueCountingFrom: [ + withContinueCountingFrom(), - 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), - }), - - { - dependencies: ['#album.trackSections', '#index'], - compute: ({ - ['#album.trackSections']: trackSections, - ['#index']: index, - }) => - accumulateSum( - trackSections - .slice(0, index) - .map(section => section.tracks.length)), - }, + exposeDependency({dependency: '#continueCountingFrom'}), ], }); @@ -570,15 +891,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'}, }, }; @@ -595,16 +928,14 @@ export class TrackSection extends Thing { 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); @@ -615,8 +946,8 @@ export class TrackSection extends Thing { : `#${albumIndex + 1}`); const range = - (albumIndex >= 0 && first !== null && length !== null - ? `: ${first + 1}-${first + length + 1}` + (albumIndex >= 0 && first !== null && last !== null + ? `: ${first}-${last}` : ''); parts.push(` (${colors.yellow(num + range)} in ${colors.green(albumName)})`); |