From 23e6c0caf03596ef5e8b03eb9ac634e08a117c38 Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Wed, 30 Jun 2021 13:07:53 +0900 Subject: [PATCH] artist art pull from last.fm or album covers --- src/components/common/AlbumArt.tsx | 34 +++ src/components/common/AlbumView.tsx | 58 +----- src/components/common/ArtistArt.tsx | 193 ++++++++++++++++++ src/components/common/ArtistView.tsx | 55 +++++ src/components/common/Button.tsx | 32 +++ .../common/{AlbumCover.tsx => CoverArt.tsx} | 37 ++-- src/components/common/GradientScrollView.tsx | 15 ++ src/components/library/AlbumsTab.tsx | 4 +- src/components/library/ArtistsTab.tsx | 61 ++++-- .../navigation/LibraryTopTabNavigator.tsx | 67 +++--- src/hooks/player.ts | 1 + src/models/music.ts | 10 +- src/state/music.ts | 50 ++++- src/subsonic/api.ts | 9 +- src/subsonic/elements.ts | 47 +++++ src/subsonic/params.ts | 4 + src/subsonic/responses.ts | 62 ++---- src/util.ts | 11 + 18 files changed, 582 insertions(+), 168 deletions(-) create mode 100644 src/components/common/AlbumArt.tsx create mode 100644 src/components/common/ArtistArt.tsx create mode 100644 src/components/common/ArtistView.tsx create mode 100644 src/components/common/Button.tsx rename src/components/common/{AlbumCover.tsx => CoverArt.tsx} (64%) create mode 100644 src/components/common/GradientScrollView.tsx create mode 100644 src/util.ts diff --git a/src/components/common/AlbumArt.tsx b/src/components/common/AlbumArt.tsx new file mode 100644 index 0000000..f78fecd --- /dev/null +++ b/src/components/common/AlbumArt.tsx @@ -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 = () => ( + + + + ); + + return ( + + ); +} + +export default React.memo(AlbumArt); diff --git a/src/components/common/AlbumView.tsx b/src/components/common/AlbumView.tsx index 15c1610..9522379 100644 --- a/src/components/common/AlbumView.tsx +++ b/src/components/common/AlbumView.tsx @@ -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 ( - setOpacity(0.6)} - onPressOut={() => setOpacity(1)} - onLongPress={() => setOpacity(1)} - style={{ - backgroundColor: colors.accent, - paddingHorizontal: 24, - minHeight: 42, - justifyContent: 'center', - borderRadius: 1000, - opacity, - }} - > - {title} - - ); -} - -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 ( - - - - + ); } - const AlbumView: React.FC<{ id: string, title: string; diff --git a/src/components/common/ArtistArt.tsx b/src/components/common/ArtistArt.tsx new file mode 100644 index 0000000..0f2727f --- /dev/null +++ b/src/components/common/ArtistArt.tsx @@ -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}) => ( + + {children} + +); + +const FourUp: React.FC<{ + height: number, + width: number, + coverArtUris: string[]; +}> = ({ height, width, coverArtUris }) => { + const halfHeight = height / 2; + const halfWidth = width / 2; + + return ( + + + + + + + + + + + ); +}; + +const ThreeUp: React.FC<{ + height: number, + width: number, + coverArtUris: string[]; +}> = ({ height, width, coverArtUris }) => { + const halfHeight = height / 2; + const halfWidth = width / 2; + + return ( + + + + + + + + + + ); +}; + +const TwoUp: React.FC<{ + height: number, + width: number, + coverArtUris: string[]; +}> = ({ height, width, coverArtUris }) => { + const halfHeight = height / 2; + + return ( + + + + + + + + + ); +}; + +const OneUp: React.FC<{ + height: number, + width: number, + coverArtUris: string[]; +}> = ({ height, width, coverArtUris }) => { + return ( + + + + ); +}; + +const NoneUp: React.FC<{ + height: number, + width: number, +}> = ({ height, width }) => { + return ( + + + + ); +}; + +const ArtistArt: React.FC<{ + height: number, + width: number, + mediumImageUrl?: string; + coverArtUris?: string[] +}> = ({ height, width, mediumImageUrl, coverArtUris }) => { + const Placeholder = () => { + if (coverArtUris && coverArtUris.length >= 4) { + return ; + } + if (coverArtUris && coverArtUris.length === 3) { + return ; + } + if (coverArtUris && coverArtUris.length === 2) { + return ; + } + if (coverArtUris && coverArtUris.length === 1) { + return ; + } + return ; + } + + return ( + + + + ); +} + +export default React.memo(ArtistArt); diff --git a/src/components/common/ArtistView.tsx b/src/components/common/ArtistView.tsx new file mode 100644 index 0000000..6ce2de5 --- /dev/null +++ b/src/components/common/ArtistView.tsx @@ -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 ( + + {artist.name} + + + ) +} + +const ArtistView: React.FC<{ + id: string, + title: string; +}> = ({ id, title }) => { + const navigation = useNavigation(); + + useEffect(() => { + navigation.setOptions({ title }); + }); + + return ( + Loading...}> + + + ); +}; + +export default React.memo(ArtistView); diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx new file mode 100644 index 0000000..12fae07 --- /dev/null +++ b/src/components/common/Button.tsx @@ -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 ( + setOpacity(0.6)} + onPressOut={() => setOpacity(1)} + onLongPress={() => setOpacity(1)} + style={{ + backgroundColor: colors.accent, + paddingHorizontal: 24, + minHeight: 42, + justifyContent: 'center', + borderRadius: 1000, + opacity, + }} + > + {title} + + ); +} + +export default Button; diff --git a/src/components/common/AlbumCover.tsx b/src/components/common/CoverArt.tsx similarity index 64% rename from src/components/common/AlbumCover.tsx rename to src/components/common/CoverArt.tsx index f5dfe5e..e6a4c74 100644 --- a/src/components/common/AlbumCover.tsx +++ b/src/components/common/CoverArt.tsx @@ -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 }) => ( - - - + + + ); const CoverArt = () => ( - + <> setPlaceholderVisible(true)} + onError={() => { + setLoading(false); + setPlaceholderVisible(true); + }} onLoadEnd={() => setLoading(false)} /> - + ); return ( - {!coverArtUri ? : } + {!coverArtUri ? : } ); } -export default React.memo(AlbumCover); +export default React.memo(CoverArt); diff --git a/src/components/common/GradientScrollView.tsx b/src/components/common/GradientScrollView.tsx new file mode 100644 index 0000000..9c6a6ab --- /dev/null +++ b/src/components/common/GradientScrollView.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { ScrollView, ScrollViewProps } from 'react-native'; +import GradientBackground from './GradientBackground'; + +const GradientScrollView: React.FC = (props) => ( + + + {props.children} + +); + +export default GradientScrollView; diff --git a/src/components/library/AlbumsTab.tsx b/src/components/library/AlbumsTab.tsx index 2483ec4..1edf13b 100644 --- a/src/components/library/AlbumsTab.tsx +++ b/src/components/library/AlbumsTab.tsx @@ -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 })} > - = ({ item }) => ( - - = ({ item }) => { + const navigation = useNavigation(); + const artistInfo = useAtomValue(artistInfoAtomFamily(item.id)); + + return ( + - {item.name} - + onPress={() => navigation.navigate('ArtistView', { id: item.id, title: item.name })} + > + + {/* */} + {item.name} + + ); +}; + +const ArtistItemLoader: React.FC<{ item: Artist }> = (props) => ( + Loading...}> + + ); const ArtistsList = () => { @@ -39,7 +62,7 @@ const ArtistsList = () => { }); const renderItem: React.FC<{ item: Artist }> = ({ item }) => ( - + ); return ( diff --git a/src/components/navigation/LibraryTopTabNavigator.tsx b/src/components/navigation/LibraryTopTabNavigator.tsx index b0bd15f..f8dacce 100644 --- a/src/components/navigation/LibraryTopTabNavigator.tsx +++ b/src/components/navigation/LibraryTopTabNavigator.tsx @@ -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 = () => ( ( type LibraryStackParamList = { LibraryTopTabs: undefined, AlbumView: { id: string, title: string }; + ArtistView: { id: string, title: string }; } type AlbumScreenNavigationProp = StackNavigationProp; @@ -66,8 +63,41 @@ const AlbumScreen: React.FC = ({ route }) => ( ); +type ArtistScreenNavigationProp = StackNavigationProp; +type ArtistScreenRouteProp = RouteProp; +type ArtistScreenProps = { + route: ArtistScreenRouteProp, + navigation: ArtistScreenNavigationProp, +}; + +const ArtistScreen: React.FC = ({ route }) => ( + +); + const Stack = createStackNavigator(); +const itemScreenOptions = { + title: '', + headerStyle: { + height: 50, + backgroundColor: colors.gradient.high, + }, + headerTitleContainerStyle: { + marginLeft: -14, + }, + headerLeftContainerStyle: { + marginLeft: 8, + }, + headerTitleStyle: { + ...text.header, + }, + headerBackImage: () => , +} + const LibraryStackNavigator = () => ( @@ -79,27 +109,12 @@ const LibraryStackNavigator = () => ( , - }} + options={itemScreenOptions} + /> + diff --git a/src/hooks/player.ts b/src/hooks/player.ts index ca8feea..9f2b256 100644 --- a/src/hooks/player.ts +++ b/src/hooks/player.ts @@ -9,6 +9,7 @@ function mapSongToTrack(song: Song): Track { artist: song.artist || 'Unknown Artist', url: song.streamUri, artwork: song.coverArtUri, + duration: song.duration, } } diff --git a/src/models/music.ts b/src/models/music.ts index 6d70bf2..f846f9a 100644 --- a/src/models/music.ts +++ b/src/models/music.ts @@ -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 { diff --git a/src/state/music.ts b/src/state/music.ts index 09ec7c8..d3a2f82 100644 --- a/src/state/music.ts +++ b/src/state/music.ts @@ -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([]); @@ -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 atom(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, diff --git a/src/subsonic/api.ts b/src/subsonic/api.ts index 1a1b149..d5a7967 100644 --- a/src/subsonic/api.ts +++ b/src/subsonic/api.ts @@ -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(xml, new GetArtistInfo2Response(xml)); } + async getArtist(params: GetArtistParams): Promise> { + const xml = await this.apiGetXml('getArtist', params); + return new SubsonicResponse(xml, new GetArtistResponse(xml)); + } + // // Album/song lists // diff --git a/src/subsonic/elements.ts b/src/subsonic/elements.ts index 77cf713..a112216 100644 --- a/src/subsonic/elements.ts +++ b/src/subsonic/elements.ts @@ -72,6 +72,53 @@ export class ArtistElement extends BaseArtistElement { } } +export class BaseArtistInfoElement { + 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 { + constructor(e: Element) { + super(e, ArtistElement); + } +} +export class ArtistInfo2Element extends BaseArtistInfoElement { + constructor(e: Element) { + super(e, ArtistID3Element); + } +} + export class DirectoryElement { id: string; parent?: string; diff --git a/src/subsonic/params.ts b/src/subsonic/params.ts index 2f3cc3b..688248c 100644 --- a/src/subsonic/params.ts +++ b/src/subsonic/params.ts @@ -23,6 +23,10 @@ export type GetAlbumParams = { id: string; } +export type GetArtistParams = { + id: string; +} + // // Album/song lists diff --git a/src/subsonic/responses.ts b/src/subsonic/responses.ts index a265a4e..637dc47 100644 --- a/src/subsonic/responses.ts +++ b/src/subsonic/responses.ts @@ -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 { - 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 { - constructor(xml: Document) { - super(xml, ArtistElement); - } -} +export class GetArtistInfo2Response { + artistInfo: ArtistInfo2Element; -export class GetArtistInfo2Response extends BaseGetArtistInfoResponse { constructor(xml: Document) { - super(xml, ArtistID3Element); + this.artistInfo = new ArtistInfo2Element(xml.getElementsByTagName('artistInfo2')[0]); } } diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..72d9897 --- /dev/null +++ b/src/util.ts @@ -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; +}