Repository: ROpdebee/mb-userscripts (ts)

Issue:
ECAU track number comment uses wrong sort order
Track comments use lexicographic ordering. Looks weird or ist that intentional?

[Release “Free Loaders: The Soundtrack” by Sore Losers - MusicBrainz](https://beta.musicbrainz.org/release/16ae377d-547b-418f-bb5e-ba993fe1f030/)
    Tracks 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 20, 3, 4, 5, 6, 7, 8, 9
[Release “The Poodle Magic Pig Type” by Fred Händl feat. Mark Watson - MusicBrainz](https://beta.musicbrainz.org/release/93635a11-1b40-4093-aa1e-2870949df251/)
    Tracks 1, 13, 2, 3, 4, 9

Pull request:
fix(ecau): properly sort track numbers in track image comment
Also adds an ESLint rule to warn about this next time.

Fixes #330

Patch:
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 9fc516f3..59d799d9 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -82,6 +82,7 @@ module.exports = {
                 '@typescript-eslint/prefer-optional-chain': ['error'],
                 '@typescript-eslint/prefer-reduce-type-parameter': ['error'],
                 '@typescript-eslint/prefer-ts-expect-error': ['error'],
+                '@typescript-eslint/require-array-sort-compare': ['error'],
                 '@typescript-eslint/type-annotation-spacing': ['error'],
             }
         }, { // Override per eslint-plugin-jest documentation.
diff --git a/src/lib/util/array.ts b/src/lib/util/array.ts
index c1ef1fd4..3e9c8ad0 100644
--- a/src/lib/util/array.ts
+++ b/src/lib/util/array.ts
@@ -31,3 +31,12 @@ export function groupBy<T, K, V>(array: T[], keyFn: (el: T) => K, valTransform:
 
     return map;
 }
+
+/**
+ * Sort an array of strings using `Intl.Collator`. Array is modified in-place,
+ * and returned.
+ */
+export function collatedSort(array: string[]): string[] {
+    const coll = new Intl.Collator('en', { numeric: true });
+    return array.sort(coll.compare.bind(coll));
+}
diff --git a/src/mb_enhanced_cover_art_uploads/providers/base.ts b/src/mb_enhanced_cover_art_uploads/providers/base.ts
index f52e26a5..794d349b 100644
--- a/src/mb_enhanced_cover_art_uploads/providers/base.ts
+++ b/src/mb_enhanced_cover_art_uploads/providers/base.ts
@@ -1,6 +1,6 @@
 import { LOGGER } from '@lib/logging/logger';
 import { ArtworkTypeIDs } from '@lib/MB/CoverArt';
-import { filterNonNull, groupBy } from '@lib/util/array';
+import { collatedSort, filterNonNull, groupBy } from '@lib/util/array';
 import { blobToDigest } from '@lib/util/blob';
 import { parseDOM, qs } from '@lib/util/dom';
 import { gmxhr } from '@lib/util/xhr';
@@ -255,6 +255,9 @@ export abstract class ProviderWithTrackImages extends CoverArtProvider {
         if (!definedTrackNumbers.length) return '';
 
         const prefix = definedTrackNumbers.length === 1 ? 'Track' : 'Tracks';
-        return `${prefix} ${definedTrackNumbers.sort().join(', ')}`;
+        // Use a collated sort here to make sure we keep numeric ordering.
+        // We can't just parse track numbers to actual numbers here, as the
+        // tracks may reasonably be numbered as strings (e.g. Vinyl sides)
+        return `${prefix} ${collatedSort(definedTrackNumbers).join(', ')}`;
     }
 }
diff --git a/src/mb_enhanced_cover_art_uploads/providers/rateyourmusic.ts b/src/mb_enhanced_cover_art_uploads/providers/rateyourmusic.ts
index d858c3fe..d3c8e3ba 100644
--- a/src/mb_enhanced_cover_art_uploads/providers/rateyourmusic.ts
+++ b/src/mb_enhanced_cover_art_uploads/providers/rateyourmusic.ts
@@ -10,7 +10,10 @@ export class RateYourMusicProvider extends CoverArtProvider {
     supportedDomains = ['rateyourmusic.com'];
     favicon = 'https://e.snmc.io/2.5/img/sonemic.png';
     name = 'RateYourMusic';
-    urlRegex = /\/release\/album\/([^/]+\/[^/]+)(?:\/|$)/;
+    // Include release type in the ID to make sure it doesn't redirect a single
+    // to an album and vice versa, and also to simplify the URL transformation
+    // below.
+    urlRegex = /\/release\/((?:album|single)\/[^/]+\/[^/]+)(?:\/|$)/;
 
     async findImages(url: URL): Promise<CoverArt[]> {
         const releaseId = this.extractId(url);
@@ -19,7 +22,7 @@ export class RateYourMusicProvider extends CoverArtProvider {
         // must be logged in to get the full-res image. We can't use the
         // thumbnails in case the user is not logged in, since they're served
         // as WebP, which isn't supported by CAA (yet).
-        const buyUrl = `https://rateyourmusic.com/release/album/${releaseId}/buy`;
+        const buyUrl = `https://rateyourmusic.com/release/${releaseId}/buy`;
         const buyDoc = parseDOM(await this.fetchPage(new URL(buyUrl)), buyUrl);
 
         if (qsMaybe('.header_profile_logged_in', buyDoc) === null) {


Test patch:
diff --git a/tests/unit/mb_enhanced_cover_art_uploads/providers/base.test.ts b/tests/unit/mb_enhanced_cover_art_uploads/providers/base.test.ts
index 8578eea8..28556739 100644
--- a/tests/unit/mb_enhanced_cover_art_uploads/providers/base.test.ts
+++ b/tests/unit/mb_enhanced_cover_art_uploads/providers/base.test.ts
@@ -258,6 +258,26 @@ describe('providers with track images', () => {
                 .toBe('Tracks 1, 3');
         });
 
+        it('properly sorts track numbers in comment', async () => {
+            const trackImages = [{
+                url: 'https://example.com/123',
+                trackNumber: '3',
+            }, {
+                url: 'https://example.com/123',
+                trackNumber: '10',
+            }, {
+                url: 'https://example.com/123',
+                trackNumber: '2',
+            }, {
+                url: 'https://example.com/123',
+                trackNumber: '1',
+            }];
+            const results = await fakeProvider.mergeTrackImages(trackImages, 'https://example.com/x', false);
+
+            expect(results.find((img) => img.url.pathname === '/123')?.comment)
+                .toBe('Tracks 1, 2, 3, 10');
+        });
+
         it('sets no comment if track image cannot be determined', async () => {
             const trackImages = [{
                 url: 'https://example.com/123',
diff --git a/tests/unit/mb_enhanced_cover_art_uploads/providers/rateyourmusic.test.ts b/tests/unit/mb_enhanced_cover_art_uploads/providers/rateyourmusic.test.ts
index 125d91e3..1f07a0b6 100644
--- a/tests/unit/mb_enhanced_cover_art_uploads/providers/rateyourmusic.test.ts
+++ b/tests/unit/mb_enhanced_cover_art_uploads/providers/rateyourmusic.test.ts
@@ -16,11 +16,19 @@ describe('rateyourmusic provider', () => {
         const supportedUrls = [{
             desc: 'album URLs',
             url: 'https://rateyourmusic.com/release/album/fishmans/long-season.p/',
-            id: 'fishmans/long-season.p',
+            id: 'album/fishmans/long-season.p',
         }, {
             desc: 'album buy URLs',
             url: 'https://rateyourmusic.com/release/album/fishmans/long-season.p/buy/',
-            id: 'fishmans/long-season.p',
+            id: 'album/fishmans/long-season.p',
+        }, {
+            desc: 'single URLs',
+            url: 'https://rateyourmusic.com/release/single/hot_dad/undertale/',
+            id: 'single/hot_dad/undertale',
+        }, {
+            desc: 'single buy URLs',
+            url: 'https://rateyourmusic.com/release/single/hot_dad/undertale/buy',
+            id: 'single/hot_dad/undertale',
         }];
 
         const unsupportedUrls = [{