diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt index 1255c19..60e4421 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Chapter.kt @@ -15,7 +15,9 @@ import ir.armor.tachidesk.impl.util.awaitSingle import ir.armor.tachidesk.model.database.table.ChapterTable import ir.armor.tachidesk.model.database.table.MangaTable import ir.armor.tachidesk.model.database.table.PageTable +import ir.armor.tachidesk.model.database.table.toDataClass import ir.armor.tachidesk.model.dataclass.ChapterDataClass +import org.jetbrains.exposed.sql.SortOrder.DESC import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert @@ -26,42 +28,51 @@ import org.jetbrains.exposed.sql.update object Chapter { /** get chapter list when showing a manga */ - suspend fun getChapterList(mangaId: Int): List { - val mangaDetails = getManga(mangaId) - val source = getHttpSource(mangaDetails.sourceId.toLong()) - - val chapterList = source.fetchChapterList( - SManga.create().apply { - title = mangaDetails.title - url = mangaDetails.url - } - ).awaitSingle() - - val chapterCount = chapterList.count() - - return transaction { - chapterList.reversed().forEachIndexed { index, fetchedChapter -> - val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() - if (chapterEntry == null) { - ChapterTable.insert { - it[url] = fetchedChapter.url - it[name] = fetchedChapter.name - it[date_upload] = fetchedChapter.date_upload - it[chapter_number] = fetchedChapter.chapter_number - it[scanlator] = fetchedChapter.scanlator - - it[chapterIndex] = index + 1 - it[manga] = mangaId + suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean): List { + return if (!onlineFetch) { + transaction { + ChapterTable.select { ChapterTable.manga eq mangaId }.orderBy(ChapterTable.chapterIndex to DESC) + .map { + ChapterTable.toDataClass(it) } - } else { - ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) { - it[name] = fetchedChapter.name - it[date_upload] = fetchedChapter.date_upload - it[chapter_number] = fetchedChapter.chapter_number - it[scanlator] = fetchedChapter.scanlator + } + } else { - it[chapterIndex] = index + 1 - it[manga] = mangaId + val mangaDetails = getManga(mangaId) + val source = getHttpSource(mangaDetails.sourceId.toLong()) + val chapterList = source.fetchChapterList( + SManga.create().apply { + title = mangaDetails.title + url = mangaDetails.url + } + ).awaitSingle() + + val chapterCount = chapterList.count() + + transaction { + chapterList.reversed().forEachIndexed { index, fetchedChapter -> + val chapterEntry = ChapterTable.select { ChapterTable.url eq fetchedChapter.url }.firstOrNull() + if (chapterEntry == null) { + ChapterTable.insert { + it[url] = fetchedChapter.url + it[name] = fetchedChapter.name + it[date_upload] = fetchedChapter.date_upload + it[chapter_number] = fetchedChapter.chapter_number + it[scanlator] = fetchedChapter.scanlator + + it[chapterIndex] = index + 1 + it[manga] = mangaId + } + } else { + ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) { + it[name] = fetchedChapter.name + it[date_upload] = fetchedChapter.date_upload + it[chapter_number] = fetchedChapter.chapter_number + it[scanlator] = fetchedChapter.scanlator + + it[chapterIndex] = index + 1 + it[manga] = mangaId + } } } } @@ -83,10 +94,12 @@ object Chapter { } } - val dbChapterMap = transaction { ChapterTable.selectAll() } - .associateBy({ it[ChapterTable.url] }, { it }) + val dbChapterMap = transaction { + ChapterTable.select { ChapterTable.manga eq mangaId } + .associateBy({ it[ChapterTable.url] }, { it }) + } - chapterList.mapIndexed { index, it -> + return chapterList.mapIndexed { index, it -> val dbChapter = dbChapterMap.getValue(it.url) diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt index 106818f..527974c 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Manga.kt @@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.SManga import ir.armor.tachidesk.impl.MangaList.proxyThumbnailUrl import ir.armor.tachidesk.impl.Source.getSource +import ir.armor.tachidesk.impl.util.CachedImageResponse.clearCachedImage import ir.armor.tachidesk.impl.util.CachedImageResponse.getCachedImageResponse import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource import ir.armor.tachidesk.impl.util.await @@ -35,17 +36,17 @@ object Manga { text } - suspend fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass { + suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } - return if (mangaEntry[MangaTable.initialized]) { + return if (mangaEntry[MangaTable.initialized] && !onlineFetch) { MangaDataClass( mangaId, mangaEntry[MangaTable.sourceReference].toString(), mangaEntry[MangaTable.url], mangaEntry[MangaTable.title], - if (proxyThumbnail) proxyThumbnailUrl(mangaId) else mangaEntry[MangaTable.thumbnail_url], + proxyThumbnailUrl(mangaId), true, @@ -55,7 +56,8 @@ object Manga { mangaEntry[MangaTable.genre], MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, mangaEntry[MangaTable.inLibrary], - getSource(mangaEntry[MangaTable.sourceReference]) + getSource(mangaEntry[MangaTable.sourceReference]), + false ) } else { // initialize manga val source = getHttpSource(mangaEntry[MangaTable.sourceReference]) @@ -81,8 +83,9 @@ object Manga { } } + clearMangaThumbnail(mangaId) + mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } - val newThumbnail = mangaEntry[MangaTable.thumbnail_url] MangaDataClass( mangaId, @@ -90,7 +93,7 @@ object Manga { mangaEntry[MangaTable.url], mangaEntry[MangaTable.title], - if (proxyThumbnail) proxyThumbnailUrl(mangaId) else newThumbnail, + proxyThumbnailUrl(mangaId), true, @@ -100,28 +103,37 @@ object Manga { fetchedManga.genre, MangaStatus.valueOf(fetchedManga.status).name, false, - getSource(mangaEntry[MangaTable.sourceReference]) + getSource(mangaEntry[MangaTable.sourceReference]), + true ) } } private val applicationDirs by DI.global.instance() suspend fun getMangaThumbnail(mangaId: Int): Pair { - val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } val saveDir = applicationDirs.thumbnailsRoot val fileName = mangaId.toString() return getCachedImageResponse(saveDir, fileName) { + getManga(mangaId) // make sure is initialized + + val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! } + val sourceId = mangaEntry[MangaTable.sourceReference] val source = getHttpSource(sourceId) - var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url] - if (thumbnailUrl == null || thumbnailUrl.isEmpty()) { - thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!! - } + + val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]!! source.client.newCall( GET(thumbnailUrl, source.headers) ).await() } } + + suspend fun clearMangaThumbnail(mangaId: Int) { + val saveDir = applicationDirs.thumbnailsRoot + val fileName = mangaId.toString() + + clearCachedImage(saveDir, fileName) + } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt index 32b4525..2a8e424 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/CachedImageResponse.kt @@ -64,4 +64,11 @@ object CachedImageResponse { throw Exception("request error! ${response.code}") } } + + suspend fun clearCachedImage(saveDir: String, fileName: String) { + val cachedFile = findFileNameStartingWith(saveDir, fileName) + cachedFile?.also { + File(it).delete() + } + } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/model/database/table/ChapterTable.kt b/server/src/main/kotlin/ir/armor/tachidesk/model/database/table/ChapterTable.kt index 674935c..4c34044 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/model/database/table/ChapterTable.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/model/database/table/ChapterTable.kt @@ -7,7 +7,9 @@ package ir.armor.tachidesk.model.database.table * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import ir.armor.tachidesk.model.dataclass.ChapterDataClass import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ResultRow object ChapterTable : IntIdTable() { val url = varchar("url", 2048) @@ -25,3 +27,17 @@ object ChapterTable : IntIdTable() { val manga = reference("manga", MangaTable) } + +fun ChapterTable.toDataClass(chapterEntry: ResultRow) = + ChapterDataClass( + chapterEntry[ChapterTable.url], + chapterEntry[ChapterTable.name], + chapterEntry[ChapterTable.date_upload], + chapterEntry[ChapterTable.chapter_number], + chapterEntry[ChapterTable.scanlator], + chapterEntry[ChapterTable.manga].value, + chapterEntry[ChapterTable.isRead], + chapterEntry[ChapterTable.isBookmarked], + chapterEntry[ChapterTable.lastPageRead], + chapterEntry[ChapterTable.chapterIndex], + ) diff --git a/server/src/main/kotlin/ir/armor/tachidesk/model/dataclass/MangaDataClass.kt b/server/src/main/kotlin/ir/armor/tachidesk/model/dataclass/MangaDataClass.kt index 03bb5c8..0474743 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/model/dataclass/MangaDataClass.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/model/dataclass/MangaDataClass.kt @@ -25,7 +25,9 @@ data class MangaDataClass( val genre: String? = null, val status: String = MangaStatus.UNKNOWN.name, val inLibrary: Boolean = false, - val source: SourceDataClass? = null + val source: SourceDataClass? = null, + + val freshData: Boolean = false ) data class PagedMangaListDataClass( diff --git a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt index faded10..a597389 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt @@ -191,9 +191,11 @@ object JavalinSetup { // get manga info app.get("/api/v1/manga/:mangaId/") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() + val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean() + ctx.json( future { - getManga(mangaId) + getManga(mangaId, onlineFetch) } ) } @@ -254,7 +256,10 @@ object JavalinSetup { // get chapter list when showing a manga app.get("/api/v1/manga/:mangaId/chapters") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() - ctx.json(future { getChapterList(mangaId) }) + + val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean() + + ctx.json(future { getChapterList(mangaId, onlineFetch) }) } // used to display a chapter, get a chapter in order to show it's pages diff --git a/webUI/react/src/components/ChapterCard.tsx b/webUI/react/src/components/ChapterCard.tsx index 7f356b9..b41bf92 100644 --- a/webUI/react/src/components/ChapterCard.tsx +++ b/webUI/react/src/components/ChapterCard.tsx @@ -50,13 +50,14 @@ const useStyles = makeStyles((theme) => ({ interface IProps{ chapter: IChapter + triggerChaptersUpdate: () => void } export default function ChapterCard(props: IProps) { const classes = useStyles(); const history = useHistory(); const theme = useTheme(); - const { chapter } = props; + const { chapter, triggerChaptersUpdate } = props; const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toISOString().slice(0, 10); @@ -71,12 +72,12 @@ export default function ChapterCard(props: IProps) { }; const sendChange = (key: string, value: any) => { - console.log(`${key} -> ${value}`); handleClose(); const formData = new FormData(); formData.append(key, value); - client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData); + client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData) + .then(() => triggerChaptersUpdate()); }; return ( diff --git a/webUI/react/src/components/MangaDetails.tsx b/webUI/react/src/components/MangaDetails.tsx index ec43702..60ac609 100644 --- a/webUI/react/src/components/MangaDetails.tsx +++ b/webUI/react/src/components/MangaDetails.tsx @@ -198,7 +198,7 @@ export default function MangaDetails(props: IProps) {
- Manga Thumbnail + Manga Thumbnail

diff --git a/webUI/react/src/screens/Manga.tsx b/webUI/react/src/screens/Manga.tsx index 464614d..c43e523 100644 --- a/webUI/react/src/screens/Manga.tsx +++ b/webUI/react/src/screens/Manga.tsx @@ -43,9 +43,9 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -const InnerItem = React.memo(({ chapters, index }: any) => ( - -)); +// const InnerItem = React.memo(({ chapters, index }: any) => ( +// +// )); export default function Manga() { const classes = useStyles(); @@ -58,23 +58,37 @@ export default function Manga() { const [manga, setManga] = useState(); const [chapters, setChapters] = useState([]); + const [chapterUpdateTriggerer, setChapterUpdateTriggerer] = useState(0); + + function triggerChaptersUpdate() { + setChapterUpdateTriggerer(chapterUpdateTriggerer + 1); + } useEffect(() => { - client.get(`/api/v1/manga/${id}/`) - .then((response) => response.data) - .then((data: IManga) => { - setManga(data); - setTitle(data.title); - }); - }, []); + if (manga === undefined || !manga.freshData) { + client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`) + .then((response) => response.data) + .then((data: IManga) => { + setManga(data); + setTitle(data.title); + }); + } + }, [manga]); useEffect(() => { - client.get(`/api/v1/manga/${id}/chapters`) + const shouldFetchOnline = chapters.length > 0 && chapterUpdateTriggerer === 0; + client.get(`/api/v1/manga/${id}/chapters?onlineFetch=${shouldFetchOnline}`) .then((response) => response.data) .then((data) => setChapters(data)); - }, []); + }, [chapters.length, chapterUpdateTriggerer]); - const itemContent = (index:any) => ; + // const itemContent = (index:any) => ; + const itemContent = (index:any) => ( + + ); return (
diff --git a/webUI/react/src/typings.d.ts b/webUI/react/src/typings.d.ts index 1af443d..b9792c6 100644 --- a/webUI/react/src/typings.d.ts +++ b/webUI/react/src/typings.d.ts @@ -50,6 +50,8 @@ interface IManga { inLibrary: boolean source: ISource + + freshData: boolean } interface IChapter {