« get me outta code hell

super basic album ↔ track linking - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2022-02-08 09:46:22 -0400
committer(quasar) nebula <qznebula@protonmail.com>2022-02-08 09:46:41 -0400
commitc56a1eb78b76b52cbeeed6fe16d3511c1fd41147 (patch)
tree889d20714901831c885bb2bf5afde1f24c351ed2
parent687783a53deecddc9d43020a5c10c5800fa37796 (diff)
super basic album ↔ track linking
-rw-r--r--src/thing/album.js42
-rw-r--r--src/thing/cacheable-object.js36
-rw-r--r--src/thing/track.js54
-rw-r--r--src/thing/validators.js8
-rwxr-xr-xsrc/upd8.js125
5 files changed, 210 insertions, 55 deletions
diff --git a/src/thing/album.js b/src/thing/album.js
index 8a9fde2..426796b 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 3c14101..9af4160 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 75df109..d0e88ac 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 1bc7fd7..8392222 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 ae69a7d..a267c6f 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();
+});