diff options
Diffstat (limited to 'src/data/things')
-rw-r--r-- | src/data/things/album.js | 300 | ||||
-rw-r--r-- | src/data/things/artist.js | 5 | ||||
-rw-r--r-- | src/data/things/flash.js | 183 | ||||
-rw-r--r-- | src/data/things/index.js | 7 | ||||
-rw-r--r-- | src/data/things/language.js | 66 | ||||
-rw-r--r-- | src/data/things/track.js | 21 |
6 files changed, 469 insertions, 113 deletions
diff --git a/src/data/things/album.js b/src/data/things/album.js index 336d777..e9f55b2 100644 --- a/src/data/things/album.js +++ b/src/data/things/album.js @@ -1,20 +1,25 @@ 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 {empty} from '#sugar'; +import {accumulateSum, empty} from '#sugar'; import Thing from '#thing'; -import {isDate} from '#validators'; +import {isColor, isDate, validateWikiData} from '#validators'; import {parseAdditionalFiles, parseContributors, parseDate, parseDimensions} from '#yaml'; -import {exposeDependency, exposeUpdateValueOrContinue} +import {exitWithoutDependency, exposeDependency, exposeUpdateValueOrContinue} from '#composite/control-flow'; -import {exitWithoutContribs} from '#composite/wiki-data'; +import {withPropertyFromObject} from '#composite/data'; +import {exitWithoutContribs, withDirectory, withResolvedReference} + from '#composite/wiki-data'; import { additionalFiles, @@ -31,16 +36,24 @@ import { referenceList, simpleDate, simpleString, + singleReference, urls, wikiData, } from '#composite/wiki-properties'; -import {withTracks, withTrackSections} from '#composite/things/album'; +import {withTracks} from '#composite/things/album'; +import {withAlbum} from '#composite/things/track-section'; export class Album extends Thing { static [Thing.referenceType] = 'album'; - static [Thing.getPropertyDescriptors] = ({ArtTag, Artist, Group, Track}) => ({ + static [Thing.getPropertyDescriptors] = ({ + ArtTag, + Artist, + Group, + Track, + TrackSection, + }) => ({ // Update & expose name: name('Unnamed Album'), @@ -48,6 +61,8 @@ export class Album extends Thing { directory: directory(), urls: urls(), + alwaysReferenceTracksByDirectory: flag(false), + bandcampAlbumIdentifier: simpleString(), bandcampArtworkIdentifier: simpleString(), @@ -92,6 +107,11 @@ export class Album extends Thing { simpleString(), ], + coverArtDimensions: [ + exitWithoutContribs({contribs: 'coverArtistContribs'}), + dimensions(), + ], + bannerDimensions: [ exitWithoutContribs({contribs: 'bannerArtistContribs'}), dimensions(), @@ -104,10 +124,11 @@ export class Album extends Thing { commentary: commentary(), additionalFiles: additionalFiles(), - trackSections: [ - withTrackSections(), - exposeDependency({dependency: '#trackSections'}), - ], + trackSections: referenceList({ + referenceType: input.value('unqualified-track-section'), + data: 'ownTrackSectionData', + find: input.value(find.unqualifiedTrackSection), + }), artistContribs: contributionList(), coverArtistContribs: contributionList(), @@ -148,11 +169,8 @@ export class Album extends Thing { class: input.value(Group), }), - // Only the tracks which belong to this album. - // Necessary for computing the track list, so provide this statically - // or keep it updated. - ownTrackData: wikiData({ - class: input.value(Track), + ownTrackSectionData: wikiData({ + class: input.value(TrackSection), }), // Expose only @@ -221,6 +239,10 @@ export class Album extends Thing { 'Album': {property: 'name'}, 'Directory': {property: 'directory'}, + 'Always Reference Tracks By Directory': { + property: 'alwaysReferenceTracksByDirectory', + }, + 'Bandcamp Album ID': { property: 'bandcampAlbumIdentifier', transform: String, @@ -261,6 +283,11 @@ export class Album extends Thing { 'Cover Art File Extension': {property: 'coverArtFileExtension'}, 'Track Art File Extension': {property: 'trackCoverArtFileExtension'}, + 'Cover Art Dimensions': { + property: 'coverArtDimensions', + transform: parseDimensions, + }, + 'Wallpaper Artists': { property: 'wallpaperArtistContribs', transform: parseContributors, @@ -329,68 +356,77 @@ export class Album extends Thing { headerDocumentThing: Album, entryDocumentThing: document => ('Section' in document - ? TrackSectionHelper + ? TrackSection : Track), save(results) { const albumData = []; + const trackSectionData = []; const trackData = []; for (const {header: album, entries} of results) { - // We can't mutate an array once it's set as a property value, - // so prepare the track sections that will show up in a track list - // all the way before actually applying them. (It's okay to mutate - // an individual section before applying it, since those are just - // generic objects; they aren't Things in and of themselves.) const trackSections = []; - const ownTrackData = []; - let currentTrackSection = { + let currentTrackSection = new TrackSection(); + let currentTrackSectionTracks = []; + + Object.assign(currentTrackSection, { name: `Default Track Section`, isDefaultTrackSection: true, - tracks: [], - }; + }); const albumRef = Thing.getReference(album); const closeCurrentTrackSection = () => { - if (!empty(currentTrackSection.tracks)) { - trackSections.push(currentTrackSection); + if ( + currentTrackSection.isDefaultTrackSection && + empty(currentTrackSectionTracks) + ) { + return; } + + currentTrackSection.tracks = + currentTrackSectionTracks + .map(track => Thing.getReference(track)); + + currentTrackSection.ownTrackData = + currentTrackSectionTracks; + + currentTrackSection.ownAlbumData = + [album]; + + trackSections.push(currentTrackSection); + trackSectionData.push(currentTrackSection); }; for (const entry of entries) { - if (entry instanceof TrackSectionHelper) { + if (entry instanceof TrackSection) { closeCurrentTrackSection(); - - currentTrackSection = { - name: entry.name, - color: entry.color, - dateOriginallyReleased: entry.dateOriginallyReleased, - isDefaultTrackSection: false, - tracks: [], - }; - + currentTrackSection = entry; + currentTrackSectionTracks = []; continue; } + currentTrackSectionTracks.push(entry); trackData.push(entry); entry.dataSourceAlbum = albumRef; - - ownTrackData.push(entry); - currentTrackSection.tracks.push(Thing.getReference(entry)); } closeCurrentTrackSection(); albumData.push(album); - album.trackSections = trackSections; - album.ownTrackData = ownTrackData; + album.trackSections = + trackSections + .map(trackSection => + `unqualified-track-section:` + + trackSection.unqualifiedDirectory); + + album.ownTrackSectionData = trackSections; } - return {albumData, trackData}; + return {albumData, trackSectionData, trackData}; }, sort({albumData, trackData}) { @@ -400,15 +436,139 @@ export class Album extends Thing { }); } -export class TrackSectionHelper extends Thing { +export class TrackSection extends Thing { static [Thing.friendlyName] = `Track Section`; + static [Thing.referenceType] = `track-section`; + + static [Thing.getPropertyDescriptors] = ({Album, Track}) => ({ + // Update & expose - static [Thing.getPropertyDescriptors] = () => ({ name: name('Unnamed Track Section'), - color: color(), + + unqualifiedDirectory: directory(), + + color: [ + exposeUpdateValueOrContinue({ + validate: input.value(isColor), + }), + + withAlbum(), + + withPropertyFromObject({ + object: '#album', + property: input.value('color'), + }), + + exposeDependency({dependency: '#album.color'}), + ], + dateOriginallyReleased: simpleDate(), - isDefaultTrackGroup: flag(false), - }) + + isDefaultTrackSection: flag(false), + + album: [ + withAlbum(), + exposeDependency({dependency: '#album'}), + ], + + tracks: referenceList({ + 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), + }), + + // Expose only + + directory: [ + withAlbum(), + + exitWithoutDependency({ + dependency: '#album', + }), + + withPropertyFromObject({ + object: '#album', + property: input.value('directory'), + }), + + withDirectory({ + directory: 'unqualifiedDirectory', + }).outputs({ + '#directory': '#unqualifiedDirectory', + }), + + { + dependencies: ['#album.directory', '#unqualifiedDirectory'], + compute: ({ + ['#album.directory']: albumDirectory, + ['#unqualifiedDirectory']: unqualifiedDirectory, + }) => + albumDirectory + '/' + unqualifiedDirectory, + }, + ], + + startIndex: [ + withAlbum(), + + 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)), + }, + ], + }); + + static [Thing.findSpecs] = { + trackSection: { + referenceTypes: ['track-section'], + bindTo: 'trackSectionData', + }, + + unqualifiedTrackSection: { + referenceTypes: ['unqualified-track-section'], + + getMatchableDirectories: trackSection => + [trackSection.unqualifiedDirectory], + }, + }; static [Thing.yamlDocumentSpec] = { fields: { @@ -421,4 +581,48 @@ export class TrackSectionHelper extends Thing { }, }, }; + + [inspect.custom](depth) { + const parts = []; + + parts.push(Thing.prototype[inspect.custom].apply(this)); + + if (depth >= 0) { + let album = null; + try { + album = this.album; + } catch {} + + let first = null; + try { + first = this.startIndex; + } catch {} + + let length = null; + try { + length = this.tracks.length; + } catch {} + + album ??= CacheableObject.getUpdateValue(this, 'ownAlbumData')?.[0]; + + if (album) { + const albumName = album.name; + const albumIndex = album.trackSections.indexOf(this); + + const num = + (albumIndex === -1 + ? 'indeterminate position' + : `#${albumIndex + 1}`); + + const range = + (albumIndex >= 0 && first !== null && length !== null + ? `: ${first + 1}-${first + length + 1}` + : ''); + + parts.push(` (${colors.yellow(num + range)} in ${colors.green(albumName)})`); + } + } + + return parts.join(''); + } } diff --git a/src/data/things/artist.js b/src/data/things/artist.js index 73acba6..841d652 100644 --- a/src/data/things/artist.js +++ b/src/data/things/artist.js @@ -220,6 +220,11 @@ export class Artist extends Thing { data: 'flashData', list: input.value('contributorContribs'), }), + + flashesAsCommentator: reverseReferenceList({ + data: 'flashData', + list: input.value('commentatorArtists'), + }), }); static [Thing.getSerializeDescriptors] = ({ diff --git a/src/data/things/flash.js b/src/data/things/flash.js index 81de327..ceed79f 100644 --- a/src/data/things/flash.js +++ b/src/data/things/flash.js @@ -2,17 +2,26 @@ export const FLASH_DATA_FILE = 'flashes.yaml'; import {input} from '#composite'; import find from '#find'; +import {empty} from '#sugar'; import {sortFlashesChronologically} from '#sort'; import Thing from '#thing'; -import {anyOf, isColor, isDirectory, isNumber, isString} from '#validators'; +import {anyOf, isColor, isContentString, isDirectory, isNumber, isString} + from '#validators'; import {parseDate, parseContributors} from '#yaml'; -import {exposeDependency, exposeUpdateValueOrContinue} - from '#composite/control-flow'; import {withPropertyFromObject} from '#composite/data'; import { + exposeConstant, + exposeDependency, + exposeDependencyOrContinue, + exposeUpdateValueOrContinue, +} from '#composite/control-flow'; + +import { color, + commentary, + commentatorArtists, contentString, contributionList, directory, @@ -25,6 +34,7 @@ import { } from '#composite/wiki-properties'; import {withFlashAct} from '#composite/things/flash'; +import {withFlashSide} from '#composite/things/flash-act'; export class Flash extends Thing { static [Thing.referenceType] = 'flash'; @@ -89,6 +99,8 @@ export class Flash extends Thing { urls: urls(), + commentary: commentary(), + // Update only artistData: wikiData({ @@ -105,16 +117,23 @@ export class Flash extends Thing { // Expose only - act: { - flags: {expose: true}, + commentatorArtists: commentatorArtists(), - expose: { - dependencies: ['this', 'flashActData'], + act: [ + withFlashAct(), + exposeDependency({dependency: '#flashAct'}), + ], - compute: ({this: flash, flashActData}) => - flashActData.find((act) => act.flashes.includes(flash)) ?? null, - }, - }, + side: [ + withFlashAct(), + + withPropertyFromObject({ + object: '#flashAct', + property: input.value('side'), + }), + + exposeDependency({dependency: '#flashAct.side'}), + ], }); static [Thing.getSerializeDescriptors] = ({ @@ -153,11 +172,14 @@ export class Flash extends Thing { 'Cover Art File Extension': {property: 'coverArtFileExtension'}, 'Featured Tracks': {property: 'featuredTracks'}, + 'Contributors': { property: 'contributorContribs', transform: parseContributors, }, + 'Commentary': {property: 'commentary'}, + 'Review Points': {ignore: true}, }, }; @@ -173,19 +195,27 @@ export class FlashAct extends Thing { name: name('Unnamed Flash Act'), directory: directory(), color: color(), - listTerminology: contentString(), - jump: contentString(), + listTerminology: [ + exposeUpdateValueOrContinue({ + validate: input.value(isContentString), + }), - jumpColor: { - flags: {update: true, expose: true}, - update: {validate: isColor}, - expose: { - dependencies: ['color'], - transform: (jumpColor, {color}) => - jumpColor ?? color, - } - }, + withFlashSide(), + + withPropertyFromObject({ + object: '#flashSide', + property: input.value('listTerminology'), + }), + + exposeDependencyOrContinue({ + dependency: '#flashSide.listTerminology', + }), + + exposeConstant({ + value: input.value(null), + }), + ], flashes: referenceList({ class: input.value(Flash), @@ -198,6 +228,17 @@ export class FlashAct extends Thing { flashData: wikiData({ class: input.value(Flash), }), + + flashSideData: wikiData({ + class: input.value(FlashSide), + }), + + // Expose only + + side: [ + withFlashSide(), + exposeDependency({dependency: '#flashSide'}), + ], }); static [Thing.findSpecs] = { @@ -215,12 +256,51 @@ export class FlashAct extends Thing { 'Color': {property: 'color'}, 'List Terminology': {property: 'listTerminology'}, - 'Jump': {property: 'jump'}, - 'Jump Color': {property: 'jumpColor'}, - 'Review Points': {ignore: true}, }, }; +} + +export class FlashSide extends Thing { + static [Thing.referenceType] = 'flash-side'; + static [Thing.friendlyName] = `Flash Side`; + + static [Thing.getPropertyDescriptors] = () => ({ + // Update & expose + + name: name('Unnamed Flash Side'), + directory: directory(), + color: color(), + listTerminology: contentString(), + + acts: referenceList({ + class: input.value(FlashAct), + find: input.value(find.flashAct), + data: 'flashActData', + }), + + // Update only + + flashActData: wikiData({ + class: input.value(FlashAct), + }), + }); + + static [Thing.yamlDocumentSpec] = { + fields: { + 'Side': {property: 'name'}, + 'Directory': {property: 'directory'}, + 'Color': {property: 'color'}, + 'List Terminology': {property: 'listTerminology'}, + }, + }; + + static [Thing.findSpecs] = { + flashSide: { + referenceTypes: ['flash-side'], + bindTo: 'flashSideData', + }, + }; static [Thing.getYamlLoadingSpec] = ({ documentModes: {allInOne}, @@ -231,39 +311,56 @@ export class FlashAct extends Thing { documentMode: allInOne, documentThing: document => - ('Act' in document + ('Side' in document + ? FlashSide + : 'Act' in document ? FlashAct : Flash), save(results) { - let flashAct; - let flashRefs = []; + // JavaScript likes you. - if (results[0] && !(results[0] instanceof FlashAct)) { - throw new Error(`Expected an act at top of flash data file`); + if (!empty(results) && !(results[0] instanceof FlashSide)) { + throw new Error(`Expected a side at top of flash data file`); } - for (const thing of results) { - if (thing instanceof FlashAct) { - if (flashAct) { - Object.assign(flashAct, {flashes: flashRefs}); - } + let index = 0; + let thing; + for (; thing = results[index]; index++) { + const flashSide = thing; + const flashActRefs = []; - flashAct = thing; - flashRefs = []; - } else { - flashRefs.push(Thing.getReference(thing)); + if (results[index + 1] instanceof Flash) { + throw new Error(`Expected an act to immediately follow a side`); } - } - if (flashAct) { - Object.assign(flashAct, {flashes: flashRefs}); + for ( + index++; + (thing = results[index]) && thing instanceof FlashAct; + index++ + ) { + const flashAct = thing; + const flashRefs = []; + for ( + index++; + (thing = results[index]) && thing instanceof Flash; + index++ + ) { + flashRefs.push(Thing.getReference(thing)); + } + index--; + flashAct.flashes = flashRefs; + flashActRefs.push(Thing.getReference(flashAct)); + } + index--; + flashSide.acts = flashActRefs; } const flashData = results.filter(x => x instanceof Flash); const flashActData = results.filter(x => x instanceof FlashAct); + const flashSideData = results.filter(x => x instanceof FlashSide); - return {flashData, flashActData}; + return {flashData, flashActData, flashSideData}; }, sort({flashData}) { diff --git a/src/data/things/index.js b/src/data/things/index.js index 3bf8409..4f87f49 100644 --- a/src/data/things/index.js +++ b/src/data/things/index.js @@ -2,10 +2,10 @@ import * as path from 'node:path'; import {fileURLToPath} from 'node:url'; import {openAggregate, showAggregate} from '#aggregate'; +import CacheableObject from '#cacheable-object'; import {logError} from '#cli'; import {compositeFrom} from '#composite'; import * as serialize from '#serialize'; - import Thing from '#thing'; import * as albumClasses from './album.js'; @@ -142,7 +142,10 @@ function evaluatePropertyDescriptors() { } } - constructor.propertyDescriptors = results; + constructor[CacheableObject.propertyDescriptors] = { + ...constructor[CacheableObject.propertyDescriptors] ?? {}, + ...results, + }; }, showFailedClasses(failedClasses) { diff --git a/src/data/things/language.js b/src/data/things/language.js index 93ed40b..dbe1ff3 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -2,6 +2,7 @@ import { Temporal, toTemporalInstant } from '@js-temporal/polyfill'; import {withAggregate} from '#aggregate'; import CacheableObject from '#cacheable-object'; +import {logWarn} from '#cli'; import * as html from '#html'; import {empty} from '#sugar'; import {isLanguageCode} from '#validators'; @@ -17,6 +18,8 @@ import { import {externalFunction, flag, name} from '#composite/wiki-properties'; +export const languageOptionRegex = /{(?<name>[A-Z0-9_]+)}/g; + export class Language extends Thing { static [Thing.getPropertyDescriptors] = () => ({ // Update & expose @@ -60,14 +63,46 @@ export class Language extends Thing { strings: { flags: {update: true, expose: true}, update: {validate: (t) => typeof t === 'object'}, + expose: { - dependencies: ['inheritedStrings'], - transform(strings, {inheritedStrings}) { - if (strings || inheritedStrings) { - return {...(inheritedStrings ?? {}), ...(strings ?? {})}; - } else { - return null; + dependencies: ['inheritedStrings', 'code'], + transform(strings, {inheritedStrings, code}) { + if (!strings && !inheritedStrings) return null; + if (!inheritedStrings) return strings; + + const validStrings = { + ...inheritedStrings, + ...strings, + }; + + const optionsFromTemplate = template => + Array.from(template.matchAll(languageOptionRegex)) + .map(({groups}) => groups.name); + + for (const [key, providedTemplate] of Object.entries(strings)) { + const inheritedTemplate = inheritedStrings[key]; + if (!inheritedTemplate) continue; + + const providedOptions = optionsFromTemplate(providedTemplate); + const inheritedOptions = optionsFromTemplate(inheritedTemplate); + + const missingOptionNames = + inheritedOptions.filter(name => !providedOptions.includes(name)); + + const misplacedOptionNames = + providedOptions.filter(name => !inheritedOptions.includes(name)); + + if (!empty(missingOptionNames) || !empty(misplacedOptionNames)) { + logWarn`Not using ${code ?? '(no code)'} string ${key}:`; + if (!empty(missingOptionNames)) + logWarn`- Missing options: ${missingOptionNames.join(', ')}`; + if (!empty(misplacedOptionNames)) + logWarn`- Unexpected options: ${misplacedOptionNames.join(', ')}`; + validStrings[key] = inheritedStrings[key]; + } } + + return validStrings; }, }, }, @@ -201,7 +236,7 @@ export class Language extends Thing { const output = this.#iterateOverTemplate({ template: this.strings[key], - match: /{(?<name>[A-Z0-9_]+)}/g, + match: languageOptionRegex, insert: ({name: optionName}, canceledForming) => { if (optionsMap.has(optionName)) { @@ -357,6 +392,7 @@ export class Language extends Thing { // contents, if needed. #wrapSanitized(content) { return html.tags(content, { + [html.blessAttributes]: true, [html.joinChildren]: '', [html.noEdgeWhitespace]: true, }); @@ -522,7 +558,7 @@ export class Language extends Thing { } formatExternalLink(url, { - style = 'normal', + style = 'platform', context = 'generic', } = {}) { if (!this.externalLinkSpec) { @@ -540,10 +576,16 @@ export class Language extends Thing { isExternalLinkStyle(style); - return getExternalLinkStringOfStyleFromDescriptors(url, style, this.externalLinkSpec, { - language: this, - context, - }); + const result = + getExternalLinkStringOfStyleFromDescriptors(url, style, this.externalLinkSpec, { + language: this, + context, + }); + + // It's possible for there to not actually be any string available for the + // given URL, style, and context, and we want this to be detectable via + // html.blank(). + return result ?? html.blank(); } formatIndex(value) { diff --git a/src/data/things/track.js b/src/data/things/track.js index 697dad4..725b1bb 100644 --- a/src/data/things/track.js +++ b/src/data/things/track.js @@ -13,6 +13,7 @@ import { parseAdditionalNames, parseContributors, parseDate, + parseDimensions, parseDuration, } from '#yaml'; @@ -34,6 +35,7 @@ import { commentatorArtists, contentString, contributionList, + dimensions, directory, duration, flag, @@ -158,13 +160,15 @@ export class Track extends Thing { exposeDependency({dependency: '#album.trackArtDate'}), ], + coverArtDimensions: [ + exitWithoutUniqueCoverArt(), + dimensions(), + ], + commentary: commentary(), lyrics: [ - inheritFromOriginalRelease({ - property: input.value('lyrics'), - }), - + inheritFromOriginalRelease(), contentString(), ], @@ -189,7 +193,6 @@ export class Track extends Thing { artistContribs: [ inheritFromOriginalRelease({ - property: input.value('artistContribs'), notFoundValue: input.value([]), }), @@ -213,7 +216,6 @@ export class Track extends Thing { contributorContribs: [ inheritFromOriginalRelease({ - property: input.value('contributorContribs'), notFoundValue: input.value([]), }), @@ -248,7 +250,6 @@ export class Track extends Thing { referencedTracks: [ inheritFromOriginalRelease({ - property: input.value('referencedTracks'), notFoundValue: input.value([]), }), @@ -261,7 +262,6 @@ export class Track extends Thing { sampledTracks: [ inheritFromOriginalRelease({ - property: input.value('sampledTracks'), notFoundValue: input.value([]), }), @@ -389,6 +389,11 @@ export class Track extends Thing { 'Cover Art File Extension': {property: 'coverArtFileExtension'}, + 'Cover Art Dimensions': { + property: 'coverArtDimensions', + transform: parseDimensions, + }, + 'Has Cover Art': { property: 'disableUniqueCoverArt', transform: value => |