Repository: ROpdebee/mb-userscripts (ts)

Issue:
Apple Music: Fetch original file format
IMU maximises all Apple Music/iTunes covers to a PNG version. However, the source image might not be a PNG. We should add an exception to the maximisation so that we don't unnecessarily consume more bandwidth and storage for a "lossless" version of a lossy source.

For Apple Music, we should be able to determine whether the source image is a JPEG through its URL, see https://beta.musicbrainz.org/edit/81901201. However, old iTunes-style cover art URLs don't contain that filename. The only way to still get those links, AFAIK, is through the old iTunes API. However, a-tisket still uses this old iTunes API, so seeding covers from there will always lead to a PNG unless we skip maximising altogether, which doesn't seem like the right solution. I also doubt we can determine the correct Apple Music URL from an iTunes cover URL. Instead, we should let the a-tisket seeder seed the Apple Music/iTunes release URL which can get processed by the Apple Music provider to determine the Apple Music cover art URL, which then has the original filename through which we can identify the original file format.

All of that is possible, but we should be very careful with _cached_ a-tisket pages. If we seed the release URL itself, the provider will grab the current cover art, even though the cached page may display old cover art which was replaced. So for cached pages, we should fall back to seeding the original cover art URL.

For any direct iTunes cover art links where we cannot or should not use the Apple Music release page to find the Apple Music cover art URL, we should maybe use JPEG images by default instead of PNG.

Pull request description:
feat(ecau): grab Apple Music original source images
Closes #80 again.

Fast-tracked this because it's been bothering me for quite a while.

Patch:
diff --git a/src/mb_enhanced_cover_art_uploads/docs/supported_providers.md b/src/mb_enhanced_cover_art_uploads/docs/supported_providers.md
index 93e824c5..8a7077f5 100644
--- a/src/mb_enhanced_cover_art_uploads/docs/supported_providers.md
+++ b/src/mb_enhanced_cover_art_uploads/docs/supported_providers.md
@@ -9,7 +9,7 @@ The following table describes the types of links supported by MB: Upload to CAA
 | AllMusic | ✔️ | ❌ |
 | Amazon | ✔️ | Partial | Supports digital and physical music and audiobook products. Not all Amazon image types can be mapped to CAA cover art types. |
 | Amazon Music | ✔️ | ✔️ | Converted into main Amazon MP3/Streaming product links, these tend to give larger images. |
-| Apple Music/iTunes | ✔️ | ✔️ | Maximised images use format of the source image, where possible. See issue [#80](https://github.com/ROpdebee/mb-userscripts/issues/80) and PR [#142](https://github.com/ROpdebee/mb-userscripts/pull/142). |
+| Apple Music/iTunes | ✔️ | ✔️ | Maximised to the original source image. See issue [#80](https://github.com/ROpdebee/mb-userscripts/issues/80). |
 | Bandcamp | ✔️ | ✔️ | Grabs custom track images. For non-square covers, a square thumbnail will be added too. Custom domains currently not supported. |
 | Beatport | ✔️ | ✔️ | Beware of upscales. 1400x1400 on older releases is probably upscaled. |
 | Deezer | ✔️ | ✔️ |
diff --git a/src/mb_enhanced_cover_art_uploads/maximise.ts b/src/mb_enhanced_cover_art_uploads/maximise.ts
index c79b4601..7550978e 100644
--- a/src/mb_enhanced_cover_art_uploads/maximise.ts
+++ b/src/mb_enhanced_cover_art_uploads/maximise.ts
@@ -91,27 +91,38 @@ IMU_EXCEPTIONS.set('img.discogs.com', async (smallurl) => {
 // 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. When the original image is a JPEG,
-    // we want to fetch a JPEG version. Although the PNG is of slightly better
-    // quality due to generational loss when a JPEG is re-encoded, the quality
-    // loss is so minor that the additional costs of downloading, uploading,
-    // and storing the PNG are unjustifiable. See #80.
+    // original source image was PNG or JPEG. We can grab the source image
+    // instead. See #80.
 
     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
-        // We're still conservative though, if it's not a JPEG, we won't
-        // return the JPEG version
-        const pathParts = imgGeneric.url.pathname.split('/');
-        if (/\.jpe?g$/i.test(pathParts[pathParts.length - 2])) {
+        // 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('/');
+        }
+
+        if (sourceUrl.href !== imgGeneric.url.href) {
             results.push({
                 ...imgGeneric,
-                url: new URL(imgGeneric.url.href.replace(/\.png$/i, '.jpg'))
+                url: sourceUrl,
             });
         }
 
-        // Always return the original maximised URL as a backup
+        // Always return the original maximised URL as a backup, e.g. for when
+        // the source image is webp.
         results.push(imgGeneric);
     }
 


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 938c7a86..7ac5ecac 100644
--- a/tests/unit/mb_enhanced_cover_art_uploads/maximise.test.ts
+++ b/tests/unit/mb_enhanced_cover_art_uploads/maximise.test.ts
@@ -109,47 +109,23 @@ describe('maximising Apple Music images', () => {
         });
     });
 
-    it('retains PNG format if original is PNG', async () => {
+    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'));
-        let result = await it.next();
-
-        expect(result.done).toBeFalse();
-        expect(result.value.url.href).toBe('https://is5-ssl.mzstatic.com/image/thumb/Music124/v4/58/34/98/58349857-55bb-62ae-81d4-4a2726e33528/5060786561909.png/999999999x0w-999.png');
-
-        result = await it.next();
-
-        expect(result.done).toBeTrue();
-    });
-
-    it('uses JPEG format if original is JPEG', async () => {
-        const it = getMaximisedCandidates(new URL('https://is3-ssl.mzstatic.com/image/thumb/Music124/v4/73/bd/c8/73bdc8dc-9ab2-ce6e-e581-4bb3d9e559fc/190295474089.jpg/999999999x0w-999.png'));
         const 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.jpg/999999999x0w-999.jpg');
+        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('uses PNG format as backup for JPEG', async () => {
-        const it = getMaximisedCandidates(new URL('https://is3-ssl.mzstatic.com/image/thumb/Music124/v4/73/bd/c8/73bdc8dc-9ab2-ce6e-e581-4bb3d9e559fc/190295474089.jpg/999999999x0w-999.png'));
+    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'));
         let 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.jpg/999999999x0w-999.jpg');
+        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');
 
         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.jpg/999999999x0w-999.png');
-
-        result = await it.next();
-
-        expect(result.done).toBeTrue();
-    });
-
-    it('keeps original maximised URL if format is unknown', 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'));
-        let 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');
 
@@ -167,7 +143,7 @@ describe('maximising Apple Music images', () => {
         let 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.jpg');
+        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();
 
@@ -180,12 +156,20 @@ describe('maximising Apple Music images', () => {
         expect(fakeImu).toHaveBeenCalledOnce();
     });
 
-    it('retains PNG for old-style iTunes URLs', async () => {
+    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 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');
+    });
+
+    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://is2-ssl.mzstatic.com/image/thumb/Music114/v4/61/67/d4/6167d478-a353-28be-75fa-0ebbc74808e2/source/999999999x0w-999.png');
+        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();