Repository: ROpdebee/mb-userscripts (ts)

Issue:
Drop IMU exceptions which are no longer necessary
We can probably remove our custom maximisation logic for Apple Music by now, IMU has implemented support for source images in https://github.com/qsniyg/maxurl/commit/56e0fce886bd328e8a155fc69dd62ff954fa0053 (but I have not tested it yet).

Same for 7digital, native support has been added in https://github.com/qsniyg/maxurl/commit/9e50ab7dff78df686b4422dcbcb4cf418ba5df22.

Pull request description:
feat(ecau): bump IMU to latest version
More maximisation!

Exceptions for 7digital have been removed, exception for Apple Music has been changed to mark the `/source` URLs that are likely broken as such, and we'll skip warning the user when we can't fetch a likely broken image.

Closes #392.

We should also perhaps create some more IMU tickets to add Jamendo and DatPiff.

Patch:
diff --git a/src/mb_enhanced_cover_art_uploads/fetch.ts b/src/mb_enhanced_cover_art_uploads/fetch.ts
index 647f814e..f26a1362 100644
--- a/src/mb_enhanced_cover_art_uploads/fetch.ts
+++ b/src/mb_enhanced_cover_art_uploads/fetch.ts
@@ -94,6 +94,8 @@ export class ImageFetcher {
                     }
                     break;
                 } catch (err) {
+                    // istanbul ignore if: Fine.
+                    if (maxCandidate.likely_broken) continue;
                     LOGGER.warn(`Skipping maximised candidate ${maxCandidate.url}`, err);
                 }
             }
diff --git a/src/mb_enhanced_cover_art_uploads/maximise.ts b/src/mb_enhanced_cover_art_uploads/maximise.ts
index cf3d14e5..26b7c3e1 100644
--- a/src/mb_enhanced_cover_art_uploads/maximise.ts
+++ b/src/mb_enhanced_cover_art_uploads/maximise.ts
@@ -4,6 +4,7 @@ import type { GMxmlHttpRequest } from '@lib/compat';
 import { LOGGER } from '@lib/logging/logger';
 import { retryTimes } from '@lib/util/async';
 import { DispatchMap } from '@lib/util/domain_dispatch';
+import { urlBasename } from '@lib/util/urls';
 
 import { DiscogsProvider } from './providers/discogs';
 
@@ -234,6 +235,7 @@ export interface MaximisedImage {
     url: URL;
     filename: string;
     headers: Record<string, string>;
+    likely_broken?: boolean;
 }
 
 export async function* getMaximisedCandidates(smallurl: URL): AsyncGenerator<MaximisedImage, undefined, undefined> {
@@ -267,7 +269,7 @@ async function* maximiseGeneric(smallurl: URL): AsyncIterable<MaximisedImage> {
 
     for (const maximisedResult of results) {
         // Filter out results that will definitely not work
-        if (maximisedResult.fake || maximisedResult.bad || maximisedResult.likely_broken) continue;
+        if (maximisedResult.fake || maximisedResult.bad) continue;
         try {
             yield {
                 ...maximisedResult,
@@ -285,67 +287,33 @@ IMU_EXCEPTIONS.set('i.discogs.com', async (smallurl) => {
     const fullSizeURL = await DiscogsProvider.maximiseImage(smallurl);
     return [{
         url: fullSizeURL,
-        filename: DiscogsProvider.getFilenameFromUrl(smallurl) ?? '',
+        filename: DiscogsProvider.getFilenameFromUrl(smallurl),
         headers: {},
     }];
 });
 
 // Apple Music
 IMU_EXCEPTIONS.set('*.mzstatic.com', async (smallurl) => {
-    // For Apple Music, IMU always returns a PNG, regardless of whether the
-    // original source image was PNG or JPEG. We can grab the source image
-    // instead. See #80.
+    // For Apple Music, IMU always returns a `/source` URL first, but it may
+    // not always exist. Mark such a URL as likely broken so that we don't warn
+    // for those. We don't reorder the candidates to ensure we always get the
+    // largest possible image.
 
     const results: MaximisedImage[] = [];
-    for await (const imgGeneric of maximiseGeneric(smallurl)) {
-        // Assume original file name is penultimate component of pathname, e.g.
-        // https://is5-ssl.mzstatic.com/image/thumb/Music124/v4/58/34/98/58349857-55bb-62ae-81d4-4a2726e33528/5060786561909.png/999999999x0w-999.png
-        // Transform it to the following:
-        // https://a1.mzstatic.com/us/r1000/063/Music124/v4/58/34/98/58349857-55bb-62ae-81d4-4a2726e33528/5060786561909.png
-        // i.e. change domain to a1, path prefix to us/r1000/063, and drop trailing conversion name.
-        // This should also work for images returned by the old iTunes API.
-        const sourceUrl = new URL(imgGeneric.url);
-        sourceUrl.hostname = 'a1.mzstatic.com';
-
-        // Be a bit defensive and don't try to transform URLs which we don't recognize
-        if (sourceUrl.pathname.startsWith('/image/thumb')) {
-            sourceUrl.pathname = sourceUrl.pathname.replace(/^\/image\/thumb/, '/us/r1000/063');
-        }
-        if (sourceUrl.pathname.split('/').length === 12) {
-            // Drop the trailing conversion filename
-            sourceUrl.pathname = sourceUrl.pathname.split('/').slice(0, -1).join('/');
-        }
+    const smallOriginalName = smallurl.href.match(/(?:[a-f0-9]{2}\/){3}[a-f0-9-]{36}\/([^/]+)/)?.[1];
 
-        if (sourceUrl.pathname !== imgGeneric.url.pathname) {
-            results.push({
-                ...imgGeneric,
-                url: sourceUrl,
-            });
+    for await (const imgGeneric of maximiseGeneric(smallurl)) {
+        if (urlBasename(imgGeneric.url) === 'source' && smallOriginalName !== 'source') {
+            // Mark the `/source` image as likely broken if the alleged original
+            // name isn't "source", but instead e.g. "20UMGIM63158.rgb.jpg".
+            imgGeneric.likely_broken = true;
         }
-
-        // Always return the original maximised URL as a backup, e.g. for when
-        // the source image is webp.
         results.push(imgGeneric);
     }
 
     return results;
 });
 
-// IMU has no definitions for 7digital yet, so adding an exception here as a workaround
-// Upstream issue: https://github.com/qsniyg/maxurl/issues/922
-IMU_EXCEPTIONS.set('artwork-cdn.7static.com', async (smallurl) => {
-    // According to https://docs.7digital.com/reference#image-sizes, 800x800
-    // and 500x500 aren't available on some images, so add 350 as well as a
-    // backup.
-    return ['800', '500', '350'].map((size) => {
-        return {
-            url: new URL(smallurl.href.replace(/_\d+\.jpg$/, `_${size}.jpg`)),
-            filename: '',
-            headers: {},
-        };
-    });
-});
-
 IMU_EXCEPTIONS.set('usercontent.jamendo.com', async (smallurl) => {
     return [{
         url: new URL(smallurl.href.replace(/([&?])width=\d+/, '$1width=0')),
diff --git a/src/mb_enhanced_cover_art_uploads/meta.ts b/src/mb_enhanced_cover_art_uploads/meta.ts
index 347753f0..5b305cb0 100644
--- a/src/mb_enhanced_cover_art_uploads/meta.ts
+++ b/src/mb_enhanced_cover_art_uploads/meta.ts
@@ -27,7 +27,7 @@ const metadata: UserscriptMetadata = {
         'GM.getResourceURL',
     ],
     connect: '*',
-    require: ['https://github.com/qsniyg/maxurl/blob/563626fe3b7c5ed3f6dc19d90a356746c68b5b4b/userscript.user.js?raw=true'],
+    require: ['https://github.com/qsniyg/maxurl/blob/e2b9dc7e3dce254a5b6d645e077aa82cba4570d5/userscript.user.js?raw=true'],
     resource: ['amazonFavicon https://www.amazon.com/favicon.ico'],
 };
 
diff --git a/src/mb_enhanced_cover_art_uploads/providers/discogs.ts b/src/mb_enhanced_cover_art_uploads/providers/discogs.ts
index 25ce2362..9a17ac07 100644
--- a/src/mb_enhanced_cover_art_uploads/providers/discogs.ts
+++ b/src/mb_enhanced_cover_art_uploads/providers/discogs.ts
@@ -108,7 +108,7 @@ export class DiscogsProvider extends CoverArtProvider {
         return metadata;
     }
 
-    static getFilenameFromUrl(url: URL): string | undefined {
+    static getFilenameFromUrl(url: URL): string {
         // E.g. https://i.discogs.com/aRe2RbRXu0g4PvRjrPgQKb_YmFWO3Y0CYc098S8Q1go/rs:fit/g:sm/q:90/h:600/w:576/czM6Ly9kaXNjb2dz/LWltYWdlcy9SLTk4/OTI5MTItMTU3OTQ1/NjcwNy0yMzIwLmpw/ZWc.jpeg
         // First part is signature, everything following containing colon is param.
         // Last part is base64-encoded S3 URL, split with slashes.
@@ -119,14 +119,14 @@ export class DiscogsProvider extends CoverArtProvider {
         // Cut off the extension added by Discogs, this leads to decoding errors.
         // eslint-disable-next-line @delagen/deprecation/deprecation -- Incorrect environment
         const s3UrlDecoded = atob(s3Url.slice(0, s3Url.indexOf('.')));
-        return s3UrlDecoded.split('/').pop();
+        return s3UrlDecoded.split('/').pop()!;
     }
 
     static async maximiseImage(url: URL): Promise<URL> {
         // Maximising by querying the API for all images of the release, finding
         // the right one, and extracting the "full size" (i.e., 600x600 JPEG) URL.
         const imageName = this.getFilenameFromUrl(url);
-        const releaseId = imageName?.match(/^R-(\d+)/)?.[1];
+        const releaseId = imageName.match(/^R-(\d+)/)?.[1];
 
         /* istanbul ignore if: Should never happen on valid image */
         if (!releaseId) return url;


Test patch:
diff --git a/tests/unit/mb_enhanced_cover_art_uploads/maximise.test.ts b/tests/unit/mb_enhanced_cover_art_uploads/maximise.test.ts
index d1ae9f92..4758a39b 100644
--- a/tests/unit/mb_enhanced_cover_art_uploads/maximise.test.ts
+++ b/tests/unit/mb_enhanced_cover_art_uploads/maximise.test.ts
@@ -110,103 +110,59 @@ describe('maximising Apple Music images', () => {
         });
     });
 
-    it('transform Apple Music images to source URL', async () => {
-        const it = getMaximisedCandidates(new URL('https://is5-ssl.mzstatic.com/image/thumb/Music124/v4/58/34/98/58349857-55bb-62ae-81d4-4a2726e33528/5060786561909.png/999999999x0w-999.png'));
-        const result = await it.next();
-
-        expect(result.done).toBeFalse();
-        expect(result.value!.url.href).toBe('https://a1.mzstatic.com/us/r1000/063/Music124/v4/58/34/98/58349857-55bb-62ae-81d4-4a2726e33528/5060786561909.png');
-    });
+    it('marks /source URLs as likely broken if original name is not "source"', async () => {
+        fakeImu.mockImplementation((url, options) => {
+            options.cb?.([{
+                url: 'https://a1.mzstatic.com/us/r1000/063/Music114/v4/cc/fc/dc/ccfcdc3b-5d9b-6305-8d59-687db6c67fd2/source',
+            }, {
+                url: 'https://a1.mzstatic.com/us/r1000/063/Music114/v4/cc/fc/dc/ccfcdc3b-5d9b-6305-8d59-687db6c67fd2/20UMGIM63158.rgb.jpg',
+            }] as unknown as maxurlResult[]);
+        });
 
-    it('keeps original maximised URL as backup', async () => {
-        const it = getMaximisedCandidates(new URL('https://is3-ssl.mzstatic.com/image/thumb/Music124/v4/73/bd/c8/73bdc8dc-9ab2-ce6e-e581-4bb3d9e559fc/190295474089.webp/999999999x0w-999.png'));
+        const it = getMaximisedCandidates(new URL('https://is4-ssl.mzstatic.com/image/thumb/Music114/v4/cc/fc/dc/ccfcdc3b-5d9b-6305-8d59-687db6c67fd2/20UMGIM63158.rgb.jpg/1000x1000bb.webp'));
         let result = await it.next();
 
         expect(result.done).toBeFalse();
-        expect(result.value!.url.href).toBe('https://a1.mzstatic.com/us/r1000/063/Music124/v4/73/bd/c8/73bdc8dc-9ab2-ce6e-e581-4bb3d9e559fc/190295474089.webp');
+        expect(result.value!.url.href).toEndWith('/source');
+        expect(result.value!.likely_broken).toBeTrue();
 
         result = await it.next();
 
         expect(result.done).toBeFalse();
-        expect(result.value!.url.href).toBe('https://is3-ssl.mzstatic.com/image/thumb/Music124/v4/73/bd/c8/73bdc8dc-9ab2-ce6e-e581-4bb3d9e559fc/190295474089.webp/999999999x0w-999.png');
-
-        result = await it.next();
-
-        expect(result.done).toBeTrue();
+        expect(result.value!.url.href).toEndWith('/20UMGIM63158.rgb.jpg');
+        expect(result.value!.likely_broken).toBeFalsy();
     });
 
-    it('maximises images', async () => {
-        fakeImu.mockImplementationOnce((_url, options) => {
-            options.cb?.([{ url: 'https://is4-ssl.mzstatic.com/image/thumb/Music115/v4/f1/e6/ad/f1e6adf1-fce1-a7fa-2f9c-e37e32738306/075679767103.jpg/999999999x0w-999.png' } as unknown as maxurlResult]);
+    it('does not mark /source URLs as likely broken if original name is "source"', async () => {
+        fakeImu.mockImplementation((url, options) => {
+            options.cb?.([{
+                url: 'https://a1.mzstatic.com/us/r1000/063/Music114/v4/6e/ff/f5/6efff51c-17f2-1d8b-21f3-b8029fa28434/source',
+            }, {
+                url: 'https://is3-ssl.mzstatic.com/image/thumb/Music114/v4/6e/ff/f5/6efff51c-17f2-1d8b-21f3-b8029fa28434/source/999999999x0w-999.png',
+            }] as unknown as maxurlResult[]);
         });
 
-        const it = getMaximisedCandidates(new URL('https://is4-ssl.mzstatic.com/image/thumb/Music115/v4/f1/e6/ad/f1e6adf1-fce1-a7fa-2f9c-e37e32738306/075679767103.jpg/160x160bb-60.jpg'));
-        let result = await it.next();
-
-        expect(result.done).toBeFalse();
-        expect(result.value!.url.href).toBe('https://a1.mzstatic.com/us/r1000/063/Music115/v4/f1/e6/ad/f1e6adf1-fce1-a7fa-2f9c-e37e32738306/075679767103.jpg');
-
-        result = await it.next();
-
-        expect(result.done).toBeFalse();
-        expect(result.value!.url.href).toBe('https://is4-ssl.mzstatic.com/image/thumb/Music115/v4/f1/e6/ad/f1e6adf1-fce1-a7fa-2f9c-e37e32738306/075679767103.jpg/999999999x0w-999.png');
-
-        result = await it.next();
-
-        expect(result.done).toBeTrue();
-        expect(fakeImu).toHaveBeenCalledOnce();
-    });
-
-    it('transforms old-style iTunes URLs into source images', async () => {
-        const it = getMaximisedCandidates(new URL('https://is2-ssl.mzstatic.com/image/thumb/Music114/v4/61/67/d4/6167d478-a353-28be-75fa-0ebbc74808e2/source/999999999x0w-999.png'));
+        const it = getMaximisedCandidates(new URL('https://is3-ssl.mzstatic.com/image/thumb/Music114/v4/6e/ff/f5/6efff51c-17f2-1d8b-21f3-b8029fa28434/source/9999x9999-100.jpg'));
         const result = await it.next();
 
         expect(result.done).toBeFalse();
-        expect(result.value!.url.href).toBe('https://a1.mzstatic.com/us/r1000/063/Music114/v4/61/67/d4/6167d478-a353-28be-75fa-0ebbc74808e2/source');
+        expect(result.value!.url.href).toEndWith('/source');
+        expect(result.value!.likely_broken).toBeFalsy();
     });
 
-    it('does not transform URLs that already point to source image', async () => {
-        const it = getMaximisedCandidates(new URL('https://a1.mzstatic.com/us/r1000/063/Music114/v4/61/67/d4/6167d478-a353-28be-75fa-0ebbc74808e2/source'));
-        let result = await it.next();
-
-        expect(result.done).toBeFalse();
-        expect(result.value!.url.href).toBe('https://a1.mzstatic.com/us/r1000/063/Music114/v4/61/67/d4/6167d478-a353-28be-75fa-0ebbc74808e2/source');
-
-        result = await it.next();
-
-        expect(result.done).toBeTrue();
-    });
-});
+    it('marks /source URL as likely broken if smallurl is already maximised', async () => {
+        fakeImu.mockImplementation((url, options) => {
+            options.cb?.([{
+                url: 'https://a1.mzstatic.com/us/r1000/063/Music111/v4/e1/dc/68/e1dc6808-6d55-1e38-a34d-a3807d488859/source',
+            }] as unknown as maxurlResult[]);
+        });
 
-describe('maximising 7digital images', () => {
-    it('returns 800x800 image first', async () => {
-        const it = getMaximisedCandidates(new URL('https://artwork-cdn.7static.com/static/img/sleeveart/00/143/859/0014385941_350.jpg'));
+        const it = getMaximisedCandidates(new URL('https://a1.mzstatic.com/us/r1000/063/Music111/v4/e1/dc/68/e1dc6808-6d55-1e38-a34d-a3807d488859/191061355977.jpg'));
         const result = await it.next();
 
         expect(result.done).toBeFalse();
-        expect(result.value!.url.href).toBe('https://artwork-cdn.7static.com/static/img/sleeveart/00/143/859/0014385941_800.jpg');
-    });
-
-    it('includes smaller images as backup', async () => {
-        const it = getMaximisedCandidates(new URL('https://artwork-cdn.7static.com/static/img/sleeveart/00/143/859/0014385941_350.jpg'));
-        let result = await it.next();
-
-        expect(result.done).toBeFalse();
-        expect(result.value!.url.pathname).toEndWith('_800.jpg');
-
-        result = await it.next();
-
-        expect(result.done).toBeFalse();
-        expect(result.value!.url.pathname).toEndWith('_500.jpg');
-
-        result = await it.next();
-
-        expect(result.done).toBeFalse();
-        expect(result.value!.url.pathname).toEndWith('_350.jpg');
-
-        result = await it.next();
-
-        expect(result.done).toBeTrue();
+        expect(result.value!.url.href).toEndWith('/source');
+        expect(result.value!.likely_broken).toBeTrue();
     });
 });