mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +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 SplashPage from './src/components/SplashPage';
|
||||
import RootNavigator from './src/components/navigation/RootNavigator';
|
||||
import MusicManager from './src/components/MusicManager';
|
||||
|
||||
const App = () => (
|
||||
<RecoilRoot>
|
||||
<SplashPage>
|
||||
<MusicManager />
|
||||
<NavigationContainer>
|
||||
<RootNavigator />
|
||||
</NavigationContainer>
|
||||
|
||||
28
package-lock.json
generated
28
package-lock.json
generated
@ -25,7 +25,6 @@
|
||||
"react-native-reanimated": "^2.2.0",
|
||||
"react-native-safe-area-context": "^3.2.0",
|
||||
"react-native-screens": "^3.4.0",
|
||||
"react-native-sqlite-storage": "^5.0.0",
|
||||
"react-native-tab-view": "^2.16.0",
|
||||
"react-native-track-player": "^1.2.7",
|
||||
"recoil": "^0.3.1",
|
||||
@ -39,7 +38,6 @@
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/md5": "^2.3.0",
|
||||
"@types/react-native": "^0.64.5",
|
||||
"@types/react-native-sqlite-storage": "^5.0.0",
|
||||
"@types/react-test-renderer": "^16.9.2",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/xmldom": "^0.1.30",
|
||||
@ -2840,12 +2838,6 @@
|
||||
"@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": {
|
||||
"version": "16.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.5.tgz",
|
||||
@ -10601,14 +10593,6 @@
|
||||
"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": {
|
||||
"version": "2.16.0",
|
||||
"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-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": {
|
||||
"version": "16.9.5",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.16.0",
|
||||
"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-safe-area-context": "^3.2.0",
|
||||
"react-native-screens": "^3.4.0",
|
||||
"react-native-sqlite-storage": "^5.0.0",
|
||||
"react-native-tab-view": "^2.16.0",
|
||||
"react-native-track-player": "^1.2.7",
|
||||
"recoil": "^0.3.1",
|
||||
@ -41,7 +40,6 @@
|
||||
"@types/jest": "^26.0.23",
|
||||
"@types/md5": "^2.3.0",
|
||||
"@types/react-native": "^0.64.5",
|
||||
"@types/react-native-sqlite-storage": "^5.0.0",
|
||||
"@types/react-test-renderer": "^16.9.2",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@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 { Button, FlatList, Text, View } from 'react-native';
|
||||
import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { Artist } from '../models/music';
|
||||
import { artistsState, isLibraryRefreshingState, libraryRefreshState } from '../state/music';
|
||||
import { artistsState } from '../state/music';
|
||||
|
||||
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
||||
<View>
|
||||
@ -32,8 +32,6 @@ const List = () => {
|
||||
|
||||
const ListPlusControls = () => {
|
||||
const resetArtists = useResetRecoilState(artistsState);
|
||||
const setLibraryRefresh = useSetRecoilState(libraryRefreshState);
|
||||
const isLibraryRefreshing = useRecoilValue(isLibraryRefreshingState);
|
||||
|
||||
return (
|
||||
<View>
|
||||
@ -41,11 +39,6 @@ const ListPlusControls = () => {
|
||||
title='Reset to default'
|
||||
onPress={resetArtists}
|
||||
/>
|
||||
<Button
|
||||
title='Refresh Library'
|
||||
onPress={() => setLibraryRefresh(true)}
|
||||
disabled={isLibraryRefreshing}
|
||||
/>
|
||||
<List />
|
||||
</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 { Button, TextInput, View, Text } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import md5 from 'md5';
|
||||
import React from 'react';
|
||||
import { Button, Text, View } from 'react-native';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import md5 from 'md5';
|
||||
import { musicDb } from '../clients';
|
||||
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 [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 TestControls = () => {
|
||||
const navigation = useNavigation();
|
||||
return (
|
||||
<View>
|
||||
<RecreateDbButton db={musicDb} title='Music' />
|
||||
<Button
|
||||
title='Now Playing'
|
||||
onPress={() => navigation.navigate('Now Playing')}
|
||||
@ -88,7 +62,7 @@ const ServerSettingsView = () => {
|
||||
|
||||
const SettingsView = () => (
|
||||
<View>
|
||||
<DbControls />
|
||||
<TestControls />
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||
<ServerSettingsView />
|
||||
</React.Suspense>
|
||||
|
||||
@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import RNFS from 'react-native-fs';
|
||||
import TrackPlayer, { Track } from 'react-native-track-player';
|
||||
import { musicDb } from '../clients';
|
||||
import paths from '../paths';
|
||||
|
||||
async function mkdir(path: string): Promise<void> {
|
||||
@ -29,12 +28,6 @@ const SplashPage: React.FC<{}> = ({ children }) => {
|
||||
await mkdir(paths.songCache);
|
||||
await mkdir(paths.songs);
|
||||
|
||||
await musicDb.openDb();
|
||||
|
||||
if (!(await musicDb.dbExists())) {
|
||||
await musicDb.createDb();
|
||||
}
|
||||
|
||||
await TrackPlayer.setupPlayer();
|
||||
TrackPlayer.updateOptions({
|
||||
capabilities: [
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FlatList, Text, View } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
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 textStyles from '../../styles/text';
|
||||
import TopTabContainer from '../common/TopTabContainer';
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Text, View, Image, FlatList } from 'react-native';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FlatList, Image, Text, View } from 'react-native';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Artist } from '../../models/music';
|
||||
import { useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { artistsState, artistsUpdatingState, useUpdateArtists } from '../../state/music';
|
||||
import textStyles from '../../styles/text';
|
||||
import TopTabContainer from '../common/TopTabContainer';
|
||||
import { artistsState, artistsUpdatingState, useUpdateArtists } from '../../state/music';
|
||||
|
||||
const ArtistItem: React.FC<{ item: Artist } > = ({ item }) => (
|
||||
<View style={{
|
||||
|
||||
@ -24,3 +24,33 @@ export interface Song {
|
||||
name: string;
|
||||
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, DefaultValue, selector, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { musicDb } from '../clients';
|
||||
import { Album, Artist, Song } from '../models/music';
|
||||
import paths from '../paths';
|
||||
import { atom, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Album, Artist } from '../models/music';
|
||||
import { SubsonicApiClient } from '../subsonic/api';
|
||||
import { activeServer } from './settings';
|
||||
import RNFS from 'react-native-fs';
|
||||
|
||||
export const artistsState = atom<Artist[]>({
|
||||
key: 'artistsState',
|
||||
@ -15,7 +11,7 @@ export const artistsState = atom<Artist[]>({
|
||||
export const artistsUpdatingState = atom<boolean>({
|
||||
key: 'artistsUpdatingState',
|
||||
default: false,
|
||||
})
|
||||
});
|
||||
|
||||
export const useUpdateArtists = () => {
|
||||
const server = useRecoilValue(activeServer);
|
||||
@ -54,7 +50,7 @@ export const albumsState = atom<Album[]>({
|
||||
export const albumsUpdatingState = atom<boolean>({
|
||||
key: 'albumsUpdatingState',
|
||||
default: false,
|
||||
})
|
||||
});
|
||||
|
||||
export const useUpdateAlbums = () => {
|
||||
const server = useRecoilValue(activeServer);
|
||||
@ -87,78 +83,3 @@ export const useUpdateAlbums = () => {
|
||||
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: [
|
||||
({ onSet }) => {
|
||||
onSet((newValue, oldValue) => {
|
||||
onSet((newValue) => {
|
||||
if (!(newValue instanceof DefaultValue)) {
|
||||
setAppSettings(newValue);
|
||||
}
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
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> {
|
||||
try {
|
||||
return await AsyncStorage.getItem(key);
|
||||
@ -40,59 +33,3 @@ export async function multiSet(items: string[][]): Promise<void> {
|
||||
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 { DbStorage } from './db';
|
||||
import { DownloadedSong } from '../models/music';
|
||||
import { getItem, multiGet, multiSet } from './asyncstorage';
|
||||
|
||||
export class MusicDb extends DbStorage {
|
||||
constructor() {
|
||||
super({ name: 'music.db', location: 'default' });
|
||||
}
|
||||
const key = {
|
||||
downloadedSongKeys: '@downloadedSongKeys',
|
||||
downloadedAlbumKeys: '@downloadedAlbumKeys',
|
||||
downloadedArtistKeys: '@downloadedArtistKeys',
|
||||
downloadedPlaylistKeys: '@downloadedPlaylistKeys',
|
||||
};
|
||||
|
||||
async createDb(): Promise<void> {
|
||||
await this.initDb(tx => {
|
||||
tx.executeSql(`
|
||||
CREATE TABLE artists (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
starred TEXT,
|
||||
coverArt TEXT
|
||||
);
|
||||
`);
|
||||
tx.executeSql(`
|
||||
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
|
||||
);
|
||||
`);
|
||||
});
|
||||
}
|
||||
export async function getDownloadedSongs(): Promise<DownloadedSong[]> {
|
||||
const keysItem = await getItem(key.downloadedSongKeys);
|
||||
const keys: string[] = keysItem ? JSON.parse(keysItem) : [];
|
||||
|
||||
async getArtists(): Promise<Artist[]> {
|
||||
return (await this.executeSql(`
|
||||
SELECT * FROM artists;
|
||||
`))[0].rows.raw().map(x => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
starred: x.starred ? new Date(x.starred) : undefined,
|
||||
coverArt: x.coverArt || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
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,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
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,
|
||||
})]),
|
||||
]);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user