feat: add extra metadata to the album view
This commit is contained in:
@@ -25,3 +25,9 @@ export const SubHeader = styled(Text)`
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Paragraph = styled(Text)`
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
`;
|
||||||
@@ -59,5 +59,6 @@
|
|||||||
"you-are-offline-message": "You are currently offline. You can only play previously downloaded music.",
|
"you-are-offline-message": "You are currently offline. You can only play previously downloaded music.",
|
||||||
"playing-on": "Playing on",
|
"playing-on": "Playing on",
|
||||||
"local-playback": "Local playback",
|
"local-playback": "Local playback",
|
||||||
"streaming": "Streaming"
|
"streaming": "Streaming",
|
||||||
|
"total-duration": "Total duration"
|
||||||
}
|
}
|
||||||
@@ -59,5 +59,6 @@
|
|||||||
"you-are-offline-message": "Je bent op dit moment offline. Je kunt alleen eerder gedownloade nummers afspelen.",
|
"you-are-offline-message": "Je bent op dit moment offline. Je kunt alleen eerder gedownloade nummers afspelen.",
|
||||||
"playing-on": "Speelt af op",
|
"playing-on": "Speelt af op",
|
||||||
"local-playback": "Lokaal afspelen",
|
"local-playback": "Lokaal afspelen",
|
||||||
"streaming": "Streamen"
|
"streaming": "Streamen",
|
||||||
|
"total-duration": "Totale duur"
|
||||||
}
|
}
|
||||||
@@ -58,3 +58,4 @@ export type LocaleKeys = 'play-next'
|
|||||||
| 'playing-on'
|
| 'playing-on'
|
||||||
| 'local-playback'
|
| 'local-playback'
|
||||||
| 'streaming'
|
| 'streaming'
|
||||||
|
| 'total-duration'
|
||||||
@@ -1,15 +1,56 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useRoute, RouteProp } from '@react-navigation/native';
|
import { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
|
||||||
import { useAppDispatch, useTypedSelector } from 'store';
|
import { useAppDispatch, useTypedSelector } from 'store';
|
||||||
import TrackListView from './components/TrackListView';
|
import TrackListView from './components/TrackListView';
|
||||||
import { fetchTracksByAlbum } from 'store/music/actions';
|
import { fetchAlbum, fetchTracksByAlbum } from 'store/music/actions';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS';
|
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from 'CONSTANTS';
|
||||||
import { t } from '@localisation';
|
import { t } from '@localisation';
|
||||||
import { StackParams } from 'screens/types';
|
import { NavigationProp, StackParams } from 'screens/types';
|
||||||
|
import { SubHeader, Text } from 'components/Typography';
|
||||||
|
import { ScrollView } from 'react-native-gesture-handler';
|
||||||
|
import { useGetImage } from 'utility/JellyfinApi';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import FastImage from 'react-native-fast-image';
|
||||||
|
import { Dimensions, Pressable, useColorScheme } from 'react-native';
|
||||||
|
import { Container } from '@shopify/react-native-skia/lib/typescript/src/renderer/Container';
|
||||||
|
import AlbumImage from './components/AlbumImage';
|
||||||
|
|
||||||
type Route = RouteProp<StackParams, 'Album'>;
|
type Route = RouteProp<StackParams, 'Album'>;
|
||||||
|
|
||||||
|
const Screen = Dimensions.get('screen');
|
||||||
|
|
||||||
|
const Cover = styled(AlbumImage)`
|
||||||
|
height: ${Screen.width / 2.8};
|
||||||
|
width: ${Screen.width / 2.8};
|
||||||
|
border-radius: 12px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function SimilarAlbum({ id }: { id: string }) {
|
||||||
|
const navigation = useNavigation<NavigationProp>();
|
||||||
|
const getImage = useGetImage();
|
||||||
|
const album = useTypedSelector((state) => state.music.albums.entities[id]);
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
album && navigation.push('Album', { id, album });
|
||||||
|
}, [id, album, navigation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
style={({ pressed }) => ({
|
||||||
|
opacity: pressed ? 0.5 : 1.0,
|
||||||
|
width: Screen.width / 2.8,
|
||||||
|
marginRight: 12
|
||||||
|
})}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
<Cover key={id} source={{ uri: getImage(id) }} />
|
||||||
|
<Text numberOfLines={1} style={{ fontSize: 13 }}>{album?.Name}</Text>
|
||||||
|
<Text numberOfLines={1} style={{ opacity: 0.5, fontSize: 13 }}>{album?.Artists.join(', ')}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const Album: React.FC = () => {
|
const Album: React.FC = () => {
|
||||||
const { params: { id } } = useRoute<Route>();
|
const { params: { id } } = useRoute<Route>();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -19,7 +60,10 @@ const Album: React.FC = () => {
|
|||||||
const albumTracks = useTypedSelector((state) => state.music.tracks.byAlbum[id]);
|
const albumTracks = useTypedSelector((state) => state.music.tracks.byAlbum[id]);
|
||||||
|
|
||||||
// Define a function for refreshing this entity
|
// Define a function for refreshing this entity
|
||||||
const refresh = useCallback(() => { dispatch(fetchTracksByAlbum(id)); }, [id, dispatch]);
|
const refresh = useCallback(() => {
|
||||||
|
dispatch(fetchTracksByAlbum(id));
|
||||||
|
dispatch(fetchAlbum(id));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
// Auto-fetch the track data periodically
|
// Auto-fetch the track data periodically
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -39,7 +83,21 @@ const Album: React.FC = () => {
|
|||||||
shuffleButtonText={t('shuffle-album')}
|
shuffleButtonText={t('shuffle-album')}
|
||||||
downloadButtonText={t('download-album')}
|
downloadButtonText={t('download-album')}
|
||||||
deleteButtonText={t('delete-album')}
|
deleteButtonText={t('delete-album')}
|
||||||
/>
|
>
|
||||||
|
{album?.Overview && (
|
||||||
|
<Text style={{ opacity: 0.5, lineHeight: 20, fontSize: 12, paddingBottom: 24 }}>{album?.Overview}</Text>
|
||||||
|
)}
|
||||||
|
{album?.Similar && (
|
||||||
|
<>
|
||||||
|
<SubHeader>Similar albums</SubHeader>
|
||||||
|
<ScrollView horizontal style={{ marginLeft: -24, marginTop: 8, marginBottom: 36 }} contentContainerStyle={{ paddingLeft: 24 }} showsHorizontalScrollIndicator={false}>
|
||||||
|
{album.Similar.map((id) => (
|
||||||
|
<SimilarAlbum id={id} key={id} />
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</TrackListView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function AlbumImage(props: FastImageProps) {
|
|||||||
|
|
||||||
if (!props.source || hasError) {
|
if (!props.source || hasError) {
|
||||||
return (
|
return (
|
||||||
<Container source={colorScheme === 'light' ? require('assets/images/empty-album-light.png') : require('assets/images/empty-album-dark.png')} />
|
<Container {...props} source={colorScheme === 'light' ? require('assets/images/empty-album-light.png') : require('assets/images/empty-album-dark.png')} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { PropsWithChildren, useCallback, useMemo } from 'react';
|
||||||
import { ScrollView, RefreshControl, StyleSheet, View } from 'react-native';
|
import { ScrollView, RefreshControl, StyleSheet, View } from 'react-native';
|
||||||
import { useGetImage } from 'utility/JellyfinApi';
|
import { useGetImage } from 'utility/JellyfinApi';
|
||||||
import styled, { css } from 'styled-components/native';
|
import styled, { css } from 'styled-components/native';
|
||||||
@@ -26,6 +26,7 @@ import { Text } from 'components/Typography';
|
|||||||
import CoverImage from 'components/CoverImage';
|
import CoverImage from 'components/CoverImage';
|
||||||
import ticksToDuration from 'utility/ticksToDuration';
|
import ticksToDuration from 'utility/ticksToDuration';
|
||||||
import { useNavigatorPadding } from 'utility/SafeNavigatorView';
|
import { useNavigatorPadding } from 'utility/SafeNavigatorView';
|
||||||
|
import { t } from '@localisation';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
index: {
|
index: {
|
||||||
@@ -55,7 +56,7 @@ const TrackContainer = styled.View<{ isPlaying: boolean }>`
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface TrackListViewProps {
|
export interface TrackListViewProps extends PropsWithChildren<{}> {
|
||||||
title?: string;
|
title?: string;
|
||||||
artist?: string;
|
artist?: string;
|
||||||
trackIds: EntityId[];
|
trackIds: EntityId[];
|
||||||
@@ -81,6 +82,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
deleteButtonText,
|
deleteButtonText,
|
||||||
listNumberingStyle = 'album',
|
listNumberingStyle = 'album',
|
||||||
itemDisplayStyle = 'album',
|
itemDisplayStyle = 'album',
|
||||||
|
children
|
||||||
}) => {
|
}) => {
|
||||||
const defaultStyles = useDefaultStyles();
|
const defaultStyles = useDefaultStyles();
|
||||||
const navigatorPadding = useNavigatorPadding();
|
const navigatorPadding = useNavigatorPadding();
|
||||||
@@ -89,6 +91,11 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
const tracks = useTypedSelector((state) => state.music.tracks.entities);
|
const tracks = useTypedSelector((state) => state.music.tracks.entities);
|
||||||
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
|
const isLoading = useTypedSelector((state) => state.music.tracks.isLoading);
|
||||||
const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds));
|
const downloadedTracks = useTypedSelector(selectDownloadedTracks(trackIds));
|
||||||
|
const totalDuration = useMemo(() => (
|
||||||
|
trackIds.reduce<number>((sum, trackId) => (
|
||||||
|
sum + (tracks[trackId]?.RunTimeTicks || 0)
|
||||||
|
), 0)
|
||||||
|
), [trackIds, tracks]);
|
||||||
|
|
||||||
// Retrieve helpers
|
// Retrieve helpers
|
||||||
const getImage = useGetImage();
|
const getImage = useGetImage();
|
||||||
@@ -157,7 +164,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
? i + 1
|
? i + 1
|
||||||
: tracks[trackId]?.IndexNumber}
|
: tracks[trackId]?.IndexNumber}
|
||||||
</Text>
|
</Text>
|
||||||
<View>
|
<View style={{ flexShrink: 1 }}>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
...currentTrack?.backendId === trackId && styles.activeText,
|
...currentTrack?.backendId === trackId && styles.activeText,
|
||||||
@@ -197,6 +204,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
</TrackContainer>
|
</TrackContainer>
|
||||||
</TouchableHandler>
|
</TouchableHandler>
|
||||||
)}
|
)}
|
||||||
|
<Text style={{ paddingTop: 24, paddingBottom: 12, textAlign: 'center', opacity: 0.5 }}>{t('total-duration')}: {ticksToDuration(totalDuration)}</Text>
|
||||||
<WrappableButtonRow style={{ marginTop: 24 }}>
|
<WrappableButtonRow style={{ marginTop: 24 }}>
|
||||||
<WrappableButton
|
<WrappableButton
|
||||||
icon={CloudDownArrow}
|
icon={CloudDownArrow}
|
||||||
@@ -214,6 +222,7 @@ const TrackListView: React.FC<TrackListViewProps> = ({
|
|||||||
/>
|
/>
|
||||||
</WrappableButtonRow>
|
</WrappableButtonRow>
|
||||||
</View>
|
</View>
|
||||||
|
{children}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import music from 'store/music';
|
|||||||
import { t } from '@localisation';
|
import { t } from '@localisation';
|
||||||
import Button from 'components/Button';
|
import Button from 'components/Button';
|
||||||
import styled from 'styled-components/native';
|
import styled from 'styled-components/native';
|
||||||
import { Text } from 'components/Typography';
|
import { Paragraph } from 'components/Typography';
|
||||||
import { useAppDispatch } from 'store';
|
import { useAppDispatch } from 'store';
|
||||||
import { useHeaderHeight } from '@react-navigation/elements';
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ export default function CacheSettings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container contentInset={{ top: headerHeight }}>
|
<Container contentInset={{ top: headerHeight }}>
|
||||||
<Text>{t('setting-cache-description')}</Text>
|
<Paragraph>{t('setting-cache-description')}</Paragraph>
|
||||||
<ClearCache title={t('reset-cache')} onPress={handleClearCache} />
|
<ClearCache title={t('reset-cache')} onPress={handleClearCache} />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { NavigationProp } from '../..';
|
|||||||
import { useTypedSelector } from 'store';
|
import { useTypedSelector } from 'store';
|
||||||
import { t } from '@localisation';
|
import { t } from '@localisation';
|
||||||
import Button from 'components/Button';
|
import Button from 'components/Button';
|
||||||
import { Text } from 'components/Typography';
|
import { Paragraph } from 'components/Typography';
|
||||||
import { useHeaderHeight } from '@react-navigation/elements';
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
|
|
||||||
|
|
||||||
@@ -34,15 +34,15 @@ export default function LibrarySettings() {
|
|||||||
return (
|
return (
|
||||||
<Container contentInset={{ top: headerHeight }}>
|
<Container contentInset={{ top: headerHeight }}>
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<Text style={defaultStyles.text}>{t('jellyfin-server-url')}</Text>
|
<Paragraph style={defaultStyles.text}>{t('jellyfin-server-url')}</Paragraph>
|
||||||
<Input placeholder="https://jellyfin.yourserver.com/" value={jellyfin?.uri} editable={false} style={defaultStyles.input} />
|
<Input placeholder="https://jellyfin.yourserver.com/" value={jellyfin?.uri} editable={false} style={defaultStyles.input} />
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<Text style={defaultStyles.text}>{t('jellyfin-access-token')}</Text>
|
<Paragraph style={defaultStyles.text}>{t('jellyfin-access-token')}</Paragraph>
|
||||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.access_token} editable={false} style={defaultStyles.input} />
|
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.access_token} editable={false} style={defaultStyles.input} />
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<Text style={defaultStyles.text}>{t('jellyfin-user-id')}</Text>
|
<Paragraph style={defaultStyles.text}>{t('jellyfin-user-id')}</Paragraph>
|
||||||
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.user_id} editable={false} style={defaultStyles.input} />
|
<Input placeholder="deadbeefdeadbeefdeadbeef" value={jellyfin?.user_id} editable={false} style={defaultStyles.input} />
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
<Button title={t('set-jellyfin-server')} onPress={handleSetLibrary} />
|
<Button title={t('set-jellyfin-server')} onPress={handleSetLibrary} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Text } from 'components/Typography';
|
import { Paragraph, Text } from 'components/Typography';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Switch } from 'react-native-gesture-handler';
|
import { Switch } from 'react-native-gesture-handler';
|
||||||
@@ -92,7 +92,7 @@ function renderHeader(question: Question, index: number, isActive: boolean) {
|
|||||||
function renderContent(question: Question) {
|
function renderContent(question: Question) {
|
||||||
return (
|
return (
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<Text>{question.content}</Text>
|
<Paragraph>{question.content}</Paragraph>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -112,9 +112,9 @@ export default function Sentry() {
|
|||||||
return (
|
return (
|
||||||
<ScrollView contentInset={{ top: headerHeight }}>
|
<ScrollView contentInset={{ top: headerHeight }}>
|
||||||
<Container>
|
<Container>
|
||||||
<Text>{t('error-reporting-description')}</Text>
|
<Paragraph>{t('error-reporting-description')}</Paragraph>
|
||||||
<Text />
|
<Paragraph />
|
||||||
<Text>{t('error-reporting-rationale')}</Text>
|
<Paragraph>{t('error-reporting-rationale')}</Paragraph>
|
||||||
|
|
||||||
<SwitchContainer>
|
<SwitchContainer>
|
||||||
<Label>{t('error-reporting')}</Label>
|
<Label>{t('error-reporting')}</Label>
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ export const fetchTracksByAlbum = createAsyncThunk<AlbumTrack[], string, AsyncTh
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fetchAlbum = createAsyncThunk<Album, string, AsyncThunkAPI>(
|
||||||
|
'/albums/single',
|
||||||
|
async (ItemId, thunkAPI) => {
|
||||||
|
const credentials = thunkAPI.getState().settings.jellyfin;
|
||||||
|
return retrieveAlbum(credentials, ItemId) as Promise<Album>;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type SearchAndFetchResults = {
|
type SearchAndFetchResults = {
|
||||||
albums: Album[];
|
albums: Album[];
|
||||||
results: (Album | AlbumTrack)[];
|
results: (Album | AlbumTrack)[];
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
searchAndFetchAlbums,
|
searchAndFetchAlbums,
|
||||||
playlistAdapter,
|
playlistAdapter,
|
||||||
fetchAllPlaylists,
|
fetchAllPlaylists,
|
||||||
fetchTracksByPlaylist
|
fetchTracksByPlaylist,
|
||||||
|
fetchAlbum
|
||||||
} from './actions';
|
} from './actions';
|
||||||
import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit';
|
import { createSlice, Dictionary, EntityId } from '@reduxjs/toolkit';
|
||||||
import { Album, AlbumTrack, Playlist } from './types';
|
import { Album, AlbumTrack, Playlist } from './types';
|
||||||
@@ -70,6 +71,15 @@ const music = createSlice({
|
|||||||
builder.addCase(fetchAllAlbums.pending, (state) => { state.albums.isLoading = true; });
|
builder.addCase(fetchAllAlbums.pending, (state) => { state.albums.isLoading = true; });
|
||||||
builder.addCase(fetchAllAlbums.rejected, (state) => { state.albums.isLoading = false; });
|
builder.addCase(fetchAllAlbums.rejected, (state) => { state.albums.isLoading = false; });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch single album
|
||||||
|
*/
|
||||||
|
builder.addCase(fetchAlbum.fulfilled, (state, { payload }) => {
|
||||||
|
albumAdapter.upsertOne(state.albums, payload);
|
||||||
|
});
|
||||||
|
builder.addCase(fetchAlbum.pending, (state) => { state.albums.isLoading = true; });
|
||||||
|
builder.addCase(fetchAlbum.rejected, (state) => { state.albums.isLoading = false; });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch most recent albums
|
* Fetch most recent albums
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ export interface Album {
|
|||||||
Tracks?: string[];
|
Tracks?: string[];
|
||||||
lastRefreshed?: number;
|
lastRefreshed?: number;
|
||||||
DateCreated: string;
|
DateCreated: string;
|
||||||
|
Overview?: string;
|
||||||
|
Similar?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlbumTrack {
|
export interface AlbumTrack {
|
||||||
@@ -95,3 +97,7 @@ export interface Playlist {
|
|||||||
Tracks?: string[];
|
Tracks?: string[];
|
||||||
lastRefreshed?: number;
|
lastRefreshed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SimilarAlbum {
|
||||||
|
Id: string;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Track } from 'react-native-track-player';
|
import { Track } from 'react-native-track-player';
|
||||||
import { AppState, useTypedSelector } from 'store';
|
import { AppState, useTypedSelector } from 'store';
|
||||||
import { Album, AlbumTrack } from 'store/music/types';
|
import { Album, AlbumTrack, SimilarAlbum } from 'store/music/types';
|
||||||
|
|
||||||
type Credentials = AppState['settings']['jellyfin'];
|
type Credentials = AppState['settings']['jellyfin'];
|
||||||
|
|
||||||
@@ -86,8 +86,14 @@ export async function retrieveAllAlbums(credentials: Credentials) {
|
|||||||
*/
|
*/
|
||||||
export async function retrieveAlbum(credentials: Credentials, id: string): Promise<Album> {
|
export async function retrieveAlbum(credentials: Credentials, id: string): Promise<Album> {
|
||||||
const config = generateConfig(credentials);
|
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)
|
return fetch(`${credentials?.uri}/Users/${credentials?.user_id}/Items/${id}`, config)
|
||||||
.then(response => response.json());
|
.then(response => response.json() as Promise<Album>)
|
||||||
|
.then(album => ({ ...album, Similar }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestAlbumsOptions = {
|
const latestAlbumsOptions = {
|
||||||
@@ -96,7 +102,6 @@ const latestAlbumsOptions = {
|
|||||||
SortOrder: 'Ascending',
|
SortOrder: 'Ascending',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the most recently added albums on the Jellyfin server
|
* Retrieve the most recently added albums on the Jellyfin server
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
function ticksToDuration(ticks: number) {
|
function ticksToDuration(ticks: number) {
|
||||||
const seconds = Math.round(ticks / 10000000);
|
const seconds = Math.round(ticks / 10000000);
|
||||||
const minutes = Math.round(seconds / 60);
|
const minutes = Math.floor(seconds / 60);
|
||||||
const hours = Math.round(minutes / 60);
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
||||||
return `${hours > 0 ? hours + ':' : ''}${minutes}:${(seconds % 60).toString().padStart(2, '0')}`;
|
return `${hours > 0 ? hours + ':' : ''}${minutes}:${(seconds % 60).toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user