diff --git a/README.md b/README.md
index b675146..0e3ca84 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# jellyfin-spicetify
-WIP: A Spicetify extension to integrate your Jellyfin music library into Spotify
+WIP: Spicetify extension to integrate your Jellyfin music library into Spotify
## Downloads
@@ -13,6 +13,7 @@ WIP: A Spicetify extension to integrate your Jellyfin music library into Spotify
- Stream music from Jellyfin instead of Spotify
- Play tracks that exist on Jellyfin but aren't available on Spotify
+- Easily toggle between Jellyfin and Spotify audio
## Known Limitations
diff --git a/src/jellyfin.ts b/src/jellyfin.ts
index 16b5fb5..00d79af 100644
--- a/src/jellyfin.ts
+++ b/src/jellyfin.ts
@@ -55,7 +55,7 @@ export async function tryAutoLogin() {
Spicetify.showNotification("Jellyfin session expired. Please log in again.", true);
} else {
Spicetify.showNotification("Failed to connect to Jellyfin.", true);
- console.error("Jellyfin init error:", error);
+ console.error("[Jellyfin]: init error:", error);
}
}
}
diff --git a/src/main.ts b/src/main.ts
index 9a5ab25..667fd79 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -11,10 +11,9 @@ async function main() {
await new Promise((resolve) => setTimeout(resolve, 100));
}
- // Topbar button for settings
const icon = ``;
-
let hasLoaded = false;
+
new Spicetify.Topbar.Button("Jellyfin", icon, () => {
if (!hasLoaded) {
Spicetify.showNotification("Jellyfin is still loading, please wait...", true);
@@ -36,18 +35,31 @@ async function main() {
player.registerEvents();
search.init();
- new Spicetify.ContextMenu.Item(
- "Toggle Jellyfin",
- () => {},
- (uris) => {
- // Only show context menu on tracks
- if (uris.length === 1 && Spicetify.URI.fromString(uris[0]).type === Spicetify.URI.Type.TRACK) {
- return true;
+ const playerButton = new Spicetify.Playbar.Button(
+ "Toggle Jellyfin Audio",
+ icon,
+ (self) => {
+ if (self.active) {
+ player.hijackActive.set(false);
+ player.audio.pause();
+ Spicetify.Player.setVolume(player.currentVolume);
+ } else {
+ const oldVolume = player.currentVolume;
+ Spicetify.Player.setVolume(0); // Set Spotify audio volume
+ player.hijackActive.set(true);
+ Spicetify.Player.setVolume(oldVolume); // Hijack is active, set Jellyfin audio volume
+
+ player.audio.currentTime = Spicetify.Player.getProgress() / 1000; // Sync position
+ if (Spicetify.Player.isPlaying()) player.audio.play();
}
- return false;
},
- icon as any,
- ).register();
+ !player.canUseJellyfin.get(),
+ player.hijackActive.get(),
+ );
+ player.canUseJellyfin.subscribe((v) => (playerButton.disabled = !v));
+ player.hijackActive.subscribe((v) => (playerButton.active = v));
+ playerButton.register();
+
hasLoaded = true;
}
diff --git a/src/player.ts b/src/player.ts
index c49f611..39aeeb7 100644
--- a/src/player.ts
+++ b/src/player.ts
@@ -1,12 +1,17 @@
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api/search-api";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api/playstate-api";
-import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models";
+import { BaseItemKind, SearchHint } from "@jellyfin/sdk/lib/generated-client/models";
import * as jellyfin from "./jellyfin";
import { settings } from "./settingsStore";
+import { signal } from "./utils";
export const audio = new Audio();
-export let hijackActive = false;
-export let currentVolume = 0.5;
+export const canUseJellyfin = signal(false);
+export let hijackActive = signal(false);
+export let currentVolume = Spicetify.Player.getVolume() || 0.5;
+let currentItemId: string | null = null;
+let oldTime = 0;
+let lastProgressReport = 0;
const BITRATE_MAP: Record = {
high: "320000",
@@ -14,11 +19,11 @@ const BITRATE_MAP: Record = {
low: "128000",
};
-let currentItemId: string | null = null;
-let oldTime = 0;
-let lastProgressReport = 0;
-export function setHijackActive(value: boolean) {
- hijackActive = value;
+export function jellyfinToLocalUri(trackInfo: SearchHint): string {
+ const encode = (s: string) => encodeURIComponent(s ?? "").replace(/%20/g, "+");
+ const durationSecs = trackInfo.RunTimeTicks ? Math.floor(trackInfo.RunTimeTicks / 10000000) : 0;
+
+ return `spotify:local:${encode(trackInfo.Artists?.[0] ?? "Unknown artist")}:${trackInfo.Id}:${encode(trackInfo.Name ?? "Unknown title")}:${durationSecs}`;
}
export async function playTrack(id: string) {
@@ -26,10 +31,10 @@ export async function playTrack(id: string) {
try {
const oldVolume = hijackActive ? currentVolume : Spicetify.Player.getVolume();
- if (!hijackActive) Spicetify.Player.setVolume(0); // Set Spotify audio volume to 0
+ if (!hijackActive.get()) Spicetify.Player.setVolume(0); // Set Spotify audio volume to 0
- hijackActive = true;
- Spicetify.Player.setVolume(oldVolume); // Volume is now hijacked, will now set Jellyfin audio volume and also update the volume slider
+ hijackActive.set(true);
+ Spicetify.Player.setVolume(oldVolume); // Hijack active, set Jellyfin audio volume and also update the volume slider
const params = new URLSearchParams({
api_key: jellyfin.api.accessToken ?? "",
@@ -46,7 +51,7 @@ export async function playTrack(id: string) {
});
audio.src = `${jellyfin.api.basePath}/Audio/${id}/universal?${params}`;
- console.log("[Jellyfin] Attempting to play:", audio.src);
+ console.log("[Jellyfin]: Attempting to play:", audio.src);
await audio.play();
if (settings.reportPlayback) {
@@ -58,9 +63,9 @@ export async function playTrack(id: string) {
});
}
} catch (error) {
- console.error("An error occurred trying to play a track on Jellyfin", error);
+ console.error("[Jellyfin]: An error occurred trying to play a track", error);
Spicetify.showNotification("An error occurred trying to play a track on Jellyfin", true);
- hijackActive = false;
+ hijackActive.set(false);
}
}
@@ -68,6 +73,8 @@ export function registerEvents() {
// Search Jellyfin for song and play that instead if found
Spicetify.Player.addEventListener("songchange", async (event) => {
if (!settings.hijack || !jellyfin.api || !event) return;
+ hijackActive.set(false);
+ canUseJellyfin.set(false);
if (currentItemId) {
getPlaystateApi(jellyfin.api).reportPlaybackStopped({
@@ -87,19 +94,22 @@ export function registerEvents() {
const item = results.data.SearchHints?.[0];
if (!item?.Id) {
- hijackActive = false;
+ hijackActive.set(false);
audio.pause();
Spicetify.Player.setVolume(currentVolume);
return;
}
Spicetify.showNotification("Playing on Jellyfin");
+ canUseJellyfin.set(true);
playTrack(item.Id);
+
+ audio.currentTime = oldTime; // sync up with Spotify, due to loading times
});
// Play/pause Jellyfin audio
Spicetify.Player.addEventListener("onplaypause", async (event) => {
- if (!hijackActive || !jellyfin.api) return;
+ if (!hijackActive.get() || !jellyfin.api) return;
if (event?.data.isPaused) {
audio.pause();
@@ -120,10 +130,10 @@ export function registerEvents() {
// Seeking support
Spicetify.Player.addEventListener("onprogress", async (event) => {
- if (!hijackActive || !jellyfin.api || !event) return;
+ if (!canUseJellyfin.get() || !jellyfin.api || !event) return;
// Only report playback every 10s
- if (settings.reportPlayback && currentItemId && event.data - lastProgressReport > 10000) {
+ if (hijackActive.get() && settings.reportPlayback && currentItemId && event.data - lastProgressReport > 10000) {
getPlaystateApi(jellyfin.api).reportPlaybackProgress({
playbackProgressInfo: {
ItemId: currentItemId,
@@ -154,7 +164,7 @@ export function registerEvents() {
apply(target, thisArg, args) {
currentVolume = args[0];
- if (hijackActive) {
+ if (hijackActive.get()) {
audio.volume = Math.pow(currentVolume, 3) * 0.425;
if (volumeSlider) volumeSlider.style.setProperty("--progress-bar-transform", `${currentVolume * 100}%`);
return;
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..585a309
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,12 @@
+export function signal(initial: T) {
+ let value = initial;
+ const listeners = new Set<(v: T) => void>();
+ return {
+ get: () => value,
+ set: (v: T) => {
+ value = v;
+ listeners.forEach((l) => l(v));
+ },
+ subscribe: (l: (v: T) => void) => listeners.add(l),
+ };
+}