diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2023-08-15 22:05:36 -0300 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2023-08-15 22:05:36 -0300 |
commit | 7e7117e2e1c3d72393289f63695d4f86d358e7ed (patch) | |
tree | 68d6e3c5b16328f282ce7f6b4d4536a36aa2cd70 | |
parent | d908377fa3d7a90df344744a9d2429c4a4095d01 (diff) |
data, yaml: inherit music-related properties from original release
When a track has 'Originally Released As', these fields are now automatically inherited: * Artists * Contributors * Referenced Tracks * Sampled Tracks Including any of these fields alongside 'Originally Released As' is an error. Corresponding properties are valid, but ignored. This uses a new "compositional" style to define how each of these properties inherits while retaining the original behavior for tracks that aren't re-releases, and avoids hard-coding much of anything!
-rw-r--r-- | src/data/things/track.js | 100 | ||||
-rw-r--r-- | src/data/yaml.js | 79 |
2 files changed, 150 insertions, 29 deletions
diff --git a/src/data/things/track.js b/src/data/things/track.js index 98a9fdd1..36e3adbe 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -245,35 +245,43 @@ export class Track extends Thing { }, }, - artistContribs: Thing.common.dynamicInheritContribs( - null, - 'artistContribsByRef', - 'artistContribsByRef', - 'albumData', - Track.findAlbum - ), - - contributorContribs: Thing.common.dynamicContribs('contributorContribsByRef'), - - coverArtistContribs: Thing.common.dynamicInheritContribs( - 'hasCoverArt', - 'coverArtistContribsByRef', - 'trackCoverArtistContribsByRef', - 'albumData', - Track.findAlbum - ), - - referencedTracks: Thing.common.dynamicThingsFromReferenceList( - 'referencedTracksByRef', - 'trackData', - find.track - ), - - sampledTracks: Thing.common.dynamicThingsFromReferenceList( - 'sampledTracksByRef', - 'trackData', - find.track - ), + artistContribs: + Track.inheritFromOriginalRelease('artistContribs', [], + Thing.common.dynamicInheritContribs( + null, + 'artistContribsByRef', + 'artistContribsByRef', + 'albumData', + Track.findAlbum)), + + contributorContribs: + Track.inheritFromOriginalRelease('contributorContribs', [], + Thing.common.dynamicContribs('contributorContribsByRef')), + + // 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: + Thing.common.dynamicInheritContribs( + 'hasCoverArt', + 'coverArtistContribsByRef', + 'trackCoverArtistContribsByRef', + 'albumData', + Track.findAlbum), + + referencedTracks: + Track.inheritFromOriginalRelease('referencedTracks', [], + Thing.common.dynamicThingsFromReferenceList( + 'referencedTracksByRef', + 'trackData', + find.track)), + + sampledTracks: + Track.inheritFromOriginalRelease('sampledTracks', [], + Thing.common.dynamicThingsFromReferenceList( + 'sampledTracksByRef', + 'trackData', + find.track)), // Specifically exclude re-releases from this list - while it's useful to // get from a re-release to the tracks it references, re-releases aren't @@ -373,6 +381,40 @@ export class Track extends Thing { return false; } + static inheritFromOriginalRelease( + originalProperty, + originalMissingValue, + ownPropertyDescriptor + ) { + return { + flags: {expose: true}, + + expose: { + dependencies: [ + ...ownPropertyDescriptor.expose.dependencies, + 'originalReleaseTrackByRef', + 'trackData', + ], + + compute(dependencies) { + const { + originalReleaseTrackByRef, + trackData, + } = dependencies; + + if (originalReleaseTrackByRef) { + if (!trackData) return originalMissingValue; + const original = find.track(originalReleaseTrackByRef, trackData, {mode: 'quiet'}); + if (!original) return originalMissingValue; + return original[originalProperty]; + } + + return ownPropertyDescriptor.expose.compute(dependencies); + }, + }, + }; + } + [inspect.custom]() { const base = Thing.prototype[inspect.custom].apply(this); diff --git a/src/data/yaml.js b/src/data/yaml.js index 627f559c..7420ee4a 100644 --- a/src/data/yaml.js +++ b/src/data/yaml.js @@ -70,6 +70,7 @@ function makeProcessDocument( // Each key and value are a field name (not an update() property) and a // function which takes the value for that field and returns the value which // will be passed on to update(). + // fieldTransformations = {}, // Mapping of Thing.update() source properties to field names. @@ -78,13 +79,36 @@ function makeProcessDocument( // shorthand convenience because properties are generally typical // camel-cased JS properties, while fields may contain whitespace and be // more easily represented as quoted strings. + // propertyFieldMapping, // Completely ignored fields. These won't throw an unknown field error if // they're present in a document, but they won't be used for Thing property // generation, either. Useful for stuff that's present in data files but not // yet implemented as part of a Thing's data model! + // ignoredFields = [], + + // List of fields which are invalid when coexisting in a document. + // Data objects are generally allowing with regards to what properties go + // together, allowing for properties to be set separately from each other + // instead of complaining about invalid or unused-data cases. But it's + // useful to see these kinds of errors when actually validating YAML files! + // + // Each item of this array should itself be an object with a descriptive + // message and a list of fields. Of those fields, none should ever coexist + // with any other. For example: + // + // [ + // {message: '...', fields: ['A', 'B', 'C']}, + // {message: '...', fields: ['C', 'D']}, + // ] + // + // ...means A can't coexist with B or C, B can't coexist with A or C, and + // C can't coexist iwth A, B, or D - but it's okay for D to coexist with + // A or B. + // + invalidFieldCombinations = [], } ) { if (!thingClass) { @@ -132,6 +156,19 @@ function makeProcessDocument( throw new makeProcessDocument.UnknownFieldsError(unknownFields); } + const presentFields = Object.keys(document); + + const fieldCombinationErrors = []; + for (const {message, fields} of invalidFieldCombinations) { + const fieldsPresent = presentFields.filter(field => fields.includes(field)); + if (fieldsPresent.length > 1) { + fieldCombinationErrors.push(new makeProcessDocument.FieldCombinationError(fieldsPresent, message)); + } + } + if (!empty(fieldCombinationErrors)) { + throw new makeProcessDocument.FieldCombinationsError(fieldCombinationErrors); + } + const fieldValues = {}; for (const [field, value] of documentEntries) { @@ -175,6 +212,26 @@ makeProcessDocument.UnknownFieldsError = class UnknownFieldsError extends Error } }; +makeProcessDocument.FieldCombinationsError = class FieldCombinationsError extends AggregateError { + constructor(errors) { + super(errors, `Errors in combinations of fields present`); + } +}; + +makeProcessDocument.FieldCombinationError = class FieldCombinationError extends Error { + constructor(fields, message) { + const combinePart = `Don't combine ${fields.map(field => color.red(field)).join(', ')}`; + + const messagePart = + (message + ? `: ${message}` + : ``); + + super(combinePart + messagePart); + this.fields = fields; + } +} + export const processAlbumDocument = makeProcessDocument(T.Album, { fieldTransformations: { 'Artists': parseContributors, @@ -285,6 +342,28 @@ export const processTrackDocument = makeProcessDocument(T.Track, { coverArtistContribsByRef: 'Cover Artists', artTagsByRef: 'Art Tags', }, + + invalidFieldCombinations: [ + {message: `Re-releases inherit references from the original`, fields: [ + 'Originally Released As', + 'Referenced Tracks', + ]}, + + {message: `Re-releases inherit samples from the original`, fields: [ + 'Originally Released As', + 'Sampled Tracks', + ]}, + + {message: `Re-releases inherit artists from the original`, fields: [ + 'Originally Released As', + 'Artists', + ]}, + + {message: `Re-releases inherit contributors from the original`, fields: [ + 'Originally Released As', + 'Contributors', + ]}, + ], }); export const processArtistDocument = makeProcessDocument(T.Artist, { |