added top songs to artist view

This commit is contained in:
austinried 2021-07-17 10:39:18 +09:00
parent 62a721ba4d
commit de342c0830
10 changed files with 168 additions and 119 deletions

View File

@ -14,7 +14,7 @@ interface ArtistArtSizeProps {
} }
interface ArtistArtXUpProps extends ArtistArtSizeProps { interface ArtistArtXUpProps extends ArtistArtSizeProps {
coverArtUris: string[] albumCoverUris: string[]
} }
interface ArtistArtProps extends ArtistArtSizeProps { interface ArtistArtProps extends ArtistArtSizeProps {
@ -39,7 +39,7 @@ const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, chi
) )
} }
const FourUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris }) => { const FourUp = React.memo<ArtistArtXUpProps>(({ height, width, albumCoverUris }) => {
const halfHeight = height / 2 const halfHeight = height / 2
const halfWidth = width / 2 const halfWidth = width / 2
@ -47,24 +47,24 @@ const FourUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris }) =
<PlaceholderContainer height={height} width={width}> <PlaceholderContainer height={height} width={width}>
<View style={[styles.artRow, { width, height: halfHeight }]}> <View style={[styles.artRow, { width, height: halfHeight }]}>
<FastImage <FastImage
source={{ uri: coverArtUris[0] }} source={{ uri: albumCoverUris[0] }}
style={{ height: halfHeight, width: halfWidth }} style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
<FastImage <FastImage
source={{ uri: coverArtUris[1] }} source={{ uri: albumCoverUris[1] }}
style={{ height: halfHeight, width: halfWidth }} style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
</View> </View>
<View style={[styles.artRow, { width, height: halfHeight }]}> <View style={[styles.artRow, { width, height: halfHeight }]}>
<FastImage <FastImage
source={{ uri: coverArtUris[2] }} source={{ uri: albumCoverUris[2] }}
style={{ height: halfHeight, width: halfWidth }} style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
<FastImage <FastImage
source={{ uri: coverArtUris[3] }} source={{ uri: albumCoverUris[3] }}
style={{ height: halfHeight, width: halfWidth }} style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
@ -73,7 +73,7 @@ const FourUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris }) =
) )
}) })
const ThreeUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris }) => { const ThreeUp = React.memo<ArtistArtXUpProps>(({ height, width, albumCoverUris }) => {
const halfHeight = height / 2 const halfHeight = height / 2
const halfWidth = width / 2 const halfWidth = width / 2
@ -81,19 +81,19 @@ const ThreeUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris })
<PlaceholderContainer height={height} width={width}> <PlaceholderContainer height={height} width={width}>
<View style={[styles.artRow, { width, height: halfHeight }]}> <View style={[styles.artRow, { width, height: halfHeight }]}>
<FastImage <FastImage
source={{ uri: coverArtUris[0] }} source={{ uri: albumCoverUris[0] }}
style={{ height: halfHeight, width }} style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
</View> </View>
<View style={[styles.artRow, { width, height: halfHeight }]}> <View style={[styles.artRow, { width, height: halfHeight }]}>
<FastImage <FastImage
source={{ uri: coverArtUris[1] }} source={{ uri: albumCoverUris[1] }}
style={{ height: halfHeight, width: halfWidth }} style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
<FastImage <FastImage
source={{ uri: coverArtUris[2] }} source={{ uri: albumCoverUris[2] }}
style={{ height: halfHeight, width: halfWidth }} style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
@ -102,21 +102,21 @@ const ThreeUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris })
) )
}) })
const TwoUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris }) => { const TwoUp = React.memo<ArtistArtXUpProps>(({ height, width, albumCoverUris }) => {
const halfHeight = height / 2 const halfHeight = height / 2
return ( return (
<PlaceholderContainer height={height} width={width}> <PlaceholderContainer height={height} width={width}>
<View style={[styles.artRow, { width, height: halfHeight }]}> <View style={[styles.artRow, { width, height: halfHeight }]}>
<FastImage <FastImage
source={{ uri: coverArtUris[0] }} source={{ uri: albumCoverUris[0] }}
style={{ height: halfHeight, width }} style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
</View> </View>
<View style={[styles.artRow, { width, height: halfHeight }]}> <View style={[styles.artRow, { width, height: halfHeight }]}>
<FastImage <FastImage
source={{ uri: coverArtUris[1] }} source={{ uri: albumCoverUris[1] }}
style={{ height: halfHeight, width }} style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover} resizeMode={FastImage.resizeMode.cover}
/> />
@ -125,9 +125,9 @@ const TwoUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris }) =>
) )
}) })
const OneUp = React.memo<ArtistArtXUpProps>(({ height, width, coverArtUris }) => ( const OneUp = React.memo<ArtistArtXUpProps>(({ height, width, albumCoverUris }) => (
<PlaceholderContainer height={height} width={width}> <PlaceholderContainer height={height} width={width}>
<FastImage source={{ uri: coverArtUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} /> <FastImage source={{ uri: albumCoverUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
</PlaceholderContainer> </PlaceholderContainer>
)) ))
@ -141,22 +141,22 @@ const ArtistArt = React.memo<ArtistArtProps>(({ id, height, width }) => {
const Placeholder = () => { const Placeholder = () => {
const none = <NoneUp height={height} width={width} /> const none = <NoneUp height={height} width={width} />
if (!artistArt || !artistArt.coverArtUris) { if (!artistArt || !artistArt.albumCoverUris) {
return none return none
} }
const { coverArtUris } = artistArt const { albumCoverUris } = artistArt
if (coverArtUris.length >= 4) { if (albumCoverUris.length >= 4) {
return <FourUp height={height} width={width} coverArtUris={coverArtUris} /> return <FourUp height={height} width={width} albumCoverUris={albumCoverUris} />
} }
if (coverArtUris.length === 3) { if (albumCoverUris.length === 3) {
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} /> return <ThreeUp height={height} width={width} albumCoverUris={albumCoverUris} />
} }
if (coverArtUris.length === 2) { if (albumCoverUris.length === 2) {
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} /> return <TwoUp height={height} width={width} albumCoverUris={albumCoverUris} />
} }
if (coverArtUris.length === 1) { if (albumCoverUris.length === 1) {
return <OneUp height={height} width={width} coverArtUris={coverArtUris} /> return <OneUp height={height} width={width} albumCoverUris={albumCoverUris} />
} }
return none return none

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { ActivityIndicator, LayoutChangeEvent, StyleSheet, View } from 'react-native' import { ActivityIndicator, LayoutChangeEvent, StyleSheet, View, ViewStyle } from 'react-native'
import FastImage from 'react-native-fast-image' import FastImage from 'react-native-fast-image'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
import IconFA5 from 'react-native-vector-icons/FontAwesome5' import IconFA5 from 'react-native-vector-icons/FontAwesome5'
@ -12,7 +12,8 @@ const CoverArt: React.FC<{
width?: string | number width?: string | number
coverArtUri?: string coverArtUri?: string
resizeMode?: keyof typeof FastImage.resizeMode resizeMode?: keyof typeof FastImage.resizeMode
}> = ({ PlaceholderComponent, placeholderIcon, height, width, coverArtUri, resizeMode }) => { style?: ViewStyle
}> = ({ PlaceholderComponent, placeholderIcon, height, width, coverArtUri, resizeMode, style }) => {
const [placeholderVisible, setPlaceholderVisible] = useState(false) const [placeholderVisible, setPlaceholderVisible] = useState(false)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 }) const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 })
@ -47,7 +48,7 @@ const CoverArt: React.FC<{
} }
return ( return (
<View style={{ ...styles.container, height, width }} onLayout={onLayout}> <View style={[style, { height, width }]} onLayout={onLayout}>
{coverArtUri ? <Image /> : <></>} {coverArtUri ? <Image /> : <></>}
<View style={{ ...styles.placeholderContainer, opacity: placeholderVisible ? 1 : 0 }}> <View style={{ ...styles.placeholderContainer, opacity: placeholderVisible ? 1 : 0 }}>
{PlaceholderComponent ? <PlaceholderComponent /> : <Placeholder />} {PlaceholderComponent ? <PlaceholderComponent /> : <Placeholder />}

View File

@ -0,0 +1,84 @@
import { Song } from '@app/models/music'
import { currentTrackAtom } from '@app/state/trackplayer'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useAtomValue } from 'jotai/utils'
import React from 'react'
import { GestureResponderEvent, StyleSheet, Text, View } from 'react-native'
import IconFA from 'react-native-vector-icons/FontAwesome'
import IconMat from 'react-native-vector-icons/MaterialIcons'
import CoverArt from './CoverArt'
import PressableOpacity from './PressableOpacity'
const SongItem: React.FC<{
song: Song
onPress?: (event: GestureResponderEvent) => void
showArt?: boolean
subtitle?: 'artist' | 'album'
}> = ({ song, onPress, showArt, subtitle }) => {
const currentTrack = useAtomValue(currentTrackAtom)
subtitle = subtitle || 'artist'
return (
<View style={styles.container}>
<PressableOpacity onPress={onPress} style={styles.item}>
{showArt ? <CoverArt coverArtUri={song.coverArtThumbUri} style={styles.art} height={50} width={50} /> : <></>}
<View style={styles.text}>
<Text style={[styles.title, { color: currentTrack?.id === song.id ? colors.accent : colors.text.primary }]}>
{song.title}
</Text>
<Text style={styles.subtitle}>{song[subtitle]}</Text>
</View>
</PressableOpacity>
<View style={styles.controls}>
<PressableOpacity onPress={undefined}>
<IconFA name="star-o" size={26} color={colors.text.primary} />
</PressableOpacity>
<PressableOpacity onPress={undefined} style={styles.more}>
<IconMat name="more-vert" size={32} color="white" />
</PressableOpacity>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
marginBottom: 14,
minHeight: 50,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
item: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
},
art: {
marginRight: 10,
},
text: {
flex: 1,
},
title: {
fontSize: 16,
fontFamily: font.semiBold,
},
subtitle: {
fontSize: 14,
fontFamily: font.regular,
color: colors.text.secondary,
},
controls: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 10,
},
more: {
marginLeft: 8,
},
})
export default React.memo(SongItem)

View File

@ -9,12 +9,14 @@ export interface ArtistInfo extends Artist {
mediumImageUrl?: string mediumImageUrl?: string
largeImageUrl?: string largeImageUrl?: string
coverArtUris: string[] albumCoverUris: string[]
topSongs: Song[]
} }
export interface ArtistArt { export interface ArtistArt {
uri?: string uri?: string
coverArtUris: string[] albumCoverUris: string[]
} }
export interface AlbumListItem { export interface AlbumListItem {

View File

@ -1,78 +1,16 @@
import Button from '@app/components/Button'
import CoverArt from '@app/components/CoverArt'
import GradientBackground from '@app/components/GradientBackground'
import ImageGradientScrollView from '@app/components/ImageGradientScrollView'
import SongItem from '@app/components/SongItem'
import { albumAtomFamily } from '@app/state/music'
import { useSetQueue } from '@app/state/trackplayer'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { ActivityIndicator, GestureResponderEvent, StyleSheet, Text, View } from 'react-native' import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
import IconFA from 'react-native-vector-icons/FontAwesome'
import IconMat from 'react-native-vector-icons/MaterialIcons'
import { albumAtomFamily } from '@app/state/music'
import { currentTrackAtom, useSetQueue } from '@app/state/trackplayer'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import Button from '@app/components/Button'
import GradientBackground from '@app/components/GradientBackground'
import ImageGradientScrollView from '@app/components/ImageGradientScrollView'
import PressableOpacity from '@app/components/PressableOpacity'
import CoverArt from '@app/components/CoverArt'
const SongItem: React.FC<{
id: string
title: string
artist?: string
track?: number
onPress: (event: GestureResponderEvent) => void
}> = ({ id, title, artist, onPress }) => {
const currentTrack = useAtomValue(currentTrackAtom)
return (
<View style={songStyles.container}>
<PressableOpacity onPress={onPress} style={songStyles.text}>
<Text style={{ ...songStyles.title, color: currentTrack?.id === id ? colors.accent : colors.text.primary }}>
{title}
</Text>
<Text style={songStyles.subtitle}>{artist}</Text>
</PressableOpacity>
<View style={songStyles.controls}>
<PressableOpacity onPress={undefined}>
<IconFA name="star-o" size={26} color={colors.text.primary} />
</PressableOpacity>
<PressableOpacity onPress={undefined} style={songStyles.more}>
<IconMat name="more-vert" size={32} color="white" />
</PressableOpacity>
</View>
</View>
)
}
const songStyles = StyleSheet.create({
container: {
marginTop: 20,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
text: {
flex: 1,
alignItems: 'flex-start',
width: 100,
},
title: {
fontSize: 16,
fontFamily: font.semiBold,
},
subtitle: {
fontSize: 14,
fontFamily: font.regular,
color: colors.text.secondary,
},
controls: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 10,
},
more: {
marginLeft: 8,
},
})
const AlbumDetails: React.FC<{ const AlbumDetails: React.FC<{
id: string id: string
@ -111,14 +49,7 @@ const AlbumDetails: React.FC<{
} }
}) })
.map(s => ( .map(s => (
<SongItem <SongItem key={s.id} song={s} onPress={() => setQueue(album.songs, album.name, s.id)} />
key={s.id}
id={s.id}
title={s.title}
artist={s.artist}
track={s.track}
onPress={() => setQueue(album.songs, album.name, s.id)}
/>
))} ))}
</View> </View>
</View> </View>
@ -181,7 +112,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
}, },
songs: { songs: {
marginTop: 10, marginTop: 26,
marginBottom: 30, marginBottom: 30,
width: '100%', width: '100%',
}, },

View File

@ -1,6 +1,7 @@
import CoverArt from '@app/components/CoverArt' import CoverArt from '@app/components/CoverArt'
import GradientScrollView from '@app/components/GradientScrollView' import GradientScrollView from '@app/components/GradientScrollView'
import PressableOpacity from '@app/components/PressableOpacity' import PressableOpacity from '@app/components/PressableOpacity'
import SongItem from '@app/components/SongItem'
import { Album } from '@app/models/music' import { Album } from '@app/models/music'
import { artistInfoAtomFamily } from '@app/state/music' import { artistInfoAtomFamily } from '@app/state/music'
import colors from '@app/styles/colors' import colors from '@app/styles/colors'
@ -22,7 +23,6 @@ const AlbumItem = React.memo<{
return ( return (
<PressableOpacity <PressableOpacity
onPress={() => navigation.navigate('AlbumView', { id: album.id, title: album.name })} onPress={() => navigation.navigate('AlbumView', { id: album.id, title: album.name })}
key={album.id}
style={[styles.albumItem, { width }]}> style={[styles.albumItem, { width }]}>
<CoverArt coverArtUri={album.coverArtThumbUri} height={height} width={width} /> <CoverArt coverArtUri={album.coverArtThumbUri} height={height} width={width} />
<Text style={styles.albumTitle}>{album.name}</Text> <Text style={styles.albumTitle}>{album.name}</Text>
@ -52,6 +52,10 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
<Text style={styles.title}>{artist.name}</Text> <Text style={styles.title}>{artist.name}</Text>
</View> </View>
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.header}>Top Songs</Text>
{artist.topSongs.map(s => (
<SongItem key={s.id} song={s} showArt={true} subtitle="album" />
))}
<Text style={styles.header}>Albums</Text> <Text style={styles.header}>Albums</Text>
<View style={styles.albums} onLayout={layout.onLayout}> <View style={styles.albums} onLayout={layout.onLayout}>
{artist.albums.map(a => ( {artist.albums.map(a => (
@ -112,7 +116,8 @@ const styles = StyleSheet.create({
fontFamily: font.bold, fontFamily: font.bold,
fontSize: 24, fontSize: 24,
color: colors.text.primary, color: colors.text.primary,
marginTop: 14, marginTop: 20,
marginBottom: 14,
}, },
artistImage: { artistImage: {
position: 'absolute', position: 'absolute',
@ -120,7 +125,6 @@ const styles = StyleSheet.create({
height: artistImageHeight, height: artistImageHeight,
}, },
albums: { albums: {
marginTop: 14,
width: '100%', width: '100%',
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',

View File

@ -131,7 +131,9 @@ export const artistInfoAtomFamily = atomFamily((id: string) =>
client.getArtist({ id }), client.getArtist({ id }),
client.getArtistInfo2({ id }), client.getArtistInfo2({ id }),
]) ])
return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, client) const topSongsResponse = await client.getTopSongs({ artist: artistResponse.data.artist.name, count: 50 })
return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, topSongsResponse.data.songs, client)
}), }),
) )
@ -142,7 +144,7 @@ export const artistArtAtomFamily = atomFamily((id: string) =>
return undefined return undefined
} }
const coverArtUris = artistInfo.albums const albumCoverUris = artistInfo.albums
.filter(a => a.coverArtThumbUri !== undefined) .filter(a => a.coverArtThumbUri !== undefined)
.sort((a, b) => { .sort((a, b) => {
if (b.year && a.year) { if (b.year && a.year) {
@ -154,7 +156,7 @@ export const artistArtAtomFamily = atomFamily((id: string) =>
.map(a => a.coverArtThumbUri) as string[] .map(a => a.coverArtThumbUri) as string[]
return { return {
coverArtUris, albumCoverUris,
uri: artistInfo.largeImageUrl, uri: artistInfo.largeImageUrl,
} }
}), }),
@ -171,12 +173,13 @@ function mapArtistID3toArtist(artist: ArtistID3Element): Artist {
function mapArtistInfo( function mapArtistInfo(
artistResponse: GetArtistResponse, artistResponse: GetArtistResponse,
info: ArtistInfo2Element, info: ArtistInfo2Element,
topSongs: ChildElement[],
client: SubsonicApiClient, client: SubsonicApiClient,
): ArtistInfo { ): ArtistInfo {
const { artist, albums } = artistResponse const { artist, albums } = artistResponse
const mappedAlbums = albums.map(a => mapAlbumID3toAlbum(a, client)) const mappedAlbums = albums.map(a => mapAlbumID3toAlbum(a, client))
const coverArtUris = mappedAlbums const albumCoverUris = mappedAlbums
.sort((a, b) => { .sort((a, b) => {
if (a.year && b.year) { if (a.year && b.year) {
return b.year - a.year return b.year - a.year
@ -190,9 +193,10 @@ function mapArtistInfo(
return { return {
...mapArtistID3toArtist(artist), ...mapArtistID3toArtist(artist),
albums: mappedAlbums, albums: mappedAlbums,
coverArtUris, albumCoverUris,
mediumImageUrl: info.mediumImageUrl, mediumImageUrl: info.mediumImageUrl,
largeImageUrl: info.largeImageUrl, largeImageUrl: info.largeImageUrl,
topSongs: topSongs.map(c => mapChildToSong(c, client)).slice(0, 5),
} }
} }

View File

@ -10,6 +10,7 @@ import {
GetCoverArtParams, GetCoverArtParams,
GetIndexesParams, GetIndexesParams,
GetMusicDirectoryParams, GetMusicDirectoryParams,
GetTopSongsParams,
StreamParams, StreamParams,
} from '@app/subsonic/params' } from '@app/subsonic/params'
import { import {
@ -22,6 +23,7 @@ import {
GetArtistsResponse, GetArtistsResponse,
GetIndexesResponse, GetIndexesResponse,
GetMusicDirectoryResponse, GetMusicDirectoryResponse,
GetTopSongsResponse,
SubsonicResponse, SubsonicResponse,
} from '@app/subsonic/responses' } from '@app/subsonic/responses'
import { Server } from '@app/models/settings' import { Server } from '@app/models/settings'
@ -165,6 +167,11 @@ export class SubsonicApiClient {
return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml)) return new SubsonicResponse<GetArtistResponse>(xml, new GetArtistResponse(xml))
} }
async getTopSongs(params: GetTopSongsParams): Promise<SubsonicResponse<GetTopSongsResponse>> {
const xml = await this.apiGetXml('getTopSongs', params)
return new SubsonicResponse<GetTopSongsResponse>(xml, new GetTopSongsResponse(xml))
}
// //
// Album/song lists // Album/song lists
// //

View File

@ -27,6 +27,11 @@ export type GetArtistParams = {
id: string id: string
} }
export type GetTopSongsParams = {
artist: string
count?: number
}
// //
// Album/song lists // Album/song lists
// //

View File

@ -116,6 +116,17 @@ export class GetAlbumResponse {
} }
} }
export class GetTopSongsResponse {
songs: ChildElement[] = []
constructor(xml: Document) {
const childElements = xml.getElementsByTagName('song')
for (let i = 0; i < childElements.length; i++) {
this.songs.push(new ChildElement(childElements[i]))
}
}
}
// //
// Album/song lists // Album/song lists
// //