Files
lnreader/src/database/queries/ChapterQueries.ts
T
2024-06-08 01:42:47 +07:00

539 lines
13 KiB
TypeScript

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';
const db = SQLite.openDatabase('lnreader.db');
const insertChapterQuery = `
INSERT OR IGNORE INTO Chapter (path, name, releaseTime, novelId, chapterNumber, page, position)
VALUES (?, ?, ?, ?, ?, ?, ?)
`;
export const insertChapters = async (
novelId: number,
chapters?: ChapterItem[],
) => {
if (!chapters?.length) {
return;
}
db.transaction(tx => {
chapters.forEach((chapter, index) => {
tx.executeSql(
insertChapterQuery,
[
chapter.path,
chapter.name,
chapter.releaseTime || '',
novelId,
chapter.chapterNumber || null,
chapter.page || '1',
index,
],
(txObj, { insertId }) => {
if (!insertId || insertId < 0) {
tx.executeSql(
`
UPDATE Chapter SET
page = ?, position = ?
WHERE path = ? AND novelId = ? (AND page != ? OR position != ?)
`,
[
chapter.page || '1',
index,
chapter.path,
novelId,
chapter.page || '1',
index,
],
);
}
},
);
});
});
};
const getPageChaptersQuery = (sort = 'ORDER BY position ASC', filter = '') =>
`SELECT * FROM Chapter WHERE novelId = ? AND page = ? ${filter} ${sort}`;
export const getCustomPages = (
novelId: number,
): Promise<{ page: string }[]> => {
return new Promise(resolve => {
db.transaction(tx => {
tx.executeSql(
'SELECT DISTINCT page from Chapter WHERE novelId = ?',
[novelId],
(txObj, { rows }) => resolve(rows._array),
);
});
});
};
export const getNovelChapters = (novelId: number): Promise<ChapterInfo[]> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
'SELECT * FROM Chapter WHERE novelId = ?',
[novelId],
(txObj, { rows }) => resolve((rows as any)._array),
txnErrorCallback,
);
}),
);
};
export const getPageChapters = (
novelId: number,
sort?: string,
filter?: string,
page?: string,
): Promise<ChapterInfo[]> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getPageChaptersQuery(sort, filter),
[novelId, page || '1'],
(txObj, { rows }) => resolve((rows as any)._array),
txnErrorCallback,
);
}),
);
};
const getPrevChapterQuery = `
SELECT
*
FROM
Chapter
WHERE
novelId = ?
AND
id < ?
`;
export const getPrevChapter = (
novelId: number,
chapterId: number,
): Promise<ChapterInfo> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getPrevChapterQuery,
[novelId, chapterId],
(_txObj, results) =>
resolve(results.rows.item(results.rows.length - 1)),
() => {
showToast(getString('readerScreen.noPreviousChapter'));
return false;
},
);
}),
);
};
const getNextChapterQuery = `
SELECT
*
FROM
Chapter
WHERE
novelId = ?
AND
id > ?
`;
export const getNextChapter = (
novelId: number,
chapterId: number,
): Promise<ChapterInfo> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getNextChapterQuery,
[novelId, chapterId],
(_txObj, results) => resolve(results.rows.item(0)),
() => {
showToast(getString('readerScreen.noNextChapter'));
return false;
},
);
}),
);
};
const markChapterReadQuery = 'UPDATE Chapter SET `unread` = 0 WHERE id = ?';
export const markChapterRead = async (chapterId: number) => {
db.transaction(tx => {
tx.executeSql(markChapterReadQuery, [chapterId], noop, (_txObj, _error) => {
// console.log(error)
return false;
});
});
};
const markChapterUnreadQuery = 'UPDATE Chapter SET `unread` = 1 WHERE id = ?';
export const markChapterUnread = async (chapterId: number) => {
db.transaction(tx => {
tx.executeSql(markChapterUnreadQuery, [chapterId], noop, txnErrorCallback);
});
};
const markAllChaptersReadQuery =
'UPDATE Chapter SET `unread` = 0 WHERE novelId = ?';
export const markAllChaptersRead = async (novelId: number) => {
db.transaction(tx => {
tx.executeSql(markAllChaptersReadQuery, [novelId], noop, txnErrorCallback);
});
};
const markAllChaptersUnreadQuery =
'UPDATE Chapter SET `unread` = 1 WHERE novelId = ?';
export const markAllChaptersUnread = async (novelId: number) => {
db.transaction(tx => {
tx.executeSql(
markAllChaptersUnreadQuery,
[novelId],
noop,
txnErrorCallback,
);
});
};
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 imageb64 = await plugin.fetchImage(url);
const fileurl = folder + i + '.b64.png';
elem.attr('src', `file://${fileurl}`);
FileManager.writeFile(fileurl, imageb64, 'base64');
}
}
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);
} catch (error) {
throw new Error(getString('novelScreen.deleteChapterError'));
}
};
// delete downloaded chapter
export const deleteChapter = async (
pluginId: string,
novelId: number,
chapterId: number,
) => {
const updateIsDownloadedQuery =
'UPDATE Chapter SET isDownloaded = 0 WHERE id = ?';
await deleteDownloadedFiles(pluginId, novelId, chapterId);
db.transaction(tx => {
tx.executeSql(updateIsDownloadedQuery, [chapterId], noop, txnErrorCallback);
});
};
export const deleteChapters = async (
pluginId: string,
novelId: number,
chapters?: ChapterInfo[],
) => {
if (!chapters?.length) {
return;
}
const chapterIdsString = chapters?.map(chapter => chapter.id).toString();
const updateIsDownloadedQuery = `UPDATE Chapter SET isDownloaded = 0 WHERE id IN (${chapterIdsString})`;
await Promise.all(
chapters?.map(chapter =>
deleteDownloadedFiles(pluginId, novelId, chapter.id),
),
);
db.transaction(tx => {
tx.executeSql(
updateIsDownloadedQuery,
undefined,
undefined,
txnErrorCallback,
);
});
};
export const deleteDownloads = async (chapters: DownloadedChapter[]) => {
await Promise.all(
chapters?.map(chapter => {
deleteDownloadedFiles(chapter.pluginId, chapter.novelId, chapter.id);
}),
);
db.transaction(tx => {
tx.executeSql('UPDATE Chapter SET isDownloaded = 0', [], () =>
showToast(getString('novelScreen.deletedAllDownloads')),
);
});
};
const getReadDownloadedChapters = async (): Promise<DownloadedChapter[]> => {
return new Promise(resolve => {
db.transaction(tx => {
tx.executeSql(
`
SELECT Chapter.id, Chapter.novelId, pluginId
FROM Chapter
JOIN Novel
ON Novel.id = Chapter.novelId AND unread = 0 AND isDownloaded = 1`,
[],
(txObj, { rows }) => resolve((rows as any)._array),
);
});
});
};
export const deleteReadChaptersFromDb = async () => {
const chapters = await getReadDownloadedChapters();
await Promise.all(
chapters?.map(chapter => {
deleteDownloadedFiles(chapter.pluginId, chapter.novelId, chapter.novelId);
}),
);
const chapterIdsString = chapters?.map(chapter => chapter.id).toString();
const updateIsDownloadedQuery = `UPDATE Chapter SET isDownloaded = 0 WHERE id IN (${chapterIdsString})`;
db.transaction(tx => {
tx.executeSql(updateIsDownloadedQuery, [], noop, txnErrorCallback);
});
showToast(getString('novelScreen.readChaptersDeleted'));
};
export const updateChapterProgress = async (
chapterId: number,
progress: number,
) => {
db.transaction(tx => {
tx.executeSql('UPDATE Chapter SET progress = ? WHERE id = ?', [
progress,
chapterId,
]);
});
};
const bookmarkChapterQuery =
'UPDATE Chapter SET bookmark = (CASE WHEN bookmark = 0 THEN 1 ELSE 0 END) WHERE id = ?';
export const bookmarkChapter = async (chapterId: number) => {
db.transaction(tx => {
tx.executeSql(bookmarkChapterQuery, [chapterId]);
});
};
const markPreviuschaptersReadQuery =
'UPDATE Chapter SET `unread` = 0 WHERE id <= ? AND novelId = ?';
export const markPreviuschaptersRead = async (
chapterId: number,
novelId: number,
) => {
db.transaction(tx => {
tx.executeSql(
markPreviuschaptersReadQuery,
[chapterId, novelId],
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log(error)
return false;
},
);
});
};
const markPreviousChaptersUnreadQuery =
'UPDATE Chapter SET `unread` = 1 WHERE id <= ? AND novelId = ?';
export const markPreviousChaptersUnread = async (
chapterId: number,
novelId: number,
) => {
db.transaction(tx => {
tx.executeSql(
markPreviousChaptersUnreadQuery,
[chapterId, novelId],
(_txObj, _res) => {},
(_txObj, _error) => {
// console.log(error)
return false;
},
);
});
};
const getDownloadedChaptersQuery = `
SELECT
Chapter.*,
Novel.pluginId, Novel.name as novelName, Novel.cover as novelCover, Novel.path as novelPath
FROM Chapter
JOIN Novel
ON Chapter.novelId = Novel.id
WHERE Chapter.isDownloaded = 1
`;
export const getDownloadedChapters = (): Promise<DownloadedChapter[]> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getDownloadedChaptersQuery,
undefined,
(txObj, { rows }) => {
resolve(rows._array);
},
(_txObj, _error) => {
// console.log('Error ', error)
return false;
},
);
}),
);
};
const getUpdatesQuery = `
SELECT
Chapter.*,
pluginId, Novel.id as novelId, Novel.name as novelName, Novel.path as novelPath, cover as novelCover
FROM
Chapter
JOIN
Novel
ON Chapter.novelId = Novel.id AND Chapter.updatedTime IS NOT NULL
ORDER BY Chapter.updatedTime DESC
`;
export const getUpdatesFromDb = (): Promise<Update[]> => {
return new Promise(resolve =>
db.transaction(tx => {
tx.executeSql(
getUpdatesQuery,
[],
(txObj, { rows }) => resolve((rows as any)._array),
txnErrorCallback,
);
}),
);
};
export const clearUpdates = () => {
db.transaction(tx => {
tx.executeSql(
'UPDATE Chapter SET updatedTime = NULL',
[],
noop,
txnErrorCallback,
);
});
};