mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 17:39:27 +01:00
impl songs, whole library refresh
This commit is contained in:
parent
c8ed5bf5cb
commit
50be0a6f85
2
App.tsx
2
App.tsx
@ -3,10 +3,12 @@ import { NavigationContainer } from '@react-navigation/native';
|
|||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
import SplashPage from './src/components/SplashPage';
|
import SplashPage from './src/components/SplashPage';
|
||||||
import RootNavigator from './src/components/navigation/RootNavigator';
|
import RootNavigator from './src/components/navigation/RootNavigator';
|
||||||
|
import MusicManager from './src/components/MusicManager';
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<SplashPage>
|
<SplashPage>
|
||||||
|
<MusicManager />
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<RootNavigator />
|
<RootNavigator />
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button, FlatList, Text, View } from 'react-native';
|
import { Button, FlatList, Text, View } from 'react-native';
|
||||||
import { useRecoilValue, useResetRecoilState } from 'recoil';
|
import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { artistsState, useUpdateArtists } from '../state/artists';
|
|
||||||
import { Artist } from '../models/music';
|
import { Artist } from '../models/music';
|
||||||
|
import { artistsState, isLibraryRefreshingState, libraryRefreshState } from '../state/music';
|
||||||
|
|
||||||
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
||||||
<View>
|
<View>
|
||||||
@ -32,7 +32,8 @@ const List = () => {
|
|||||||
|
|
||||||
const ListPlusControls = () => {
|
const ListPlusControls = () => {
|
||||||
const resetArtists = useResetRecoilState(artistsState);
|
const resetArtists = useResetRecoilState(artistsState);
|
||||||
const updateArtists = useUpdateArtists();
|
const setLibraryRefresh = useSetRecoilState(libraryRefreshState);
|
||||||
|
const isLibraryRefreshing = useRecoilValue(isLibraryRefreshingState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
@ -41,8 +42,9 @@ const ListPlusControls = () => {
|
|||||||
onPress={resetArtists}
|
onPress={resetArtists}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
title='Update from API'
|
title='Refresh Library'
|
||||||
onPress={updateArtists}
|
onPress={() => setLibraryRefresh(true)}
|
||||||
|
disabled={isLibraryRefreshing}
|
||||||
/>
|
/>
|
||||||
<List />
|
<List />
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
104
src/components/MusicManager.tsx
Normal file
104
src/components/MusicManager.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { ActivityIndicator, useWindowDimensions, View } from 'react-native';
|
||||||
|
import { useSetRecoilState, useRecoilValue, useRecoilState } from 'recoil';
|
||||||
|
import { Album, Artist, Song } from '../models/music';
|
||||||
|
import { albumsState, artistsState, isLibraryRefreshingState, libraryRefreshState, songsState } from '../state/music';
|
||||||
|
import { activeServer } from '../state/settings';
|
||||||
|
import colors from '../styles/colors';
|
||||||
|
import { SubsonicApiClient } from '../subsonic/api';
|
||||||
|
|
||||||
|
const RefreshManager = () => {
|
||||||
|
const setArtists = useSetRecoilState(artistsState);
|
||||||
|
const setAlbums = useSetRecoilState(albumsState);
|
||||||
|
const setSongs = useSetRecoilState(songsState);
|
||||||
|
|
||||||
|
const server = useRecoilValue(activeServer);
|
||||||
|
|
||||||
|
const [libraryRefresh, setLibraryRefresh] = useRecoilState(libraryRefreshState);
|
||||||
|
const [isLibraryRefreshing, setIsLibraryRefreshing] = useRecoilState(isLibraryRefreshingState);
|
||||||
|
|
||||||
|
const updateLibrary = async () => {
|
||||||
|
if (!libraryRefresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLibraryRefresh(false);
|
||||||
|
|
||||||
|
if (isLibraryRefreshing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLibraryRefreshing(true);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const client = new SubsonicApiClient(server);
|
||||||
|
|
||||||
|
const artistsResponse = await client.getArtists();
|
||||||
|
const artists: Artist[] = artistsResponse.data.artists.map(x => ({
|
||||||
|
id: x.id,
|
||||||
|
name: x.name,
|
||||||
|
starred: x.starred,
|
||||||
|
coverArt: x.coverArt,
|
||||||
|
}));
|
||||||
|
setArtists(artists);
|
||||||
|
|
||||||
|
const albumsResponse = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 });
|
||||||
|
const albums: Album[] = albumsResponse.data.albums
|
||||||
|
.filter(x => x.artistId !== undefined)
|
||||||
|
.map(x => ({
|
||||||
|
id: x.id,
|
||||||
|
artistId: x.artistId as string,
|
||||||
|
name: x.name,
|
||||||
|
coverArt: x.coverArt,
|
||||||
|
}));
|
||||||
|
setAlbums(albums);
|
||||||
|
|
||||||
|
const songs: Song[] = [];
|
||||||
|
for (const album of albums) {
|
||||||
|
const songsResponse = await client.getAlbum({ id: album.id });
|
||||||
|
const albumSongs: Song[] = songsResponse.data.songs.map(x => ({
|
||||||
|
id: x.id,
|
||||||
|
albumId: album.id,
|
||||||
|
artistId: album.artistId,
|
||||||
|
name: x.title,
|
||||||
|
starred: x.starred,
|
||||||
|
}));
|
||||||
|
songs.push(...albumSongs);
|
||||||
|
}
|
||||||
|
setSongs(songs);
|
||||||
|
|
||||||
|
setIsLibraryRefreshing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateLibrary();
|
||||||
|
});
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MusicManager = () => {
|
||||||
|
const isLibraryRefreshing = useRecoilValue(isLibraryRefreshingState);
|
||||||
|
const layout = useWindowDimensions();
|
||||||
|
|
||||||
|
const RefreshIndicator = () => (
|
||||||
|
<ActivityIndicator size={'large'} color={colors.accent} style={{
|
||||||
|
backgroundColor: colors.accentLow,
|
||||||
|
position: 'absolute',
|
||||||
|
left: layout.width / 2 - 18,
|
||||||
|
top: layout.height / 2 - 18,
|
||||||
|
elevation: 999,
|
||||||
|
}}/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
{isLibraryRefreshing ? <RefreshIndicator /> : <></>}
|
||||||
|
<React.Suspense fallback={<></>}>
|
||||||
|
<RefreshManager />
|
||||||
|
</React.Suspense>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MusicManager;
|
||||||
@ -3,6 +3,7 @@ import { Text, View } from 'react-native';
|
|||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
import TrackPlayer, { Track } from 'react-native-track-player';
|
import TrackPlayer, { Track } from 'react-native-track-player';
|
||||||
import { musicDb, settingsDb } from '../clients';
|
import { musicDb, settingsDb } from '../clients';
|
||||||
|
import paths from '../paths';
|
||||||
|
|
||||||
async function mkdir(path: string): Promise<void> {
|
async function mkdir(path: string): Promise<void> {
|
||||||
const exists = await RNFS.exists(path);
|
const exists = await RNFS.exists(path);
|
||||||
@ -24,11 +25,9 @@ const SplashPage: React.FC<{}> = ({ children }) => {
|
|||||||
const minSplashTime = new Promise(resolve => setTimeout(resolve, 1));
|
const minSplashTime = new Promise(resolve => setTimeout(resolve, 1));
|
||||||
|
|
||||||
const prepare = async () => {
|
const prepare = async () => {
|
||||||
const filesPath = RNFS.DocumentDirectoryPath;
|
await mkdir(paths.imageCache);
|
||||||
|
await mkdir(paths.songCache);
|
||||||
await mkdir(`${filesPath}/image_cache`);
|
await mkdir(paths.songs);
|
||||||
await mkdir(`${filesPath}/song_cache`);
|
|
||||||
await mkdir(`${filesPath}/songs`);
|
|
||||||
|
|
||||||
await musicDb.openDb();
|
await musicDb.openDb();
|
||||||
await settingsDb.openDb();
|
await settingsDb.openDb();
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import FastImage from 'react-native-fast-image';
|
|||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Album } from '../../models/music';
|
import { Album } from '../../models/music';
|
||||||
import { albumsState, useCoverArtUri, useUpdateAlbums } from '../../state/albums';
|
import { albumsState, useCoverArtUri } from '../../state/music';
|
||||||
import colors from '../../styles/colors';
|
import colors from '../../styles/colors';
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text';
|
||||||
import TopTabContainer from '../common/TopTabContainer';
|
import TopTabContainer from '../common/TopTabContainer';
|
||||||
@ -91,30 +91,13 @@ const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
|
|||||||
|
|
||||||
const AlbumsList = () => {
|
const AlbumsList = () => {
|
||||||
const albums = useRecoilValue(albumsState);
|
const albums = useRecoilValue(albumsState);
|
||||||
const updateAlbums = useUpdateAlbums();
|
|
||||||
|
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
setRefreshing(true);
|
|
||||||
await updateAlbums();
|
|
||||||
setRefreshing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!refreshing && Object.keys(albums).length === 0) {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={Object.values(albums)}
|
data={albums}
|
||||||
renderItem={AlbumListRenderItem}
|
renderItem={AlbumListRenderItem}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
onRefresh={refresh}
|
|
||||||
refreshing={refreshing}
|
|
||||||
numColumns={3}
|
numColumns={3}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import React from 'react';
|
|||||||
import { Text, View, Image, FlatList } from 'react-native';
|
import { Text, View, Image, FlatList } from 'react-native';
|
||||||
import { Artist } from '../../models/music';
|
import { Artist } from '../../models/music';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { artistsState } from '../../state/artists';
|
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text';
|
||||||
import TopTabContainer from '../common/TopTabContainer';
|
import TopTabContainer from '../common/TopTabContainer';
|
||||||
|
import { artistsState } from '../../state/music';
|
||||||
|
|
||||||
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
||||||
<View style={{
|
<View style={{
|
||||||
|
|||||||
@ -7,7 +7,16 @@ export interface Artist {
|
|||||||
|
|
||||||
export interface Album {
|
export interface Album {
|
||||||
id: string;
|
id: string;
|
||||||
|
artistId: string;
|
||||||
name: string;
|
name: string;
|
||||||
starred?: Date;
|
starred?: Date;
|
||||||
coverArt?: string;
|
coverArt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Song {
|
||||||
|
id: string;
|
||||||
|
albumId: string;
|
||||||
|
artistId: string;
|
||||||
|
name: string;
|
||||||
|
starred?: Date;
|
||||||
|
}
|
||||||
|
|||||||
7
src/paths.ts
Normal file
7
src/paths.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import RNFS from 'react-native-fs';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
imageCache: `${RNFS.DocumentDirectoryPath}/image_cache`,
|
||||||
|
songCache: `${RNFS.DocumentDirectoryPath}/song_cache`,
|
||||||
|
songs: `${RNFS.DocumentDirectoryPath}/songs`,
|
||||||
|
};
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { atom, DefaultValue, selector, useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import { SubsonicApiClient } from '../subsonic/api';
|
|
||||||
import { activeServer } from './settings'
|
|
||||||
import { Artist } from '../models/music';
|
|
||||||
import { musicDb } from '../clients';
|
|
||||||
|
|
||||||
export const artistsState = atom<Artist[]>({
|
|
||||||
key: 'artistsState',
|
|
||||||
default: selector({
|
|
||||||
key: 'artistsState/default',
|
|
||||||
get: () => musicDb.getArtists(),
|
|
||||||
}),
|
|
||||||
effects_UNSTABLE: [
|
|
||||||
({ onSet }) => {
|
|
||||||
onSet((newValue) => {
|
|
||||||
if (!(newValue instanceof DefaultValue)) {
|
|
||||||
musicDb.updateArtists(newValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useUpdateArtists = () => {
|
|
||||||
const setArtists = useSetRecoilState(artistsState);
|
|
||||||
const server = useRecoilValue(activeServer);
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
if (!server) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
|
||||||
const response = await client.getArtists();
|
|
||||||
|
|
||||||
setArtists(response.data.artists.map(x => ({
|
|
||||||
id: x.id,
|
|
||||||
name: x.name,
|
|
||||||
coverArt: x.coverArt,
|
|
||||||
})));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,12 +1,30 @@
|
|||||||
import { atom, DefaultValue, selector, selectorFamily, useRecoilValue, useSetRecoilState } from 'recoil';
|
|
||||||
import { SubsonicApiClient } from '../subsonic/api';
|
|
||||||
import { activeServer } from './settings'
|
|
||||||
import { Album } from '../models/music';
|
|
||||||
import { musicDb } from '../clients';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { atom, DefaultValue, selector, selectorFamily, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
import { musicDb } from '../clients';
|
||||||
|
import { Album, Artist, Song } from '../models/music';
|
||||||
|
import paths from '../paths';
|
||||||
|
import { SubsonicApiClient } from '../subsonic/api';
|
||||||
|
import { activeServer } from './settings';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
|
|
||||||
export const albumsState = atom<{ [id: string]: Album }>({
|
export const artistsState = atom<Artist[]>({
|
||||||
|
key: 'artistsState',
|
||||||
|
default: selector({
|
||||||
|
key: 'artistsState/default',
|
||||||
|
get: () => musicDb.getArtists(),
|
||||||
|
}),
|
||||||
|
effects_UNSTABLE: [
|
||||||
|
({ onSet }) => {
|
||||||
|
onSet((newValue) => {
|
||||||
|
if (!(newValue instanceof DefaultValue)) {
|
||||||
|
musicDb.updateArtists(newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const albumsState = atom<Album[]>({
|
||||||
key: 'albumsState',
|
key: 'albumsState',
|
||||||
default: selector({
|
default: selector({
|
||||||
key: 'albumsState/default',
|
key: 'albumsState/default',
|
||||||
@ -16,44 +34,39 @@ export const albumsState = atom<{ [id: string]: Album }>({
|
|||||||
({ onSet }) => {
|
({ onSet }) => {
|
||||||
onSet((newValue) => {
|
onSet((newValue) => {
|
||||||
if (!(newValue instanceof DefaultValue)) {
|
if (!(newValue instanceof DefaultValue)) {
|
||||||
musicDb.updateAlbums(Object.values(newValue));
|
musicDb.updateAlbums(newValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const albumState = selectorFamily<Album, string>({
|
export const songsState = atom<Song[]>({
|
||||||
key: 'albumState',
|
key: 'songsState',
|
||||||
get: id => ({ get }) => {
|
default: selector({
|
||||||
return get(albumsState)[id];
|
key: 'songsState/default',
|
||||||
},
|
get: () => musicDb.getSongs(),
|
||||||
|
}),
|
||||||
|
effects_UNSTABLE: [
|
||||||
|
({ onSet }) => {
|
||||||
|
onSet((newValue) => {
|
||||||
|
if (!(newValue instanceof DefaultValue)) {
|
||||||
|
musicDb.updateSongs(newValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useUpdateAlbums = () => {
|
export const libraryRefreshState = atom<boolean>({
|
||||||
const setAlbums = useSetRecoilState(albumsState);
|
key: 'libraryRefreshState',
|
||||||
const server = useRecoilValue(activeServer);
|
default: false,
|
||||||
|
});
|
||||||
|
|
||||||
return async () => {
|
export const isLibraryRefreshingState = atom<boolean>({
|
||||||
if (!server) {
|
key: 'isLibraryRefreshingState',
|
||||||
return;
|
default: false,
|
||||||
}
|
});
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
|
||||||
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 50 });
|
|
||||||
|
|
||||||
const albums = response.data.albums.reduce((acc, x) => {
|
|
||||||
acc[x.id] = {
|
|
||||||
id: x.id,
|
|
||||||
name: x.name,
|
|
||||||
coverArt: x.coverArt,
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
}, {} as { [id: string]: Album });
|
|
||||||
|
|
||||||
setAlbums(albums);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useCoverArtUri(id?: string): string | undefined {
|
export function useCoverArtUri(id?: string): string | undefined {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -70,7 +83,7 @@ export function useCoverArtUri(id?: string): string | undefined {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = `${RNFS.DocumentDirectoryPath}/image_cache/${id}`;
|
const filePath = `${paths.songCache}/${id}`;
|
||||||
const fileUri = `file://${filePath}`;
|
const fileUri = `file://${filePath}`;
|
||||||
|
|
||||||
if (await RNFS.exists(filePath)) {
|
if (await RNFS.exists(filePath)) {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Album, Artist } from '../models/music';
|
import { Album, Artist, Song } from '../models/music';
|
||||||
import { DbStorage } from './db';
|
import { DbStorage } from './db';
|
||||||
|
|
||||||
export class MusicDb extends DbStorage {
|
export class MusicDb extends DbStorage {
|
||||||
@ -19,11 +19,22 @@ export class MusicDb extends DbStorage {
|
|||||||
tx.executeSql(`
|
tx.executeSql(`
|
||||||
CREATE TABLE albums (
|
CREATE TABLE albums (
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
artistId TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
starred TEXT,
|
starred TEXT,
|
||||||
coverArt TEXT
|
coverArt TEXT
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
tx.executeSql(`
|
||||||
|
CREATE TABLE songs (
|
||||||
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
|
albumId TEXT NOT NULL,
|
||||||
|
artistId TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
starred TEXT,
|
||||||
|
artist TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,39 +73,16 @@ export class MusicDb extends DbStorage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAlbum(id: string): Promise<Album> {
|
async getAlbums(): Promise<Album[]> {
|
||||||
const results = await this.executeSql(`
|
return (await this.executeSql(`
|
||||||
SELECT * FROM albums
|
SELECT * FROM albums;
|
||||||
WHERE id = ?;
|
`))[0].rows.raw().map(x => ({
|
||||||
`, [id]);
|
|
||||||
|
|
||||||
const rows = results[0].rows.raw();
|
|
||||||
return rows.map(x => ({
|
|
||||||
id: x.id,
|
id: x.id,
|
||||||
|
artistId: x.artistid,
|
||||||
name: x.name,
|
name: x.name,
|
||||||
starred: x.starred ? new Date(x.starred) : undefined,
|
starred: x.starred ? new Date(x.starred) : undefined,
|
||||||
coverArt: x.coverArt || undefined,
|
coverArt: x.coverArt || undefined,
|
||||||
}))[0];
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
async getAlbumIds(): Promise<string[]> {
|
|
||||||
return (await this.executeSql(`
|
|
||||||
SELECT id FROM albums;
|
|
||||||
`))[0].rows.raw().map(x => x.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAlbums(): Promise<{[id: string]: Album}> {
|
|
||||||
return (await this.executeSql(`
|
|
||||||
SELECT * FROM albums;
|
|
||||||
`))[0].rows.raw().reduce((acc, x) => {
|
|
||||||
acc[x.id] = {
|
|
||||||
id: x.id,
|
|
||||||
name: x.name,
|
|
||||||
starred: x.starred ? new Date(x.starred) : undefined,
|
|
||||||
coverArt: x.coverArt || undefined,
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAlbums(albums: Album[]): Promise<void> {
|
async updateAlbums(albums: Album[]): Promise<void> {
|
||||||
@ -106,18 +94,58 @@ export class MusicDb extends DbStorage {
|
|||||||
tx.executeSql(`
|
tx.executeSql(`
|
||||||
INSERT INTO albums (
|
INSERT INTO albums (
|
||||||
id,
|
id,
|
||||||
|
artistId,
|
||||||
name,
|
name,
|
||||||
starred,
|
starred,
|
||||||
coverArt
|
coverArt
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?);
|
VALUES (?, ?, ?, ?, ?);
|
||||||
`, [
|
`, [
|
||||||
a.id,
|
a.id,
|
||||||
a.name,
|
a.artistId,
|
||||||
a.starred ? a.starred.toISOString() : null,
|
a.name,
|
||||||
|
a.starred ? a.starred.toISOString() : null,
|
||||||
a.coverArt || null
|
a.coverArt || null
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSongs(): Promise<Song[]> {
|
||||||
|
return (await this.executeSql(`
|
||||||
|
SELECT * FROM songs;
|
||||||
|
`))[0].rows.raw().map(x => ({
|
||||||
|
id: x.id,
|
||||||
|
artistId: x.artistid,
|
||||||
|
albumId: x.albumId,
|
||||||
|
name: x.name,
|
||||||
|
starred: x.starred ? new Date(x.starred) : undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSongs(songs: Song[]): Promise<void> {
|
||||||
|
await this.transaction((tx) => {
|
||||||
|
tx.executeSql(`
|
||||||
|
DELETE FROM songs
|
||||||
|
`);
|
||||||
|
for (const x of songs) {
|
||||||
|
tx.executeSql(`
|
||||||
|
INSERT INTO songs (
|
||||||
|
id,
|
||||||
|
artistId,
|
||||||
|
albumId,
|
||||||
|
name,
|
||||||
|
starred
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?);
|
||||||
|
`, [
|
||||||
|
x.id,
|
||||||
|
x.artistId,
|
||||||
|
x.albumId,
|
||||||
|
x.name,
|
||||||
|
x.starred ? x.starred.toISOString() : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { DOMParser } from 'xmldom';
|
import { DOMParser } from 'xmldom';
|
||||||
import RNFS from 'react-native-fs';
|
import RNFS from 'react-native-fs';
|
||||||
import { GetAlbumList2Params, GetAlbumListParams, GetArtistInfo2Params, GetArtistInfoParams, GetCoverArtParams, GetIndexesParams } from './params';
|
import { GetAlbumList2Params, GetAlbumListParams, GetAlbumParams, GetArtistInfo2Params, GetArtistInfoParams, GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams } from './params';
|
||||||
import { GetAlbumList2Response, GetAlbumListResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistsResponse, GetIndexesResponse, SubsonicResponse } from './responses';
|
import { GetAlbumList2Response, GetAlbumListResponse, GetAlbumResponse, GetArtistInfo2Response, GetArtistInfoResponse, GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, SubsonicResponse } from './responses';
|
||||||
import { ServerSettings } from '../models/settings';
|
import { ServerSettings } from '../models/settings';
|
||||||
|
import paths from '../paths';
|
||||||
|
|
||||||
export class SubsonicApiError extends Error {
|
export class SubsonicApiError extends Error {
|
||||||
method: string;
|
method: string;
|
||||||
@ -146,6 +147,16 @@ export class SubsonicApiClient {
|
|||||||
return new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml));
|
return new SubsonicResponse<GetIndexesResponse>(xml, new GetIndexesResponse(xml));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMusicDirectory(params: GetMusicDirectoryParams): Promise<SubsonicResponse<GetMusicDirectoryResponse>> {
|
||||||
|
const xml = await this.apiGetXml('getMusicDirectory', params);
|
||||||
|
return new SubsonicResponse<GetMusicDirectoryResponse>(xml, new GetMusicDirectoryResponse(xml));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAlbum(params: GetAlbumParams): Promise<SubsonicResponse<GetAlbumResponse>> {
|
||||||
|
const xml = await this.apiGetXml('getAlbum', params);
|
||||||
|
return new SubsonicResponse<GetAlbumResponse>(xml, new GetAlbumResponse(xml));
|
||||||
|
}
|
||||||
|
|
||||||
async getArtistInfo(params: GetArtistInfoParams): Promise<SubsonicResponse<GetArtistInfoResponse>> {
|
async getArtistInfo(params: GetArtistInfoParams): Promise<SubsonicResponse<GetArtistInfoResponse>> {
|
||||||
const xml = await this.apiGetXml('getArtistInfo', params);
|
const xml = await this.apiGetXml('getArtistInfo', params);
|
||||||
return new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml));
|
return new SubsonicResponse<GetArtistInfoResponse>(xml, new GetArtistInfoResponse(xml));
|
||||||
@ -175,7 +186,7 @@ export class SubsonicApiClient {
|
|||||||
//
|
//
|
||||||
|
|
||||||
async getCoverArt(params: GetCoverArtParams): Promise<string> {
|
async getCoverArt(params: GetCoverArtParams): Promise<string> {
|
||||||
const path = `${RNFS.DocumentDirectoryPath}/image_cache/${params.id}`;
|
const path = `${paths.songCache}/${params.id}`;
|
||||||
return await this.apiDownload('getCoverArt', path, params);
|
return await this.apiDownload('getCoverArt', path, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,25 @@ export class ArtistElement extends BaseArtistElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DirectoryElement {
|
||||||
|
id: string;
|
||||||
|
parent?: string;
|
||||||
|
name: string;
|
||||||
|
starred?: Date;
|
||||||
|
userRating?: number;
|
||||||
|
averageRating?: number;
|
||||||
|
playCount?: number;
|
||||||
|
|
||||||
|
constructor(e: Element) {
|
||||||
|
this.id = requiredString(e, 'id');
|
||||||
|
this.parent = optionalString(e, 'parent');
|
||||||
|
this.name = requiredString(e, 'name');
|
||||||
|
this.starred = optionalDate(e, 'starred');
|
||||||
|
this.userRating = optionalInt(e, 'userRating');
|
||||||
|
this.averageRating = optionalFloat(e, 'averageRating');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ChildElement {
|
export class ChildElement {
|
||||||
id: string;
|
id: string;
|
||||||
parent?: string;
|
parent?: string;
|
||||||
|
|||||||
@ -15,6 +15,14 @@ export type GetArtistInfoParams = {
|
|||||||
|
|
||||||
export type GetArtistInfo2Params = GetArtistInfoParams;
|
export type GetArtistInfo2Params = GetArtistInfoParams;
|
||||||
|
|
||||||
|
export type GetMusicDirectoryParams = {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetAlbumParams = {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Album/song lists
|
// Album/song lists
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { AlbumID3Element, ArtistElement, ArtistID3Element, BaseArtistElement, ChildElement } from "./elements";
|
import { AlbumID3Element, ArtistElement, ArtistID3Element, BaseArtistElement, ChildElement, DirectoryElement } from "./elements";
|
||||||
|
|
||||||
export type ResponseStatus = 'ok' | 'failed';
|
export type ResponseStatus = 'ok' | 'failed';
|
||||||
|
|
||||||
@ -98,6 +98,34 @@ export class GetArtistInfo2Response extends BaseGetArtistInfoResponse<ArtistID3E
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class GetMusicDirectoryResponse {
|
||||||
|
directory: DirectoryElement;
|
||||||
|
children: ChildElement[] = [];
|
||||||
|
|
||||||
|
constructor(xml: Document) {
|
||||||
|
this.directory = new DirectoryElement(xml.getElementsByTagName('directory')[0]);
|
||||||
|
|
||||||
|
const childElements = xml.getElementsByTagName('child');
|
||||||
|
for (let i = 0; i < childElements.length; i++) {
|
||||||
|
this.children.push(new ChildElement(childElements[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetAlbumResponse {
|
||||||
|
album: AlbumID3Element;
|
||||||
|
songs: ChildElement[] = [];
|
||||||
|
|
||||||
|
constructor(xml: Document) {
|
||||||
|
this.album = new AlbumID3Element(xml.getElementsByTagName('album')[0]);
|
||||||
|
|
||||||
|
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
|
||||||
//
|
//
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user