artist art pull from last.fm or album covers

This commit is contained in:
austinried 2021-06-30 13:07:53 +09:00
parent 379779735e
commit 23e6c0caf0
18 changed files with 582 additions and 168 deletions

View File

@ -0,0 +1,34 @@
import React from 'react';
import FastImage from 'react-native-fast-image';
import LinearGradient from 'react-native-linear-gradient';
import colors from '../../styles/colors';
import CoverArt from './CoverArt';
const AlbumArt: React.FC<{
height: number,
width: number,
coverArtUri?: string
}> = ({ height, width, coverArtUri }) => {
const Placeholder = () => (
<LinearGradient
colors={[colors.accent, colors.accentLow]}
>
<FastImage
source={require('../../../res/record.png')}
style={{ height, width }}
resizeMode={FastImage.resizeMode.contain}
/>
</LinearGradient>
);
return (
<CoverArt
PlaceholderComponent={Placeholder}
height={height}
width={width}
coverArtUri={coverArtUri}
/>
);
}
export default React.memo(AlbumArt);

View File

@ -2,56 +2,13 @@ import { useNavigation } from '@react-navigation/native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect, useState } from 'react';
import { GestureResponderEvent, Image, Pressable, ScrollView, Text, useWindowDimensions, View } from 'react-native';
import { TrackPlayerEvents } from 'react-native-track-player';
import { useCurrentTrackId, useSetQueue } from '../../hooks/player';
import { albumAtomFamily } from '../../state/music';
import colors from '../../styles/colors';
import text from '../../styles/text';
import AlbumCover from './AlbumCover';
import GradientBackground from './GradientBackground';
function secondsToTime(s: number): string {
const seconds = s % 60;
const minutes = Math.floor(s / 60) % 60;
const hours = Math.floor(s / 60 / 60);
let time = `${minutes.toString().padStart(1, '0')}:${seconds.toString().padStart(2, '0')}`;
if (hours > 0) {
time = `${hours}:${time}`;
}
return time;
}
const Button: React.FC<{
title: string;
onPress: (event: GestureResponderEvent) => void;
}> = ({ title, onPress }) => {
const [opacity, setOpacity] = useState(1);
return (
<Pressable
onPress={onPress}
onPressIn={() => setOpacity(0.6)}
onPressOut={() => setOpacity(1)}
onLongPress={() => setOpacity(1)}
style={{
backgroundColor: colors.accent,
paddingHorizontal: 24,
minHeight: 42,
justifyContent: 'center',
borderRadius: 1000,
opacity,
}}
>
<Text style={{ ...text.button }}>{title}</Text>
</Pressable>
);
}
const songEvents = [
TrackPlayerEvents.PLAYBACK_STATE,
TrackPlayerEvents.PLAYBACK_TRACK_CHANGED,
]
import AlbumArt from './AlbumArt';
import Button from './Button';
import GradientScrollView from './GradientScrollView';
const SongItem: React.FC<{
id: string;
@ -134,7 +91,7 @@ const AlbumDetails: React.FC<{
}
return (
<ScrollView
<GradientScrollView
style={{
flex: 1,
}}
@ -142,10 +99,8 @@ const AlbumDetails: React.FC<{
alignItems: 'center',
paddingTop: coverSize / 8,
}}
overScrollMode='never'
>
<GradientBackground />
<AlbumCover
<AlbumArt
height={coverSize}
width={coverSize}
coverArtUri={album.coverArtUri}
@ -199,11 +154,10 @@ const AlbumDetails: React.FC<{
))}
</View>
</ScrollView>
</GradientScrollView>
);
}
const AlbumView: React.FC<{
id: string,
title: string;

View File

@ -0,0 +1,193 @@
import React from 'react';
import { View } from 'react-native';
import FastImage from 'react-native-fast-image';
import LinearGradient from 'react-native-linear-gradient';
import colors from '../../styles/colors';
import CoverArt from './CoverArt';
const PlaceholderContainer: React.FC<{
height: number,
width: number,
}> = ({ height, width, children}) => (
<LinearGradient
colors={[colors.accent, colors.accentLow]}
style={{
height, width,
alignItems: 'center',
justifyContent: 'center',
}}
>
{children}
</LinearGradient>
);
const FourUp: React.FC<{
height: number,
width: number,
coverArtUris: string[];
}> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2;
const halfWidth = width / 2;
return (
<PlaceholderContainer height={height} width={width}>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[0] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
<FastImage
source={{ uri: coverArtUris[1] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[2] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
<FastImage
source={{ uri: coverArtUris[3] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
</PlaceholderContainer>
);
};
const ThreeUp: React.FC<{
height: number,
width: number,
coverArtUris: string[];
}> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2;
const halfWidth = width / 2;
return (
<PlaceholderContainer height={height} width={width}>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[0] }}
style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[1] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
<FastImage
source={{ uri: coverArtUris[2] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
</PlaceholderContainer>
);
};
const TwoUp: React.FC<{
height: number,
width: number,
coverArtUris: string[];
}> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2;
return (
<PlaceholderContainer height={height} width={width}>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[0] }}
style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[1] }}
style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
</PlaceholderContainer>
);
};
const OneUp: React.FC<{
height: number,
width: number,
coverArtUris: string[];
}> = ({ height, width, coverArtUris }) => {
return (
<PlaceholderContainer height={height} width={width}>
<FastImage
source={{ uri: coverArtUris[0] }}
style={{ height, width }}
resizeMode={FastImage.resizeMode.cover}
/>
</PlaceholderContainer>
);
};
const NoneUp: React.FC<{
height: number,
width: number,
}> = ({ height, width }) => {
return (
<PlaceholderContainer height={height} width={width}>
<FastImage
source={require('../../../res/mic_on-fill.png')}
style={{
height: height - height / 4,
width: width - width / 4,
}}
resizeMode={FastImage.resizeMode.cover}
/>
</PlaceholderContainer>
);
};
const ArtistArt: React.FC<{
height: number,
width: number,
mediumImageUrl?: string;
coverArtUris?: string[]
}> = ({ height, width, mediumImageUrl, coverArtUris }) => {
const Placeholder = () => {
if (coverArtUris && coverArtUris.length >= 4) {
return <FourUp height={height} width={width} coverArtUris={coverArtUris} />;
}
if (coverArtUris && coverArtUris.length === 3) {
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} />;
}
if (coverArtUris && coverArtUris.length === 2) {
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} />;
}
if (coverArtUris && coverArtUris.length === 1) {
return <OneUp height={height} width={width} coverArtUris={coverArtUris} />;
}
return <NoneUp height={height} width={width} />;
}
return (
<View style={{
borderRadius: height / 2,
overflow: 'hidden',
}}>
<CoverArt
PlaceholderComponent={Placeholder}
height={height}
width={width}
coverArtUri={mediumImageUrl}
/>
</View>
);
}
export default React.memo(ArtistArt);

View File

@ -0,0 +1,55 @@
import { useNavigation } from '@react-navigation/native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect } from 'react';
import { Text } from 'react-native';
import { artistInfoAtomFamily } from '../../state/music';
import text from '../../styles/text';
import ArtistArt from './ArtistArt';
import GradientScrollView from './GradientScrollView';
const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
const artist = useAtomValue(artistInfoAtomFamily(id));
if (!artist) {
return <></>;
}
return (
<GradientScrollView
style={{
flex: 1,
}}
contentContainerStyle={{
alignItems: 'center',
// paddingTop: coverSize / 8,
}}
>
<Text style={text.paragraph}>{artist.name}</Text>
<ArtistArt
height={200}
width={200}
mediumImageUrl={artist.mediumImageUrl}
coverArtUris={artist.coverArtUris}
/>
</GradientScrollView>
)
}
const ArtistView: React.FC<{
id: string,
title: string;
}> = ({ id, title }) => {
const navigation = useNavigation();
useEffect(() => {
navigation.setOptions({ title });
});
return (
<React.Suspense fallback={<Text>Loading...</Text>}>
<ArtistDetails id={id} />
</React.Suspense>
);
};
export default React.memo(ArtistView);

View File

@ -0,0 +1,32 @@
import React, { useState } from 'react';
import { GestureResponderEvent, Pressable, Text } from 'react-native';
import colors from '../../styles/colors';
import text from '../../styles/text';
const Button: React.FC<{
title: string;
onPress: (event: GestureResponderEvent) => void;
}> = ({ title, onPress }) => {
const [opacity, setOpacity] = useState(1);
return (
<Pressable
onPress={onPress}
onPressIn={() => setOpacity(0.6)}
onPressOut={() => setOpacity(1)}
onLongPress={() => setOpacity(1)}
style={{
backgroundColor: colors.accent,
paddingHorizontal: 24,
minHeight: 42,
justifyContent: 'center',
borderRadius: 1000,
opacity,
}}
>
<Text style={{ ...text.button }}>{title}</Text>
</Pressable>
);
}
export default Button;

View File

@ -1,14 +1,14 @@
import React, { useState } from 'react';
import { ActivityIndicator, View } from 'react-native';
import FastImage from 'react-native-fast-image';
import LinearGradient from 'react-native-linear-gradient';
import colors from '../../styles/colors';
const AlbumCover: React.FC<{
const CoverArt: React.FC<{
PlaceholderComponent: () => JSX.Element,
height: number,
width: number,
coverArtUri?: string
}> = ({ height, width, coverArtUri }) => {
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
const [placeholderVisible, setPlaceholderVisible] = useState(false);
const [loading, setLoading] = useState(true);
@ -16,23 +16,15 @@ const AlbumCover: React.FC<{
const halfIndicatorHeight = indicatorSize === 'large' ? 18 : 10;
const Placeholder: React.FC<{ visible: boolean }> = ({ visible }) => (
<LinearGradient
colors={[colors.accent, colors.accentLow]}
style={{
height, width,
opacity: visible ? 100 : 0,
}}
>
<FastImage
source={require('../../../res/record.png')}
style={{ height, width }}
resizeMode={FastImage.resizeMode.contain}
/>
</LinearGradient>
<View style={{
opacity: visible ? 100 : 0,
}}>
<PlaceholderComponent />
</View>
);
const CoverArt = () => (
<View>
<>
<Placeholder visible={placeholderVisible} />
<ActivityIndicator
animating={loading}
@ -49,17 +41,20 @@ const AlbumCover: React.FC<{
marginTop: -height - halfIndicatorHeight * 2,
}}
resizeMode={FastImage.resizeMode.contain}
onError={() => setPlaceholderVisible(true)}
onError={() => {
setLoading(false);
setPlaceholderVisible(true);
}}
onLoadEnd={() => setLoading(false)}
/>
</View>
</>
);
return (
<View style={{ height, width }}>
{!coverArtUri ? <Placeholder visible={placeholderVisible} /> : <CoverArt />}
{!coverArtUri ? <Placeholder visible={true} /> : <CoverArt />}
</View>
);
}
export default React.memo(AlbumCover);
export default React.memo(CoverArt);

View File

@ -0,0 +1,15 @@
import React from 'react';
import { ScrollView, ScrollViewProps } from 'react-native';
import GradientBackground from './GradientBackground';
const GradientScrollView: React.FC<ScrollViewProps> = (props) => (
<ScrollView
overScrollMode='never'
{...props}
>
<GradientBackground />
{props.children}
</ScrollView>
);
export default GradientScrollView;

View File

@ -5,7 +5,7 @@ import { Pressable, Text, View } from 'react-native';
import { Album } from '../../models/music';
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '../../state/music';
import textStyles from '../../styles/text';
import AlbumCover from '../common/AlbumCover';
import AlbumArt from '../common/AlbumArt';
import GradientFlatList from '../common/GradientFlatList';
const AlbumItem: React.FC<{
@ -27,7 +27,7 @@ const AlbumItem: React.FC<{
}}
onPress={() => navigation.navigate('AlbumView', { id, title: name })}
>
<AlbumCover
<AlbumArt
width={size}
height={size}
coverArtUri={coverArtUri}

View File

@ -1,30 +1,53 @@
import { useNavigation } from '@react-navigation/native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect } from 'react';
import { Pressable } from 'react-native';
import { Image, Text, View } from 'react-native';
import { Artist } from '../../models/music';
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music';
import { artistInfoAtomFamily, artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '../../state/music';
import textStyles from '../../styles/text';
import ArtistArt from '../common/ArtistArt';
import GradientFlatList from '../common/GradientFlatList';
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginVertical: 6,
marginLeft: 6,
}}>
<Image
source={item.coverArt ? { uri: 'https://reactnative.dev/img/tiny_logo.png' } : require('../../../res/mic_on-fill.png')}
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
const navigation = useNavigation();
const artistInfo = useAtomValue(artistInfoAtomFamily(item.id));
return (
<Pressable
style={{
width: 56,
height: 56,
flexDirection: 'row',
alignItems: 'center',
marginVertical: 6,
marginLeft: 6,
}}
/>
<Text style={{
...textStyles.paragraph,
marginLeft: 12,
}}>{item.name}</Text>
</View>
onPress={() => navigation.navigate('ArtistView', { id: item.id, title: item.name })}
>
<ArtistArt
width={56}
height={56}
mediumImageUrl={artistInfo?.mediumImageUrl}
coverArtUris={artistInfo?.coverArtUris}
/>
{/* <Image
source={{ uri: 'https://reactnative.dev/img/tiny_logo.png' }}
style={{
width: 56,
height: 56,
}}
/> */}
<Text style={{
...textStyles.paragraph,
marginLeft: 12,
}}>{item.name}</Text>
</Pressable>
);
};
const ArtistItemLoader: React.FC<{ item: Artist }> = (props) => (
<React.Suspense fallback={<Text>Loading...</Text>}>
<ArtistItem { ...props } />
</React.Suspense>
);
const ArtistsList = () => {
@ -39,7 +62,7 @@ const ArtistsList = () => {
});
const renderItem: React.FC<{ item: Artist }> = ({ item }) => (
<ArtistItem item={item} />
<ArtistItemLoader item={item} />
);
return (

View File

@ -10,16 +10,12 @@ import { RouteProp } from '@react-navigation/native';
import text from '../../styles/text';
import colors from '../../styles/colors';
import FastImage from 'react-native-fast-image';
import ArtistView from '../common/ArtistView';
const Tab = createMaterialTopTabNavigator();
const LibraryTopTabNavigator = () => (
<Tab.Navigator tabBarOptions={{
// scrollEnabled: true,
tabStyle: {
// width: 100,
// paddingHorizontal: 0,
},
style: {
height: 48,
backgroundColor: colors.gradient.high,
@ -53,6 +49,7 @@ const LibraryTopTabNavigator = () => (
type LibraryStackParamList = {
LibraryTopTabs: undefined,
AlbumView: { id: string, title: string };
ArtistView: { id: string, title: string };
}
type AlbumScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'AlbumView'>;
@ -66,8 +63,41 @@ const AlbumScreen: React.FC<AlbumScreenProps> = ({ route }) => (
<AlbumView id={route.params.id} title={route.params.title} />
);
type ArtistScreenNavigationProp = StackNavigationProp<LibraryStackParamList, 'ArtistView'>;
type ArtistScreenRouteProp = RouteProp<LibraryStackParamList, 'ArtistView'>;
type ArtistScreenProps = {
route: ArtistScreenRouteProp,
navigation: ArtistScreenNavigationProp,
};
const ArtistScreen: React.FC<ArtistScreenProps> = ({ route }) => (
<ArtistView id={route.params.id} title={route.params.title} />
);
const Stack = createStackNavigator<LibraryStackParamList>();
const itemScreenOptions = {
title: '',
headerStyle: {
height: 50,
backgroundColor: colors.gradient.high,
},
headerTitleContainerStyle: {
marginLeft: -14,
},
headerLeftContainerStyle: {
marginLeft: 8,
},
headerTitleStyle: {
...text.header,
},
headerBackImage: () => <FastImage
source={require('../../../res/arrow_left-fill.png')}
tintColor={colors.text.primary}
style={{ height: 22, width: 22 }}
/>,
}
const LibraryStackNavigator = () => (
<View style={{ flex: 1 }}>
<Stack.Navigator>
@ -79,27 +109,12 @@ const LibraryStackNavigator = () => (
<Stack.Screen
name='AlbumView'
component={AlbumScreen}
options={{
title: '',
headerStyle: {
height: 50,
backgroundColor: colors.gradient.high,
},
headerTitleContainerStyle: {
marginLeft: -14,
},
headerLeftContainerStyle: {
marginLeft: 8,
},
headerTitleStyle: {
...text.header,
},
headerBackImage: () => <FastImage
source={require('../../../res/arrow_left-fill.png')}
tintColor={colors.text.primary}
style={{ height: 22, width: 22 }}
/>,
}}
options={itemScreenOptions}
/>
<Stack.Screen
name='ArtistView'
component={ArtistScreen}
options={itemScreenOptions}
/>
</Stack.Navigator>
</View>

View File

@ -9,6 +9,7 @@ function mapSongToTrack(song: Song): Track {
artist: song.artist || 'Unknown Artist',
url: song.streamUri,
artwork: song.coverArtUri,
duration: song.duration,
}
}

View File

@ -2,8 +2,14 @@ export interface Artist {
id: string;
name: string;
starred?: Date;
coverArt?: string;
coverArtUri?: string,
}
export interface ArtistInfo extends Artist {
albums: Album[];
mediumImageUrl?: string;
largeImageUrl?: string;
coverArtUris: string[];
}
export interface Album {

View File

@ -1,8 +1,9 @@
import { atom, useAtom } from 'jotai';
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils';
import { Album as Album, AlbumWithSongs, Artist, Song } from '../models/music';
import { Album as Album, AlbumWithSongs, Artist, ArtistInfo, Song } from '../models/music';
import { SubsonicApiClient } from '../subsonic/api';
import { AlbumID3Element, ChildElement } from '../subsonic/elements';
import { AlbumID3Element, ArtistID3Element, ArtistInfo2Element, ChildElement } from '../subsonic/elements';
import { GetArtistResponse } from '../subsonic/responses';
import { activeServerAtom } from './settings';
export const artistsAtom = atom<Artist[]>([]);
@ -30,8 +31,6 @@ export const useUpdateArtists = () => {
id: x.id,
name: x.name,
starred: x.starred,
coverArt: x.coverArt,
coverArtUri: x.coverArt ? client.getCoverArtUri({ id: x.coverArt }) : undefined,
})));
setUpdating(false);
}
@ -74,6 +73,49 @@ export const albumAtomFamily = atomFamily((id: string) => atom<AlbumWithSongs |
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client);
}));
export const artistInfoAtomFamily = atomFamily((id: string) => atom<ArtistInfo | undefined>(async (get) => {
const server = get(activeServerAtom);
if (!server) {
return undefined;
}
const client = new SubsonicApiClient(server);
const [artistResponse, artistInfoResponse] = await Promise.all([
client.getArtist({ id }),
client.getArtistInfo2({ id }),
]);
return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, client);
}));
function mapArtistInfo(
artistResponse: GetArtistResponse,
artistInfo: ArtistInfo2Element,
client: SubsonicApiClient
): ArtistInfo {
const info = { ...artistInfo } as any;
delete info.similarArtists;
const { artist, albums } = artistResponse
const mappedAlbums = albums.map(a => mapAlbumID3(a, client));
const coverArtUris = mappedAlbums
.sort((a, b) => {
if (a.year && b.year) {
return a.year - b.year;
} else {
return a.name.localeCompare(b.name) - 9000;
}
})
.map(a => a.coverArtThumbUri);
return {
...artist,
...info,
albums: mappedAlbums,
coverArtUris,
}
}
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
return {
...album,

View File

@ -1,7 +1,7 @@
import { DOMParser } from 'xmldom';
import RNFS from 'react-native-fs';
import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams, StreamParams } from './params';
import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, SubsonicResponse } from './responses';
import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetArtistParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams, StreamParams } from './params';
import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, SubsonicResponse } from './responses';
import { Server } from '../models/settings';
import paths from '../paths';
@ -167,6 +167,11 @@ export class SubsonicApiClient {
return new SubsonicResponse<GetArtistInfo2Response>(xml, new GetArtistInfo2Response(xml));
}
async getArtist(params: GetArtistParams): Promise<SubsonicResponse<GetArtistResponse>> {
const xml = await this.apiGetXml('getArtist', params);
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml));
}
//
// Album/song lists
//

View File

@ -72,6 +72,53 @@ export class ArtistElement extends BaseArtistElement {
}
}
export class BaseArtistInfoElement<T> {
similarArtists: T[] = [];
biography?: string;
musicBrainzId?: string;
lastFmUrl?: string;
smallImageUrl?: string;
mediumImageUrl?: string;
largeImageUrl?: string;
constructor(e: Element, artistType: new (e: Element) => T) {
if (e.getElementsByTagName('biography').length > 0) {
this.biography = e.getElementsByTagName('biography')[0].textContent as string;
}
if (e.getElementsByTagName('musicBrainzId').length > 0) {
this.musicBrainzId = e.getElementsByTagName('musicBrainzId')[0].textContent as string;
}
if (e.getElementsByTagName('lastFmUrl').length > 0) {
this.lastFmUrl = e.getElementsByTagName('lastFmUrl')[0].textContent as string;
}
if (e.getElementsByTagName('smallImageUrl').length > 0) {
this.smallImageUrl = e.getElementsByTagName('smallImageUrl')[0].textContent as string;
}
if (e.getElementsByTagName('mediumImageUrl').length > 0) {
this.mediumImageUrl = e.getElementsByTagName('mediumImageUrl')[0].textContent as string;
}
if (e.getElementsByTagName('largeImageUrl').length > 0) {
this.largeImageUrl = e.getElementsByTagName('largeImageUrl')[0].textContent as string;
}
const similarArtistElements = e.getElementsByTagName('similarArtist');
for (let i = 0; i < similarArtistElements.length; i++) {
this.similarArtists.push(new artistType(similarArtistElements[i]));
}
}
}
export class ArtistInfoElement extends BaseArtistInfoElement<ArtistElement> {
constructor(e: Element) {
super(e, ArtistElement);
}
}
export class ArtistInfo2Element extends BaseArtistInfoElement<ArtistID3Element> {
constructor(e: Element) {
super(e, ArtistID3Element);
}
}
export class DirectoryElement {
id: string;
parent?: string;

View File

@ -23,6 +23,10 @@ export type GetAlbumParams = {
id: string;
}
export type GetArtistParams = {
id: string;
}
//
// Album/song lists

View File

@ -1,4 +1,4 @@
import { AlbumID3Element, ArtistElement, ArtistID3Element, BaseArtistElement, ChildElement, DirectoryElement } from "./elements";
import { AlbumID3Element, ArtistElement, ArtistID3Element, ArtistInfo2Element, ArtistInfoElement, BaseArtistElement, BaseArtistInfoElement, ChildElement, DirectoryElement } from "./elements";
export type ResponseStatus = 'ok' | 'failed';
@ -32,6 +32,20 @@ export class GetArtistsResponse {
}
}
export class GetArtistResponse {
artist: ArtistID3Element;
albums: AlbumID3Element[] = [];
constructor(xml: Document) {
this.artist = new ArtistID3Element(xml.getElementsByTagName('artist')[0]);
const albumElements = xml.getElementsByTagName('album');
for (let i = 0; i < albumElements.length; i++) {
this.albums.push(new AlbumID3Element(albumElements[i]));
}
}
}
export class GetIndexesResponse {
ignoredArticles: string;
lastModified: number;
@ -50,51 +64,19 @@ export class GetIndexesResponse {
}
}
class BaseGetArtistInfoResponse<T extends BaseArtistElement> {
similarArtists: T[] = [];
biography?: string;
musicBrainzId?: string;
lastFmUrl?: string;
smallImageUrl?: string;
mediumImageUrl?: string;
largeImageUrl?: string;
export class GetArtistInfoResponse {
artistInfo: ArtistInfoElement;
constructor(xml: Document, artistType: new (e: Element) => T) {
if (xml.getElementsByTagName('biography').length > 0) {
this.biography = xml.getElementsByTagName('biography')[0].textContent as string;
}
if (xml.getElementsByTagName('musicBrainzId').length > 0) {
this.musicBrainzId = xml.getElementsByTagName('musicBrainzId')[0].textContent as string;
}
if (xml.getElementsByTagName('lastFmUrl').length > 0) {
this.lastFmUrl = xml.getElementsByTagName('lastFmUrl')[0].textContent as string;
}
if (xml.getElementsByTagName('smallImageUrl').length > 0) {
this.smallImageUrl = xml.getElementsByTagName('smallImageUrl')[0].textContent as string;
}
if (xml.getElementsByTagName('mediumImageUrl').length > 0) {
this.mediumImageUrl = xml.getElementsByTagName('mediumImageUrl')[0].textContent as string;
}
if (xml.getElementsByTagName('largeImageUrl').length > 0) {
this.largeImageUrl = xml.getElementsByTagName('largeImageUrl')[0].textContent as string;
}
const similarArtistElements = xml.getElementsByTagName('similarArtist');
for (let i = 0; i < similarArtistElements.length; i++) {
this.similarArtists.push(new artistType(similarArtistElements[i]));
}
constructor(xml: Document) {
this.artistInfo = new ArtistInfoElement(xml.getElementsByTagName('artistInfo')[0]);
}
}
export class GetArtistInfoResponse extends BaseGetArtistInfoResponse<ArtistElement> {
constructor(xml: Document) {
super(xml, ArtistElement);
}
}
export class GetArtistInfo2Response {
artistInfo: ArtistInfo2Element;
export class GetArtistInfo2Response extends BaseGetArtistInfoResponse<ArtistID3Element> {
constructor(xml: Document) {
super(xml, ArtistID3Element);
this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]);
}
}

11
src/util.ts Normal file
View File

@ -0,0 +1,11 @@
export function formatDuration(seconds: number): string {
const s = seconds % 60;
const m = Math.floor(seconds / 60) % 60;
const h = Math.floor(seconds / 60 / 60);
let time = `${m.toString().padStart(1, '0')}:${s.toString().padStart(2, '0')}`;
if (h > 0) {
time = `${h}:${time}`;
}
return time;
}