mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 17:39:27 +01:00
remove react-native-sqlite-storage completely
This commit is contained in:
parent
71e34a6066
commit
17f5639aef
2
App.tsx
2
App.tsx
@ -3,12 +3,10 @@ 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>
|
||||||
|
|||||||
28
package-lock.json
generated
28
package-lock.json
generated
@ -25,7 +25,6 @@
|
|||||||
"react-native-reanimated": "^2.2.0",
|
"react-native-reanimated": "^2.2.0",
|
||||||
"react-native-safe-area-context": "^3.2.0",
|
"react-native-safe-area-context": "^3.2.0",
|
||||||
"react-native-screens": "^3.4.0",
|
"react-native-screens": "^3.4.0",
|
||||||
"react-native-sqlite-storage": "^5.0.0",
|
|
||||||
"react-native-tab-view": "^2.16.0",
|
"react-native-tab-view": "^2.16.0",
|
||||||
"react-native-track-player": "^1.2.7",
|
"react-native-track-player": "^1.2.7",
|
||||||
"recoil": "^0.3.1",
|
"recoil": "^0.3.1",
|
||||||
@ -39,7 +38,6 @@
|
|||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/md5": "^2.3.0",
|
"@types/md5": "^2.3.0",
|
||||||
"@types/react-native": "^0.64.5",
|
"@types/react-native": "^0.64.5",
|
||||||
"@types/react-native-sqlite-storage": "^5.0.0",
|
|
||||||
"@types/react-test-renderer": "^16.9.2",
|
"@types/react-test-renderer": "^16.9.2",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@types/xmldom": "^0.1.30",
|
"@types/xmldom": "^0.1.30",
|
||||||
@ -2840,12 +2838,6 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react-native-sqlite-storage": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-kn5J+oLU//Jk9YshL9bcrm1Pxy4fp/7Pk8+yGZIeu/aEG8SmG75nxDzpfTopEluThDtjPDxFUWVDD0Ij/eCNJg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/react-test-renderer": {
|
"node_modules/@types/react-test-renderer": {
|
||||||
"version": "16.9.5",
|
"version": "16.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.5.tgz",
|
||||||
@ -10601,14 +10593,6 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-sqlite-storage": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-c1Joq3/tO1nmIcP8SkRZNolPSbfvY8uZg5lXse0TmjIPC0qHVbk96IMvWGyly1TmYCIpxpuDRc0/xCffDbYIvg==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react-native": ">=0.14.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-native-tab-view": {
|
"node_modules/react-native-tab-view": {
|
||||||
"version": "2.16.0",
|
"version": "2.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz",
|
||||||
@ -15456,12 +15440,6 @@
|
|||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/react-native-sqlite-storage": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-kn5J+oLU//Jk9YshL9bcrm1Pxy4fp/7Pk8+yGZIeu/aEG8SmG75nxDzpfTopEluThDtjPDxFUWVDD0Ij/eCNJg==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/react-test-renderer": {
|
"@types/react-test-renderer": {
|
||||||
"version": "16.9.5",
|
"version": "16.9.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.5.tgz",
|
||||||
@ -21405,12 +21383,6 @@
|
|||||||
"warn-once": "^0.1.0"
|
"warn-once": "^0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-native-sqlite-storage": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-native-sqlite-storage/-/react-native-sqlite-storage-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-c1Joq3/tO1nmIcP8SkRZNolPSbfvY8uZg5lXse0TmjIPC0qHVbk96IMvWGyly1TmYCIpxpuDRc0/xCffDbYIvg==",
|
|
||||||
"requires": {}
|
|
||||||
},
|
|
||||||
"react-native-tab-view": {
|
"react-native-tab-view": {
|
||||||
"version": "2.16.0",
|
"version": "2.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-2.16.0.tgz",
|
||||||
|
|||||||
@ -27,7 +27,6 @@
|
|||||||
"react-native-reanimated": "^2.2.0",
|
"react-native-reanimated": "^2.2.0",
|
||||||
"react-native-safe-area-context": "^3.2.0",
|
"react-native-safe-area-context": "^3.2.0",
|
||||||
"react-native-screens": "^3.4.0",
|
"react-native-screens": "^3.4.0",
|
||||||
"react-native-sqlite-storage": "^5.0.0",
|
|
||||||
"react-native-tab-view": "^2.16.0",
|
"react-native-tab-view": "^2.16.0",
|
||||||
"react-native-track-player": "^1.2.7",
|
"react-native-track-player": "^1.2.7",
|
||||||
"recoil": "^0.3.1",
|
"recoil": "^0.3.1",
|
||||||
@ -41,7 +40,6 @@
|
|||||||
"@types/jest": "^26.0.23",
|
"@types/jest": "^26.0.23",
|
||||||
"@types/md5": "^2.3.0",
|
"@types/md5": "^2.3.0",
|
||||||
"@types/react-native": "^0.64.5",
|
"@types/react-native": "^0.64.5",
|
||||||
"@types/react-native-sqlite-storage": "^5.0.0",
|
|
||||||
"@types/react-test-renderer": "^16.9.2",
|
"@types/react-test-renderer": "^16.9.2",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@types/xmldom": "^0.1.30",
|
"@types/xmldom": "^0.1.30",
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
import { MusicDb } from "./storage/music";
|
|
||||||
import { SubsonicApiClient } from "./subsonic/api";
|
|
||||||
|
|
||||||
export const musicDb = new MusicDb();
|
|
||||||
@ -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, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useResetRecoilState } from 'recoil';
|
||||||
import { Artist } from '../models/music';
|
import { Artist } from '../models/music';
|
||||||
import { artistsState, isLibraryRefreshingState, libraryRefreshState } from '../state/music';
|
import { artistsState } from '../state/music';
|
||||||
|
|
||||||
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
||||||
<View>
|
<View>
|
||||||
@ -32,8 +32,6 @@ const List = () => {
|
|||||||
|
|
||||||
const ListPlusControls = () => {
|
const ListPlusControls = () => {
|
||||||
const resetArtists = useResetRecoilState(artistsState);
|
const resetArtists = useResetRecoilState(artistsState);
|
||||||
const setLibraryRefresh = useSetRecoilState(libraryRefreshState);
|
|
||||||
const isLibraryRefreshing = useRecoilValue(isLibraryRefreshingState);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
@ -41,11 +39,6 @@ const ListPlusControls = () => {
|
|||||||
title='Reset to default'
|
title='Reset to default'
|
||||||
onPress={resetArtists}
|
onPress={resetArtists}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
title='Refresh Library'
|
|
||||||
onPress={() => setLibraryRefresh(true)}
|
|
||||||
disabled={isLibraryRefreshing}
|
|
||||||
/>
|
|
||||||
<List />
|
<List />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,104 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,41 +1,15 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useNavigation } from '@react-navigation/core';
|
||||||
import { Button, TextInput, View, Text } from 'react-native';
|
import md5 from 'md5';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button, Text, View } from 'react-native';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import md5 from 'md5';
|
|
||||||
import { musicDb } from '../clients';
|
|
||||||
import { appSettingsState } from '../state/settings';
|
import { appSettingsState } from '../state/settings';
|
||||||
import { DbStorage } from '../storage/db';
|
|
||||||
import { StackScreenProps } from '@react-navigation/stack';
|
|
||||||
import { useNavigation } from '@react-navigation/core';
|
|
||||||
|
|
||||||
const RecreateDbButton: React.FC<{ db: DbStorage, title: string }> = ({ db, title }) => {
|
const TestControls = () => {
|
||||||
const [inProgress, setInProgress] = useState(false);
|
|
||||||
|
|
||||||
const recreateDb = async () => {
|
|
||||||
setInProgress(true);
|
|
||||||
try{
|
|
||||||
try { await db.deleteDb(); } catch {}
|
|
||||||
await db.createDb();
|
|
||||||
} finally {
|
|
||||||
setInProgress(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
title={`Recreate ${title} DB`}
|
|
||||||
onPress={recreateDb}
|
|
||||||
disabled={inProgress}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DbControls = () => {
|
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<RecreateDbButton db={musicDb} title='Music' />
|
|
||||||
<Button
|
<Button
|
||||||
title='Now Playing'
|
title='Now Playing'
|
||||||
onPress={() => navigation.navigate('Now Playing')}
|
onPress={() => navigation.navigate('Now Playing')}
|
||||||
@ -88,7 +62,7 @@ const ServerSettingsView = () => {
|
|||||||
|
|
||||||
const SettingsView = () => (
|
const SettingsView = () => (
|
||||||
<View>
|
<View>
|
||||||
<DbControls />
|
<TestControls />
|
||||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||||
<ServerSettingsView />
|
<ServerSettingsView />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Text, View } from 'react-native';
|
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 } from '../clients';
|
|
||||||
import paths from '../paths';
|
import paths from '../paths';
|
||||||
|
|
||||||
async function mkdir(path: string): Promise<void> {
|
async function mkdir(path: string): Promise<void> {
|
||||||
@ -29,12 +28,6 @@ const SplashPage: React.FC<{}> = ({ children }) => {
|
|||||||
await mkdir(paths.songCache);
|
await mkdir(paths.songCache);
|
||||||
await mkdir(paths.songs);
|
await mkdir(paths.songs);
|
||||||
|
|
||||||
await musicDb.openDb();
|
|
||||||
|
|
||||||
if (!(await musicDb.dbExists())) {
|
|
||||||
await musicDb.createDb();
|
|
||||||
}
|
|
||||||
|
|
||||||
await TrackPlayer.setupPlayer();
|
await TrackPlayer.setupPlayer();
|
||||||
TrackPlayer.updateOptions({
|
TrackPlayer.updateOptions({
|
||||||
capabilities: [
|
capabilities: [
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FlatList, Text, View } from 'react-native';
|
import { FlatList, Text, View } from 'react-native';
|
||||||
import FastImage from 'react-native-fast-image';
|
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, albumsUpdatingState, useCoverArtUri, useUpdateAlbums } from '../../state/music';
|
import { albumsState, albumsUpdatingState, useUpdateAlbums } 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';
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Text, View, Image, FlatList } from 'react-native';
|
import { FlatList, Image, Text, View } from 'react-native';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Artist } from '../../models/music';
|
import { Artist } from '../../models/music';
|
||||||
import { useRecoilValue, useResetRecoilState } from 'recoil';
|
import { artistsState, artistsUpdatingState, useUpdateArtists } from '../../state/music';
|
||||||
import textStyles from '../../styles/text';
|
import textStyles from '../../styles/text';
|
||||||
import TopTabContainer from '../common/TopTabContainer';
|
import TopTabContainer from '../common/TopTabContainer';
|
||||||
import { artistsState, artistsUpdatingState, useUpdateArtists } from '../../state/music';
|
|
||||||
|
|
||||||
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
||||||
<View style={{
|
<View style={{
|
||||||
|
|||||||
@ -24,3 +24,33 @@ export interface Song {
|
|||||||
name: string;
|
name: string;
|
||||||
starred?: Date;
|
starred?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DownloadedSong = {
|
||||||
|
id: string;
|
||||||
|
type: 'song';
|
||||||
|
name: string;
|
||||||
|
album: string;
|
||||||
|
artist: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DownloadedAlbum = {
|
||||||
|
id: string;
|
||||||
|
type: 'album';
|
||||||
|
songs: string[];
|
||||||
|
name: string;
|
||||||
|
artist: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DownloadedArtist = {
|
||||||
|
id: string;
|
||||||
|
type: 'artist';
|
||||||
|
songs: string[];
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DownloadedPlaylist = {
|
||||||
|
id: string;
|
||||||
|
type: 'playlist';
|
||||||
|
songs: string[];
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
@ -1,11 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { atom, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { atom, DefaultValue, selector, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
import { Album, Artist } from '../models/music';
|
||||||
import { musicDb } from '../clients';
|
|
||||||
import { Album, Artist, Song } from '../models/music';
|
|
||||||
import paths from '../paths';
|
|
||||||
import { SubsonicApiClient } from '../subsonic/api';
|
import { SubsonicApiClient } from '../subsonic/api';
|
||||||
import { activeServer } from './settings';
|
import { activeServer } from './settings';
|
||||||
import RNFS from 'react-native-fs';
|
|
||||||
|
|
||||||
export const artistsState = atom<Artist[]>({
|
export const artistsState = atom<Artist[]>({
|
||||||
key: 'artistsState',
|
key: 'artistsState',
|
||||||
@ -15,7 +11,7 @@ export const artistsState = atom<Artist[]>({
|
|||||||
export const artistsUpdatingState = atom<boolean>({
|
export const artistsUpdatingState = atom<boolean>({
|
||||||
key: 'artistsUpdatingState',
|
key: 'artistsUpdatingState',
|
||||||
default: false,
|
default: false,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const useUpdateArtists = () => {
|
export const useUpdateArtists = () => {
|
||||||
const server = useRecoilValue(activeServer);
|
const server = useRecoilValue(activeServer);
|
||||||
@ -54,7 +50,7 @@ export const albumsState = atom<Album[]>({
|
|||||||
export const albumsUpdatingState = atom<boolean>({
|
export const albumsUpdatingState = atom<boolean>({
|
||||||
key: 'albumsUpdatingState',
|
key: 'albumsUpdatingState',
|
||||||
default: false,
|
default: false,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const useUpdateAlbums = () => {
|
export const useUpdateAlbums = () => {
|
||||||
const server = useRecoilValue(activeServer);
|
const server = useRecoilValue(activeServer);
|
||||||
@ -87,78 +83,3 @@ export const useUpdateAlbums = () => {
|
|||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const songsState = atom<Song[]>({
|
|
||||||
key: 'songsState',
|
|
||||||
default: selector({
|
|
||||||
key: 'songsState/default',
|
|
||||||
get: () => musicDb.getSongs(),
|
|
||||||
}),
|
|
||||||
effects_UNSTABLE: [
|
|
||||||
({ onSet }) => {
|
|
||||||
onSet((newValue) => {
|
|
||||||
if (!(newValue instanceof DefaultValue)) {
|
|
||||||
musicDb.updateSongs(newValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const libraryRefreshState = atom<boolean>({
|
|
||||||
key: 'libraryRefreshState',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const isLibraryRefreshingState = atom<boolean>({
|
|
||||||
key: 'isLibraryRefreshingState',
|
|
||||||
default: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useCoverArtUri(id?: string): string | undefined {
|
|
||||||
if (!id) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = useRecoilValue(activeServer);
|
|
||||||
|
|
||||||
const [downloadAttempted, setdownloadAttempted] = useState(false);
|
|
||||||
const [coverArtSource, setCoverArtSource] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const getCoverArt = async () => {
|
|
||||||
if (coverArtSource) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = `${paths.songCache}/${id}`;
|
|
||||||
const fileUri = `file://${filePath}`;
|
|
||||||
|
|
||||||
if (await RNFS.exists(filePath)) {
|
|
||||||
// file already in cache, return the file
|
|
||||||
setCoverArtSource(fileUri);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
// can't download without server set
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setdownloadAttempted(true);
|
|
||||||
if (downloadAttempted) {
|
|
||||||
// don't try to download more than once using this hook
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server);
|
|
||||||
await client.getCoverArt({ id });
|
|
||||||
|
|
||||||
setCoverArtSource(fileUri);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getCoverArt();
|
|
||||||
});
|
|
||||||
|
|
||||||
return coverArtSource;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export const appSettingsState = atom<AppSettings>({
|
|||||||
}),
|
}),
|
||||||
effects_UNSTABLE: [
|
effects_UNSTABLE: [
|
||||||
({ onSet }) => {
|
({ onSet }) => {
|
||||||
onSet((newValue, oldValue) => {
|
onSet((newValue) => {
|
||||||
if (!(newValue instanceof DefaultValue)) {
|
if (!(newValue instanceof DefaultValue)) {
|
||||||
setAppSettings(newValue);
|
setAppSettings(newValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
const key = {
|
|
||||||
downloadedSongKeys: '@downloadedSongKeys',
|
|
||||||
downloadedAlbumKeys: '@downloadedAlbumKeys',
|
|
||||||
downloadedArtistKeys: '@downloadedArtistKeys',
|
|
||||||
downloadedPlaylistKeys: '@downloadedPlaylistKeys',
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getItem(key: string): Promise<string | null> {
|
export async function getItem(key: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
return await AsyncStorage.getItem(key);
|
return await AsyncStorage.getItem(key);
|
||||||
@ -40,59 +33,3 @@ export async function multiSet(items: string[][]): Promise<void> {
|
|||||||
console.error(`multiSet error`, e);
|
console.error(`multiSet error`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadedSong = {
|
|
||||||
id: string;
|
|
||||||
type: 'song';
|
|
||||||
name: string;
|
|
||||||
album: string;
|
|
||||||
artist: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DownloadedAlbum = {
|
|
||||||
id: string;
|
|
||||||
type: 'album';
|
|
||||||
songs: string[];
|
|
||||||
name: string;
|
|
||||||
artist: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DownloadedArtist = {
|
|
||||||
id: string;
|
|
||||||
type: 'artist';
|
|
||||||
songs: string[];
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type DownloadedPlaylist = {
|
|
||||||
id: string;
|
|
||||||
type: 'playlist';
|
|
||||||
songs: string[];
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
|
|
||||||
const keysItem = await getItem(key.downloadedSongKeys);
|
|
||||||
const keys: string[] = keysItem ? JSON.parse(keysItem) : [];
|
|
||||||
|
|
||||||
const items = await multiGet(keys);
|
|
||||||
return items.map(x => {
|
|
||||||
const parsed = JSON.parse(x[1] as string);
|
|
||||||
return {
|
|
||||||
id: x[0],
|
|
||||||
type: 'song',
|
|
||||||
...parsed,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
|
|
||||||
await multiSet([
|
|
||||||
[key.downloadedSongKeys, JSON.stringify(items.map(x => x.id))],
|
|
||||||
...items.map(x => [x.id, JSON.stringify({
|
|
||||||
name: x.name,
|
|
||||||
album: x.album,
|
|
||||||
artist: x.artist,
|
|
||||||
})]),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
import SQLite, { DatabaseParams, ResultSet, SQLiteDatabase, Transaction } from 'react-native-sqlite-storage';
|
|
||||||
|
|
||||||
SQLite.enablePromise(true);
|
|
||||||
|
|
||||||
export abstract class DbStorage {
|
|
||||||
private dbParams: DatabaseParams;
|
|
||||||
private db: SQLiteDatabase | undefined;
|
|
||||||
|
|
||||||
constructor(dbParams: DatabaseParams) {
|
|
||||||
this.dbParams = dbParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract createDb(): Promise<void>
|
|
||||||
|
|
||||||
protected async initDb(scope: (tx: Transaction) => void): Promise<void> {
|
|
||||||
await this.transaction(tx => {
|
|
||||||
tx.executeSql(`
|
|
||||||
CREATE TABLE db_version (
|
|
||||||
version INTEGER NOT NULL
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
tx.executeSql(`
|
|
||||||
INSERT INTO db_version (version)
|
|
||||||
VALUES (?);
|
|
||||||
`, [1]);
|
|
||||||
|
|
||||||
scope(tx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async dbExists(): Promise<boolean> {
|
|
||||||
const results = await this.executeSql(`
|
|
||||||
SELECT name FROM sqlite_master
|
|
||||||
WHERE type='table' AND name='db_version';
|
|
||||||
`);
|
|
||||||
|
|
||||||
return results[0].rows.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async openDb(): Promise<void> {
|
|
||||||
this.db = await SQLite.openDatabase({ ...this.dbParams });
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteDb(): Promise<void> {
|
|
||||||
if (this.db) {
|
|
||||||
await this.db.close();
|
|
||||||
}
|
|
||||||
await SQLite.deleteDatabase({ ...this.dbParams });
|
|
||||||
}
|
|
||||||
|
|
||||||
async executeSql(sql: string, params?: any[]): Promise<[ResultSet]> {
|
|
||||||
// https://github.com/andpor/react-native-sqlite-storage/issues/410
|
|
||||||
return await (this.db as SQLiteDatabase).executeSql(sql, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async transaction(scope: (tx: Transaction) => void): Promise<void> {
|
|
||||||
await (this.db as SQLiteDatabase).transaction(scope);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,151 +1,35 @@
|
|||||||
import { Album, Artist, Song } from '../models/music';
|
import { DownloadedSong } from '../models/music';
|
||||||
import { DbStorage } from './db';
|
import { getItem, multiGet, multiSet } from './asyncstorage';
|
||||||
|
|
||||||
export class MusicDb extends DbStorage {
|
const key = {
|
||||||
constructor() {
|
downloadedSongKeys: '@downloadedSongKeys',
|
||||||
super({ name: 'music.db', location: 'default' });
|
downloadedAlbumKeys: '@downloadedAlbumKeys',
|
||||||
}
|
downloadedArtistKeys: '@downloadedArtistKeys',
|
||||||
|
downloadedPlaylistKeys: '@downloadedPlaylistKeys',
|
||||||
|
};
|
||||||
|
|
||||||
async createDb(): Promise<void> {
|
export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
|
||||||
await this.initDb(tx => {
|
const keysItem = await getItem(key.downloadedSongKeys);
|
||||||
tx.executeSql(`
|
const keys: string[] = keysItem ? JSON.parse(keysItem) : [];
|
||||||
CREATE TABLE artists (
|
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
const items = await multiGet(keys);
|
||||||
name TEXT NOT NULL,
|
return items.map(x => {
|
||||||
starred TEXT,
|
const parsed = JSON.parse(x[1] as string);
|
||||||
coverArt TEXT
|
return {
|
||||||
);
|
id: x[0],
|
||||||
`);
|
type: 'song',
|
||||||
tx.executeSql(`
|
...parsed,
|
||||||
CREATE TABLE albums (
|
};
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
|
||||||
artistId TEXT NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
starred 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
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArtists(): Promise<Artist[]> {
|
export async function setDownloadedSongs(items: DownloadedSong[]): Promise<void> {
|
||||||
return (await this.executeSql(`
|
await multiSet([
|
||||||
SELECT * FROM artists;
|
[key.downloadedSongKeys, JSON.stringify(items.map(x => x.id))],
|
||||||
`))[0].rows.raw().map(x => ({
|
...items.map(x => [x.id, JSON.stringify({
|
||||||
id: x.id,
|
|
||||||
name: x.name,
|
name: x.name,
|
||||||
starred: x.starred ? new Date(x.starred) : undefined,
|
album: x.album,
|
||||||
coverArt: x.coverArt || undefined,
|
artist: x.artist,
|
||||||
}));
|
})]),
|
||||||
}
|
|
||||||
|
|
||||||
async updateArtists(artists: Artist[]): Promise<void> {
|
|
||||||
await this.transaction((tx) => {
|
|
||||||
tx.executeSql(`
|
|
||||||
DELETE FROM artists
|
|
||||||
`);
|
|
||||||
for (const a of artists) {
|
|
||||||
tx.executeSql(`
|
|
||||||
INSERT INTO artists (
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
starred,
|
|
||||||
coverArt
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?);
|
|
||||||
`, [
|
|
||||||
a.id,
|
|
||||||
a.name,
|
|
||||||
a.starred ? a.starred.toISOString() : null,
|
|
||||||
a.coverArt || null
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAlbums(): Promise<Album[]> {
|
|
||||||
return (await this.executeSql(`
|
|
||||||
SELECT * FROM albums;
|
|
||||||
`))[0].rows.raw().map(x => ({
|
|
||||||
id: x.id,
|
|
||||||
artistId: x.artistid,
|
|
||||||
name: x.name,
|
|
||||||
starred: x.starred ? new Date(x.starred) : undefined,
|
|
||||||
coverArt: x.coverArt || undefined,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateAlbums(albums: Album[]): Promise<void> {
|
|
||||||
await this.transaction((tx) => {
|
|
||||||
tx.executeSql(`
|
|
||||||
DELETE FROM albums
|
|
||||||
`);
|
|
||||||
for (const a of albums) {
|
|
||||||
tx.executeSql(`
|
|
||||||
INSERT INTO albums (
|
|
||||||
id,
|
|
||||||
artistId,
|
|
||||||
name,
|
|
||||||
starred,
|
|
||||||
coverArt
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?);
|
|
||||||
`, [
|
|
||||||
a.id,
|
|
||||||
a.artistId,
|
|
||||||
a.name,
|
|
||||||
a.starred ? a.starred.toISOString() : 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,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user