Basic download implementation
This commit is contained in:
@@ -34,14 +34,7 @@ const baseTrackOptions: Record<string, string> = {
|
||||
*/
|
||||
export function generateTrack(track: AlbumTrack, credentials: Credentials): Track {
|
||||
// Also construct the URL for the stream
|
||||
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/${track.Id}/universal?${trackParams}`);
|
||||
const url = generateTrackUrl(track.Id, credentials);
|
||||
|
||||
return {
|
||||
url,
|
||||
@@ -55,6 +48,23 @@ export function generateTrack(track: AlbumTrack, credentials: Credentials): Trac
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
|
||||
15
src/utility/formatBytes.ts
Normal file
15
src/utility/formatBytes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
/**
|
||||
* Convert a number of bytes to a human-readable string
|
||||
* CREDIT: https://gist.github.com/zentala/1e6f72438796d74531803cc3833c039c
|
||||
*/
|
||||
export default function formatBytes(bytes: number, decimals: number = 2) {
|
||||
if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import { useTypedSelector } from 'store';
|
||||
import { generateTrack } from './JellyfinApi';
|
||||
import useQueue from './useQueue';
|
||||
|
||||
/**
|
||||
* A hook that generates a callback that can setup and start playing a
|
||||
* particular trackId in the player.
|
||||
*/
|
||||
export default function usePlayTrack() {
|
||||
const credentials = useTypedSelector(state => state.settings.jellyfin);
|
||||
const tracks = useTypedSelector(state => state.music.tracks.entities);
|
||||
const queue = useQueue();
|
||||
|
||||
return useCallback(async function playTrack(trackId: string, play: boolean = true, addToEnd: boolean = true) {
|
||||
// Get the relevant track
|
||||
const track = tracks[trackId];
|
||||
|
||||
// GUARD: Check if the track actually exists in the store
|
||||
if (!track) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate the new track for the queue
|
||||
const newTrack = generateTrack(track, credentials);
|
||||
|
||||
// Then, we'll need to check where to add the track
|
||||
if (addToEnd) {
|
||||
await TrackPlayer.add([ newTrack ]);
|
||||
|
||||
// Then we'll skip to it and play it
|
||||
if (play) {
|
||||
await TrackPlayer.skip(await (await TrackPlayer.getQueue()).length);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
} else {
|
||||
// Try and locate the current track
|
||||
const currentTrackIndex = await TrackPlayer.getCurrentTrack();
|
||||
|
||||
// Since the argument is the id to insert the track BEFORE, we need
|
||||
// to get the current track + 1
|
||||
const targetTrack = currentTrackIndex >= 0 && queue.length > 1
|
||||
? queue[currentTrackIndex + 1].id
|
||||
: undefined;
|
||||
|
||||
// Depending on whether this track exists, we either add it there,
|
||||
// or at the end of the queue.
|
||||
await TrackPlayer.add([ newTrack ], targetTrack);
|
||||
|
||||
if (play) {
|
||||
await TrackPlayer.skip(currentTrackIndex + 1);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
return newTrack;
|
||||
}, [credentials, tracks, queue]);
|
||||
}
|
||||
@@ -5,6 +5,18 @@ import { generateTrack } from './JellyfinApi';
|
||||
import { EntityId } from '@reduxjs/toolkit';
|
||||
import { shuffle as shuffleArray } from 'lodash';
|
||||
|
||||
interface PlayOptions {
|
||||
play: boolean;
|
||||
shuffle: boolean;
|
||||
method: 'add-to-end' | 'add-after-currently-playing' | 'replace';
|
||||
}
|
||||
|
||||
const defaults: PlayOptions = {
|
||||
play: true,
|
||||
shuffle: false,
|
||||
method: 'replace',
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a callback function that starts playing a full album given its
|
||||
* supplied id.
|
||||
@@ -12,36 +24,92 @@ import { shuffle as shuffleArray } from 'lodash';
|
||||
export default function usePlayTracks() {
|
||||
const credentials = useTypedSelector(state => state.settings.jellyfin);
|
||||
const tracks = useTypedSelector(state => state.music.tracks.entities);
|
||||
const downloads = useTypedSelector(state => state.downloads.entities);
|
||||
|
||||
return useCallback(async function playTracks(
|
||||
trackIds: EntityId[] | undefined,
|
||||
play: boolean = true,
|
||||
shuffle: boolean = false,
|
||||
options: Partial<PlayOptions> = {},
|
||||
): Promise<Track[] | undefined> {
|
||||
if (!trackIds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve options and queue
|
||||
const {
|
||||
play,
|
||||
shuffle,
|
||||
method,
|
||||
} = Object.assign({}, defaults, options);
|
||||
const queue = await TrackPlayer.getQueue();
|
||||
|
||||
// Convert all trackIds to the relevant format for react-native-track-player
|
||||
const newTracks = trackIds.map((trackId) => {
|
||||
const generatedTracks = trackIds.map((trackId) => {
|
||||
const track = tracks[trackId];
|
||||
|
||||
// GUARD: Check that the track actually exists in Redux
|
||||
if (!trackId || !track) {
|
||||
return;
|
||||
}
|
||||
|
||||
return generateTrack(track, credentials);
|
||||
// Retrieve the generated track from Jellyfin
|
||||
const generatedTrack = generateTrack(track, credentials);
|
||||
|
||||
// Check if a downloaded version exists, and if so rewrite the URL
|
||||
const download = downloads[trackId];
|
||||
if (download?.location) {
|
||||
generatedTrack.url = download.location;
|
||||
}
|
||||
|
||||
return generatedTrack;
|
||||
}).filter((t): t is Track => typeof t !== 'undefined');
|
||||
|
||||
// Clear the queue and add all tracks
|
||||
await TrackPlayer.reset();
|
||||
await TrackPlayer.add(shuffle ? shuffleArray(newTracks) : newTracks);
|
||||
// Potentially shuffle all tracks
|
||||
const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks;
|
||||
|
||||
// Play the queue
|
||||
if (play) {
|
||||
await TrackPlayer.play();
|
||||
// Then, we'll need to check where to add the track
|
||||
switch(method) {
|
||||
case 'add-to-end': {
|
||||
await TrackPlayer.add(newTracks);
|
||||
|
||||
// Then we'll skip to it and play it
|
||||
if (play) {
|
||||
await TrackPlayer.skip((await TrackPlayer.getQueue()).length - newTracks.length);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'add-after-currently-playing': {
|
||||
// Try and locate the current track
|
||||
const currentTrackIndex = await TrackPlayer.getCurrentTrack();
|
||||
|
||||
// Since the argument is the id to insert the track BEFORE, we need
|
||||
// to get the current track + 1
|
||||
const targetTrack = currentTrackIndex >= 0 && queue.length > 1
|
||||
? queue[currentTrackIndex + 1].id
|
||||
: undefined;
|
||||
|
||||
// Depending on whether this track exists, we either add it there,
|
||||
// or at the end of the queue.
|
||||
await TrackPlayer.add(newTracks, targetTrack);
|
||||
|
||||
if (play) {
|
||||
await TrackPlayer.skip(currentTrackIndex + 1);
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'replace': {
|
||||
await TrackPlayer.reset();
|
||||
await TrackPlayer.add(newTracks);
|
||||
|
||||
if (play) {
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return newTracks;
|
||||
}, [credentials, tracks]);
|
||||
}, [credentials, downloads, tracks]);
|
||||
}
|
||||
Reference in New Issue
Block a user