309 lines
9.8 KiB
TypeScript
309 lines
9.8 KiB
TypeScript
import TrackPlayer, { RepeatMode, State, Track } from 'react-native-track-player';
|
|
import { AppState, useTypedSelector } from '@/store';
|
|
import { Album, AlbumTrack, SimilarAlbum } from '@/store/music/types';
|
|
|
|
type Credentials = AppState['settings']['jellyfin'];
|
|
|
|
/**
|
|
* This is a convenience function that converts a set of Jellyfin credentials
|
|
* from the Redux store to a HTTP Header that authenticates the user against the
|
|
* Jellyfin server.
|
|
*/
|
|
function generateConfig(credentials: Credentials): RequestInit {
|
|
return {
|
|
headers: {
|
|
'X-Emby-Authorization': `MediaBrowser Client="", Device="", DeviceId="", Version="", Token="${credentials?.access_token}"`
|
|
}
|
|
};
|
|
}
|
|
|
|
const baseTrackOptions: Record<string, string> = {
|
|
// This must be set to support client seeking
|
|
TranscodingProtocol: 'http',
|
|
TranscodingContainer: 'aac',
|
|
Container: 'mp3,aac,m4a,m4b|aac,flac,alac,m4a,m4b|alac',
|
|
static: 'true',
|
|
};
|
|
|
|
/**
|
|
* Generate a track object from a Jellyfin ItemId so that
|
|
* react-native-track-player can easily consume it.
|
|
*/
|
|
export function generateTrack(track: AlbumTrack, credentials: Credentials): Track {
|
|
// Also construct the URL for the stream
|
|
const url = generateTrackUrl(track.Id, credentials);
|
|
|
|
return {
|
|
url,
|
|
backendId: track.Id,
|
|
title: track.Name,
|
|
artist: track.Artists.join(', '),
|
|
album: track.Album,
|
|
duration: track.RunTimeTicks,
|
|
artwork: track.AlbumId
|
|
? getImage(track.AlbumId, credentials)
|
|
: getImage(track.Id, credentials),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate the track streaming url from the trackId
|
|
*/
|
|
export function generateTrackUrl(trackId: string, credentials: Credentials) {
|
|
const trackOptions = {
|
|
...baseTrackOptions,
|
|
UserId: credentials?.user_id || '',
|
|
api_key: credentials?.access_token || '',
|
|
DeviceId: credentials?.device_id || '',
|
|
};
|
|
|
|
const trackParams = new URLSearchParams(trackOptions).toString();
|
|
const url = encodeURI(`${credentials?.uri}/Audio/${trackId}/universal?`) + trackParams;
|
|
|
|
return url;
|
|
}
|
|
|
|
const albumOptions = {
|
|
SortBy: 'AlbumArtist,SortName',
|
|
SortOrder: 'Ascending',
|
|
IncludeItemTypes: 'MusicAlbum',
|
|
Recursive: 'true',
|
|
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
|
ImageTypeLimit: '1',
|
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
};
|
|
|
|
const albumParams = new URLSearchParams(albumOptions).toString();
|
|
|
|
/**
|
|
* Retrieve all albums that are available on the Jellyfin server
|
|
*/
|
|
export async function retrieveAllAlbums(credentials: Credentials) {
|
|
const config = generateConfig(credentials);
|
|
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${albumParams}`, config)
|
|
.then(response => response.json());
|
|
|
|
return albums.Items;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a single album
|
|
*/
|
|
export async function retrieveAlbum(credentials: Credentials, id: string): Promise<Album> {
|
|
const config = generateConfig(credentials);
|
|
|
|
const Similar = await fetch(`${credentials?.uri}/Items/${id}/Similar?userId=${credentials?.user_id}&limit=12`, config)
|
|
.then(response => response.json() as Promise<{ Items: SimilarAlbum[] }>)
|
|
.then((albums) => albums.Items.map((a) => a.Id));
|
|
|
|
return fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/${id}`, config)
|
|
.then(response => response.json() as Promise<Album>)
|
|
.then(album => ({ ...album, Similar }));
|
|
}
|
|
|
|
const latestAlbumsOptions = {
|
|
IncludeItemTypes: 'MusicAlbum',
|
|
Fields: 'DateCreated',
|
|
SortOrder: 'Ascending',
|
|
};
|
|
|
|
/**
|
|
* Retrieve the most recently added albums on the Jellyfin server
|
|
*/
|
|
export async function retrieveRecentAlbums(credentials: Credentials, numberOfAlbums = 24) {
|
|
const config = generateConfig(credentials);
|
|
|
|
// Generate custom config based on function input
|
|
const options = {
|
|
...latestAlbumsOptions,
|
|
Limit: numberOfAlbums.toString(),
|
|
};
|
|
const params = new URLSearchParams(options).toString();
|
|
|
|
// Retrieve albums
|
|
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/Latest?${params}`, config)
|
|
.then(response => response.json());
|
|
|
|
return albums;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a single album from the Emby server
|
|
*/
|
|
export async function retrieveAlbumTracks(ItemId: string, credentials: Credentials) {
|
|
const singleAlbumOptions = {
|
|
ParentId: ItemId,
|
|
SortBy: 'SortName',
|
|
};
|
|
const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString();
|
|
|
|
const config = generateConfig(credentials);
|
|
const album = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${singleAlbumParams}`, config)
|
|
.then(response => response.json());
|
|
|
|
return album.Items;
|
|
}
|
|
|
|
/**
|
|
* Retrieve an image URL for a given ItemId
|
|
*/
|
|
export function getImage(ItemId: string, credentials: Credentials): string {
|
|
return encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`);
|
|
}
|
|
|
|
/**
|
|
* Create a hook that can convert ItemIds to image URLs
|
|
*/
|
|
export function useGetImage() {
|
|
const credentials = useTypedSelector((state) => state.settings.jellyfin);
|
|
return (ItemId: string) => getImage(ItemId, credentials);
|
|
}
|
|
|
|
const trackParams = {
|
|
SortBy: 'AlbumArtist,SortName',
|
|
SortOrder: 'Ascending',
|
|
IncludeItemTypes: 'Audio',
|
|
Recursive: 'true',
|
|
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
|
};
|
|
|
|
/**
|
|
* Retrieve all possible tracks that can be found in Jellyfin
|
|
*/
|
|
export async function retrieveAllTracks(credentials: Credentials) {
|
|
const config = generateConfig(credentials);
|
|
const tracks = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${trackParams}`, config)
|
|
.then(response => response.json());
|
|
|
|
return tracks.Items;
|
|
}
|
|
|
|
const searchParams = {
|
|
IncludeItemTypes: 'Audio,MusicAlbum',
|
|
SortBy: 'Album,SortName',
|
|
SortOrder: 'Ascending',
|
|
Recursive: 'true',
|
|
};
|
|
|
|
/**
|
|
* Remotely search the Jellyfin library for a particular search term
|
|
*/
|
|
export async function searchItem(
|
|
credentials: Credentials,
|
|
term: string, limit = 24
|
|
): Promise<(Album | AlbumTrack)[]> {
|
|
const config = generateConfig(credentials);
|
|
|
|
const params = new URLSearchParams({
|
|
...searchParams,
|
|
SearchTerm: term,
|
|
Limit: limit.toString(),
|
|
}).toString();
|
|
|
|
const results = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${params}`, config)
|
|
.then(response => response.json());
|
|
|
|
return results.Items;
|
|
}
|
|
|
|
const playlistOptions = {
|
|
SortBy: 'SortName',
|
|
SortOrder: 'Ascending',
|
|
IncludeItemTypes: 'Playlist',
|
|
Recursive: 'true',
|
|
Fields: 'PrimaryImageAspectRatio,SortName,BasicSyncInfo,DateCreated',
|
|
ImageTypeLimit: '1',
|
|
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb',
|
|
MediaTypes: 'Audio',
|
|
};
|
|
|
|
/**
|
|
* Retrieve all albums that are available on the Jellyfin server
|
|
*/
|
|
export async function retrieveAllPlaylists(credentials: Credentials) {
|
|
const config = generateConfig(credentials);
|
|
const playlistParams = new URLSearchParams(playlistOptions).toString();
|
|
|
|
const albums = await fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items?${playlistParams}`, config)
|
|
.then(response => response.json());
|
|
|
|
return albums.Items;
|
|
}
|
|
|
|
/**
|
|
* Retrieve all albums that are available on the Jellyfin server
|
|
*/
|
|
export async function retrievePlaylistTracks(ItemId: string, credentials: Credentials) {
|
|
const singlePlaylistOptions = {
|
|
SortBy: 'SortName',
|
|
UserId: credentials?.user_id || '',
|
|
};
|
|
const singlePlaylistParams = new URLSearchParams(singlePlaylistOptions).toString();
|
|
|
|
const config = generateConfig(credentials);
|
|
const playlists = await fetch(`${credentials?.uri}/Playlists/${ItemId}/Items?${singlePlaylistParams}`, config)
|
|
.then(response => response.json());
|
|
|
|
return playlists.Items;
|
|
}
|
|
|
|
/**
|
|
* This maps the react-native-track-player RepeatMode to a RepeatMode that is
|
|
* expected by Jellyfin when reporting playback events.
|
|
*/
|
|
const RepeatModeMap: Record<RepeatMode, string> = {
|
|
[RepeatMode.Off]: 'RepeatNone',
|
|
[RepeatMode.Track]: 'RepeatOne',
|
|
[RepeatMode.Queue]: 'RepeatAll',
|
|
};
|
|
|
|
/**
|
|
* This will generate the payload that is required for playback events and send
|
|
* it to the supplied path.
|
|
*/
|
|
export async function sendPlaybackEvent(path: string, credentials: Credentials, trackIndex?: number) {
|
|
// Extract all data from react-native-track-player
|
|
const [
|
|
currentTrack, position, repeatMode, volume, queue, state,
|
|
] = await Promise.all([
|
|
TrackPlayer.getCurrentTrack(),
|
|
TrackPlayer.getPosition(),
|
|
TrackPlayer.getRepeatMode(),
|
|
TrackPlayer.getVolume(),
|
|
TrackPlayer.getQueue(),
|
|
TrackPlayer.getState(),
|
|
]);
|
|
|
|
// Switch between overriden track index and current track
|
|
const track = trackIndex !== undefined ? trackIndex : currentTrack;
|
|
|
|
// Generate a payload from the gathered data
|
|
const payload = {
|
|
VolumeLevel: volume * 100,
|
|
IsMuted: false,
|
|
IsPaused: state === State.Paused,
|
|
RepeatMode: RepeatModeMap[repeatMode],
|
|
ShuffleMode: 'Sorted',
|
|
PositionTicks: Math.round(position * 10_000_000),
|
|
PlaybackRate: 1,
|
|
PlayMethod: 'transcode',
|
|
MediaSourceId: track !== null ? queue[track].backendId : null,
|
|
ItemId: track !== null ? queue[track].backendId : null,
|
|
CanSeek: true,
|
|
PlaybackStartTimeTicks: null,
|
|
};
|
|
|
|
// Generate a config from the credentials and dispatch the request
|
|
const config = generateConfig(credentials);
|
|
await fetch(`${credentials?.uri}${path}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...config.headers,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(payload),
|
|
// Swallow and errors from the request
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
} |