reporting playback to jellyfin server for lastfm scrobbling

This commit is contained in:
Violet Caulfield
2024-12-29 07:01:10 -06:00
parent a64c8f3918
commit 5093a67f2c
9 changed files with 84 additions and 13 deletions

View File

@@ -55,6 +55,7 @@ export function Card(props: CardProps) {
{props.blurhash && !imageLoaded && (
<Blurhash
blurhash={props.blurhash}
{...dimensions}
/>
)}

View File

@@ -10,8 +10,8 @@ import { usePlayerContext } from "../../../player/provider";
export default function RecentlyPlayed(): React.JSX.Element {
const { resetQueue: clearQueue, addToQueue } = usePlayerContext();
const { apiClient } = useApiClientContext();
const { addToQueue, resetQueue } = usePlayerContext();
const { apiClient, sessionId } = useApiClientContext();
const { recentTracks } = useHomeContext();
useEffect(() => {
@@ -37,8 +37,8 @@ export default function RecentlyPlayed(): React.JSX.Element {
itemId={recentlyPlayedTrack.AlbumId!}
marginRight={20}
onPress={async () => {
await clearQueue();
await addToQueue([mapDtoToTrack(apiClient!, recentlyPlayedTrack)])
await resetQueue(false);
await addToQueue([mapDtoToTrack(apiClient!, sessionId, recentlyPlayedTrack)])
play();
}}
>

View File

@@ -6,7 +6,6 @@ import { usePlayerContext } from "../../player/provider";
import { BottomTabNavigationEventMap, BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
import { NavigationHelpers, ParamListBase } from "@react-navigation/native";
import { BlurView } from "@react-native-community/blur";
import { pause, play, skipToNext } from "react-native-track-player/lib/src/trackPlayer";
import Icon from "../Global/icon";
import { Text } from "../Global/text";
import { Colors } from "../../enums/colors";
@@ -15,10 +14,10 @@ export function Miniplayer({ navigation }: { navigation : NavigationHelpers<Para
const playbackState = usePlaybackState();
const theme = useTheme();
const activeTrack = useActiveTrack() as JellifyTrack | undefined;
const { play, pause } = usePlayerContext();
return (
<BlurView>
<XStack height={"$8"} onPress={() => navigation.navigate("Player")}>
@@ -45,7 +44,6 @@ export function Miniplayer({ navigation }: { navigation : NavigationHelpers<Para
<Icon
large
name="fast-forward"
onPress={() => skipToNext()}
/>
</XStack>
</XStack>

View File

@@ -2,16 +2,17 @@ import { Api } from '@jellyfin/sdk';
import React, { createContext, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react';
import { useApi } from '../api/queries';
import _ from 'lodash';
import { QueryObserverResult, RefetchOptions } from '@tanstack/react-query';
import { storage } from '../constants/storage';
import { MMKVStorageKeys } from '../enums/mmkv-storage-keys';
import { JellifyServer } from '../types/JellifyServer';
import { JellifyLibrary } from '../types/JellifyLibrary';
import { JellifyUser } from '../types/JellifyUser';
import { uuid } from "uuidv4";
interface JellyfinApiClientContext {
apiClient: Api | undefined;
apiPending: boolean;
sessionId: string;
server: JellifyServer | undefined;
setServer: React.Dispatch<SetStateAction<JellifyServer | undefined>>;
user: JellifyUser | undefined;
@@ -27,6 +28,7 @@ const JellyfinApiClientContextInitializer = () => {
const serverJson = storage.getString(MMKVStorageKeys.Server);
const libraryJson = storage.getString(MMKVStorageKeys.Library);
const [sessionId, setSessionId] = useState<string>(uuid())
const [user, setUser] = useState<JellifyUser | undefined>(userJson ? (JSON.parse(userJson) as JellifyUser) : undefined);
const [server, setServer] = useState<JellifyServer | undefined>(serverJson ? (JSON.parse(serverJson) as JellifyServer) : undefined);
const [library, setLibrary] = useState<JellifyLibrary | undefined>(libraryJson ? (JSON.parse(libraryJson) as JellifyLibrary) : undefined);
@@ -96,6 +98,7 @@ const JellyfinApiClientContextInitializer = () => {
return {
apiClient,
apiPending,
sessionId,
server,
setServer,
user,
@@ -110,6 +113,7 @@ export const JellyfinApiClientContext =
createContext<JellyfinApiClientContext>({
apiClient: undefined,
apiPending: true,
sessionId: "",
server: undefined,
setServer: () => {},
user: undefined,
@@ -125,6 +129,7 @@ export const JellyfinApiClientProvider: ({ children }: {
const {
apiClient,
apiPending,
sessionId,
server,
setServer,
user,
@@ -140,6 +145,7 @@ export const JellyfinApiClientProvider: ({ children }: {
<JellyfinApiClientContext.Provider value={{
apiClient,
apiPending,
sessionId,
server,
setServer,
user,

View File

@@ -10,7 +10,7 @@ const container = "opus,mp3,aac,m4a,flac,webma,webm,wav,ogg,mpa,wma";
// TODO: Make this configurable
const transcodingContainer = "m4a";
export function mapDtoToTrack(api: Api, item: BaseItemDto, queuingType?: QueuingType) {
export function mapDtoToTrack(api: Api, sessionId: string, item: BaseItemDto, queuingType?: QueuingType) {
const urlParams = {
"Container": container,
@@ -20,6 +20,7 @@ export function mapDtoToTrack(api: Api, item: BaseItemDto, queuingType?: Queuing
"EnableRedirection": true,
"api_key": api.accessToken,
"StartTimeTicks": 0,
"PlaySessionId": sessionId,
}
return {
@@ -32,6 +33,8 @@ export function mapDtoToTrack(api: Api, item: BaseItemDto, queuingType?: Queuing
album: item.Album,
artist: item.Artists?.join(", "),
duration: item.RunTimeTicks,
ItemId: item.Id!,
QueuingType: queuingType ?? QueuingType.DirectlyQueued
} as JellifyTrack
}

20
package-lock.json generated
View File

@@ -42,7 +42,8 @@
"react-native-track-player": "^4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.2.0",
"tamagui": "^1.115.5"
"tamagui": "^1.115.5",
"uuidv4": "^6.2.13"
},
"devDependencies": {
"@babel/core": "^7.20.0",
@@ -9148,6 +9149,12 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"license": "MIT"
},
"node_modules/@types/yargs": {
"version": "17.0.33",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
@@ -22278,6 +22285,17 @@
"uuid": "dist/bin/uuid"
}
},
"node_modules/uuidv4": {
"version": "6.2.13",
"resolved": "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz",
"integrity": "sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"license": "MIT",
"dependencies": {
"@types/uuid": "8.3.4",
"uuid": "8.3.2"
}
},
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",

View File

@@ -44,7 +44,8 @@
"react-native-track-player": "^4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.2.0",
"tamagui": "^1.115.5"
"tamagui": "^1.115.5",
"uuidv4": "^6.2.13"
},
"devDependencies": {
"@babel/core": "^7.20.0",

View File

@@ -3,9 +3,11 @@ import { JellifyTrack } from "../types/JellifyTrack";
import { storage } from "../constants/storage";
import { MMKVStorageKeys } from "../enums/mmkv-storage-keys";
import { findPlayQueueIndexStart } from "./helpers";
import { add, reset, setupPlayer } from "react-native-track-player/lib/src/trackPlayer";
import { add, reset, play as rntpPlay, pause as rntpPause, skipToNext, skipToPrevious, setupPlayer, getActiveTrack } from "react-native-track-player/lib/src/trackPlayer";
import _ from "lodash";
import { buildNewQueue } from "./helpers/queue";
import { useApiClientContext } from "../components/jellyfin-api-provider";
import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api";
interface PlayerContext {
showPlayer: boolean;
@@ -13,6 +15,8 @@ interface PlayerContext {
showMiniplayer: boolean;
setShowMiniplayer: React.Dispatch<SetStateAction<boolean>>;
queue: JellifyTrack[];
play: () => Promise<void>,
pause: () => Promise<void>,
resetQueue: (hideMiniplayer : boolean | undefined) => Promise<void>;
addToQueue: (tracks: JellifyTrack[]) => Promise<void>;
setPlayerState: React.Dispatch<SetStateAction<null>>;
@@ -21,10 +25,15 @@ interface PlayerContext {
const PlayerContextInitializer = () => {
const queueJson = storage.getString(MMKVStorageKeys.PlayQueue);
const { apiClient, sessionId } = useApiClientContext();
const playStateApi = getPlaystateApi(apiClient!)
//#region State
const [showPlayer, setShowPlayer] = useState<boolean>(false);
const [showMiniplayer, setShowMiniplayer] = useState<boolean>(false);
const [queue, setQueue] = useState<JellifyTrack[]>(queueJson ? JSON.parse(queueJson) : []);
//#endregion State
//#region RNTP Setup
setupPlayer().then(() => console.debug("Player setup successfully"));
@@ -32,6 +41,31 @@ const PlayerContextInitializer = () => {
const [playerState, setPlayerState] = useState(null);
//#endregion RNTP Setup
//#region Functions
const play = async () => {
rntpPlay();
const activeTrack = await getActiveTrack() as JellifyTrack;
playStateApi.reportPlaybackStart({
playbackStartInfo: {
SessionId: sessionId,
ItemId: activeTrack.ItemId
}
})
}
const pause = async () => {
rntpPause();
const activeTrack = await getActiveTrack() as JellifyTrack;
playStateApi.reportPlaybackStopped({
playbackStopInfo: {
SessionId: sessionId,
ItemId: activeTrack.ItemId
}
})
}
const resetQueue = async (hideMiniplayer: boolean | undefined) => {
console.debug("Clearing queue")
await reset();
@@ -50,6 +84,7 @@ const PlayerContextInitializer = () => {
setShowMiniplayer(true);
}
//#endregion Functions
return {
showPlayer,
@@ -57,6 +92,8 @@ const PlayerContextInitializer = () => {
showMiniplayer,
setShowMiniplayer,
queue,
play,
pause,
addToQueue,
resetQueue,
setPlayerState,
@@ -69,6 +106,8 @@ export const PlayerContext = createContext<PlayerContext>({
showMiniplayer: false,
setShowMiniplayer: () => {},
queue: [],
play: async () => {},
pause: async () => {},
resetQueue: async () => {},
addToQueue: async ([]) => {},
setPlayerState: () => {},
@@ -81,6 +120,8 @@ export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JS
showMiniplayer,
setShowMiniplayer,
queue,
play,
pause,
resetQueue,
addToQueue,
setPlayerState,
@@ -92,6 +133,8 @@ export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JS
showMiniplayer,
setShowMiniplayer,
queue,
play,
pause,
resetQueue,
addToQueue,
setPlayerState,

View File

@@ -20,6 +20,7 @@ export interface JellifyTrack extends Track {
rating?: RatingType | undefined;
isLiveStream?: boolean | undefined;
ItemId: string | undefined;
Year?: number | null | undefined;
IndexNumber?: number | null | undefined;
ParentIndexNumber?: number | null | undefined;