Skip to content

Commit

Permalink
Feat: Service Manager (#1124)
Browse files Browse the repository at this point in the history
* feat: ServiceManager & import multiple epub files

* Fix: merge chapter based on index of toc in file list

* notifcation when jobs get done

* Fix epub: only read table of content in navmap

* add update library to service manager

* add drive backup/restore to service manager

* add self host backup/restore to service manager

* add migrate novel to service manager

* add download chapter to service manager

* clean up
  • Loading branch information
nyagami authored Jul 5, 2024
1 parent 15aed61 commit 0787967
Show file tree
Hide file tree
Showing 31 changed files with 989 additions and 1,187 deletions.
6 changes: 0 additions & 6 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@ import LottieSplashScreen from 'react-native-lottie-splash-screen';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Provider as PaperProvider } from 'react-native-paper';
import * as Notifications from 'expo-notifications';
import BackgroundService from 'react-native-background-actions';

import { createTables } from '@database/db';
import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary';

import Main from './src/navigators/Main';
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
import { MMKVStorage } from '@utils/mmkv/mmkv';
import { BACKGROUND_ACTION } from '@services/constants';

Notifications.setNotificationHandler({
handleNotification: async () => {
Expand All @@ -32,9 +29,6 @@ Notifications.setNotificationHandler({

createTables();
LottieSplashScreen.hide();
if (!BackgroundService.isRunning()) {
MMKVStorage.delete(BACKGROUND_ACTION);
}

const App = () => {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
Expand Down Expand Up @@ -64,35 +65,87 @@ class EpubUtil(context: ReactApplicationContext) : ReactContextBaseJavaModule(co
return "OEBPS/content.opf" // default
}

private fun mergeChapters(
entryList: List<ChapterEntry>,
fileList: List<String>,
contentDir: String
): ReadableArray {
val foundIndexes = entryList.mapIndexed { index, entry ->
val foundIndex = fileList.indexOf(entry.href)
if (foundIndex < index) -1
else foundIndex
}
val linkedIndex = foundIndexes.mapIndexed { arrayIndex, foundIndex ->
if (foundIndex == -1) Pair(-1, -1)
else {
var nextIndex = -1
for (j in arrayIndex + 1 until foundIndexes.size) {
if (foundIndexes[j] != -1) {
nextIndex = foundIndexes[j]
break
}
}
if (nextIndex == -1) nextIndex = fileList.size
Pair(foundIndex, nextIndex)
}
}

val chapters: WritableArray = WritableNativeArray()
linkedIndex.forEachIndexed { arrayIndex, (firstIndex, lastIndex) ->
val chapter = WritableNativeMap()
val entryPath = "${contentDir}/${entryList[arrayIndex].href}"
chapter.putString("name", entryList[arrayIndex].name)
if (firstIndex == -1 || firstIndex == lastIndex) {
chapter.putString("path", entryPath)
} else {
val mergedChapterPath = "${entryPath}-copy-$arrayIndex.html"
chapter.putString("path", mergedChapterPath)
val entryFile = File(entryPath)
val mergedFile = File(mergedChapterPath)
mergedFile.createNewFile()
mergedFile.appendBytes(entryFile.readBytes())
for (i in firstIndex + 1 until lastIndex) {
val file = File("${contentDir}/${fileList[i]}")
mergedFile.appendBytes(file.readBytes())
}
}
chapters.pushMap(chapter)
}
return chapters
}

private fun getNovelMetadata(file: File, contentDir: String): ReadableMap {
val novel: WritableMap = WritableNativeMap()
val chapters: WritableArray = WritableNativeArray()
val parser = initParse(file)
val refMap = HashMap<String, String>()
val entryList = mutableListOf<ChapterEntry>()
val fileList = mutableListOf<String>()
val tocFile = File(contentDir, "toc.ncx")
if (tocFile.exists()) {
val tocParser = initParse(tocFile)
var label = ""
while(tocParser.next() != XmlPullParser.END_DOCUMENT){
if(tocParser.name == "navMap") break;
}
while (tocParser.next() != XmlPullParser.END_DOCUMENT) {
val tag = tocParser.name
if(tag == "navMap") break;
if (tag != null) {
if (tag == "text") {
label = readText(tocParser)
} else if (tag == "content") {
val href = cleanUrl(tocParser.getAttributeValue(null, "src"))
if(label.isNotBlank()){
if (label.isNotBlank() && File("${contentDir}/$href").exists()) {
entryList.add(ChapterEntry(name = label, href = href))
}
label = ""
}
}
}
}else{
} else {
throw Error("Table of content doesn't exist!")
}
var cover: String? = null
var entryIndex = 0;
while (parser.next() != XmlPullParser.END_DOCUMENT) {
val tag = parser.name
if (tag != null) {
Expand All @@ -104,21 +157,12 @@ class EpubUtil(context: ReactApplicationContext) : ReactContextBaseJavaModule(co
refMap[id] = href
}
}

"itemref" -> {
val idRef = parser.getAttributeValue(null, "idref")
val href = refMap[idRef]
val chapterFile = File("$contentDir/$href")
if (chapterFile.exists()) {
if (entryIndex == 0 || entryIndex < entryList.size && entryList[entryIndex].href == href) {
val newChapter = WritableNativeMap()
newChapter.putString("path", chapterFile.path)
newChapter.putString("name", entryList[entryIndex].name)
chapters.pushMap(newChapter)
entryIndex += 1
}else{
// merge to previous entry
File("$contentDir/${entryList[entryIndex - 1].href}").appendBytes(chapterFile.readBytes())
}
if (href != null && File("$contentDir/$href").exists()) {
fileList.add(href)
}
}

Expand All @@ -136,24 +180,25 @@ class EpubUtil(context: ReactApplicationContext) : ReactContextBaseJavaModule(co
parser.next()
}
}
val chapters = mergeChapters(entryList, fileList, contentDir)
novel.putArray("chapters", chapters)
if (cover != null) {
val coverPath = contentDir + "/" + refMap[cover]
novel.putString("cover", coverPath)
} else {
// try scanning Images dir if exists
val imageDir = File("$contentDir/Images")
if(imageDir.exists() && imageDir.isDirectory){
if (imageDir.exists() && imageDir.isDirectory) {
imageDir.listFiles()?.forEach { img ->
if (img.isFile && img.name.lowercase().contains("cover|illus".toRegex())){
if (img.isFile && img.name.lowercase().contains("cover|illus".toRegex())) {
cover = img.path
}
}
}
if (cover != null){
if (cover != null) {
novel.putString("cover", cover)
}
}
novel.putArray("chapters", chapters)
return novel
}
}
120 changes: 19 additions & 101 deletions src/database/queries/ChapterQueries.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import * as SQLite from 'expo-sqlite';
import { showToast } from '@utils/showToast';
import { getPlugin } from '@plugins/pluginManager';
import { ChapterInfo, DownloadedChapter } from '../types';
import { ChapterItem } from '@plugins/types';

import * as cheerio from 'cheerio';
import { txnErrorCallback } from '@database/utils/helpers';
import { Plugin } from '@plugins/types';
import { Update } from '../types';
import { noop } from 'lodash-es';
import { getString } from '@strings/translations';
import FileManager from '@native/FileManager';
import { NOVEL_STORAGE } from '@utils/Storages';
import { downloadFile } from '@plugins/helpers/fetch';

const db = SQLite.openDatabase('lnreader.db');
const insertChapterQuery = `
Expand Down Expand Up @@ -94,6 +90,21 @@ export const getNovelChapters = (novelId: number): Promise<ChapterInfo[]> => {
);
};

export const getChapter = async (
chapterId: number,
): Promise<ChapterInfo | null> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
'SELECT * FROM Chapter WHERE id = ?',
[chapterId],
(txObj, { rows }) => resolve(rows.item(0)),
txnErrorCallback,
);
}),
);
};

export const getPageChapters = (
novelId: number,
sort?: string,
Expand Down Expand Up @@ -126,7 +137,7 @@ const getPrevChapterQuery = `
export const getPrevChapter = (
novelId: number,
chapterId: number,
): Promise<ChapterInfo> => {
): Promise<ChapterInfo | null> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
Expand Down Expand Up @@ -157,7 +168,7 @@ const getNextChapterQuery = `
export const getNextChapter = (
novelId: number,
chapterId: number,
): Promise<ChapterInfo> => {
): Promise<ChapterInfo | null> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
Expand Down Expand Up @@ -215,107 +226,14 @@ export const markAllChaptersUnread = async (novelId: number) => {
});
};

const createChapterFolder = async (
path: string,
data: {
pluginId: string;
novelId: number;
chapterId: number;
},
): Promise<string> => {
const mkdirIfNot = async (p: string, nomedia: boolean) => {
const nomediaPath =
p + (p.charAt(p.length - 1) === '/' ? '' : '/') + '.nomedia';
if (!(await FileManager.exists(p))) {
await FileManager.mkdir(p);
if (nomedia) {
await FileManager.writeFile(nomediaPath, ',');
}
}
};

await mkdirIfNot(path, false);

const { pluginId, novelId, chapterId } = data;
await mkdirIfNot(`${path}/${pluginId}/${novelId}/${chapterId}/`, true);
return `${path}/${pluginId}/${novelId}/${chapterId}/`;
};

const downloadFiles = async (
html: string,
plugin: Plugin,
novelId: number,
chapterId: number,
): Promise<void> => {
try {
const folder = await createChapterFolder(NOVEL_STORAGE, {
pluginId: plugin.id,
novelId,
chapterId,
}).catch(error => {
throw error;
});
const loadedCheerio = cheerio.load(html);
const imgs = loadedCheerio('img').toArray();
for (let i = 0; i < imgs.length; i++) {
const elem = loadedCheerio(imgs[i]);
const url = elem.attr('src');
if (url) {
const fileurl = folder + i + '.b64.png';
elem.attr('src', `file://${fileurl}`);
await downloadFile(url, fileurl, plugin.imageRequestInit);
}
}
await FileManager.writeFile(folder + 'index.html', loadedCheerio.html());
} catch (error) {
throw error;
}
};

// novelId for determine folder %LNReaderDownloadDir%/novelId/ChapterId/
export const downloadChapter = async (
pluginId: string,
novelId: number,
chapterId: number,
chapterPath: string,
) => {
try {
const plugin = getPlugin(pluginId);
if (!plugin) {
throw new Error(getString('downloadScreen.pluginNotFound'));
}
const chapterText = await plugin.parseChapter(chapterPath);
if (chapterText && chapterText.length) {
await downloadFiles(chapterText, plugin, novelId, chapterId);
db.transaction(tx => {
tx.executeSql('UPDATE Chapter SET isDownloaded = 1 WHERE id = ?', [
chapterId,
]);
});
} else {
throw new Error(getString('downloadScreen.chapterEmptyOrScrapeError'));
}
} catch (error) {
throw error;
}
};

const deleteDownloadedFiles = async (
pluginId: string,
novelId: number,
chapterId: number,
) => {
try {
const path = await createChapterFolder(NOVEL_STORAGE, {
pluginId,
novelId,
chapterId,
});
const files = await FileManager.readDir(path);
for (let i = 0; i < files.length; i++) {
await FileManager.unlink(files[i].path);
}
await FileManager.unlink(path);
const chapterFolder = `${NOVEL_STORAGE}/${pluginId}/${novelId}/${chapterId}`;
await FileManager.unlink(chapterFolder);
} catch (error) {
throw new Error(getString('novelScreen.deleteChapterError'));
}
Expand Down
19 changes: 17 additions & 2 deletions src/database/queries/NovelQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,22 @@ export const getAllNovels = async (): Promise<NovelInfo[]> => {
);
};

export const getNovel = async (
export const getNovelById = async (
novelId: number,
): Promise<NovelInfo | null> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
'SELECT * FROM Novel WHERE id = ?',
[novelId],
(txObj, { rows }) => resolve(rows.item(0)),
txnErrorCallback,
);
}),
);
};

export const getNovelByPath = async (
novelPath: string,
pluginId: string,
): Promise<NovelInfo | null> => {
Expand All @@ -101,7 +116,7 @@ export const switchNovelToLibrary = async (
novelPath: string,
pluginId: string,
) => {
const novel = await getNovel(novelPath, pluginId);
const novel = await getNovelByPath(novelPath, pluginId);
if (novel) {
db.transaction(tx => {
tx.executeSql(
Expand Down
Loading

0 comments on commit 0787967

Please sign in to comment.