« get me outta code hell

content: listArtistsByLatestContribution: chunk-based rework - 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>2023-08-28 19:19:39 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-08-28 19:20:16 -0300
commit715bd8eff86c20b3773a66e96e9c9d6ae8602644 (patch)
tree3c3fccfa6a3611cec7bd0346f67f0a4a20277e10
parent6b76c7a0c852b87b3ff2e4f3a11fe15424618511 (diff)
content: listArtistsByLatestContribution: chunk-based rework
-rw-r--r--src/content/dependencies/listArtistsByLatestContribution.js594
-rw-r--r--src/strings-default.json6
2 files changed, 279 insertions, 321 deletions
diff --git a/src/content/dependencies/listArtistsByLatestContribution.js b/src/content/dependencies/listArtistsByLatestContribution.js
index b6ea8e96..edb02e0d 100644
--- a/src/content/dependencies/listArtistsByLatestContribution.js
+++ b/src/content/dependencies/listArtistsByLatestContribution.js
@@ -1,15 +1,16 @@
-import {transposeArrays, empty, stitchArrays} from '#sugar';
+import {empty, stitchArrays} from '#sugar';
+import T from '#things';
 
 import {
   chunkMultipleArrays,
-  compareCaseLessSensitive,
-  compareDates,
-  filterMultipleArrays,
-  reduceMultipleArrays,
   sortAlphabetically,
+  sortAlbumsTracksChronologically,
+  sortFlashesChronologically,
   sortMultipleArrays,
 } from '#wiki-data';
 
+const {Album, Flash} = T;
+
 export default {
   contentDependencies: [
     'generateListingPage',
@@ -20,348 +21,303 @@ export default {
 
   extraDependencies: ['html', 'language', 'wikiData'],
 
-  sprawl({artistData, wikiInfo}) {
-    return {
-      artistData,
-      enableFlashesAndGames: wikiInfo.enableFlashesAndGames,
-    };
-  },
+  sprawl: ({albumData, artistData, flashData, trackData, wikiInfo}) =>
+    ({albumData, artistData, flashData, trackData,
+      enableFlashesAndGames: wikiInfo.enableFlashesAndGames}),
 
   query(sprawl, spec) {
-    const query = {
-      spec,
-      enableFlashesAndGames: sprawl.enableFlashesAndGames,
-    };
-
-    const queryContributionInfo = (
-      artistsKey,
-      chunkThingsKey,
-      datesKey,
-      datelessArtistsKey,
-      fn,
-    ) => {
-      const artists = sortAlphabetically(sprawl.artistData.slice());
-
-      // Each value stored in dateLists, corresponding to each artist,
-      // is going to be a list of dates and nulls. Any nulls represent
-      // a contribution which isn't associated with a particular date.
-      const [chunkThingLists, dateLists] =
-        transposeArrays(artists.map(artist => fn(artist)));
-
-      // Scrap artists who don't even have any relevant contributions.
-      // These artists may still have other contributions across the wiki, but
-      // they weren't returned by the callback and so aren't relevant to this
-      // list.
-      filterMultipleArrays(
-        artists,
-        chunkThingLists,
-        dateLists,
-        (artists, chunkThings, dates) => !empty(dates));
-
-      // Also exclude artists whose remaining contributions are all dateless.
-      // But keep track of the artists removed here, since they'll be displayed
-      // in an additional list in the final listing page.
-      const {removed: [datelessArtists]} =
-        filterMultipleArrays(
-          artists,
-          chunkThingLists,
-          dateLists,
-          (artist, chunkThings, dates) => !empty(dates.filter(Boolean)));
-
-      // Cut out dateless contributions. They're not relevant to finding the
-      // latest date.
-      for (const [chunkThings, dates] of transposeArrays([chunkThingLists, dateLists])) {
-        filterMultipleArrays(chunkThings, dates, (chunkThing, date) => date);
+    //
+    // First main step is to get the latest thing each artist has contributed
+    // to, and the date associated with that contribution! Some notes:
+    //
+    // * Album and track contributions are considered before flashes, so
+    //   they'll take priority if an artist happens to have multiple contribs
+    //   landing on the same date to both an album and a flash.
+    //
+    // * The final (album) contribution list is chunked by album, but also by
+    //   date, because an individual album can cover a variety of dates.
+    //
+    // * If an artist has contributed both artworks and tracks to the album
+    //   containing their latest contribution, then that will be indicated
+    //   in an annotation, but *only if* those contributions were also on
+    //   the same date.
+    //
+    // * If an artist made contributions to multiple albums on the same date,
+    //   then the first of the *albums* sorted chronologically (latest first)
+    //   is the one that will count.
+    //
+    // * Same for artists who've contributed to multiple flashes which were
+    //   released on the same date.
+    //
+    // * The map may exclude artists none of whose contributions were dated.
+    //
+
+    const artistLatestContribMap = new Map();
+
+    const considerDate = (artist, date, thing, contribution) => {
+      if (!date) {
+        return;
       }
 
-      const [chunkThings, dates] =
-        transposeArrays(
-          transposeArrays([chunkThingLists, dateLists])
-            .map(([chunkThings, dates]) =>
-              reduceMultipleArrays(
-                chunkThings, dates,
-                (accChunkThing, accDate, chunkThing, date) =>
-                  (date && date > accDate
-                    ? [chunkThing, date]
-                    : [accChunkThing, accDate]))));
-
-      sortMultipleArrays(artists, dates, chunkThings,
-        (artistA, artistB, dateA, dateB, chunkThingA, chunkThingB) => {
-          const dateComparison = compareDates(dateA, dateB, {latestFirst: true});
-          if (dateComparison !== 0) {
-            return dateComparison;
-          }
-
-          // TODO: Compare alphabetically, not just by directory.
-          return compareCaseLessSensitive(chunkThingA.directory, chunkThingB.directory);
-        });
-
-      const chunks =
-        chunkMultipleArrays(artists, dates, chunkThings,
-          (artist, lastArtist, date, lastDate, chunkThing, lastChunkThing) =>
-            +date !== +lastDate || chunkThing !== lastChunkThing);
+      if (artistLatestContribMap.has(artist)) {
+        const latest = artistLatestContribMap.get(artist);
+        if (latest.date > date) {
+          return;
+        }
 
-      query[chunkThingsKey] =
-        chunks.map(([artists, dates, chunkThings]) => chunkThings[0]);
-
-      query[datesKey] =
-        chunks.map(([artists, dates, chunkThings]) => dates[0]);
+        if (latest.date === date) {
+          if (latest.thing === thing) {
+            // May combine differnt contributions to the same thing and date.
+            latest.contribution.add(contribution);
+          }
 
-      query[artistsKey] =
-        chunks.map(([artists, dates, chunkThings]) => artists);
+          // Earlier-processed things of same date take priority.
+          return;
+        }
+      }
 
-      query[datelessArtistsKey] = datelessArtists;
+      // First entry for artist or more recent contribution than latest date.
+      artistLatestContribMap.set(artist, {
+        date,
+        thing,
+        contribution: new Set([contribution]),
+      });
     };
 
-    queryContributionInfo(
-      'artistsByTrackContributions',
-      'albumsByTrackContributions',
-      'datesByTrackContributions',
-      'datelessArtistsByTrackContributions',
-      artist => {
-        const tracks =
-          [...artist.tracksAsArtist, ...artist.tracksAsContributor]
-            .filter(track => !track.originalReleaseTrack);
-
-        const albums = tracks.map(track => track.album);
-        const dates = tracks.map(track => track.date);
+    const getArtists = (thing, key) => thing[key].map(({who}) => who);
 
-        return [albums, dates];
-      });
+    const albumsLatestFirst = sortAlbumsTracksChronologically(sprawl.albumData.slice());
+    const tracksLatestFirst = sortAlbumsTracksChronologically(sprawl.trackData.slice());
+    const flashesLatestFirst = sortFlashesChronologically(sprawl.flashData.slice());
 
-    queryContributionInfo(
-      'artistsByArtworkContributions',
-      'albumsByArtworkContributions',
-      'datesByArtworkContributions',
-      'datelessArtistsByArtworkContributions',
-      artist => [
-        [
-          ...artist.tracksAsCoverArtist.map(track => track.album),
-          ...artist.albumsAsCoverArtist,
-          ...artist.albumsAsWallpaperArtist,
-          ...artist.albumsAsBannerArtist,
-        ],
-        [
-          // TODO: Per-artwork dates, see #90.
-          ...artist.tracksAsCoverArtist.map(track => track.coverArtDate),
-          ...artist.albumsAsCoverArtist.map(album => album.coverArtDate),
-          ...artist.albumsAsWallpaperArtist.map(album => album.coverArtDate),
-          ...artist.albumsAsBannerArtist.map(album => album.coverArtDate),
-        ],
-      ]);
-
-    if (sprawl.enableFlashesAndGames) {
-      queryContributionInfo(
-        'artistsByFlashContributions',
-        'flashesByFlashContributions',
-        'datesByFlashContributions',
-        'datelessArtistsByFlashContributions',
-        artist => [
-          [
-            ...artist.flashesAsContributor,
-          ],
-          [
-            ...artist.flashesAsContributor.map(flash => flash.date),
-          ],
-        ]);
+    for (const album of albumsLatestFirst) {
+      for (const artist of new Set([
+        ...getArtists(album, 'coverArtistContribs'),
+        ...getArtists(album, 'wallpaperArtistContribs'),
+        ...getArtists(album, 'bannerArtistContribs'),
+      ])) {
+        // Might combine later with 'track' of the same album and date.
+        considerDate(artist, album.coverArtDate, album, 'artwork');
+      }
     }
 
-    return query;
-  },
-
-  relations(relation, query) {
-    const relations = {};
-
-    relations.page =
-      relation('generateListingPage', query.spec);
-
-    // Track contributors
-
-    relations.albumLinksByTrackContributions =
-      query.albumsByTrackContributions
-        .map(album => relation('linkAlbum', album));
-
-    relations.artistLinksByTrackContributions =
-      query.artistsByTrackContributions
-        .map(artists =>
-          artists.map(artist => relation('linkArtist', artist)));
-
-    relations.datelessArtistLinksByTrackContributions =
-      query.datelessArtistsByTrackContributions
-        .map(artist => relation('linkArtist', artist));
-
-    // Artwork contributors
-
-    relations.albumLinksByArtworkContributions =
-      query.albumsByArtworkContributions
-        .map(album => relation('linkAlbum', album));
+    for (const track of tracksLatestFirst) {
+      for (const artist of getArtists(track, 'coverArtistContribs')) {
+        // No special effect if artist already has 'artwork' for the same album and date.
+        considerDate(artist, track.coverArtDate, track.album, 'artwork');
+      }
 
-    relations.artistLinksByArtworkContributions =
-      query.artistsByArtworkContributions
-        .map(artists =>
-          artists.map(artist => relation('linkArtist', artist)));
+      for (const artist of new Set([
+        ...getArtists(track, 'artistContribs'),
+        ...getArtists(track, 'contributorContribs'),
+      ])) {
+        // Might be combining with 'artwork' of the same album and date.
+        considerDate(artist, track.date, track.album, 'track');
+      }
+    }
 
-    relations.datelessArtistLinksByArtworkContributions =
-      query.datelessArtistsByArtworkContributions
-        .map(artist => relation('linkArtist', artist));
+    for (const flash of flashesLatestFirst) {
+      for (const artist of getArtists(flash, 'contributorContribs')) {
+        // Won't take priority above album contributions of the same date.
+        considerDate(artist, flash.date, flash, 'flash');
+      }
+    }
 
-    // Flash contributors
+    //
+    // Next up is to sort all the processed artist information!
+    //
+    // Entries with the same album/flash and the same date go together first,
+    // with the following rules for sorting artists therein:
+    //
+    // * If the contributions are different, which can only happen for albums,
+    //   then it's tracks-only first, tracks + artworks next, and artworks-only
+    //   last.
+    //
+    // * If the contributions are the same, then sort alphabetically.
+    //
+    // Entries with different albums/flashes follow another set of rules:
+    //
+    // * Later dates come before earlier dates.
+    //
+    // * On the same date, albums come before flashes.
+    //
+    // * Things of the same type *and* date are sorted alphabetically.
+    //
+
+    const artistsAlphabetically =
+      sortAlphabetically(sprawl.artistData.slice());
+
+    const artists =
+      Array.from(artistLatestContribMap.keys());
+
+    const artistContribEntries =
+      Array.from(artistLatestContribMap.values());
+
+    const artistThings =
+      artistContribEntries.map(({thing}) => thing);
+
+    const artistDates =
+      artistContribEntries.map(({date}) => date);
+
+    const artistContributions =
+      artistContribEntries.map(({contribution}) => contribution);
+
+    sortMultipleArrays(artistThings, artistDates, artistContributions, artists,
+      (thing1, thing2, date1, date2, contrib1, contrib2, artist1, artist2) => {
+        if (date1 === date2 && thing1 === thing2) {
+          // Move artwork-only contribs after contribs with tracks.
+          if (!contrib1.has('track') && contrib2.has('track')) return 1;
+          if (!contrib2.has('track') && contrib1.has('track')) return -1;
+
+          // Move track-only contribs before tracks with tracks and artwork.
+          if (!contrib1.has('artwork') && contrib2.has('artwork')) return -1;
+          if (!contrib2.has('artwork') && contrib1.has('artwork')) return 1;
+
+          // Sort artists of the same type of contribution alphabetically,
+          // referring to a previous sort.
+          const index1 = artistsAlphabetically.indexOf(artist1);
+          const index2 = artistsAlphabetically.indexOf(artist2);
+          return index1 - index2;
+        } else {
+          // Move later dates before earlier ones.
+          if (date1 !== date2) return date2 - date1;
+
+          // Move albums before flashes.
+          if (thing1 instanceof Album && thing2 instanceof Flash) return -1;
+          if (thing1 instanceof Flash && thing2 instanceof Album) return 1;
+
+          // Sort two albums or two flashes alphabetically, referring to a
+          // previous sort (which was chronological but includes the correct
+          // ordering for things released on the same date).
+          const thingsLatestFirst =
+            (thing1 instanceof Album
+              ? albumsLatestFirst
+              : flashesLatestFirst);
+          const index1 = thingsLatestFirst.indexOf(thing1);
+          const index2 = thingsLatestFirst.indexOf(thing2);
+          return index2 - index1;
+        }
+      });
 
-    if (query.enableFlashesAndGames) {
-      relations.flashLinksByFlashContributions =
-        query.flashesByFlashContributions
-          .map(flash => relation('linkFlash', flash));
+    // Last off, turn the flat sorted list into a proper chunked list, now that
+    // entries going in the same chunk are sorted correctly next to each other.
+    // Then extract the parts that are useful for displaying on the listing!
 
-      relations.artistLinksByFlashContributions =
-        query.artistsByFlashContributions
-          .map(artists =>
-            artists.map(artist => relation('linkArtist', artist)));
+    const chunks =
+      chunkMultipleArrays(artistThings, artistDates, artistContributions, artists,
+        (thing, lastThing, date, lastDate) =>
+          thing !== lastThing ||
+          +date !== +lastDate);
 
-      relations.datelessArtistLinksByFlashContributions =
-        query.datelessArtistsByFlashContributions
-          .map(artist => relation('linkArtist', artist));
-    }
+    const chunkThings =
+      chunks.map(([artistThings, , , ]) => artistThings[0]);
 
-    return relations;
-  },
+    const chunkDates =
+      chunks.map(([, artistDates, , ]) => artistDates[0]);
 
-  data(query) {
-    const data = {};
+    const chunkArtistContributions =
+      chunks.map(([, , artistContributions, ]) => artistContributions);
 
-    data.enableFlashesAndGames = query.enableFlashesAndGames;
+    const chunkArtists =
+      chunks.map(([, , , artists]) => artists);
 
-    data.datesByTrackContributions = query.datesByTrackContributions;
-    data.datesByArtworkContributions = query.datesByArtworkContributions;
+    // And one bonus step - keep track of all the artists whose contributions
+    // were all without date.
 
-    if (query.enableFlashesAndGames) {
-      data.datesByFlashContributions = query.datesByFlashContributions;
-    }
+    const datelessArtists =
+      artistsAlphabetically
+        .filter(artist => !artists.includes(artist));
 
-    return data;
+    return {
+      spec,
+      chunkThings,
+      chunkDates,
+      chunkArtistContributions,
+      chunkArtists,
+      datelessArtists,
+    };
   },
 
-  generate(data, relations, {html, language}) {
-    const chunkTitles = Object.fromEntries(
-      ([
-        ['tracks', [
-          'album',
-          relations.albumLinksByTrackContributions,
-          data.datesByTrackContributions,
-        ]],
-
-        ['artworks', [
-          'album',
-          relations.albumLinksByArtworkContributions,
-          data.datesByArtworkContributions,
-        ]],
-
-        data.enableFlashesAndGames &&
-          ['flashes', [
-            'flash',
-            relations.flashLinksByFlashContributions,
-            data.datesByFlashContributions,
-          ]],
-      ]).filter(Boolean)
-        .map(([key, [stringsKey, links, dates]]) => [
-          key,
-          stitchArrays({link: links, date: dates})
-            .map(({link, date}) =>
-              html.tag('dt',
-                language.$(`listingPage.listArtists.byLatest.chunk.title.${stringsKey}`, {
-                  [stringsKey]: link,
-                  date: language.formatDate(date),
-                }))),
-        ]));
-
-    const chunkItems = Object.fromEntries(
-      ([
-        ['tracks', relations.artistLinksByTrackContributions],
-        ['artworks', relations.artistLinksByArtworkContributions],
-        data.enableFlashesAndGames &&
-          ['flashes', relations.artistLinksByFlashContributions],
-      ]).filter(Boolean)
-        .map(([key, artistLinkLists]) => [
-          key,
-          artistLinkLists.map(artistLinks =>
-            html.tag('dd',
-              html.tag('ul',
-                artistLinks.map(artistLink =>
-                  html.tag('li',
-                    language.$('listingPage.listArtists.byLatest.chunk.item', {
-                      artist: artistLink,
-                    })))))),
-        ]));
-
-    const lists = Object.fromEntries(
-      ([
-        ['tracks', [
-          chunkTitles.tracks,
-          chunkItems.tracks,
-          relations.datelessArtistLinksByTrackContributions,
-        ]],
-
-        ['artworks', [
-          chunkTitles.artworks,
-          chunkItems.artworks,
-          relations.datelessArtistLinksByArtworkContributions,
-        ]],
-
-        data.enableFlashesAndGames &&
-          ['flashes', [
-            chunkTitles.flashes,
-            chunkItems.flashes,
-            relations.datelessArtistLinksByFlashContributions,
-          ]],
-      ]).filter(Boolean)
-        .map(([key, [titles, items, datelessArtistLinks]]) => [
-          key,
-          html.tags([
-            html.tag('dl',
-              stitchArrays({
-                title: titles,
-                items: items,
-              }).map(({title, items}) => [title, items])),
-
-            !empty(datelessArtistLinks) && [
-              html.tag('p',
-                language.$('listingPage.listArtists.byLatest.dateless.title')),
-
-              html.tag('ul',
-                datelessArtistLinks.map(artistLink =>
-                  html.tag('li',
-                    language.$('listingPage.listArtists.byLatest.dateless.item', {
-                      artist: artistLink,
-                    })))),
-            ],
-          ]),
-        ]));
-
+  relations: (relation, query) => ({
+    page:
+      relation('generateListingPage', query.spec),
+
+    chunkAlbumLinks:
+      query.chunkThings
+        .map(thing =>
+          (thing instanceof Album
+            ? relation('linkAlbum', thing)
+            : null)),
+
+    chunkFlashLinks:
+      query.chunkThings
+        .map(thing =>
+          (thing instanceof Flash
+            ? relation('linkFlash', thing)
+            : null)),
+
+    chunkArtistLinks:
+      query.chunkArtists
+        .map(artists => artists
+          .map(artist => relation('linkArtist', artist))),
+
+    datelessArtistLinks:
+      query.datelessArtists
+        .map(artist => relation('linkArtist', artist)),
+  }),
+
+  data: (query) => ({
+    chunkDates: query.chunkDates,
+    chunkArtistContributions: query.chunkArtistContributions,
+  }),
+
+  generate(data, relations, {language}) {
     return relations.page.slots({
-      type: 'custom',
-      content:
-        html.tag('div', {class: 'content-columns'}, [
-          html.tag('div', {class: 'column'}, [
-            html.tag('h2',
-              language.$('listingPage.misc.trackContributors')),
-
-            lists.tracks,
-          ]),
-
-          html.tag('div', {class: 'column'}, [
-            html.tag('h2',
-              language.$(
-                'listingPage.misc.artContributors')),
-
-            lists.artworks,
-
-            lists.flashes && [
-              html.tag('h2',
-                language.$('listingPage.misc.flashContributors')),
-
-              lists.flashes,
-            ],
-          ]),
-        ]),
+      type: 'chunks',
+
+      chunkTitles:
+        stitchArrays({
+          albumLink: relations.chunkAlbumLinks,
+          flashLink: relations.chunkFlashLinks,
+          date: data.chunkDates,
+        }).map(({albumLink, flashLink, date}) => ({
+            date: language.formatDate(date),
+            ...(albumLink
+              ? {stringsKey: 'album', album: albumLink}
+              : {stringsKey: 'flash', flash: flashLink}),
+          }))
+          .concat(
+            (empty(relations.datelessArtistLinks)
+              ? []
+              : [{stringsKey: 'dateless'}])),
+
+      chunkRows:
+        stitchArrays({
+          artistLinks: relations.chunkArtistLinks,
+          contributions: data.chunkArtistContributions,
+        }).map(({artistLinks, contributions}) =>
+            stitchArrays({
+              artistLink: artistLinks,
+              contribution: contributions,
+            }).map(({artistLink, contribution}) => ({
+                artist: artistLink,
+                stringsKey:
+                  (contribution.has('track') && contribution.has('artwork')
+                    ? 'tracksAndArt'
+                 : contribution.has('track')
+                    ? 'tracks'
+                 : contribution.has('artwork')
+                    ? 'art'
+                    : null),
+              })))
+          .concat(
+            (empty(relations.datelessArtistLinks)
+              ? []
+              : [
+                  relations.datelessArtistLinks.map(artistLink => ({
+                    artist: artistLink,
+                  })),
+                ])),
     });
   },
 };
diff --git a/src/strings-default.json b/src/strings-default.json
index d170b86b..e893a5ce 100644
--- a/src/strings-default.json
+++ b/src/strings-default.json
@@ -389,9 +389,11 @@
   "listingPage.listArtists.byLatest.title.short": "...by Latest Contribution",
   "listingPage.listArtists.byLatest.chunk.title.album": "{ALBUM} ({DATE})",
   "listingPage.listArtists.byLatest.chunk.title.flash": "{FLASH} ({DATE})",
+  "listingPage.listArtists.byLatest.chunk.title.dateless": "These artists' contributions aren't dated:",
   "listingPage.listArtists.byLatest.chunk.item": "{ARTIST}",
-  "listingPage.listArtists.byLatest.dateless.title": "These artists' contributions aren't dated:",
-  "listingPage.listArtists.byLatest.dateless.item": "{ARTIST}",
+  "listingPage.listArtists.byLatest.chunk.item.tracks": "{ARTIST} (tracks)",
+  "listingPage.listArtists.byLatest.chunk.item.tracksAndArt": "{ARTIST} (tracks, art)",
+  "listingPage.listArtists.byLatest.chunk.item.art": "{ARTIST} (art)",
   "listingPage.listGroups.byName.title": "Groups - by Name",
   "listingPage.listGroups.byName.title.short": "...by Name",
   "listingPage.listGroups.byName.item": "{GROUP} ({GALLERY})",