From c56a1eb78b76b52cbeeed6fe16d3511c1fd41147 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 8 Feb 2022 09:46:22 -0400 Subject: super basic album ↔ track linking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/thing/album.js | 42 +++++++++----- src/thing/cacheable-object.js | 36 +++++++++++- src/thing/track.js | 54 ++++++++++++++++++ src/thing/validators.js | 8 ++- src/upd8.js | 125 +++++++++++++++++++++++++++++------------- 5 files changed, 210 insertions(+), 55 deletions(-) (limited to 'src') diff --git a/src/thing/album.js b/src/thing/album.js index 8a9fde2c..426796b0 100644 --- a/src/thing/album.js +++ b/src/thing/album.js @@ -1,6 +1,5 @@ import CacheableObject from './cacheable-object.js'; import Thing from './thing.js'; -import find from '../util/find.js'; import { isBoolean, @@ -10,6 +9,7 @@ import { isDate, isDimensions, isDirectory, + isInstance, isFileExtension, isName, isURL, @@ -20,6 +20,10 @@ import { validateReferenceList, } from './validators.js'; +import Track from './track.js'; + +import find from '../util/find.js'; + export class TrackGroup extends CacheableObject { static propertyDescriptors = { // Update & expose @@ -64,7 +68,12 @@ export class TrackGroup extends CacheableObject { expose: { dependencies: ['tracksByRef', 'trackData'], compute: ({ tracksByRef, trackData }) => ( - tracksByRef.map(ref => find.track(ref, {wikiData: {trackData}}))) + (tracksByRef && trackData + ? (tracksByRef + .map(ref => find.track(ref, {wikiData: {trackData}})) + .filter(Boolean)) + : []) + ) } } }; @@ -227,26 +236,29 @@ export default class Album extends Thing { update: {validate: isCommentary} }, + // Update only + + trackData: { + flags: {update: true}, + update: {validate: validateArrayItems(x => x instanceof Track)} + }, + // Expose only - /* tracks: { flags: {expose: true}, expose: { - dependencies: ['trackReferences', 'wikiData'], - compute: ({trackReferences, wikiData}) => ( - trackReferences.map(ref => find.track(ref, {wikiData}))) + dependencies: ['trackGroups', 'trackData'], + compute: ({ trackGroups, trackData }) => ( + (trackGroups && trackData + ? (trackGroups + .flatMap(group => group.tracksByRef ?? []) + .map(ref => find.track(ref, {wikiData: {trackData}})) + .filter(Boolean)) + : []) + ) } }, - */ - - // Update only - - /* - wikiData: { - flags: {update: true} - } - */ }; } diff --git a/src/thing/cacheable-object.js b/src/thing/cacheable-object.js index 3c14101c..9af41603 100644 --- a/src/thing/cacheable-object.js +++ b/src/thing/cacheable-object.js @@ -83,6 +83,8 @@ function inspect(value) { } export default class CacheableObject { + static instance = Symbol('CacheableObject `this` instance'); + #propertyUpdateValues = Object.create(null); #propertyUpdateCacheInvalidators = Object.create(null); @@ -100,6 +102,19 @@ export default class CacheableObject { constructor() { this.#defineProperties(); this.#initializeUpdatingPropertyValues(); + + if (CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) { + return new Proxy(this, { + get: (obj, key) => { + if (!Object.hasOwn(obj, key)) { + if (key !== 'constructor') { + CacheableObject._invalidAccesses.add(`(${obj.constructor.name}).${key}`); + } + } + return obj[key]; + } + }); + } } #initializeUpdatingPropertyValues() { @@ -227,7 +242,8 @@ export default class CacheableObject { const dependencyKeys = expose.dependencies || []; const dependencyGetters = dependencyKeys.map(key => () => [key, this.#propertyUpdateValues[key]]); - const getAllDependencies = () => Object.fromEntries(dependencyGetters.map(f => f())); + const getAllDependencies = () => Object.fromEntries(dependencyGetters.map(f => f()) + .concat([[this.constructor.instance, this]])); if (flags.update) { return () => transform(this.#propertyUpdateValues[property], getAllDependencies()); @@ -268,4 +284,22 @@ export default class CacheableObject { } }; } + + static DEBUG_SLOW_TRACK_INVALID_PROPERTIES = false; + static _invalidAccesses = new Set(); + + static showInvalidAccesses() { + if (!this.DEBUG_SLOW_TRACK_INVALID_PROPERTIES) { + return; + } + + if (!this._invalidAccesses.size) { + return; + } + + console.log(`${this._invalidAccesses.size} unique invalid accesses:`); + for (const line of this._invalidAccesses) { + console.log(` - ${line}`); + } + } } diff --git a/src/thing/track.js b/src/thing/track.js index 75df109a..d0e88acf 100644 --- a/src/thing/track.js +++ b/src/thing/track.js @@ -16,6 +16,11 @@ import { validateReferenceList, } from './validators.js'; +import Album from './album.js'; +import ArtTag from './art-tag.js'; + +import find from '../util/find.js'; + export default class Track extends Thing { static [Thing.referenceType] = 'track'; @@ -112,6 +117,55 @@ export default class Track extends Thing { // Update only + albumData: { + flags: {update: true}, + update: {validate: validateArrayItems(x => x instanceof Album)} + }, + + artTagData: { + flags: {update: true}, + update: {validate: validateArrayItems(x => x instanceof ArtTag)} + }, + // Expose only + + album: { + flags: {expose: true}, + + expose: { + dependencies: ['albumData'], + compute: ({ [this.instance]: track, albumData }) => ( + albumData?.find(album => album.tracks.includes(track)) ?? null) + } + }, + + date: { + flags: {expose: true}, + + expose: { + dependencies: ['albumData', 'dateFirstReleased'], + compute: ({ albumData, dateFirstReleased, [this.instance]: track }) => ( + dateFirstReleased ?? + albumData?.find(album => album.tracks.includes(track))?.date ?? + null + ) + } + }, + + artTags: { + flags: {expose: true}, + + expose: { + dependencies: ['artTagsByRef', 'artTagData'], + + compute: ({ artTagsByRef, artTagData }) => ( + (artTagsByRef && artTagData + ? (artTagsByRef + .map(ref => find.tag(ref, {wikiData: {tagData: artTagData}})) + .filter(Boolean)) + : []) + ) + } + } }; } diff --git a/src/thing/validators.js b/src/thing/validators.js index 1bc7fd72..83922229 100644 --- a/src/thing/validators.js +++ b/src/thing/validators.js @@ -96,7 +96,7 @@ export function isStringNonEmpty(value) { // Complex types (non-primitives) -function isInstance(value, constructor) { +export function isInstance(value, constructor) { isObject(value); if (!(value instanceof constructor)) @@ -133,7 +133,11 @@ export function isArray(value) { function validateArrayItemsHelper(itemValidator) { return (item, index) => { try { - itemValidator(item); + const value = itemValidator(item); + + if (value !== true) { + throw new Error(`Expected validator to return true`); + } } catch (error) { error.message = `(index: ${color.green(index)}, item: ${inspect(item)}) ${error.message}`; throw error; diff --git a/src/upd8.js b/src/upd8.js index ae69a7d9..a267c6fb 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -94,6 +94,7 @@ import unbound_link, {getLinkThemeString} from './util/link.js'; import Album, { TrackGroup } from './thing/album.js'; import Artist from './thing/artist.js'; import ArtTag from './thing/art-tag.js'; +import CacheableObject from './thing/cacheable-object.js'; import Flash, { FlashAct } from './thing/flash.js'; import Group, { GroupCategory } from './thing/group.js'; import HomepageLayout, { @@ -2164,6 +2165,13 @@ async function main() { }, queue: {alias: 'queue-size'}, + // This option is super slow and has the potential for bugs! It puts + // CacheableObject in a mode where every instance is a Proxy which will + // keep track of invalid property accesses. + 'show-invalid-property-accesses': { + type: 'flag' + }, + [parseOptions.handleUnknown]: () => {} }); @@ -2214,6 +2222,12 @@ async function main() { if (thumbsOnly) return; } + const showInvalidPropertyAccesses = miscOptions['show-invalid-property-accesses'] ?? false; + + if (showInvalidPropertyAccesses) { + CacheableObject.DEBUG_SLOW_TRACK_INVALID_PROPERTIES = true; + } + const defaultStrings = await processLanguageFile(path.join(__dirname, DEFAULT_STRINGS_FILE)); if (defaultStrings.error) { logError`Error loading default strings: ${defaultStrings.error}`; @@ -2254,27 +2268,6 @@ async function main() { logInfo`Writing all languages.`; } - /* - // Update languages o8ject with the wiki-specified default language! - // This will make page files for that language 8e gener8ted at the root - // directory, instead of the language-specific su8directory. - if (WD.wikiInfo.defaultLanguage) { - if (Object.keys(languages).includes(WD.wikiInfo.defaultLanguage)) { - languages.default = languages[WD.wikiInfo.defaultLanguage]; - } else { - logError`Wiki info file specified default language is ${WD.wikiInfo.defaultLanguage}, but no such language file exists!`; - if (langPath) { - logError`Check if an appropriate file exists in ${langPath}?`; - } else { - logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`; - } - return; - } - } else { - languages.default = defaultStrings; - } - */ - // 8ut wait, you might say, how do we know which al8um these data files // correspond to???????? You wouldn't dare suggest we parse the actual // paths returned 8y this function, which ought to 8e of effectively @@ -2392,9 +2385,6 @@ async function main() { albumData.push(album); } - sortByDate(albumData); - sortByDate(trackData); - Object.assign(wikiData, {albumData, trackData}); } }, @@ -2539,7 +2529,7 @@ async function main() { save(results) { results.sort(sortByName); - wikiData.tagData = results; + wikiData.artTagData = results; } }, @@ -2720,7 +2710,7 @@ async function main() { if (wikiData.flashData) logInfo` - ${wikiData.flashData.length} flashes (${wikiData.flashActData.length} acts)`; logInfo` - ${wikiData.groupData.length} groups (${wikiData.groupCategoryData.length} categories)`; - logInfo` - ${wikiData.tagData.length} art tags`; + logInfo` - ${wikiData.artTagData.length} art tags`; if (wikiData.newsData) logInfo` - ${wikiData.newsData.length} news entries`; logInfo` - ${wikiData.staticPageData.length} static pages`; @@ -2750,21 +2740,72 @@ async function main() { } } - process.exit(); + if (!WD.wikiInfo) { + logError`Can't proceed without wiki info file (${WIKI_INFO_FILE}) successfully loading`; + return; + } + + + // Data linking! Basically, provide (portions of) wikiData to the Things + // which require it - they'll expose dynamically computed properties as a + // result (many of which are required for page HTML generation). + + for (const album of WD.albumData) { + album.trackData = WD.trackData; + + for (const trackGroup of album.trackGroups) { + trackGroup.trackData = WD.trackData; + } + } + + for (const track of WD.trackData) { + track.albumData = WD.albumData; + track.artTagData = WD.artTagData; + } + + // Extra organization stuff needed for listings and the like. + + Object.assign(wikiData, { + albumData: sortByDate(WD.albumData.slice()), + trackData: sortByDate(WD.trackData.slice()) + }); + + console.log(WD.trackData[0].name, WD.trackData[0].album.name); + console.log(WD.albumData[0].name, WD.albumData[0].tracks[0].name); + + return; + + // Update languages o8ject with the wiki-specified default language! + // This will make page files for that language 8e gener8ted at the root + // directory, instead of the language-specific su8directory. + if (WD.wikiInfo.defaultLanguage) { + if (Object.keys(languages).includes(WD.wikiInfo.defaultLanguage)) { + languages.default = languages[WD.wikiInfo.defaultLanguage]; + } else { + logError`Wiki info file specified default language is ${WD.wikiInfo.defaultLanguage}, but no such language file exists!`; + if (langPath) { + logError`Check if an appropriate file exists in ${langPath}?`; + } else { + logError`Be sure to specify ${'--lang'} or ${'HSMUSIC_LANG'} with the path to language files.`; + } + return; + } + } else { + languages.default = defaultStrings; + } { - const tagNames = new Set([...WD.trackData, ...WD.albumData].flatMap(thing => thing.artTags)); + const tagRefs = new Set([...WD.trackData, ...WD.albumData].flatMap(thing => thing.artTagsByRef ?? [])); - for (let { name, isCW } of WD.tagData) { - if (isCW) { - name = 'cw: ' + name; + for (const ref of tagRefs) { + if (find.tag(ref, {wikiData})) { + tagRefs.delete(ref); } - tagNames.delete(name); } - if (tagNames.size) { - for (const name of Array.from(tagNames).sort()) { - console.log(`\x1b[33;1m- Missing tag: "${name}"\x1b[0m`); + if (tagRefs.size) { + for (const ref of Array.from(tagRefs).sort()) { + console.log(`\x1b[33;1m- Missing tag: "${ref}"\x1b[0m`); } return; } @@ -2774,7 +2815,9 @@ async function main() { WD.justEverythingSortedByArtDateMan = sortByArtDate(WD.justEverythingMan.slice()); // console.log(JSON.stringify(justEverythingSortedByArtDateMan.map(toAnythingMan), null, 2)); - const artistNames = Array.from(new Set([ + return; + + const artistRefs = Array.from(new Set([ ...WD.artistData.filter(artist => !artist.alias).map(artist => artist.name), ...[ ...WD.albumData.flatMap(album => [ @@ -3417,4 +3460,12 @@ async function main() { logInfo`Written!`; } -main().catch(error => console.error(error)); +main().catch(error => { + if (error instanceof AggregateError) { + showAggregate(error); + } else { + console.error(error); + } +}).then(() => { + CacheableObject.showInvalidAccesses(); +}); -- cgit 1.3.0-6-gf8a5