diff options
Diffstat (limited to 'src/data/things/track.js')
| -rw-r--r-- | src/data/things/track.js | 1436 |
1 files changed, 1149 insertions, 287 deletions
diff --git a/src/data/things/track.js b/src/data/things/track.js index a0d2f641..39a1804f 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -3,43 +3,69 @@ import {inspect} from 'node:util'; import CacheableObject from '#cacheable-object'; import {colors} from '#cli'; import {input} from '#composite'; -import find from '#find'; +import {onlyItem} from '#sugar'; +import {sortByDate} from '#sort'; import Thing from '#thing'; -import {isBoolean, isColor, isContributionList, isDate, isFileExtension} - from '#validators'; +import {getKebabCase} from '#wiki-data'; + +import { + isBoolean, + isColor, + isContentString, + isContributionList, + isDate, + isFileExtension, + validateReference, +} from '#validators'; import { parseAdditionalFiles, parseAdditionalNames, parseAnnotatedReferences, + parseArtwork, + parseCommentary, parseContributors, + parseCreditingSources, + parseReferencingSources, parseDate, parseDimensions, parseDuration, + parseLyrics, } from '#yaml'; -import {withPropertyFromObject} from '#composite/data'; - import { exitWithoutDependency, + exitWithoutUpdateValue, exposeConstant, exposeDependency, exposeDependencyOrContinue, exposeUpdateValueOrContinue, exposeWhetherDependencyAvailable, + withAvailabilityFilter, + withResultOfAvailabilityCheck, } from '#composite/control-flow'; import { + fillMissingListItems, + withFilteredList, + withFlattenedList, + withIndexInList, + withMappedList, + withPropertiesFromObject, + withPropertyFromList, + withPropertyFromObject, +} from '#composite/data'; + +import { withRecontextualizedContributionList, withRedatedContributionList, withResolvedContribs, + withResolvedReference, } from '#composite/wiki-data'; import { - additionalFiles, - additionalNameList, - commentary, commentatorArtists, + constitutibleArtworkList, contentString, contributionList, dimensions, @@ -50,200 +76,271 @@ import { referenceList, referencedArtworkList, reverseReferenceList, - reverseReferencedArtworkList, simpleDate, simpleString, - singleReference, + soupyFind, + soupyReverse, thing, + thingList, urls, wikiData, } from '#composite/wiki-properties'; import { - exitWithoutUniqueCoverArt, - inheritContributionListFromOriginalRelease, - inheritFromOriginalRelease, - trackReverseReferenceList, - withAlbum, - withAlwaysReferenceByDirectory, - withContainingTrackSection, - withDate, - withDirectorySuffix, - withHasUniqueCoverArt, - withOriginalRelease, - withOtherReleases, - withPropertyFromAlbum, - withSuffixDirectoryFromAlbum, - withTrackArtDate, + inheritContributionListFromMainRelease, + inheritFromMainRelease, } from '#composite/things/track'; export class Track extends Thing { static [Thing.referenceType] = 'track'; + static [Thing.wikiData] = 'trackData'; + + static [Thing.constitutibleProperties] = [ + // Contributions currently aren't being observed for constitution. + // 'artistContribs', // from main release or album + // 'contributorContribs', // from main release + // 'coverArtistContribs', // from main release + + 'trackArtworks', // from inline fields + ]; static [Thing.getPropertyDescriptors] = ({ + AdditionalFile, + AdditionalName, Album, ArtTag, - Artist, - Flash, + Artwork, + CommentaryEntry, + CreditingSourcesEntry, + LyricsEntry, + ReferencingSourcesEntry, TrackSection, WikiInfo, }) => ({ - // Update & expose + // > Update & expose - Internal relationships - name: name('Unnamed Track'), + album: thing({ + class: input.value(Album), + }), - directory: [ - withDirectorySuffix(), + trackSection: thing({ + class: input.value(TrackSection), + }), - directory({ - suffix: '#directorySuffix', - }), - ], + // > Update & expose - Identifying metadata - suffixDirectoryFromAlbum: [ - { - dependencies: [ - input.updateValue({validate: isBoolean}), - ], + name: name('Unnamed Track'), + nameText: contentString(), - compute: (continuation, { - [input.updateValue()]: value, - }) => continuation({ - ['#flagValue']: value ?? false, - }), - }, + directory: directory({ + suffix: 'directorySuffix', + }), + + suffixDirectoryFromAlbum: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), - withSuffixDirectoryFromAlbum({ - flagValue: '#flagValue', + withPropertyFromObject({ + object: 'trackSection', + property: input.value('suffixTrackDirectories'), }), exposeDependency({ - dependency: '#suffixDirectoryFromAlbum', - }) + dependency: '#trackSection.suffixTrackDirectories', + }), ], - additionalNames: additionalNameList(), - - bandcampTrackIdentifier: simpleString(), - bandcampArtworkIdentifier: simpleString(), - - duration: duration(), - urls: urls(), - dateFirstReleased: simpleDate(), - - color: [ + // Controls how find.track works - it'll never be matched by + // a reference just to the track's name, which means you don't + // have to always reference some *other* (much more commonly + // referenced) track by directory instead of more naturally by name. + alwaysReferenceByDirectory: [ exposeUpdateValueOrContinue({ - validate: input.value(isColor), + validate: input.value(isBoolean), }), - withContainingTrackSection(), - withPropertyFromObject({ - object: '#trackSection', - property: input.value('color'), + object: 'album', + property: input.value('alwaysReferenceTracksByDirectory'), }), - exposeDependencyOrContinue({dependency: '#trackSection.color'}), + // Falsy mode means this exposes true if the album's property is true, + // but continues if the property is false (which is also the default). + exposeDependencyOrContinue({ + dependency: '#album.alwaysReferenceTracksByDirectory', + mode: input.value('falsy'), + }), - withPropertyFromAlbum({ - property: input.value('color'), + exitWithoutDependency({ + dependency: '_mainRelease', + value: input.value(false), }), - exposeDependency({dependency: '#album.color'}), - ], + exitWithoutDependency({ + dependency: 'mainReleaseTrack', + value: input.value(false), + }), - alwaysReferenceByDirectory: [ - withAlwaysReferenceByDirectory(), - exposeDependency({dependency: '#alwaysReferenceByDirectory'}), + withPropertyFromObject({ + object: 'mainReleaseTrack', + property: input.value('name'), + }), + + { + dependencies: ['name', '#mainReleaseTrack.name'], + compute: ({ + ['name']: name, + ['#mainReleaseTrack.name']: mainReleaseName, + }) => + getKebabCase(name) === + getKebabCase(mainReleaseName), + }, ], - // Disables presenting the track as though it has its own unique artwork. - // This flag should only be used in select circumstances, i.e. to override - // an album's trackCoverArtists. This flag supercedes that property, as well - // as the track's own coverArtists. - disableUniqueCoverArt: flag(), + // Album or track. The exposed value is really just what's provided here, + // whether or not a matching track is found on a provided album, for + // example. When presenting or processing, read `mainReleaseTrack`. + mainRelease: [ + exitWithoutUpdateValue({ + validate: input.value( + validateReference(['album', 'track'])), + }), - // File extension for track's corresponding media file. This represents the - // track's unique cover artwork, if any, and does not inherit the extension - // of the album's main artwork. It does inherit trackCoverArtFileExtension, - // if present on the album. - coverArtFileExtension: [ - exitWithoutUniqueCoverArt(), + { + dependencies: ['name'], + transform: (ref, continuation, {name: ownName}) => + (ref === 'same name single' + ? continuation(ref, { + ['#albumOrTrackReference']: null, + ['#sameNameSingleReference']: ownName, + }) + : continuation(ref, { + ['#albumOrTrackReference']: ref, + ['#sameNameSingleReference']: null, + })), + }, - exposeUpdateValueOrContinue({ - validate: input.value(isFileExtension), + withResolvedReference({ + ref: '#albumOrTrackReference', + find: soupyFind.input('trackMainReleasesOnly'), + }).outputs({ + '#resolvedReference': '#matchingTrack', }), - withPropertyFromAlbum({ - property: input.value('trackCoverArtFileExtension'), + withResolvedReference({ + ref: '#albumOrTrackReference', + find: soupyFind.input('album'), + }).outputs({ + '#resolvedReference': '#matchingAlbum', }), - exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), - - exposeConstant({ - value: input.value('jpg'), + withResolvedReference({ + ref: '#sameNameSingleReference', + find: soupyFind.input('albumSinglesOnly'), + findOptions: input.value({ + fuzz: { + capitalization: true, + kebab: true, + }, + }), + }).outputs({ + '#resolvedReference': '#sameNameSingle', }), - ], - coverArtDate: [ - withTrackArtDate({ - from: input.updateValue({ - validate: isDate, - }), + exposeDependencyOrContinue({ + dependency: '#sameNameSingle', }), - exposeDependency({dependency: '#trackArtDate'}), - ], + { + dependencies: [ + '#matchingTrack', + '#matchingAlbum', + ], - coverArtDimensions: [ - exitWithoutUniqueCoverArt(), + compute: (continuation, { + ['#matchingTrack']: matchingTrack, + ['#matchingAlbum']: matchingAlbum, + }) => + (matchingTrack && matchingAlbum + ? continuation() + : matchingTrack ?? matchingAlbum + ? matchingTrack ?? matchingAlbum + : null), + }, - withPropertyFromAlbum({ - property: input.value('trackDimensions'), + withPropertyFromObject({ + object: '#matchingAlbum', + property: input.value('tracks'), }), - exposeDependencyOrContinue({dependency: '#album.trackDimensions'}), + { + dependencies: [ + '#matchingAlbum.tracks', + '#matchingTrack', + ], - dimensions(), + compute: ({ + ['#matchingAlbum.tracks']: matchingAlbumTracks, + ['#matchingTrack']: matchingTrack, + }) => + (matchingAlbumTracks.includes(matchingTrack) + ? matchingTrack + : null), + }, ], - commentary: commentary(), - creditSources: commentary(), + bandcampTrackIdentifier: simpleString(), + bandcampArtworkIdentifier: simpleString(), - lyrics: [ - inheritFromOriginalRelease(), - contentString(), - ], + additionalNames: thingList({ + class: input.value(AdditionalName), + }), - additionalFiles: additionalFiles(), - sheetMusicFiles: additionalFiles(), - midiProjectFiles: additionalFiles(), + dateFirstReleased: simpleDate(), - originalReleaseTrack: singleReference({ - class: input.value(Track), - find: input.value(find.track), - data: 'trackData', - }), + // > Update & expose - Credits and contributors - // Internal use only - for directly identifying an album inside a track's - // util.inspect display, if it isn't indirectly available (by way of being - // included in an album's track list). - dataSourceAlbum: singleReference({ - class: input.value(Album), - find: input.value(find.album), - data: 'albumData', - }), + artistText: [ + exposeUpdateValueOrContinue({ + validate: input.value(isContentString), + }), - artistContribs: [ - inheritContributionListFromOriginalRelease(), + withPropertyFromObject({ + object: 'album', + property: input.value('trackArtistText'), + }), + + exposeDependency({ + dependency: '#album.trackArtistText', + }), + ], + + artistTextInLists: [ + exposeUpdateValueOrContinue({ + validate: input.value(isContentString), + }), + + exposeDependencyOrContinue({ + dependency: '_artistText', + }), + + withPropertyFromObject({ + object: 'album', + property: input.value('trackArtistText'), + }), - withDate(), + exposeDependency({ + dependency: '#album.trackArtistText', + }), + ], + artistContribs: [ withResolvedContribs({ from: input.updateValue({validate: isContributionList}), thingProperty: input.thisProperty(), artistProperty: input.value('trackArtistContributions'), - date: '#date', + date: 'date', }).outputs({ '#resolvedContribs': '#artistContribs', }), @@ -253,61 +350,141 @@ export class Track extends Thing { mode: input.value('empty'), }), - withPropertyFromAlbum({ - property: input.value('artistContribs'), + // Specifically inherit artist contributions later than artist contribs. + // Secondary releases' artists may differ from the main release. + inheritContributionListFromMainRelease(), + + withPropertyFromObject({ + object: 'album', + property: input.value('trackArtistContribs'), }), withRecontextualizedContributionList({ - list: '#album.artistContribs', + list: '#album.trackArtistContribs', artistProperty: input.value('trackArtistContributions'), }), withRedatedContributionList({ - list: '#album.artistContribs', - date: '#date', + list: '#album.trackArtistContribs', + date: 'date', }), - exposeDependency({dependency: '#album.artistContribs'}), + exposeDependency({dependency: '#album.trackArtistContribs'}), ], contributorContribs: [ - inheritContributionListFromOriginalRelease(), - - withDate(), + inheritContributionListFromMainRelease(), contributionList({ - date: '#date', + date: 'date', artistProperty: input.value('trackContributorContributions'), }), ], - // Cover artists aren't inherited from the original release, since it - // typically varies by release and isn't defined by the musical qualities - // of the track. - coverArtistContribs: [ - exitWithoutUniqueCoverArt({ + // > Update & expose - General configuration + + countInArtistTotals: [ + exposeUpdateValueOrContinue({ + validate: input.value(isBoolean), + }), + + withPropertyFromObject({ + object: 'trackSection', + property: input.value('countTracksInArtistTotals'), + }), + + exposeDependency({dependency: '#trackSection.countTracksInArtistTotals'}), + ], + + disableUniqueCoverArt: flag(), + disableDate: flag(), + + // > Update & expose - General metadata + + duration: duration(), + + color: [ + exposeUpdateValueOrContinue({ + validate: input.value(isColor), + }), + + withPropertyFromObject({ + object: 'trackSection', + property: input.value('color'), + }), + + exposeDependencyOrContinue({dependency: '#trackSection.color'}), + + withPropertyFromObject({ + object: 'album', + property: input.value('color'), + }), + + exposeDependency({dependency: '#album.color'}), + ], + + needsLyrics: [ + exposeUpdateValueOrContinue({ + mode: input.value('falsy'), + validate: input.value(isBoolean), + }), + + exitWithoutDependency({ + dependency: '_lyrics', + mode: input.value('empty'), + value: input.value(false), + }), + + withPropertyFromList({ + list: '_lyrics', + property: input.value('helpNeeded'), + }), + + { + dependencies: ['#lyrics.helpNeeded'], + compute: ({ + ['#lyrics.helpNeeded']: helpNeeded, + }) => + helpNeeded.includes(true) + }, + ], + + urls: urls(), + + // > Update & expose - Artworks + + trackArtworks: [ + exitWithoutDependency({ + dependency: 'hasUniqueCoverArt', + mode: input.value('falsy'), value: input.value([]), }), - withTrackArtDate({ - fallback: input.value(true), + constitutibleArtworkList.fromYAMLFieldSpec + .call(this, 'Track Artwork'), + ], + + coverArtistContribs: [ + exitWithoutDependency({ + dependency: 'hasUniqueCoverArt', + mode: input.value('falsy'), + value: input.value([]), }), withResolvedContribs({ from: input.updateValue({validate: isContributionList}), - thingProperty: input.thisProperty(), + thingProperty: input.value('coverArtistContribs'), artistProperty: input.value('trackCoverArtistContributions'), - date: '#trackArtDate', - }).outputs({ - '#resolvedContribs': '#coverArtistContribs', + date: 'coverArtDate', }), exposeDependencyOrContinue({ - dependency: '#coverArtistContribs', + dependency: '#resolvedContribs', mode: input.value('empty'), }), - withPropertyFromAlbum({ + withPropertyFromObject({ + object: 'album', property: input.value('trackCoverArtistContribs'), }), @@ -318,165 +495,658 @@ export class Track extends Thing { withRedatedContributionList({ list: '#album.trackCoverArtistContribs', - date: '#trackArtDate', + date: 'coverArtDate', }), - exposeDependency({dependency: '#album.trackCoverArtistContribs'}), + exposeDependency({ + dependency: '#album.trackCoverArtistContribs', + }), ], - referencedTracks: [ - inheritFromOriginalRelease({ - notFoundValue: input.value([]), + coverArtDate: [ + exitWithoutDependency({ + dependency: 'hasUniqueCoverArt', + mode: input.value('falsy'), }), - referenceList({ - class: input.value(Track), - find: input.value(find.track), - data: 'trackData', + exposeUpdateValueOrContinue({ + validate: input.value(isDate), + }), + + withPropertyFromObject({ + object: 'album', + property: input.value('trackArtDate'), + }), + + exposeDependencyOrContinue({ + dependency: '#album.trackArtDate', + }), + + exposeDependency({ + dependency: 'date', }), ], - sampledTracks: [ - inheritFromOriginalRelease({ - notFoundValue: input.value([]), + coverArtFileExtension: [ + exitWithoutDependency({ + dependency: 'hasUniqueCoverArt', + mode: input.value('falsy'), }), - referenceList({ - class: input.value(Track), - find: input.value(find.track), - data: 'trackData', + exposeUpdateValueOrContinue({ + validate: input.value(isFileExtension), + }), + + withPropertyFromObject({ + object: 'album', + property: input.value('trackCoverArtFileExtension'), + }), + + exposeDependencyOrContinue({dependency: '#album.trackCoverArtFileExtension'}), + + exposeConstant({ + value: input.value('jpg'), + }), + ], + + coverArtDimensions: [ + exitWithoutDependency({ + dependency: 'hasUniqueCoverArt', + mode: input.value('falsy'), + }), + + exposeUpdateValueOrContinue(), + + withPropertyFromObject({ + object: 'album', + property: input.value('trackDimensions'), }), + + exposeDependencyOrContinue({dependency: '#album.trackDimensions'}), + + dimensions(), ], artTags: [ - exitWithoutUniqueCoverArt({ + exitWithoutDependency({ + dependency: 'hasUniqueCoverArt', + mode: input.value('falsy'), value: input.value([]), }), referenceList({ class: input.value(ArtTag), - find: input.value(find.artTag), - data: 'artTagData', + find: soupyFind.input('artTag'), }), ], referencedArtworks: [ - exitWithoutUniqueCoverArt({ + exitWithoutDependency({ + dependency: 'hasUniqueCoverArt', + mode: input.value('falsy'), value: input.value([]), }), - withTrackArtDate({ - fallback: input.value(true), + referencedArtworkList(), + ], + + // > Update & expose - Referenced tracks + + previousProductionTracks: [ + inheritFromMainRelease(), + + referenceList({ + class: input.value(Track), + find: soupyFind.input('trackMainReleasesOnly'), }), + ], + + referencedTracks: [ + inheritFromMainRelease(), - referencedArtworkList({ - date: '#trackArtDate', + referenceList({ + class: input.value(Track), + find: soupyFind.input('trackMainReleasesOnly'), }), ], - // Update only + sampledTracks: [ + inheritFromMainRelease(), - albumData: wikiData({ - class: input.value(Album), + referenceList({ + class: input.value(Track), + find: soupyFind.input('trackMainReleasesOnly'), + }), + ], + + // > Update & expose - Additional files + + additionalFiles: thingList({ + class: input.value(AdditionalFile), }), - artistData: wikiData({ - class: input.value(Artist), + sheetMusicFiles: thingList({ + class: input.value(AdditionalFile), }), - artTagData: wikiData({ - class: input.value(ArtTag), + midiProjectFiles: thingList({ + class: input.value(AdditionalFile), + }), + + // > Update & expose - Content entries + + lyrics: [ + // TODO: Inherited lyrics are literally the same objects, so of course + // their .thing properties aren't going to point back to this one, and + // certainly couldn't be recontextualized... + inheritFromMainRelease(), + + thingList({ + class: input.value(LyricsEntry), + }), + ], + + commentary: thingList({ + class: input.value(CommentaryEntry), }), - flashData: wikiData({ - class: input.value(Flash), + creditingSources: thingList({ + class: input.value(CreditingSourcesEntry), }), - trackData: wikiData({ - class: input.value(Track), + referencingSources: thingList({ + class: input.value(ReferencingSourcesEntry), }), - trackSectionData: wikiData({ - class: input.value(TrackSection), + // > Update only + + find: soupyFind(), + reverse: soupyReverse(), + + // used for referencedArtworkList (mixedFind) + artworkData: wikiData({ + class: input.value(Artwork), }), + // used for withMatchingContributionPresets (indirectly by Contribution) wikiInfo: thing({ class: input.value(WikiInfo), }), - // Expose only + // > Expose only + + isTrack: [ + exposeConstant({ + value: input.value(true), + }), + ], commentatorArtists: commentatorArtists(), - album: [ - withAlbum(), - exposeDependency({dependency: '#album'}), + directorySuffix: [ + exitWithoutDependency({ + dependency: 'suffixDirectoryFromAlbum', + mode: input.value('falsy'), + }), + + withPropertyFromObject({ + object: 'trackSection', + property: input.value('directorySuffix'), + }), + + exposeDependency({ + dependency: '#trackSection.directorySuffix', + }), ], date: [ - withDate(), - exposeDependency({dependency: '#date'}), + { + dependencies: ['disableDate'], + compute: (continuation, {disableDate}) => + (disableDate + ? null + : continuation()), + }, + + exposeDependencyOrContinue({ + dependency: 'dateFirstReleased', + }), + + withPropertyFromObject({ + object: 'album', + property: input.value('date'), + }), + + exposeDependency({ + dependency: '#album.date', + }), ], - hasUniqueCoverArt: [ - withHasUniqueCoverArt(), - exposeDependency({dependency: '#hasUniqueCoverArt'}), + trackNumber: [ + // Zero is the fallback, not one, but in most albums the first track + // (and its intended output by this composition) will be one. + exitWithoutDependency({ + dependency: 'trackSection', + value: input.value(0), + }), + + withPropertiesFromObject({ + object: 'trackSection', + properties: input.value(['tracks', 'startCountingFrom']), + }), + + withIndexInList({ + list: '#trackSection.tracks', + item: input.myself(), + }), + + exitWithoutDependency({ + dependency: '#index', + value: input.value(0), + mode: input.value('index'), + }), + + { + dependencies: ['#trackSection.startCountingFrom', '#index'], + compute: ({ + ['#trackSection.startCountingFrom']: startCountingFrom, + ['#index']: index, + }) => startCountingFrom + index, + }, ], - isOriginalRelease: [ - withOriginalRelease(), + // Whether or not the track has "unique" cover artwork - a cover which is + // specifically associated with this track in particular, rather than with + // the track's album as a whole. This is typically used to select between + // displaying the track artwork and a fallback, such as the album artwork + // or a placeholder. (This property is named hasUniqueCoverArt instead of + // the usual hasCoverArt to emphasize that it does not inherit from the + // album.) + // + // hasUniqueCoverArt is based only around the presence of *specified* + // cover artist contributions, not whether the references to artists on those + // contributions actually resolve to anything. It completely evades interacting + // with find/replace. + hasUniqueCoverArt: [ + { + dependencies: ['disableUniqueCoverArt'], + compute: (continuation, {disableUniqueCoverArt}) => + (disableUniqueCoverArt + ? false + : continuation()), + }, + + withResultOfAvailabilityCheck({ + from: '_coverArtistContribs', + mode: input.value('empty'), + }), + + { + dependencies: ['#availability'], + compute: (continuation, { + ['#availability']: availability, + }) => + (availability + ? true + : continuation()), + }, + + withPropertyFromObject({ + object: 'album', + property: input.value('trackCoverArtistContribs'), + internal: input.value(true), + }), + + withResultOfAvailabilityCheck({ + from: '#album.trackCoverArtistContribs', + mode: input.value('empty'), + }), + { + dependencies: ['#availability'], + compute: (continuation, { + ['#availability']: availability, + }) => + (availability + ? true + : continuation()), + }, + + exitWithoutDependency({ + dependency: '_trackArtworks', + mode: input.value('empty'), + value: input.value(false), + }), + + withPropertyFromList({ + list: '_trackArtworks', + property: input.value('artistContribs'), + internal: input.value(true), + }), + + // Since we're getting the update value for each artwork's artistContribs, + // it may not be set at all, and in that case won't be exposing as []. + fillMissingListItems({ + list: '#trackArtworks.artistContribs', + fill: input.value([]), + }), + + withFlattenedList({ + list: '#trackArtworks.artistContribs', + }), + + withResultOfAvailabilityCheck({ + from: '#flattenedList', + mode: input.value('empty'), + }), + + exposeDependency({ + dependency: '#availability', + }), + ], + + isMainRelease: [ exposeWhetherDependencyAvailable({ - dependency: '#originalRelease', + dependency: 'mainReleaseTrack', negate: input.value(true), }), ], - isRerelease: [ - withOriginalRelease(), - + isSecondaryRelease: [ exposeWhetherDependencyAvailable({ - dependency: '#originalRelease', + dependency: 'mainReleaseTrack', }), ], - otherReleases: [ - withOtherReleases(), - exposeDependency({dependency: '#otherReleases'}), + mainReleaseTrack: [ + exitWithoutDependency({ + dependency: 'mainRelease', + }), + + withPropertyFromObject({ + object: 'mainRelease', + property: input.value('isTrack'), + }), + + { + dependencies: ['mainRelease', '#mainRelease.isTrack'], + compute: (continuation, { + ['mainRelease']: mainRelease, + ['#mainRelease.isTrack']: mainReleaseIsTrack, + }) => + (mainReleaseIsTrack + ? mainRelease + : continuation()), + }, + + { + dependencies: ['name', '_directory'], + compute: (continuation, { + ['name']: ownName, + ['_directory']: ownDirectory, + }) => { + const ownNameKebabed = getKebabCase(ownName); + + return continuation({ + ['#mapItsNameLikeName']: + name => getKebabCase(name) === ownNameKebabed, + + ['#mapItsDirectoryLikeDirectory']: + (ownDirectory + ? directory => directory === ownDirectory + : () => false), + + ['#mapItsNameLikeDirectory']: + (ownDirectory + ? name => getKebabCase(name) === ownDirectory + : () => false), + + ['#mapItsDirectoryLikeName']: + directory => directory === ownNameKebabed, + }); + }, + }, + + withPropertyFromObject({ + object: 'mainRelease', + property: input.value('tracks'), + }), + + withPropertyFromList({ + list: '#mainRelease.tracks', + property: input.value('mainRelease'), + internal: input.value(true), + }), + + withAvailabilityFilter({ + from: '#mainRelease.tracks.mainRelease', + }), + + withMappedList({ + list: '#availabilityFilter', + map: input.value(item => !item), + }).outputs({ + '#mappedList': '#availabilityFilter', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#availabilityFilter', + }).outputs({ + '#filteredList': '#mainRelease.tracks', + }), + + withPropertyFromList({ + list: '#mainRelease.tracks', + property: input.value('name'), + }), + + withPropertyFromList({ + list: '#mainRelease.tracks', + property: input.value('directory'), + internal: input.value(true), + }), + + withMappedList({ + list: '#mainRelease.tracks.name', + map: '#mapItsNameLikeName', + }).outputs({ + '#mappedList': '#filterItsNameLikeName', + }), + + withMappedList({ + list: '#mainRelease.tracks.directory', + map: '#mapItsDirectoryLikeDirectory', + }).outputs({ + '#mappedList': '#filterItsDirectoryLikeDirectory', + }), + + withMappedList({ + list: '#mainRelease.tracks.name', + map: '#mapItsNameLikeDirectory', + }).outputs({ + '#mappedList': '#filterItsNameLikeDirectory', + }), + + withMappedList({ + list: '#mainRelease.tracks.directory', + map: '#mapItsDirectoryLikeName', + }).outputs({ + '#mappedList': '#filterItsDirectoryLikeName', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#filterItsNameLikeName', + }).outputs({ + '#filteredList': '#matchingItsNameLikeName', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#filterItsDirectoryLikeDirectory', + }).outputs({ + '#filteredList': '#matchingItsDirectoryLikeDirectory', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#filterItsNameLikeDirectory', + }).outputs({ + '#filteredList': '#matchingItsNameLikeDirectory', + }), + + withFilteredList({ + list: '#mainRelease.tracks', + filter: '#filterItsDirectoryLikeName', + }).outputs({ + '#filteredList': '#matchingItsDirectoryLikeName', + }), + + { + dependencies: [ + '#matchingItsNameLikeName', + '#matchingItsDirectoryLikeDirectory', + '#matchingItsNameLikeDirectory', + '#matchingItsDirectoryLikeName', + ], + + compute: (continuation, { + ['#matchingItsNameLikeName']: NLN, + ['#matchingItsDirectoryLikeDirectory']: DLD, + ['#matchingItsNameLikeDirectory']: NLD, + ['#matchingItsDirectoryLikeName']: DLN, + }) => continuation({ + ['#mainReleaseTrack']: + onlyItem(DLD) ?? + onlyItem(NLN) ?? + onlyItem(DLN) ?? + onlyItem(NLD) ?? + null, + }), + }, + + { + dependencies: ['#mainReleaseTrack', input.myself()], + compute: ({ + ['#mainReleaseTrack']: mainReleaseTrack, + [input.myself()]: thisTrack, + }) => + (mainReleaseTrack === thisTrack + ? null + : mainReleaseTrack), + }, ], - referencedByTracks: trackReverseReferenceList({ - list: input.value('referencedTracks'), + // Only has any value for main releases, because secondary releases + // are never secondary to *another* secondary release. + secondaryReleases: reverseReferenceList({ + reverse: soupyReverse.input('tracksWhichAreSecondaryReleasesOf'), }), - sampledByTracks: trackReverseReferenceList({ - list: input.value('sampledTracks'), - }), + allReleases: [ + { + dependencies: [ + 'mainReleaseTrack', + 'secondaryReleases', + input.myself(), + ], - featuredInFlashes: reverseReferenceList({ - data: 'flashData', - list: input.value('featuredTracks'), - }), + compute: (continuation, { + mainReleaseTrack, + secondaryReleases, + [input.myself()]: thisTrack, + }) => + (mainReleaseTrack + ? continuation({ + ['#mainReleaseTrack']: mainReleaseTrack, + ['#secondaryReleaseTracks']: mainReleaseTrack.secondaryReleases, + }) + : continuation({ + ['#mainReleaseTrack']: thisTrack, + ['#secondaryReleaseTracks']: secondaryReleases, + })), + }, + + { + dependencies: [ + '#mainReleaseTrack', + '#secondaryReleaseTracks', + ], + + compute: ({ + ['#mainReleaseTrack']: mainReleaseTrack, + ['#secondaryReleaseTracks']: secondaryReleaseTracks, + }) => + sortByDate([mainReleaseTrack, ...secondaryReleaseTracks]), + }, + ], + + otherReleases: [ + { + dependencies: [input.myself(), 'allReleases'], + compute: ({ + [input.myself()]: thisTrack, + ['allReleases']: allReleases, + }) => + allReleases.filter(track => track !== thisTrack), + }, + ], - referencedByArtworks: [ - exitWithoutUniqueCoverArt({ + commentaryFromMainRelease: [ + exitWithoutDependency({ + dependency: 'mainReleaseTrack', value: input.value([]), }), - reverseReferencedArtworkList(), + withPropertyFromObject({ + object: 'mainReleaseTrack', + property: input.value('commentary'), + }), + + exposeDependency({ + dependency: '#mainReleaseTrack.commentary', + }), + ], + + groups: [ + withPropertyFromObject({ + object: 'album', + property: input.value('groups'), + }), + + exposeDependency({ + dependency: '#album.groups', + }), ], + + followingProductionTracks: reverseReferenceList({ + reverse: soupyReverse.input('tracksWhichAreFollowingProductionsOf'), + }), + + referencedByTracks: reverseReferenceList({ + reverse: soupyReverse.input('tracksWhichReference'), + }), + + sampledByTracks: reverseReferenceList({ + reverse: soupyReverse.input('tracksWhichSample'), + }), + + featuredInFlashes: reverseReferenceList({ + reverse: soupyReverse.input('flashesWhichFeature'), + }), }); static [Thing.yamlDocumentSpec] = { fields: { + // Identifying metadata + 'Track': {property: 'name'}, + 'Track Text': {property: 'nameText'}, 'Directory': {property: 'directory'}, 'Suffix Directory': {property: 'suffixDirectoryFromAlbum'}, - - 'Additional Names': { - property: 'additionalNames', - transform: parseAdditionalNames, - }, + 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, + 'Main Release': {property: 'mainRelease'}, 'Bandcamp Track ID': { property: 'bandcampTrackIdentifier', @@ -488,17 +1158,86 @@ export class Track extends Thing { transform: String, }, + 'Additional Names': { + property: 'additionalNames', + transform: parseAdditionalNames, + }, + + 'Date First Released': { + property: 'dateFirstReleased', + transform: parseDate, + }, + + // Credits and contributors + + 'Artist Text': {property: 'artistText'}, + 'Artist Text In Lists': {property: 'artistTextInLists'}, + + 'Artists': { + property: 'artistContribs', + transform: parseContributors, + }, + + 'Contributors': { + property: 'contributorContribs', + transform: parseContributors, + }, + + // General configuration + + 'Count In Artist Totals': {property: 'countInArtistTotals'}, + + 'Has Cover Art': { + property: 'disableUniqueCoverArt', + transform: value => + (typeof value === 'boolean' + ? !value + : value), + }, + + 'Has Date': { + property: 'disableDate', + transform: value => + (typeof value === 'boolean' + ? !value + : value), + }, + + // General metadata + 'Duration': { property: 'duration', transform: parseDuration, }, 'Color': {property: 'color'}, + + 'Needs Lyrics': { + property: 'needsLyrics', + }, + 'URLs': {property: 'urls'}, - 'Date First Released': { - property: 'dateFirstReleased', - transform: parseDate, + // Artworks + + 'Track Artwork': { + property: 'trackArtworks', + transform: + parseArtwork({ + thingProperty: 'trackArtworks', + dimensionsFromThingProperty: 'coverArtDimensions', + fileExtensionFromThingProperty: 'coverArtFileExtension', + dateFromThingProperty: 'coverArtDate', + artTagsFromThingProperty: 'artTags', + referencedArtworksFromThingProperty: 'referencedArtworks', + artistContribsFromThingProperty: 'coverArtistContribs', + artistContribsArtistProperty: 'trackCoverArtistContributions', + }), + }, + + 'Cover Artists': { + property: 'coverArtistContribs', + transform: parseContributors, }, 'Cover Art Date': { @@ -513,19 +1252,20 @@ export class Track extends Thing { transform: parseDimensions, }, - 'Has Cover Art': { - property: 'disableUniqueCoverArt', - transform: value => - (typeof value === 'boolean' - ? !value - : value), + 'Art Tags': {property: 'artTags'}, + + 'Referenced Artworks': { + property: 'referencedArtworks', + transform: parseAnnotatedReferences, }, - 'Always Reference By Directory': {property: 'alwaysReferenceByDirectory'}, + // Referenced tracks - 'Lyrics': {property: 'lyrics'}, - 'Commentary': {property: 'commentary'}, - 'Credit Sources': {property: 'creditSources'}, + 'Previous Productions': {property: 'previousProductionTracks'}, + 'Referenced Tracks': {property: 'referencedTracks'}, + 'Sampled Tracks': {property: 'sampledTracks'}, + + // Additional files 'Additional Files': { property: 'additionalFiles', @@ -542,61 +1282,58 @@ export class Track extends Thing { transform: parseAdditionalFiles, }, - 'Originally Released As': {property: 'originalReleaseTrack'}, - 'Referenced Tracks': {property: 'referencedTracks'}, - 'Sampled Tracks': {property: 'sampledTracks'}, + // Content entries - 'Referenced Artworks': { - property: 'referencedArtworks', - transform: parseAnnotatedReferences, + 'Lyrics': { + property: 'lyrics', + transform: parseLyrics, }, - 'Franchises': {ignore: true}, - 'Inherit Franchises': {ignore: true}, - - 'Artists': { - property: 'artistContribs', - transform: parseContributors, + 'Commentary': { + property: 'commentary', + transform: parseCommentary, }, - 'Contributors': { - property: 'contributorContribs', - transform: parseContributors, + 'Crediting Sources': { + property: 'creditingSources', + transform: parseCreditingSources, }, - 'Cover Artists': { - property: 'coverArtistContribs', - transform: parseContributors, + 'Referencing Sources': { + property: 'referencingSources', + transform: parseReferencingSources, }, - 'Art Tags': {property: 'artTags'}, + // Shenanigans + 'Franchises': {ignore: true}, + 'Inherit Franchises': {ignore: true}, 'Review Points': {ignore: true}, }, invalidFieldCombinations: [ - {message: `Rereleases inherit references from the original`, fields: [ - 'Originally Released As', - 'Referenced Tracks', + {message: `Secondary releases never count in artist totals`, fields: [ + 'Main Release', + 'Count In Artist Totals', ]}, - {message: `Rereleases inherit samples from the original`, fields: [ - 'Originally Released As', - 'Sampled Tracks', + {message: `Secondary releases inherit references from the main one`, fields: [ + 'Main Release', + 'Referenced Tracks', ]}, - {message: `Rereleases inherit artists from the original`, fields: [ - 'Originally Released As', - 'Artists', + {message: `Secondary releases inherit samples from the main one`, fields: [ + 'Main Release', + 'Sampled Tracks', ]}, - {message: `Rereleases inherit contributors from the original`, fields: [ - 'Originally Released As', + {message: `Secondary releases inherit contributors from the main one`, fields: [ + 'Main Release', 'Contributors', ]}, - {message: `Rereleases inherit lyrics from the original`, fields: [ - 'Originally Released As', + {message: `Secondary releases inherit lyrics from the main one`, fields: [ + 'Main Release', 'Lyrics', ]}, @@ -617,6 +1354,7 @@ export class Track extends Thing { static [Thing.findSpecs] = { track: { referenceTypes: ['track'], + bindTo: 'trackData', getMatchableNames: track => @@ -625,12 +1363,12 @@ export class Track extends Thing { : [track.name]), }, - trackOriginalReleasesOnly: { + trackMainReleasesOnly: { referenceTypes: ['track'], bindTo: 'trackData', include: track => - !CacheableObject.getUpdateValue(track, 'originalReleaseTrack'), + !CacheableObject.getUpdateValue(track, 'mainRelease'), // It's still necessary to check alwaysReferenceByDirectory here, since // it may be set manually (with `Always Reference By Directory: true`), @@ -643,7 +1381,12 @@ export class Track extends Thing { }, trackWithArtwork: { - referenceTypes: ['track'], + referenceTypes: [ + 'track', + 'track-referencing-artworks', + 'track-referenced-artworks', + ], + bindTo: 'trackData', include: track => @@ -654,32 +1397,151 @@ export class Track extends Thing { ? [] : [track.name]), }, + + trackPrimaryArtwork: { + [Thing.findThisThingOnly]: false, + + referenceTypes: [ + 'track', + 'track-referencing-artworks', + 'track-referenced-artworks', + ], + + bindTo: 'artworkData', + + include: (artwork, {Artwork, Track}) => + artwork instanceof Artwork && + artwork.thing instanceof Track && + artwork === artwork.thing.trackArtworks[0], + + getMatchableNames: ({thing: track}) => + (track.alwaysReferenceByDirectory + ? [] + : [track.name]), + + getMatchableDirectories: ({thing: track}) => + [track.directory], + }, + }; + + static [Thing.reverseSpecs] = { + tracksWhichReference: { + bindTo: 'trackData', + + referencing: track => track.isMainRelease ? [track] : [], + referenced: track => track.referencedTracks, + }, + + tracksWhichSample: { + bindTo: 'trackData', + + referencing: track => track.isMainRelease ? [track] : [], + referenced: track => track.sampledTracks, + }, + + tracksWhoseArtworksFeature: { + bindTo: 'trackData', + + referencing: track => [track], + referenced: track => track.artTags, + }, + + trackArtistContributionsBy: + soupyReverse.contributionsBy('trackData', 'artistContribs'), + + trackContributorContributionsBy: + soupyReverse.contributionsBy('trackData', 'contributorContribs'), + + trackCoverArtistContributionsBy: + soupyReverse.artworkContributionsBy('trackData', 'trackArtworks'), + + tracksWithCommentaryBy: { + bindTo: 'trackData', + + referencing: track => [track], + referenced: track => track.commentatorArtists, + }, + + tracksWhichAreSecondaryReleasesOf: { + bindTo: 'trackData', + + referencing: track => track.isSecondaryRelease ? [track] : [], + referenced: track => [track.mainReleaseTrack], + }, + + tracksWhichAreFollowingProductionsOf: { + bindTo: 'trackData', + + referencing: track => track, + referenced: track => track.previousProductionTracks, + }, }; // Track YAML loading is handled in album.js. static [Thing.getYamlLoadingSpec] = null; + getOwnAdditionalFilePath(_file, filename) { + if (!this.album) return null; + + return [ + 'media.albumAdditionalFile', + this.album.directory, + filename, + ]; + } + + getOwnArtworkPath(artwork) { + if (!this.album) return null; + + return [ + 'media.trackCover', + this.album.directory, + + (artwork.unqualifiedDirectory + ? this.directory + '-' + artwork.unqualifiedDirectory + : this.directory), + + artwork.fileExtension, + ]; + } + + countOwnContributionInContributionTotals(_contrib) { + if (!this.countInArtistTotals) { + return false; + } + + if (this.isSecondaryRelease) { + return false; + } + + return true; + } + + countOwnContributionInDurationTotals(_contrib) { + if (!this.countInArtistTotals) { + return false; + } + + if (this.isSecondaryRelease) { + return false; + } + + return true; + } + [inspect.custom](depth) { const parts = []; parts.push(Thing.prototype[inspect.custom].apply(this)); - if (CacheableObject.getUpdateValue(this, 'originalReleaseTrack')) { - parts.unshift(`${colors.yellow('[rerelease]')} `); + if (CacheableObject.getUpdateValue(this, 'mainRelease')) { + parts.unshift(`${colors.yellow('[secrelease]')} `); } let album; if (depth >= 0) { - try { - album = this.album; - } catch (_error) { - // Computing album might crash for any reason, which we don't want to - // distract from another error we might be trying to work out at the - // moment (for which debugging might involve inspecting this track!). - } - - album ??= this.dataSourceAlbum; + album = this.album; } if (album) { |