fix: fuzzy search tracks

This commit is contained in:
trafficlunar 2026-03-12 22:15:26 +00:00
parent 6196e95e30
commit f8664bfea8
4 changed files with 45 additions and 21 deletions

View file

@ -6,6 +6,7 @@
"name": "jellyfin-spicetify", "name": "jellyfin-spicetify",
"dependencies": { "dependencies": {
"@jellyfin/sdk": "^0.13.0", "@jellyfin/sdk": "^0.13.0",
"fuse.js": "^7.1.0",
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^25.3.3", "@types/node": "^25.3.3",
@ -111,6 +112,8 @@
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],

View file

@ -17,6 +17,7 @@
"esbuild-css-modules-plugin": "^3.1.5" "esbuild-css-modules-plugin": "^3.1.5"
}, },
"dependencies": { "dependencies": {
"@jellyfin/sdk": "^0.13.0" "@jellyfin/sdk": "^0.13.0",
"fuse.js": "^7.1.0"
} }
} }

View file

@ -40,9 +40,7 @@ async function main() {
icon, icon,
(self) => { (self) => {
if (self.active) { if (self.active) {
player.hijackActive.set(false); player.stop();
player.audio.pause();
Spicetify.Player.setVolume(player.currentVolume);
} else { } else {
const oldVolume = player.currentVolume; const oldVolume = player.currentVolume;
Spicetify.Player.setVolume(0); // Set Spotify audio volume Spicetify.Player.setVolume(0); // Set Spotify audio volume

View file

@ -1,6 +1,9 @@
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api/search-api"; import { getSearchApi } from "@jellyfin/sdk/lib/utils/api/search-api";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api/playstate-api"; import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api/playstate-api";
import { BaseItemKind, SearchHint } from "@jellyfin/sdk/lib/generated-client/models"; import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models";
import Fuse from "fuse.js";
import * as jellyfin from "./jellyfin"; import * as jellyfin from "./jellyfin";
import { settings } from "./settingsStore"; import { settings } from "./settingsStore";
import { signal } from "./utils"; import { signal } from "./utils";
@ -19,11 +22,11 @@ const BITRATE_MAP: Record<string, string> = {
low: "128000", low: "128000",
}; };
export function jellyfinToLocalUri(trackInfo: SearchHint): string { // Stop Jellfin audio
const encode = (s: string) => encodeURIComponent(s ?? "").replace(/%20/g, "+"); export function stop() {
const durationSecs = trackInfo.RunTimeTicks ? Math.floor(trackInfo.RunTimeTicks / 10000000) : 0; hijackActive.set(false);
audio.pause();
return `spotify:local:${encode(trackInfo.Artists?.[0] ?? "Unknown artist")}:${trackInfo.Id}:${encode(trackInfo.Name ?? "Unknown title")}:${durationSecs}`; Spicetify.Player.setVolume(currentVolume);
} }
export async function playTrack(id: string) { export async function playTrack(id: string) {
@ -87,23 +90,40 @@ export function registerEvents() {
currentItemId = null; currentItemId = null;
} }
const results = await getSearchApi(jellyfin.api).getSearchHints({ const trackName = event.data.item.name;
searchTerm: event.data.item.name, const searchResults = await getSearchApi(jellyfin.api).getSearchHints({
searchTerm: trackName,
includeItemTypes: [BaseItemKind.Audio], includeItemTypes: [BaseItemKind.Audio],
limit: 1, limit: 32,
}); });
const item = results.data.SearchHints?.[0]; if (!searchResults.data.SearchHints || searchResults.data.SearchHints.length === 0) {
if (!item?.Id) { stop();
hijackActive.set(false); return;
audio.pause(); }
Spicetify.Player.setVolume(currentVolume);
// Fuzzy search
const artists = event.data.item.artists?.map((a) => a.name).join(" ") ?? "";
const list = searchResults.data.SearchHints.map((v) => ({
id: v.Id ?? "",
name: v.Name ?? "Unknown title",
artists: (v.Artists ?? ["Unknown artist"]).join(" "),
}));
const results = new Fuse(list, {
keys: ["name", "artists"],
threshold: 0.5,
}).search(`${trackName} ${artists}`);
const track = results[0]?.item;
if (!track) {
stop();
return; return;
} }
Spicetify.showNotification("Playing on Jellyfin"); Spicetify.showNotification("Playing on Jellyfin");
canUseJellyfin.set(true); canUseJellyfin.set(true);
playTrack(item.Id); playTrack(track.id);
audio.currentTime = oldTime; // sync up with Spotify, due to loading times audio.currentTime = oldTime; // sync up with Spotify, due to loading times
}); });
@ -147,12 +167,14 @@ export function registerEvents() {
// onprogress polls every 100ms, small time difference means normal playback // onprogress polls every 100ms, small time difference means normal playback
const timeDiff = Math.abs(event.data - oldTime); const timeDiff = Math.abs(event.data - oldTime);
if (Math.abs(timeDiff - 100) < 100) { if (Math.abs(timeDiff - 100) < 200) {
// Allow 100ms tolerance // Allow 200ms tolerance
oldTime = event.data; oldTime = event.data;
return; return;
} }
console.log(event.data, oldTime);
audio.currentTime = event.data / 1000; audio.currentTime = event.data / 1000;
oldTime = event.data; oldTime = event.data;
}); });