« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data
diff options
context:
space:
mode:
Diffstat (limited to 'src/data')
-rw-r--r--src/data/checks.js9
-rw-r--r--src/data/things/AdditionalFile.js56
-rw-r--r--src/data/things/Artist.js12
-rw-r--r--src/data/things/Language.js19
-rw-r--r--src/data/things/Track.js24
-rw-r--r--src/data/things/additional-file/AdditionalFile.js109
-rw-r--r--src/data/things/additional-file/MidiProjectFile.js28
-rw-r--r--src/data/things/additional-file/MiscellaneousAdditionalFile.js28
-rw-r--r--src/data/things/additional-file/SheetMusicFile.js28
-rw-r--r--src/data/things/additional-file/index.js5
-rw-r--r--src/data/things/album/Album.js21
-rw-r--r--src/data/things/album/TrackSection.js7
-rw-r--r--src/data/things/index.js2
-rw-r--r--src/data/yaml.js37
14 files changed, 299 insertions, 86 deletions
diff --git a/src/data/checks.js b/src/data/checks.js
index 01b5cf9e..6909f011 100644
--- a/src/data/checks.js
+++ b/src/data/checks.js
@@ -248,6 +248,10 @@ export function filterReferenceErrors(wikiData, {
   find,
   bindFind,
 }) {
+  const additionalFileShape = {
+    artistContribs: '_contrib',
+  };
+
   const referenceSpec = [
     ['albumData', {
       artistContribs: '_contrib',
@@ -295,6 +299,9 @@ export function filterReferenceErrors(wikiData, {
       featuredTracks: 'track',
     }],
 
+    ['midiProjectFileData', additionalFileShape],
+    ['miscellaneousAdditionalFileData', additionalFileShape],
+
     ['musicVideoData', {
       artistContribs: '_contrib',
       contributorContribs: '_contrib',
@@ -304,6 +311,8 @@ export function filterReferenceErrors(wikiData, {
       albums: 'album',
     }],
 
+    ['sheetMusicFileData', additionalFileShape],
+
     ['trackData', {
       artistContribs: '_contrib',
       contributorContribs: '_contrib',
diff --git a/src/data/things/AdditionalFile.js b/src/data/things/AdditionalFile.js
deleted file mode 100644
index e3f309a6..00000000
--- a/src/data/things/AdditionalFile.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import {input} from '#composite';
-import Thing from '#thing';
-import {isString, validateArrayItems} from '#validators';
-
-import {exposeConstant, exposeUpdateValueOrContinue}
-  from '#composite/control-flow';
-import {contentString, simpleString, thing} from '#composite/wiki-properties';
-
-export class AdditionalFile extends Thing {
-  static [Thing.friendlyName] = `Additional File`;
-
-  static [Thing.getPropertyDescriptors] = () => ({
-    // Update & expose
-
-    thing: thing(),
-
-    title: simpleString(),
-
-    description: contentString(),
-
-    filenames: [
-      exposeUpdateValueOrContinue({
-        validate: input.value(validateArrayItems(isString)),
-      }),
-
-      exposeConstant({
-        value: input.value([]),
-      }),
-    ],
-
-    // Expose only
-
-    isAdditionalFile: [
-      exposeConstant({
-        value: input.value(true),
-      }),
-    ],
-  });
-
-  static [Thing.yamlDocumentSpec] = {
-    fields: {
-      'Title': {property: 'title'},
-      'Description': {property: 'description'},
-      'Files': {property: 'filenames'},
-    },
-  };
-
-  get paths() {
-    if (!this.thing) return null;
-    if (!this.thing.getOwnAdditionalFilePath) return null;
-
-    return (
-      this.filenames.map(filename =>
-        this.thing.getOwnAdditionalFilePath(this, filename)));
-  }
-}
diff --git a/src/data/things/Artist.js b/src/data/things/Artist.js
index 64798527..89da3c88 100644
--- a/src/data/things/Artist.js
+++ b/src/data/things/Artist.js
@@ -273,6 +273,18 @@ export class Artist extends Thing {
       exposeDependency('#otherArtistContributions'),
     ],
 
+    miscellaneousAdditionalFileArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('miscellaneousAdditionalFileArtistContributionsBy'),
+    }),
+
+    sheetMusicFileArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('sheetMusicFileArtistContributionsBy'),
+    }),
+
+    midiProjectFileArtistContributions: reverseReferenceList({
+      reverse: soupyReverse.input('midiProjectFileArtistContributionsBy'),
+    }),
+
     totalDuration: [
       withPropertyFromList('musicContributions', V('thing')),
       withPropertyFromList('#musicContributions.thing', V('isMainRelease')),
diff --git a/src/data/things/Language.js b/src/data/things/Language.js
index 2df58d19..5265d851 100644
--- a/src/data/things/Language.js
+++ b/src/data/things/Language.js
@@ -955,6 +955,7 @@ export class Language extends Thing {
 const countHelper = (stringKey, optionName = stringKey) =>
   function(value, {
     unit = false,
+    unitOnly = false,
     blankIfZero = false,
   } = {}) {
     // Null or undefined value is blank content.
@@ -967,22 +968,30 @@ const countHelper = (stringKey, optionName = stringKey) =>
       return html.blank();
     }
 
-    return this.formatString(
-      unit
+    const string =
+      (unitOnly
+        ? `count.${stringKey}.unitOnly.` + this.getUnitForm(value)
+     : unit
         ? `count.${stringKey}.withUnit.` + this.getUnitForm(value)
-        : `count.${stringKey}`,
-      {[optionName]: this.formatNumber(value)});
+        : `count.${stringKey}`);
+
+    const options =
+      (unitOnly
+        ? {}
+        : {[optionName]: this.formatNumber(value)});
+
+    return this.formatString(string, options);
   };
 
 // TODO: These are hard-coded. Is there a better way?
 Object.assign(Language.prototype, {
-  countAdditionalFiles: countHelper('additionalFiles', 'files'),
   countAlbums: countHelper('albums'),
   countArtTags: countHelper('artTags', 'tags'),
   countArtworks: countHelper('artworks'),
   countCommentaryEntries: countHelper('commentaryEntries', 'entries'),
   countContributions: countHelper('contributions'),
   countDays: countHelper('days'),
+  countFiles: countHelper('files'),
   countFlashes: countHelper('flashes'),
   countMonths: countHelper('months'),
   countTimesFeatured: countHelper('timesFeatured'),
diff --git a/src/data/things/Track.js b/src/data/things/Track.js
index 785d0080..c47729e9 100644
--- a/src/data/things/Track.js
+++ b/src/data/things/Track.js
@@ -10,6 +10,7 @@ import Thing from '#thing';
 import {compareKebabCase} from '#wiki-data';
 
 import {
+  anyOf,
   is,
   isBoolean,
   isColor,
@@ -36,8 +37,10 @@ import {
   parseDimensions,
   parseDuration,
   parseLyrics,
+  parseMidiProjectFiles,
   parseMusicVideos,
   parseReferencingSources,
+  parseSheetMusicFiles,
   parseURLs,
 } from '#yaml';
 
@@ -385,7 +388,10 @@ export class Track extends Thing {
 
     excludingURLs: [
       exposeUpdateValueOrContinue({
-        validate: input.value(isExcludingURLsReason),
+        validate: input.value(
+          anyOf(
+            is(false),
+            isExcludingURLsReason)),
       }),
 
       withPropertyFromObject('trackSection', V('excludingTrackURLs')),
@@ -1119,12 +1125,12 @@ export class Track extends Thing {
 
       'Sheet Music Files': {
         property: 'sheetMusicFiles',
-        transform: parseAdditionalFiles,
+        transform: parseSheetMusicFiles,
       },
 
       'MIDI Project Files': {
         property: 'midiProjectFiles',
-        transform: parseAdditionalFiles,
+        transform: parseMidiProjectFiles,
       },
 
       // Content entries
@@ -1184,9 +1190,9 @@ export class Track extends Thing {
         ],
       },
 
-      {message: `Don't include URLs alongside Excluding URLs`, fields: [
+      {message: `Don't include URLs alongside Excluding URLs, unless Excluding URLs is false`, fields: [
         'URLs',
-        'Excluding URLs',
+        ['Excluding URLs', v => v !== false],
       ]},
     ],
   };
@@ -1365,14 +1371,10 @@ export class Track extends Thing {
     },
   };
 
-  getOwnAdditionalFilePath(_file, filename) {
+  getOwnAdditionalFilePath(file, filename) {
     if (!this.album) return null;
 
-    return [
-      'media.albumAdditionalFile',
-      this.album.directory,
-      filename,
-    ];
+    return this.album.getOwnAdditionalFilePath(file, filename);
   }
 
   getOwnArtworkPath(artwork) {
diff --git a/src/data/things/additional-file/AdditionalFile.js b/src/data/things/additional-file/AdditionalFile.js
new file mode 100644
index 00000000..d137c741
--- /dev/null
+++ b/src/data/things/additional-file/AdditionalFile.js
@@ -0,0 +1,109 @@
+import {inspect} from 'node:util';
+
+import {colors} from '#cli';
+import {input, V} from '#composite';
+import Thing from '#thing';
+import {isString, validateArrayItems} from '#validators';
+import {parseContributors} from '#yaml';
+
+import {exposeConstant, exposeUpdateValueOrContinue}
+  from '#composite/control-flow';
+import {contributionList, contentString, simpleString, soupyFind, thing}
+  from '#composite/wiki-properties';
+
+export class AdditionalFile extends Thing {
+  static [Thing.friendlyName] = `Additional File`;
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Update & expose
+
+    thing: thing(),
+
+    title: simpleString(),
+
+    description: contentString(),
+
+    folder: simpleString(),
+
+    filenames: [
+      exposeUpdateValueOrContinue({
+        validate: input.value(validateArrayItems(isString)),
+      }),
+
+      exposeConstant(V([])),
+    ],
+
+    artistContribs: contributionList({
+      // Subclasses override with the relevant artistProperty.
+      artistProperty: input.value(null),
+    }),
+
+    // Update only
+
+    find: soupyFind(),
+
+    // Expose only
+
+    isAdditionalFile: exposeConstant(V(true)),
+
+    // The date property is generally expected by contributions.
+    // Additional files don't actually support dates, but provide a null
+    // value for convenience.
+    date: {
+      flags: {expose: true},
+      expose: {compute: () => null},
+    },
+  });
+
+  static [Thing.yamlDocumentSpec] = {
+    fields: {
+      'Title': {property: 'title'},
+      'Description': {property: 'description'},
+
+      'Artists': {
+        property: 'artistContribs',
+        transform: parseContributors,
+      },
+
+      'Folder': {property: 'folder'},
+      'Files': {property: 'filenames'},
+    },
+  };
+
+  get paths() {
+    if (!this.thing) return null;
+    if (!this.thing.getOwnAdditionalFilePath) return null;
+
+    return (
+      this.filenames.map(filename =>
+        this.thing.getOwnAdditionalFilePath(this, filename)));
+  }
+
+  [inspect.custom](depth, options, inspect) {
+    const parts = [];
+
+    parts.push(this.constructor.name);
+
+    if (this.title) {
+      parts.push(` ${colors.green(`"${this.title}"`)}`);
+    }
+
+    if (this.thing) {
+      if (depth >= 0) {
+        const newOptions = {
+          ...options,
+          depth:
+            (options.depth === null
+              ? null
+              : options.depth - 1),
+        };
+
+        parts.push(` for ${inspect(this.thing, newOptions)}`);
+      } else {
+        parts.push(` for ${colors.blue(Thing.inspectReference(this.thing))}`);
+      }
+    }
+
+    return parts.join('');
+  }
+}
diff --git a/src/data/things/additional-file/MidiProjectFile.js b/src/data/things/additional-file/MidiProjectFile.js
new file mode 100644
index 00000000..8e7c19ca
--- /dev/null
+++ b/src/data/things/additional-file/MidiProjectFile.js
@@ -0,0 +1,28 @@
+import {input, V} from '#composite';
+import Thing from '#thing';
+
+import {exposeConstant} from '#composite/control-flow';
+import {contributionList, soupyReverse} from '#composite/wiki-properties';
+
+import {AdditionalFile} from './AdditionalFile.js';
+
+export class MidiProjectFile extends AdditionalFile {
+  static [Thing.wikiData] = 'midiProjectFileData';
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Update & expose
+
+    artistContribs: contributionList({
+      artistProperty: input.value('midiProjectFileArtistContributions'),
+    }),
+
+    // Expose only
+
+    isMidiProjectFile: exposeConstant(V(true)),
+  });
+
+  static [Thing.reverseSpecs] = {
+    midiProjectFileArtistContributionsBy:
+      soupyReverse.contributionsBy('midiProjectFileData', 'artistContribs'),
+  };
+}
diff --git a/src/data/things/additional-file/MiscellaneousAdditionalFile.js b/src/data/things/additional-file/MiscellaneousAdditionalFile.js
new file mode 100644
index 00000000..0110f830
--- /dev/null
+++ b/src/data/things/additional-file/MiscellaneousAdditionalFile.js
@@ -0,0 +1,28 @@
+import {input, V} from '#composite';
+import Thing from '#thing';
+
+import {exposeConstant} from '#composite/control-flow';
+import {contributionList, soupyReverse} from '#composite/wiki-properties';
+
+import {AdditionalFile} from './AdditionalFile.js';
+
+export class MiscellaneousAdditionalFile extends AdditionalFile {
+  static [Thing.wikiData] = 'miscellaneousAdditionalFileData';
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Update & expose
+
+    artistContribs: contributionList({
+      artistProperty: input.value('miscellaneousAdditionalFileArtistContributions'),
+    }),
+
+    // Expose only
+
+    isMiscellaneousAdditionalFile: exposeConstant(V(true)),
+  });
+
+  static [Thing.reverseSpecs] = {
+    miscellaneousAdditionalFileArtistContributionsBy:
+      soupyReverse.contributionsBy('miscellaneousAdditionalFileData', 'artistContribs'),
+  };
+}
diff --git a/src/data/things/additional-file/SheetMusicFile.js b/src/data/things/additional-file/SheetMusicFile.js
new file mode 100644
index 00000000..c06cde7f
--- /dev/null
+++ b/src/data/things/additional-file/SheetMusicFile.js
@@ -0,0 +1,28 @@
+import {input, V} from '#composite';
+import Thing from '#thing';
+
+import {exposeConstant} from '#composite/control-flow';
+import {contributionList, soupyReverse} from '#composite/wiki-properties';
+
+import {AdditionalFile} from './AdditionalFile.js';
+
+export class SheetMusicFile extends AdditionalFile {
+  static [Thing.wikiData] = 'sheetMusicFileData';
+
+  static [Thing.getPropertyDescriptors] = () => ({
+    // Update & expose
+
+    artistContribs: contributionList({
+      artistProperty: input.value('sheetMusicFileArtistContributions'),
+    }),
+
+    // Expose only
+
+    isSheetMusicFile: exposeConstant(V(true)),
+  });
+
+  static [Thing.reverseSpecs] = {
+    sheetMusicFileArtistContributionsBy:
+      soupyReverse.contributionsBy('sheetMusicFileData', 'artistContribs'),
+  };
+}
diff --git a/src/data/things/additional-file/index.js b/src/data/things/additional-file/index.js
new file mode 100644
index 00000000..d8de7455
--- /dev/null
+++ b/src/data/things/additional-file/index.js
@@ -0,0 +1,5 @@
+export * from './AdditionalFile.js';
+
+export * from './MidiProjectFile.js';
+export * from './MiscellaneousAdditionalFile.js';
+export * from './SheetMusicFile.js'
diff --git a/src/data/things/album/Album.js b/src/data/things/album/Album.js
index f07d552c..201aaf4e 100644
--- a/src/data/things/album/Album.js
+++ b/src/data/things/album/Album.js
@@ -852,12 +852,21 @@ export class Album extends Thing {
     ],
   };
 
-  getOwnAdditionalFilePath(_file, filename) {
-    return [
-      'media.albumAdditionalFile',
-      this.directory,
-      filename,
-    ];
+  getOwnAdditionalFilePath(file, filename) {
+    if (file.folder) {
+      return [
+        'media.albumAdditionalFileInFolder',
+        this.directory,
+        file.folder,
+        filename,
+      ];
+    } else {
+      return [
+        'media.albumAdditionalFile',
+        this.directory,
+        filename,
+      ];
+    }
   }
 
   getOwnArtworkPath(artwork) {
diff --git a/src/data/things/album/TrackSection.js b/src/data/things/album/TrackSection.js
index 1e901a09..451f8f7b 100644
--- a/src/data/things/album/TrackSection.js
+++ b/src/data/things/album/TrackSection.js
@@ -6,6 +6,8 @@ import Thing from '#thing';
 import {parseDate, parseExcludingURLs} from '#yaml';
 
 import {
+  anyOf,
+  is,
   isBoolean,
   isColor,
   isDirectory,
@@ -121,7 +123,10 @@ export class TrackSection extends Thing {
 
     excludingTrackURLs: [
       exposeUpdateValueOrContinue({
-        validate: input.value(isExcludingURLsReason),
+        validate: input.value(
+          anyOf(
+            is(false),
+            isExcludingURLsReason)),
       }),
 
       withPropertyFromObject('album', V('excludingTrackURLs')),
diff --git a/src/data/things/index.js b/src/data/things/index.js
index 3773864b..8cd21e9d 100644
--- a/src/data/things/index.js
+++ b/src/data/things/index.js
@@ -1,5 +1,6 @@
 // Not actually the entry point for #things - that's init.js in this folder.
 
+export * from './additional-file/index.js';
 export * from './album/index.js';
 export * from './content/index.js';
 export * from './contrib/index.js';
@@ -8,7 +9,6 @@ export * from './group/index.js';
 export * from './homepage-layout/index.js';
 export * from './sorting-rule/index.js';
 
-export * from './AdditionalFile.js';
 export * from './AdditionalName.js';
 export * from './ArtTag.js';
 export * from './Artist.js';
diff --git a/src/data/yaml.js b/src/data/yaml.js
index ba627b29..15d7b0ba 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -762,25 +762,44 @@ export function parseURLs(entries) {
 }
 
 export function parseExcludingURLs(value) {
-  if (typeof value !== 'string') {
-    return value;
+  if (typeof value === 'boolean') {
+    switch (value) {
+      case true: return 'generic';
+      case false: return false;
+      // False is for nullifying an inherited reason for exclusion.
+    }
   }
 
-  switch (value) {
-    case 'paid bonus tracks': return 'paid bonus track';
+  if (typeof value === 'string') {
+    switch (value) {
+      case 'paid bonus tracks': return 'paid bonus track';
+      default: return value;
+    }
   }
 
   return value;
 }
 
-export function parseAdditionalFiles(entries, {subdoc, AdditionalFile}) {
+export function parseAdditionalFilesEntries(thingClass, entries, {subdoc}) {
   return parseArrayEntries(entries, item => {
     if (typeof item !== 'object') return item;
 
-    return subdoc(AdditionalFile, item, {bindInto: 'thing'});
+    return subdoc(thingClass, item, {bindInto: 'thing'});
   });
 }
 
+export function parseAdditionalFiles(entries, {subdoc, MiscellaneousAdditionalFile}) {
+  return parseAdditionalFilesEntries(MiscellaneousAdditionalFile, entries, {subdoc});
+}
+
+export function parseMidiProjectFiles(entries, {subdoc, MidiProjectFile}) {
+  return parseAdditionalFilesEntries(MidiProjectFile, entries, {subdoc});
+}
+
+export function parseSheetMusicFiles(entries, {subdoc, SheetMusicFile}) {
+  return parseAdditionalFilesEntries(SheetMusicFile, entries, {subdoc});
+}
+
 export function parseAdditionalNames(entries, {subdoc, AdditionalName}) {
   return parseArrayEntries(entries, item => {
     if (typeof item === 'object') {
@@ -1866,12 +1885,18 @@ export function linkWikiDataArrays(wikiData, {bindFind, bindReverse}) {
 
     ['lyricsData', [/* find */]],
 
+    ['midiProjectFileData', [/* find */]],
+
+    ['miscellaneousAdditionalFileData', [/* find */]],
+
     ['musicVideoData', [/* find */]],
 
     ['referencingSourceData', [/* find */]],
 
     ['seriesData', [/* find */]],
 
+    ['sheetMusicFileData', [/* find */]],
+
     ['trackData', [
       'artworkData',
       'wikiInfo',