mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +01:00
streaming tracks!
also managing the queue for playing from album view
This commit is contained in:
parent
666e1e3e69
commit
9e2740c84e
@ -1,7 +1,9 @@
|
|||||||
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, useState } from 'react';
|
||||||
import { ScrollView, Text, useWindowDimensions, View, Image, Pressable, GestureResponderEvent } from 'react-native';
|
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 { albumAtomFamily } from '../../state/music';
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors';
|
||||||
import text from '../../styles/text';
|
import text from '../../styles/text';
|
||||||
@ -24,15 +26,21 @@ const Button: React.FC<{
|
|||||||
title: string;
|
title: string;
|
||||||
onPress: (event: GestureResponderEvent) => void;
|
onPress: (event: GestureResponderEvent) => void;
|
||||||
}> = ({ title, onPress }) => {
|
}> = ({ title, onPress }) => {
|
||||||
|
const [opacity, setOpacity] = useState(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
|
onPressIn={() => setOpacity(0.6)}
|
||||||
|
onPressOut={() => setOpacity(1)}
|
||||||
|
onLongPress={() => setOpacity(1)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.accent,
|
backgroundColor: colors.accent,
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
minHeight: 42,
|
minHeight: 42,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
borderRadius: 1000,
|
borderRadius: 1000,
|
||||||
|
opacity,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ ...text.button }}>{title}</Text>
|
<Text style={{ ...text.button }}>{title}</Text>
|
||||||
@ -40,13 +48,90 @@ const Button: React.FC<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const songEvents = [
|
||||||
|
TrackPlayerEvents.PLAYBACK_STATE,
|
||||||
|
TrackPlayerEvents.PLAYBACK_TRACK_CHANGED,
|
||||||
|
]
|
||||||
|
|
||||||
|
const SongItem: React.FC<{
|
||||||
|
id: string;
|
||||||
|
title: string
|
||||||
|
artist?: string;
|
||||||
|
onPress: (event: GestureResponderEvent) => void;
|
||||||
|
}> = ({ id, title, artist, onPress }) => {
|
||||||
|
const [opacity, setOpacity] = useState(1);
|
||||||
|
const currentTrackId = useCurrentTrackId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
marginTop: 20,
|
||||||
|
marginLeft: 4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
onPress={onPress}
|
||||||
|
onPressIn={() => setOpacity(0.6)}
|
||||||
|
onPressOut={() => setOpacity(1)}
|
||||||
|
onLongPress={() => setOpacity(1)}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
opacity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{
|
||||||
|
...text.songListTitle,
|
||||||
|
color: currentTrackId === id ? colors.accent : colors.text.primary,
|
||||||
|
}}>{title}</Text>
|
||||||
|
<Text style={text.songListSubtitle}>{artist}</Text>
|
||||||
|
</Pressable>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 10,
|
||||||
|
}}>
|
||||||
|
{/* <Text style={text.songListSubtitle}>{secondsToTime(duration || 0)}</Text> */}
|
||||||
|
<Image
|
||||||
|
source={require('../../../res/star.png')}
|
||||||
|
style={{
|
||||||
|
height: 28,
|
||||||
|
width: 28,
|
||||||
|
tintColor: colors.text.secondary,
|
||||||
|
marginLeft: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
source={require('../../../res/more_vertical.png')}
|
||||||
|
style={{
|
||||||
|
height: 28,
|
||||||
|
width: 28,
|
||||||
|
tintColor: colors.text.secondary,
|
||||||
|
marginLeft: 12,
|
||||||
|
marginRight: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const AlbumDetails: React.FC<{
|
const AlbumDetails: React.FC<{
|
||||||
id: string,
|
id: string,
|
||||||
}> = ({ id }) => {
|
}> = ({ id }) => {
|
||||||
const album = useAtomValue(albumAtomFamily(id));
|
const album = useAtomValue(albumAtomFamily(id));
|
||||||
const layout = useWindowDimensions();
|
const layout = useWindowDimensions();
|
||||||
|
const setQueue = useSetQueue();
|
||||||
|
|
||||||
const coverSize = layout.width - layout.width / 2;
|
const coverSize = layout.width - layout.width / 2.5;
|
||||||
|
|
||||||
|
if (!album) {
|
||||||
|
return (
|
||||||
|
<Text style={text.paragraph}>No Album</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@ -61,7 +146,7 @@ const AlbumDetails: React.FC<{
|
|||||||
<AlbumCover
|
<AlbumCover
|
||||||
height={coverSize}
|
height={coverSize}
|
||||||
width={coverSize}
|
width={coverSize}
|
||||||
coverArtUri={album?.coverArtUri}
|
coverArtUri={album.coverArtUri}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={{
|
<Text style={{
|
||||||
@ -69,7 +154,7 @@ const AlbumDetails: React.FC<{
|
|||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
width: layout.width - layout.width / 8,
|
width: layout.width - layout.width / 8,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}>{album?.name}</Text>
|
}}>{album.name}</Text>
|
||||||
|
|
||||||
<Text style={{
|
<Text style={{
|
||||||
...text.itemSubtitle,
|
...text.itemSubtitle,
|
||||||
@ -78,14 +163,14 @@ const AlbumDetails: React.FC<{
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
width: layout.width - layout.width / 8,
|
width: layout.width - layout.width / 8,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}>{album?.artist}{album?.year ? ` • ${album.year}` : ''}</Text>
|
}}>{album.artist}{album.year ? ` • ${album.year}` : ''}</Text>
|
||||||
|
|
||||||
<View style={{
|
<View style={{
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
}}>
|
}}>
|
||||||
<Button
|
<Button
|
||||||
title='Play Album'
|
title='Play Album'
|
||||||
onPress={() => null}
|
onPress={() => setQueue(album.songs, album.songs[0].id)}
|
||||||
/>
|
/>
|
||||||
{/* <View style={{ width: 6, }}></View>
|
{/* <View style={{ width: 6, }}></View>
|
||||||
<Button
|
<Button
|
||||||
@ -99,49 +184,16 @@ const AlbumDetails: React.FC<{
|
|||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
marginBottom: 30,
|
marginBottom: 30,
|
||||||
}}>
|
}}>
|
||||||
{album?.songs
|
{album.songs
|
||||||
.sort((a, b) => (a.track as number) - (b.track as number))
|
.sort((a, b) => (a.track as number) - (b.track as number))
|
||||||
.map(s => (
|
.map(s => (
|
||||||
<View key={s.id} style={{
|
<SongItem
|
||||||
marginTop: 20,
|
key={s.id}
|
||||||
marginLeft: 4,
|
id={s.id}
|
||||||
flexDirection: 'row',
|
title={s.title}
|
||||||
justifyContent: 'space-between',
|
artist={s.artist}
|
||||||
alignItems: 'center',
|
onPress={() => setQueue(album.songs, s.id)}
|
||||||
}}>
|
/>
|
||||||
<View style={{
|
|
||||||
flexShrink: 1,
|
|
||||||
}}>
|
|
||||||
<Text style={text.songListTitle}>{s.title}</Text>
|
|
||||||
<Text style={text.songListSubtitle}>{s.artist}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={{
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginLeft: 10,
|
|
||||||
}}>
|
|
||||||
{/* <Text style={text.songListSubtitle}>{secondsToTime(s.duration || 0)}</Text> */}
|
|
||||||
<Image
|
|
||||||
source={require('../../../res/star.png')}
|
|
||||||
style={{
|
|
||||||
height: 28,
|
|
||||||
width: 28,
|
|
||||||
tintColor: colors.text.secondary,
|
|
||||||
marginLeft: 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
source={require('../../../res/more_vertical.png')}
|
|
||||||
style={{
|
|
||||||
height: 28,
|
|
||||||
width: 28,
|
|
||||||
tintColor: colors.text.secondary,
|
|
||||||
marginLeft: 12,
|
|
||||||
marginRight: 2,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Text, View, Image, Pressable } from 'react-native';
|
import { Text, View, Image, Pressable } from 'react-native';
|
||||||
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text';
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
const icons: {[key: string]: any} = {
|
const icons: {[key: string]: any} = {
|
||||||
home: {
|
home: {
|
||||||
@ -24,6 +25,57 @@ const icons: {[key: string]: any} = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BottomTabButton: React.FC<{
|
||||||
|
routeKey: string;
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
isFocused: boolean;
|
||||||
|
img: { regular: number, fill: number };
|
||||||
|
navigation: any;
|
||||||
|
}> = ({ routeKey, label, name, isFocused, img, navigation }) => {
|
||||||
|
const [opacity, setOpacity] = useState(1);
|
||||||
|
|
||||||
|
const onPress = () => {
|
||||||
|
const event = navigation.emit({
|
||||||
|
type: 'tabPress',
|
||||||
|
target: routeKey,
|
||||||
|
canPreventDefault: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isFocused && !event.defaultPrevented) {
|
||||||
|
navigation.navigate(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={onPress}
|
||||||
|
onPressIn={() => setOpacity(0.6)}
|
||||||
|
onPressOut={() => setOpacity(1)}
|
||||||
|
style={{
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
opacity,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FastImage
|
||||||
|
source={isFocused ? img.fill : img.regular}
|
||||||
|
style={{
|
||||||
|
height: 26,
|
||||||
|
width: 26,
|
||||||
|
}}
|
||||||
|
tintColor={isFocused ? colors.text.primary : colors.text.secondary}
|
||||||
|
/>
|
||||||
|
<Text style={{
|
||||||
|
...textStyles.xsmall,
|
||||||
|
color: isFocused ? colors.text.primary : colors.text.secondary,
|
||||||
|
}}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
|
const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigation }) => {
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View style={{
|
||||||
@ -43,46 +95,15 @@ const BottomTabBar: React.FC<BottomTabBarProps> = ({ state, descriptors, navigat
|
|||||||
? options.title
|
? options.title
|
||||||
: route.name;
|
: route.name;
|
||||||
|
|
||||||
const isFocused = state.index === index;
|
return <BottomTabButton
|
||||||
const img = icons[options.icon];
|
key={route.key}
|
||||||
|
routeKey={route.key}
|
||||||
const onPress = () => {
|
label={label}
|
||||||
const event = navigation.emit({
|
name={route.name}
|
||||||
type: 'tabPress',
|
isFocused={state.index === index}
|
||||||
target: route.key,
|
img={icons[options.icon]}
|
||||||
canPreventDefault: true,
|
navigation={navigation}
|
||||||
});
|
/>;
|
||||||
|
|
||||||
if (!isFocused && !event.defaultPrevented) {
|
|
||||||
navigation.navigate(route.name);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
key={route.key}
|
|
||||||
onPress={onPress}
|
|
||||||
style={{
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FastImage
|
|
||||||
source={isFocused ? img.fill : img.regular}
|
|
||||||
style={{
|
|
||||||
height: 26,
|
|
||||||
width: 26,
|
|
||||||
}}
|
|
||||||
tintColor={isFocused ? colors.text.primary : colors.text.secondary}
|
|
||||||
/>
|
|
||||||
<Text style={{
|
|
||||||
...textStyles.xsmall,
|
|
||||||
color: isFocused ? colors.text.primary : colors.text.secondary,
|
|
||||||
}}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
75
src/hooks/player.ts
Normal file
75
src/hooks/player.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import TrackPlayer, { STATE_NONE, STATE_STOPPED, Track, TrackPlayerEvents, useTrackPlayerEvents } from "react-native-track-player";
|
||||||
|
import { Song } from "../models/music";
|
||||||
|
|
||||||
|
function mapSongToTrack(song: Song): Track {
|
||||||
|
return {
|
||||||
|
id: song.id,
|
||||||
|
title: song.title,
|
||||||
|
artist: song.artist || 'Unknown Artist',
|
||||||
|
url: song.streamUri,
|
||||||
|
artwork: song.coverArtUri,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTrackEvents = [
|
||||||
|
TrackPlayerEvents.PLAYBACK_STATE,
|
||||||
|
TrackPlayerEvents.PLAYBACK_TRACK_CHANGED,
|
||||||
|
TrackPlayerEvents.REMOTE_STOP,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const useCurrentTrackId = () => {
|
||||||
|
const [currentTrackId, setCurrentTrackId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useTrackPlayerEvents(currentTrackEvents, async (event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case TrackPlayerEvents.PLAYBACK_STATE:
|
||||||
|
switch (event.state) {
|
||||||
|
case STATE_NONE:
|
||||||
|
case STATE_STOPPED:
|
||||||
|
setCurrentTrackId(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TrackPlayerEvents.PLAYBACK_TRACK_CHANGED:
|
||||||
|
setCurrentTrackId(await TrackPlayer.getCurrentTrack());
|
||||||
|
break;
|
||||||
|
case TrackPlayerEvents.REMOTE_STOP:
|
||||||
|
setCurrentTrackId(null);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentTrackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetQueue = () => {
|
||||||
|
return async (songs: Song[], playId?: string) => {
|
||||||
|
await TrackPlayer.reset();
|
||||||
|
const tracks = songs.map(mapSongToTrack);
|
||||||
|
|
||||||
|
if (!playId) {
|
||||||
|
await TrackPlayer.add(tracks);
|
||||||
|
} else if (playId === tracks[0].id) {
|
||||||
|
await TrackPlayer.add(tracks);
|
||||||
|
await TrackPlayer.play();
|
||||||
|
} else {
|
||||||
|
const playIndex = tracks.findIndex(t => t.id === playId);
|
||||||
|
const tracks1 = tracks.slice(0, playIndex);
|
||||||
|
const tracks2 = tracks.slice(playIndex);
|
||||||
|
|
||||||
|
console.log('tracks1: ' + JSON.stringify(tracks1.map(t => t.title)));
|
||||||
|
console.log('tracks2: ' + JSON.stringify(tracks2.map(t => t.title)));
|
||||||
|
|
||||||
|
await TrackPlayer.add(tracks2);
|
||||||
|
await TrackPlayer.play();
|
||||||
|
|
||||||
|
await TrackPlayer.add(tracks1, playId);
|
||||||
|
|
||||||
|
const queue = await TrackPlayer.getQueue();
|
||||||
|
console.log('queue: ' + JSON.stringify(queue.map(t => t.title)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/hooks/subsonic.ts
Normal file
14
src/hooks/subsonic.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { useAtomValue } from "jotai/utils"
|
||||||
|
import { activeServerAtom } from "../state/settings"
|
||||||
|
import { SubsonicApiClient } from "../subsonic/api";
|
||||||
|
|
||||||
|
export const useSubsonicApi = () => {
|
||||||
|
const activeServer = useAtomValue(activeServerAtom);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!activeServer) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return new SubsonicApiClient(activeServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -42,6 +42,9 @@ export interface Song {
|
|||||||
discNumber?: number;
|
discNumber?: number;
|
||||||
created?: Date;
|
created?: Date;
|
||||||
starred?: Date;
|
starred?: Date;
|
||||||
|
|
||||||
|
streamUri: string;
|
||||||
|
coverArtUri?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DownloadedSong = {
|
export type DownloadedSong = {
|
||||||
|
|||||||
@ -17,5 +17,7 @@ module.exports = async function() {
|
|||||||
TrackPlayer.play();
|
TrackPlayer.play();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
TrackPlayer.addEventListener('remote-next', () => TrackPlayer.skipToNext().catch(() => {}));
|
||||||
|
TrackPlayer.addEventListener('remote-previous', () => TrackPlayer.skipToPrevious().catch(() => {}));
|
||||||
};
|
};
|
||||||
|
|||||||
@ -82,8 +82,12 @@ function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapChild(child: ChildElement): Song {
|
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
||||||
return { ...child }
|
return {
|
||||||
|
...child,
|
||||||
|
streamUri: client.streamUri({ id: child.id }),
|
||||||
|
coverArtUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt }) : undefined,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapAlbumID3WithSongs(
|
function mapAlbumID3WithSongs(
|
||||||
@ -93,6 +97,6 @@ function mapAlbumID3WithSongs(
|
|||||||
): AlbumWithSongs {
|
): AlbumWithSongs {
|
||||||
return {
|
return {
|
||||||
...mapAlbumID3(album, client),
|
...mapAlbumID3(album, client),
|
||||||
songs: songs.map(s => mapChild(s)),
|
songs: songs.map(s => mapChildToSong(s, client)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,6 @@ export default {
|
|||||||
mid: '#191919',
|
mid: '#191919',
|
||||||
low: '#000000',
|
low: '#000000',
|
||||||
},
|
},
|
||||||
accent: '#c260e5',
|
accent: '#b134db',
|
||||||
accentLow: '#50285e',
|
accentLow: '#511c63',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { DOMParser } from 'xmldom';
|
import { DOMParser } from 'xmldom';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams } from './params';
|
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 { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, SubsonicResponse } from './responses';
|
||||||
import { Server } from '../models/settings';
|
import { Server } from '../models/settings';
|
||||||
import paths from '../paths';
|
import paths from '../paths';
|
||||||
@ -193,4 +193,8 @@ export class SubsonicApiClient {
|
|||||||
getCoverArtUri(params: GetCoverArtParams): string {
|
getCoverArtUri(params: GetCoverArtParams): string {
|
||||||
return this.buildUrl('getCoverArt', params);
|
return this.buildUrl('getCoverArt', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
streamUri(params: StreamParams): string {
|
||||||
|
return this.buildUrl('stream', params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,3 +62,10 @@ export type GetCoverArtParams = {
|
|||||||
id: string;
|
id: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type StreamParams = {
|
||||||
|
id: string;
|
||||||
|
maxBitRate?: number;
|
||||||
|
format?: string;
|
||||||
|
estimateContentLength?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user