remove react-native-sqlite-storage completely

This commit is contained in:
austinried 2021-06-27 10:04:28 +09:00
parent 71e34a6066
commit 17f5639aef
16 changed files with 80 additions and 547 deletions

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -1,4 +0,0 @@
import { MusicDb } from "./storage/music";
import { SubsonicApiClient } from "./subsonic/api";
export const musicDb = new MusicDb();

View File

@ -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>
);

View File

@ -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;

View File

@ -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>

View File

@ -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: [

View File

@ -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';

View File

@ -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={{

View File

@ -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;
};

View File

@ -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;
}

View File

@ -10,7 +10,7 @@ export const appSettingsState = atom<AppSettings>({
}),
effects_UNSTABLE: [
({ onSet }) => {
onSet((newValue, oldValue) => {
onSet((newValue) => {
if (!(newValue instanceof DefaultValue)) {
setAppSettings(newValue);
}

View File

@ -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,
})]),
]);
}

View File

@ -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);
}
}

View File

@ -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,
})]),
]);
}