Lyrics implementation prototype (#224)
* Lyrics implementation prototype * feat: update lyrics view * chore: add docs * chore: cleanup * feat: animate active text * fix: hide lyrics button when there are none * feat: create lyrics preview in now playing modal * fix: header overlay color Closes #224 Closes #151 Closes #100 --------- Co-authored-by: Lei Nelissen <lei@codified.nl>
This commit is contained in:
committed by
GitHub
parent
a64f52c4f9
commit
c5b1406e16
@@ -1,5 +1,6 @@
|
||||
import { Album, AlbumTrack, SimilarAlbum } from '@/store/music/types';
|
||||
import { fetchApi } from './lib';
|
||||
import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics.ts';
|
||||
|
||||
const albumOptions = {
|
||||
SortBy: 'AlbumArtist,SortName',
|
||||
@@ -39,7 +40,7 @@ const latestAlbumsOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the most recently added albums on the Jellyfin server
|
||||
* Retrieve the most recently added albums on the Jellyfin server
|
||||
*/
|
||||
export async function retrieveRecentAlbums(numberOfAlbums = 24) {
|
||||
// Generate custom config based on function input
|
||||
@@ -64,5 +65,5 @@ export async function retrieveAlbumTracks(ItemId: string) {
|
||||
const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString();
|
||||
|
||||
return fetchApi<{ Items: AlbumTrack[] }>(({ user_id }) => `/Users/${user_id}/Items?${singleAlbumParams}`)
|
||||
.then((data) => data!.Items);
|
||||
}
|
||||
.then((data) => retrieveAndInjectLyricsToTracks(data.Items));
|
||||
}
|
||||
|
||||
48
src/utility/JellyfinApi/lyrics.ts
Normal file
48
src/utility/JellyfinApi/lyrics.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { fetchApi } from './lib';
|
||||
import {AlbumTrack} from '@/store/music/types.ts';
|
||||
|
||||
interface Metadata {
|
||||
Artist: string
|
||||
Album: string
|
||||
Title: string
|
||||
Author: string
|
||||
Length: number
|
||||
By: string
|
||||
Offset: number
|
||||
Creator: string
|
||||
Version: string
|
||||
IsSynced: boolean
|
||||
}
|
||||
|
||||
interface LyricData {
|
||||
Text: string
|
||||
Start: number
|
||||
}
|
||||
|
||||
export interface Lyrics {
|
||||
Metadata: Metadata
|
||||
Lyrics: LyricData[]
|
||||
}
|
||||
|
||||
async function retrieveTrackLyrics(trackId: string): Promise<Lyrics | null> {
|
||||
return fetchApi<Lyrics>(`/Audio/${trackId}/Lyrics`)
|
||||
.catch((e) => {
|
||||
console.error('Error on fetching track lyrics: ', e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function retrieveAndInjectLyricsToTracks(tracks: AlbumTrack[]): Promise<AlbumTrack[]> {
|
||||
return Promise.all(tracks.map(async (track) => {
|
||||
if (!track.HasLyrics) {
|
||||
track.Lyrics = null;
|
||||
return track;
|
||||
}
|
||||
|
||||
track.Lyrics = await retrieveTrackLyrics(track.Id);
|
||||
|
||||
return track;
|
||||
|
||||
}));
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AlbumTrack, Playlist } from '@/store/music/types';
|
||||
import { asyncFetchStore, fetchApi } from './lib';
|
||||
import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics.ts';
|
||||
|
||||
const playlistOptions = {
|
||||
SortBy: 'SortName',
|
||||
@@ -17,7 +18,7 @@ const playlistOptions = {
|
||||
*/
|
||||
export async function retrieveAllPlaylists() {
|
||||
const playlistParams = new URLSearchParams(playlistOptions).toString();
|
||||
|
||||
|
||||
return fetchApi<{ Items: Playlist[] }>(({ user_id }) => `/Users/${user_id}/Items?${playlistParams}`)
|
||||
.then((d) => d!.Items);
|
||||
}
|
||||
@@ -34,5 +35,5 @@ export async function retrievePlaylistTracks(ItemId: string) {
|
||||
const singlePlaylistParams = new URLSearchParams(singlePlaylistOptions).toString();
|
||||
|
||||
return fetchApi<{ Items: AlbumTrack[] }>(`/Playlists/${ItemId}/Items?${singlePlaylistParams}`)
|
||||
.then((d) => d!.Items);
|
||||
}
|
||||
.then((d) => retrieveAndInjectLyricsToTracks(d.Items));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Platform } from 'react-native';
|
||||
import { Track } from 'react-native-track-player';
|
||||
import { fetchApi, getImage } from './lib';
|
||||
import store from '@/store';
|
||||
import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics';
|
||||
|
||||
const trackOptionsOsOverrides: Record<typeof Platform.OS, Record<string, string>> = {
|
||||
ios: {
|
||||
@@ -60,6 +61,8 @@ export function generateTrack(track: AlbumTrack): Track {
|
||||
artwork: track.AlbumId
|
||||
? getImage(track.AlbumId)
|
||||
: getImage(track.Id),
|
||||
hasLyrics: track.HasLyrics,
|
||||
lyrics: track.Lyrics,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,5 +80,5 @@ const trackParams = {
|
||||
*/
|
||||
export async function retrieveAllTracks() {
|
||||
return fetchApi<{ Items: AlbumTrack[] }>(({ user_id }) => `/Users/${user_id}/Items?${trackParams}`)
|
||||
.then((d) => d!.Items);
|
||||
.then((d) => retrieveAndInjectLyricsToTracks(d.Items));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import TrackPlayer, { Event, Track, useTrackPlayerEvents } from 'react-native-track-player';
|
||||
import { useTypedSelector } from '@/store';
|
||||
import { AlbumTrack } from '@/store/music/types';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import TrackPlayer, { Event, useTrackPlayerEvents, Track } from 'react-native-track-player';
|
||||
|
||||
interface CurrentTrackResponse {
|
||||
track: Track | undefined;
|
||||
albumTrack: AlbumTrack | undefined;
|
||||
index: number | undefined;
|
||||
}
|
||||
|
||||
@@ -13,12 +16,20 @@ export default function useCurrentTrack(): CurrentTrackResponse {
|
||||
const [track, setTrack] = useState<Track | undefined>();
|
||||
const [index, setIndex] = useState<number | undefined>();
|
||||
|
||||
// Retrieve entities from the store
|
||||
const entities = useTypedSelector((state) => state.music.tracks.entities);
|
||||
|
||||
// Attempt to extract the track from the store
|
||||
const albumTrack = useMemo(() => (
|
||||
entities[track?.backendId]
|
||||
), [track?.backendId, entities]);
|
||||
|
||||
// Retrieve the current track from the queue using the index
|
||||
const retrieveCurrentTrack = useCallback(async () => {
|
||||
const queue = await TrackPlayer.getQueue();
|
||||
const currentTrackIndex = await TrackPlayer.getCurrentTrack();
|
||||
if (currentTrackIndex !== null) {
|
||||
setTrack(queue[currentTrackIndex]);
|
||||
setTrack(queue[currentTrackIndex] as Track);
|
||||
setIndex(currentTrackIndex);
|
||||
} else {
|
||||
setTrack(undefined);
|
||||
@@ -28,7 +39,7 @@ export default function useCurrentTrack(): CurrentTrackResponse {
|
||||
|
||||
// Then execute the function on component mount and track changes
|
||||
useEffect(() => { retrieveCurrentTrack(); }, [retrieveCurrentTrack]);
|
||||
useTrackPlayerEvents([ Event.PlaybackTrackChanged, Event.PlaybackState ], retrieveCurrentTrack);
|
||||
|
||||
return { track, index };
|
||||
}
|
||||
useTrackPlayerEvents([ Event.PlaybackActiveTrackChanged, Event.PlaybackState ], retrieveCurrentTrack);
|
||||
|
||||
return { track, index, albumTrack };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user