mirror of
https://github.com/trafficlunar/jellyfin-spicetify.git
synced 2026-06-13 19:07:06 +00:00
feat: report playback to jellyfin server
This commit is contained in:
parent
765761693c
commit
c3b231ebb5
3 changed files with 79 additions and 19 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api/search-api";
|
import { getSearchApi } from "@jellyfin/sdk/lib/utils/api/search-api";
|
||||||
import { BaseItemKind, SearchHint } from "@jellyfin/sdk/lib/generated-client/models";
|
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api/playstate-api";
|
||||||
|
import { BaseItemKind } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import * as jellyfin from "./jellyfin";
|
import * as jellyfin from "./jellyfin";
|
||||||
import { settings } from "./settingsStore";
|
import { settings } from "./settingsStore";
|
||||||
|
|
||||||
|
|
@ -20,7 +21,13 @@ const BITRATE_MAP: Record<string, string> = {
|
||||||
low: "128000",
|
low: "128000",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let currentItemId: string | null = null;
|
||||||
|
let oldTime = 0;
|
||||||
|
let lastProgressReport = 0;
|
||||||
|
|
||||||
export async function playTrack(id: string) {
|
export async function playTrack(id: string) {
|
||||||
|
if (!jellyfin.api) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const oldVolume = Spicetify.Player.getVolume();
|
const oldVolume = Spicetify.Player.getVolume();
|
||||||
Spicetify.Player.setVolume(0); // Set Spotify audio volume to 0
|
Spicetify.Player.setVolume(0); // Set Spotify audio volume to 0
|
||||||
|
|
@ -29,22 +36,31 @@ export async function playTrack(id: string) {
|
||||||
Spicetify.Player.setVolume(oldVolume); // Volume is now hijacked, will now set Jellyfin audio volume and also update the volume slider
|
Spicetify.Player.setVolume(oldVolume); // Volume is now hijacked, will now set Jellyfin audio volume and also update the volume slider
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
api_key: jellyfin.api?.accessToken ?? "",
|
api_key: jellyfin.api.accessToken ?? "",
|
||||||
UserId: jellyfin.user ?? "",
|
userId: jellyfin.user ?? "",
|
||||||
Container: "flac,aac,mp3",
|
container: "flac,aac,mp3",
|
||||||
EnableRedirection: "true",
|
enableRedirection: "true",
|
||||||
...(settings.quality === "source" && {
|
...(settings.quality !== "source" && {
|
||||||
Container: "mp3",
|
container: "mp3",
|
||||||
AudioCodec: "mp3",
|
audioCodec: "mp3",
|
||||||
TranscodingContainer: "mp3",
|
transcodingContainer: "mp3",
|
||||||
TranscodingProtocol: "http",
|
transcodingProtocol: "http",
|
||||||
MaxStreamingBitrate: BITRATE_MAP[settings.quality],
|
maxStreamingBitrate: BITRATE_MAP[settings.quality],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
audio.src = `${jellyfin.api?.basePath}/Audio/${id}/universal?${params}`;
|
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();
|
await audio.play();
|
||||||
|
|
||||||
|
if (settings.reportPlayback) {
|
||||||
|
currentItemId = id;
|
||||||
|
getPlaystateApi(jellyfin.api).reportPlaybackStart({
|
||||||
|
playbackStartInfo: {
|
||||||
|
ItemId: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("An error occurred trying to play a track on Jellyfin", error);
|
console.error("An error occurred trying to play a track on Jellyfin", error);
|
||||||
Spicetify.showNotification("An error occurred trying to play a track on Jellyfin", true);
|
Spicetify.showNotification("An error occurred trying to play a track on Jellyfin", true);
|
||||||
|
|
@ -55,9 +71,17 @@ export async function playTrack(id: string) {
|
||||||
export function registerEvents() {
|
export function registerEvents() {
|
||||||
// Search Jellyfin for song and play that instead if found
|
// Search Jellyfin for song and play that instead if found
|
||||||
Spicetify.Player.addEventListener("songchange", async (event) => {
|
Spicetify.Player.addEventListener("songchange", async (event) => {
|
||||||
if (!settings.hijack) return;
|
if (!settings.hijack || !jellyfin.api || !event) return;
|
||||||
if (!jellyfin.api) return;
|
|
||||||
if (!event) return;
|
if (currentItemId) {
|
||||||
|
getPlaystateApi(jellyfin.api).reportPlaybackStopped({
|
||||||
|
playbackStopInfo: {
|
||||||
|
ItemId: currentItemId,
|
||||||
|
PositionTicks: Math.floor(audio.currentTime * 10_000_000),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
currentItemId = null;
|
||||||
|
}
|
||||||
|
|
||||||
const results = await getSearchApi(jellyfin.api).getSearchHints({
|
const results = await getSearchApi(jellyfin.api).getSearchHints({
|
||||||
searchTerm: event.data.item.name,
|
searchTerm: event.data.item.name,
|
||||||
|
|
@ -79,20 +103,40 @@ export function registerEvents() {
|
||||||
|
|
||||||
// Play/pause Jellyfin audio
|
// Play/pause Jellyfin audio
|
||||||
Spicetify.Player.addEventListener("onplaypause", async (event) => {
|
Spicetify.Player.addEventListener("onplaypause", async (event) => {
|
||||||
if (!hijackActive) return;
|
if (!hijackActive || !jellyfin.api) return;
|
||||||
|
|
||||||
if (event?.data.isPaused) {
|
if (event?.data.isPaused) {
|
||||||
audio.pause();
|
audio.pause();
|
||||||
} else {
|
} else {
|
||||||
await audio.play();
|
await audio.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (settings.reportPlayback && currentItemId) {
|
||||||
|
getPlaystateApi(jellyfin.api).reportPlaybackProgress({
|
||||||
|
playbackProgressInfo: {
|
||||||
|
ItemId: currentItemId,
|
||||||
|
PositionTicks: Math.floor(audio.currentTime * 10_000_000),
|
||||||
|
IsPaused: audio.paused,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seeking support
|
// Seeking support
|
||||||
let oldTime = 0;
|
|
||||||
Spicetify.Player.addEventListener("onprogress", async (event) => {
|
Spicetify.Player.addEventListener("onprogress", async (event) => {
|
||||||
if (!hijackActive) return;
|
if (!hijackActive || !jellyfin.api || !event) return;
|
||||||
if (!event) return;
|
|
||||||
|
// Only report playback every 10s
|
||||||
|
if (settings.reportPlayback && currentItemId && event.data - lastProgressReport > 10000) {
|
||||||
|
getPlaystateApi(jellyfin.api).reportPlaybackProgress({
|
||||||
|
playbackProgressInfo: {
|
||||||
|
ItemId: currentItemId,
|
||||||
|
PositionTicks: Math.floor(audio.currentTime * 10_000_000),
|
||||||
|
IsPaused: audio.paused,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
lastProgressReport = event.data;
|
||||||
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,20 @@ export default function SettingsView({ setView }: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.setting}>
|
||||||
|
<div className={styles.settingInfo}>
|
||||||
|
<h2>Report Playback</h2>
|
||||||
|
<p>Enable to report playback to your Jellyfin server</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={settings.reportPlayback}
|
||||||
|
onChange={(e) => setSettings((p) => ({ ...p, reportPlayback: e.target.checked }))}
|
||||||
|
className={styles.switch}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr className={styles.hr} />
|
<hr className={styles.hr} />
|
||||||
<button onClick={logout} className={styles.button}>
|
<button onClick={logout} className={styles.button}>
|
||||||
Log out
|
Log out
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ export interface Settings {
|
||||||
quality: "source" | "high" | "medium" | "low";
|
quality: "source" | "high" | "medium" | "low";
|
||||||
hijack: boolean;
|
hijack: boolean;
|
||||||
nonSpotifySongs: boolean;
|
nonSpotifySongs: boolean;
|
||||||
|
reportPlayback: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let settings: Settings = {
|
export let settings: Settings = {
|
||||||
quality: "source",
|
quality: "source",
|
||||||
hijack: true,
|
hijack: true,
|
||||||
nonSpotifySongs: true,
|
nonSpotifySongs: true,
|
||||||
|
reportPlayback: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setSettings(value: Settings) {
|
export function setSettings(value: Settings) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue