mirror of
https://github.com/trafficlunar/jellyfin-spicetify.git
synced 2026-06-13 19:07:06 +00:00
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
import React from "react";
|
|
import { Api, Jellyfin } from "@jellyfin/sdk";
|
|
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api/search-api";
|
|
import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models";
|
|
import SettingsModal from "./settings";
|
|
|
|
let hijackActive = false;
|
|
|
|
export const jellyfin = new Jellyfin({
|
|
clientInfo: {
|
|
name: "Spicetify",
|
|
version: "1.0.0",
|
|
},
|
|
deviceInfo: {
|
|
name: "Spotify",
|
|
id: "spotify", // TODO: should be unique?
|
|
},
|
|
});
|
|
|
|
export let jellyfinApi: Api | undefined;
|
|
export const setJellyfinApi = (api: Api) => {
|
|
jellyfinApi = api;
|
|
};
|
|
export let jellyfinUser: string | undefined;
|
|
export const setJellyfinUser = (id: string) => {
|
|
jellyfinUser = id;
|
|
};
|
|
|
|
async function main() {
|
|
while (!Spicetify.showNotification) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
|
|
const audio = new Audio();
|
|
|
|
// Topbar button for settings
|
|
new Spicetify.Topbar.Button("Jellyfin", "podcasts", () => {
|
|
Spicetify.PopupModal.display({
|
|
title: "Jellyfin",
|
|
content: React.createElement(SettingsModal) as unknown as Element,
|
|
isLarge: false,
|
|
});
|
|
});
|
|
|
|
// Search Jellyfin for song and play that instead if found
|
|
Spicetify.Player.addEventListener("songchange", async (event) => {
|
|
if (!jellyfinApi) return;
|
|
if (!event) return;
|
|
|
|
const results = await getSearchApi(jellyfinApi).getSearchHints({
|
|
searchTerm: event.data.item.name,
|
|
includeItemTypes: [BaseItemKind.Audio],
|
|
limit: 1,
|
|
});
|
|
|
|
const item = results.data.SearchHints?.[0];
|
|
if (!item?.Id) {
|
|
const oldVolume = Spicetify.Player.getVolume();
|
|
hijackActive = false;
|
|
Spicetify.Platform.PlaybackAPI.setVolume(oldVolume);
|
|
return;
|
|
}
|
|
|
|
Spicetify.showNotification("Playing on Jellyfin");
|
|
|
|
const oldVolume = Spicetify.Player.getVolume();
|
|
Spicetify.Platform.PlaybackAPI.setVolume(0); // Set Spotify audio volume to 0
|
|
|
|
hijackActive = true;
|
|
audio.src = `${jellyfinApi.basePath}/Audio/${item.Id}/universal?api_key=${jellyfinApi.accessToken}&UserId=${jellyfinUser}&Container=opus,webm|opus,mp3,aac,m4a|aac,m4a|alac,m4b|aac,flac,webma,webm|webma,wav,ogg&TranscodingContainer=ts&TranscodingProtocol=hls&AudioCodec=aac&MaxStreamingBitrate=140000000&EnableRedirection=true`;
|
|
await audio.play();
|
|
|
|
Spicetify.Platform.PlaybackAPI.setVolume(oldVolume); // Set Jellyfin audio volume to the actual volume
|
|
});
|
|
|
|
// Play/pause Jellyfin audio
|
|
Spicetify.Player.addEventListener("onplaypause", async (event) => {
|
|
if (!hijackActive) return;
|
|
|
|
if (event?.data.isPaused) {
|
|
audio.pause();
|
|
} else {
|
|
await audio.play();
|
|
}
|
|
});
|
|
|
|
// Seeking support
|
|
let oldTime = 0;
|
|
Spicetify.Player.addEventListener("onprogress", async (event) => {
|
|
if (!hijackActive) return;
|
|
if (!event) return;
|
|
|
|
// onprogress polls every 100ms, small time difference means normal playback
|
|
const timeDiff = Math.abs(event.data - oldTime);
|
|
if (Math.abs(timeDiff - 100) < 100) {
|
|
// Allow 100ms tolerance
|
|
oldTime = event.data;
|
|
return;
|
|
}
|
|
|
|
audio.currentTime = event.data / 1000;
|
|
oldTime = event.data;
|
|
});
|
|
|
|
// Change volume of Jellyfin audio instead of Spotify audio
|
|
const playback = Spicetify.Platform.PlaybackAPI;
|
|
playback.setVolume = new Proxy(playback.setVolume, {
|
|
apply(target, thisArg, args) {
|
|
if (hijackActive) {
|
|
audio.volume = args[0];
|
|
return;
|
|
} else {
|
|
return Reflect.apply(target, thisArg, args);
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
main();
|