From c8b70892fb569330ebf50f6fe1f8bf37a4840387 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 3 Jun 2024 14:09:27 +0200 Subject: [PATCH 1/4] Improve apple music API types --- frontend/js/src/utils/musickit.d.ts | 58 ++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/frontend/js/src/utils/musickit.d.ts b/frontend/js/src/utils/musickit.d.ts index 83dd823420..d7d8b578dd 100644 --- a/frontend/js/src/utils/musickit.d.ts +++ b/frontend/js/src/utils/musickit.d.ts @@ -14,6 +14,58 @@ declare namespace MusicKit { */ function getInstance(): MusicKitInstance; + interface Song { + attributes: { + albumName: string; + artistName: string; + artwork: { + bgColor: string; + height: number; + textColor1: string; + textColor2: string; + textColor3: string; + textColor4: string; + url: string; + width: number; + }; + composerName: string; + discNumber: number; + durationInMillis: number; + genreNames: string[]; + hasCredits: boolean; + hasLyrics: boolean; + isAppleDigitalMaster: boolean; + isrc: string; + name: string; + playParams: { + id: string; + kind: string; + }; + previews: [ + { + url: string; + } + ]; + releaseDate: string; + trackNumber: number; + url: string; + }; + href: string; + id: string; + type: string; + } + + interface APISearchResult { + meta: {}; + results: { + songs: { + data: Song[]; + href: string; + next: string; + }; + }; + } + /** * This class represents the Apple Music API. */ @@ -26,7 +78,11 @@ declare namespace MusicKit { * @param options An object with additional options to control how requests are made * directly to the Apple Music API. */ - music(path: string, parameters?: any, options?: any): Promise; + music( + path: string, + parameters?: any, + options?: any + ): Promise<{ data: APISearchResult }>; } /** From 712885a5e4cd6b05aff3f24449b63f3abd4b3136 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 3 Jun 2024 14:58:35 +0200 Subject: [PATCH 2/4] Remove album name from search terms for apple music search. We get worse results because the album name can muddle the search for track name and we can't specify album vs artist vs album --- frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx b/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx index 62d9319278..eca351e359 100644 --- a/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx +++ b/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx @@ -178,8 +178,9 @@ export default class AppleMusicPlayer const { onTrackNotFound } = this.props; const trackName = getTrackName(listen); const artistName = getArtistName(listen); - const releaseName = _get(listen, "track_metadata.release_name"); - const searchTerm = `${trackName} ${artistName} ${releaseName}`; + // Album name can give worse results, re;oving it from search terms + // const releaseName = _get(listen, "track_metadata.release_name"); + const searchTerm = `${trackName} ${artistName}`; if (!searchTerm) { onTrackNotFound(); return; From 2509c0f083f7ad0e10823392b9732cb465385917 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Mon, 3 Jun 2024 15:00:12 +0200 Subject: [PATCH 3/4] Add fuzzy match to apple music search First check the first result's track name according to apple music API, if it matches then we're done. If it does not match, do a fuzzy search for the best match within the search results we have and use that --- .../common/brainzplayer/AppleMusicPlayer.tsx | 25 ++++++++++++++++--- package-lock.json | 6 +++++ package.json | 1 + 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx b/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx index eca351e359..0f04fc7fdd 100644 --- a/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx +++ b/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx @@ -1,7 +1,8 @@ import * as React from "react"; -import { get as _get, isString } from "lodash"; +import { get as _get, escapeRegExp, isString } from "lodash"; import { faApple } from "@fortawesome/free-brands-svg-icons"; import { Link } from "react-router-dom"; +import fuzzysort from "fuzzysort"; import { getArtistName, getTrackName, @@ -190,11 +191,27 @@ export default class AppleMusicPlayer `/v1/catalog/{{storefrontId}}/search`, { term: searchTerm, types: "songs" } ); - const apple_music_id = response?.data?.results?.songs?.data?.[0]?.id; - if (apple_music_id) { - await this.playAppleMusicId(apple_music_id); + const candidateMatches = response?.data?.results?.songs?.data; + // Check if the first API result is a match + if ( + new RegExp(escapeRegExp(`${trackName}`), "ig").test( + candidateMatches?.[0]?.attributes.name + ) + ) { + // First result matches track title, assume it's the correct result + await this.playAppleMusicId(candidateMatches[0].id); return; } + // Fallback to best fuzzy match based on track title + const fruzzyMatches = fuzzysort.go(trackName, candidateMatches, { + key: "attributes.name", + limit: 1, + }); + if (fruzzyMatches[0]) { + await this.playAppleMusicId(fruzzyMatches[0].obj.id); + return; + } + // No good match, onTrackNotFound will be called in the code block below } catch (error) { console.debug("Apple Music API request failed:", error); } diff --git a/package-lock.json b/package-lock.json index 90ddd68ae9..022e6894e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "external-svg-loader": "^1.7.1", "fetch-retry": "^5.0.3", "file-saver": "^2.0.5", + "fuzzysort": "^3.0.1", "he": "^1.2.0", "highlight.js": "^11.6.0", "html2canvas": "^1.4.1", @@ -9391,6 +9392,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuzzysort": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.0.1.tgz", + "integrity": "sha512-jpiSrv+j2GwtI13z3vzdBAD5m7iVH23spSZwJrD657dfr6jUV2Jyc5YuSPKirmTp9OqnJbHudPIBGIId1bCWmA==" + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/package.json b/package.json index d22065c87a..32179538a2 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "external-svg-loader": "^1.7.1", "fetch-retry": "^5.0.3", "file-saver": "^2.0.5", + "fuzzysort": "^3.0.1", "he": "^1.2.0", "highlight.js": "^11.6.0", "html2canvas": "^1.4.1", From 4b3c3fa2b8f9e6cedbd4fed206a0f9a4262c3917 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 13 Jun 2024 12:46:50 +0200 Subject: [PATCH 4/4] AppleMusic fuzzy search: remove accents for matching --- .../common/brainzplayer/AppleMusicPlayer.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx b/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx index 0f04fc7fdd..257c93cc61 100644 --- a/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx +++ b/frontend/js/src/common/brainzplayer/AppleMusicPlayer.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { get as _get, escapeRegExp, isString } from "lodash"; +import { get as _get, deburr, escapeRegExp, isString } from "lodash"; import { faApple } from "@fortawesome/free-brands-svg-icons"; import { Link } from "react-router-dom"; import fuzzysort from "fuzzysort"; @@ -191,10 +191,20 @@ export default class AppleMusicPlayer `/v1/catalog/{{storefrontId}}/search`, { term: searchTerm, types: "songs" } ); - const candidateMatches = response?.data?.results?.songs?.data; + // Remove accents from both the search term and the API results + const trackNameWithoutAccents = deburr(trackName); + const candidateMatches = response?.data?.results?.songs?.data.map( + (candidate) => ({ + ...candidate, + attributes: { + ...candidate.attributes, + name: deburr(candidate.attributes.name), + }, + }) + ); // Check if the first API result is a match if ( - new RegExp(escapeRegExp(`${trackName}`), "ig").test( + new RegExp(escapeRegExp(trackNameWithoutAccents), "igu").test( candidateMatches?.[0]?.attributes.name ) ) { @@ -203,10 +213,14 @@ export default class AppleMusicPlayer return; } // Fallback to best fuzzy match based on track title - const fruzzyMatches = fuzzysort.go(trackName, candidateMatches, { - key: "attributes.name", - limit: 1, - }); + const fruzzyMatches = fuzzysort.go( + trackNameWithoutAccents, + candidateMatches, + { + key: "attributes.name", + limit: 1, + } + ); if (fruzzyMatches[0]) { await this.playAppleMusicId(fruzzyMatches[0].obj.id); return;