Compare commits

...

12 Commits

Author SHA1 Message Date
Aria Moradi a5bab7425d [CI RELEASE] try lfs fix 2021-02-05 00:11:20 +03:30
Aria Moradi 93d5ab3739 [CI RELEASE] v.0.2.0 2021-02-04 23:55:01 +03:30
Aria Moradi 3146fefb55 change build scripts 2021-02-04 23:47:16 +03:30
Aria Moradi 1ea51bb9df add launch4j 2021-02-04 23:40:40 +03:30
Aria Moradi 98bd664ab6 Tray Icon 2021-02-04 18:02:46 +03:30
Aria Moradi 61aee2e784 hint added 2021-02-04 18:02:34 +03:30
Aria Moradi 22bf49078f cached response for source list iconUrl 2021-02-04 14:53:34 +03:30
Aria Moradi 7284e0d4ae cached extension icon 2021-02-04 14:47:27 +03:30
Aria Moradi d39d075b1a [CI RELEASE] dummy file to trigger CI 2021-02-04 04:55:36 +03:30
Aria Moradi 0f6749b0c1 now support backward writing! 2021-02-04 04:48:15 +03:30
Aria Moradi 771030b911 [CI RELEASE] v.0.1.5 2021-02-04 04:28:00 +03:30
Aria Moradi 8d5744a2cf fix chapter naming, db naming 2021-02-04 04:27:25 +03:30
19 changed files with 263 additions and 111 deletions
+11 -4
View File
@@ -1,10 +1,17 @@
#!/bin/bash
cp ../master/repo/* .
new_build=$(ls | tail -1)
echo "New build file name: $new_build"
git lfs install
git lfs track "*.zip"
cp -f $new_build Tachidesk-latest.jar
cp ../master/repo/* .
new_jar_build=$(ls *.jar| tail -1)
echo "last jar build file name: $new_jar_build"
new_win32_build=$(ls *.zip| tail -1)
echo "last win32 build file name: $new_win32_build"
cp -f $new_jar_build Tachidesk-latest.jar
cp -f $new_win32_build Tachidesk-latest-win32.zip
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
+2 -1
View File
@@ -4,10 +4,11 @@
last_commit_log=$(git log -1 --pretty=format:"%s")
echo "last commit log: $last_commit_log"
filter_count=$(echo "$last_commit_log" | grep -c '\[RELEASE CI\]' )
filter_count=$(echo "$last_commit_log" | grep -e '\[RELEASE CI\]' -e '\[CI RELEASE\]' | wc -c)
echo "count is: $filter_count"
if [ "$filter_count" -gt 0 ]; then
mkdir -p repo/
cp server/build/Tachidesk-*.jar repo/
cp server/build/Tachidesk-*.zip repo/
fi
+2 -2
View File
@@ -54,12 +54,12 @@ jobs:
cd master
./scripts/getAndroid.sh
- name: Build the Jar
- name: Build Jar and launch4j
uses: eskatos/gradle-command-action@v1
with:
build-root-directory: master
wrapper-directory: master
arguments: :server:shadowJar --stacktrace
arguments: :server:windowsPackage --stacktrace
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
View File
+68 -7
View File
@@ -6,9 +6,10 @@ plugins {
application
id("com.github.johnrengelman.shadow") version "6.1.0"
id("org.jmailen.kotlinter") version "3.3.0"
id("edu.sc.seis.launch4j") version "2.4.9"
}
val TachideskVersion = "v0.1.4"
val TachideskVersion = "v0.2.0"
repositories {
@@ -76,10 +77,13 @@ dependencies {
// Exposed ORM
val exposed_version = "0.28.1"
implementation ("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation ("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation ("com.h2database:h2:1.4.199")
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("com.h2database:h2:1.4.199")
// tray icon
implementation("com.dorkbox:SystemTray:3.17")
// AndroidCompat
@@ -91,8 +95,8 @@ dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
}
val name = "ir.armor.tachidesk.Main"
application {
val name = "ir.armor.tachidesk.Main"
mainClass.set(name)
// Required by ShadowJar.
@@ -116,7 +120,7 @@ val TachideskRevision = Runtime
it.bufferedReader().use(BufferedReader::readText)
}
process.destroy()
"r"+output.trim()
"r" + output.trim()
}
@@ -139,6 +143,63 @@ tasks {
}
}
launch4j { //used for windows
mainClassName = name
bundledJrePath = "jre"
bundledJre64Bit = true
jreMinVersion = "8"
outputDir = "Tachidesk-$TachideskVersion-$TachideskRevision-win32"
icon = "${projectDir}/src/main/resources/icon/icon_round.ico"
jar = "${projectDir}/build/Tachidesk-$TachideskVersion-$TachideskRevision.jar"
}
tasks.register<Zip>("windowsPackage") {
from(fileTree("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32"))
destinationDirectory.set(File("$buildDir"))
archiveFileName.set("Tachidesk-$TachideskVersion-$TachideskRevision-win32.zip")
dependsOn("windowsPackageWorkaround2")
}
tasks.register<Delete>("windowsPackageWorkaround2") {
delete(
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jre",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/lib",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/server.exe",
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32"
)
dependsOn("windowsPackageWorkaround")
}
tasks.register<Copy>("windowsPackageWorkaround") {
from("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
dependsOn("deleteUnwantedJreDir")
}
tasks.register<Delete>("deleteUnwantedJreDir") {
delete(
"$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32/jdk8u282-b08-jre"
)
dependsOn("addJreToDistributable")
}
tasks.register<Copy>("addJreToDistributable") {
from(zipTree("$buildDir/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip"))
into("$buildDir/Tachidesk-$TachideskVersion-$TachideskRevision-win32")
eachFile {
path = path.replace(".*-jre".toRegex(),"jre")
}
dependsOn("downloadJre")
dependsOn("createExe")
}
tasks.register<de.undercouch.gradle.tasks.download.Download>("downloadJre") {
src("https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u282-b08/OpenJDK8U-jre_x86-32_windows_hotspot_8u282b08.zip")
dest(buildDir)
overwrite(false)
onlyIfModified(true)
}
tasks.withType<ShadowJar> {
destinationDir = File("$rootDir/server/build")
dependsOn("lintKotlin")
@@ -9,6 +9,7 @@ import io.javalin.Javalin
import ir.armor.tachidesk.util.applicationSetup
import ir.armor.tachidesk.util.getChapter
import ir.armor.tachidesk.util.getChapterList
import ir.armor.tachidesk.util.getExtensionIcon
import ir.armor.tachidesk.util.getExtensionList
import ir.armor.tachidesk.util.getManga
import ir.armor.tachidesk.util.getMangaList
@@ -17,10 +18,12 @@ import ir.armor.tachidesk.util.getSource
import ir.armor.tachidesk.util.getSourceList
import ir.armor.tachidesk.util.getThumbnail
import ir.armor.tachidesk.util.installAPK
import ir.armor.tachidesk.util.openInBrowser
import ir.armor.tachidesk.util.removeExtension
import ir.armor.tachidesk.util.sourceFilters
import ir.armor.tachidesk.util.sourceGlobalSearch
import ir.armor.tachidesk.util.sourceSearch
import ir.armor.tachidesk.util.systemTray
import org.kodein.di.DI
import org.kodein.di.conf.global
import xyz.nulldev.androidcompat.AndroidCompat
@@ -47,6 +50,7 @@ class Main {
// make sure everything we need exists
applicationSetup()
val tray = systemTray()
registerConfigModules()
@@ -57,17 +61,22 @@ class Main {
// start app
androidCompat.startApp(App())
// Thread(getMangaUpdateQueueThread).start()
var hasWebUiBundled: Boolean = false
val app = Javalin.create { config ->
try {
this::class.java.classLoader.getResource("/react/index.html")
hasWebUiBundled = true
config.addStaticFiles("/react")
config.addSinglePageRoot("/", "/react/index.html")
} catch (e: RuntimeException) {
println("Warning: react build files are missing.")
hasWebUiBundled = false
}
}.start(4567)
if (hasWebUiBundled) {
openInBrowser()
}
app.before() { ctx ->
// allow the client which is running on another port
@@ -94,6 +103,14 @@ class Main {
ctx.status(200)
}
app.get("/api/v1/extension/icon/:apkName") { ctx ->
val apkName = ctx.pathParam("apkName")
val result = getExtensionIcon(apkName)
ctx.result(result.first)
ctx.header("content-type", result.second)
}
app.get("/api/v1/source/list") { ctx ->
ctx.json(getSourceList())
}
@@ -16,7 +16,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
object DBMangaer {
val db by lazy {
Database.connect("jdbc:h2:${Config.dataRoot}/database.h2", "org.h2.Driver")
Database.connect("jdbc:h2:${Config.dataRoot}/database", "org.h2.Driver")
}
}
@@ -6,6 +6,7 @@ package ir.armor.tachidesk.util
import com.googlecode.dex2jar.tools.Dex2jarCmd
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.online.HttpSource
@@ -24,6 +25,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
import java.net.URL
import java.net.URLClassLoader
@@ -149,3 +151,22 @@ fun removeExtension(pkgName: String) {
File(jarPath).delete()
}
}
val network: NetworkHelper by injectLazy()
fun getExtensionIcon(apkName: String): Pair<InputStream, String> {
val iconUrl = transaction { ExtensionsTable.select { ExtensionsTable.apkName eq apkName }.firstOrNull()!! }[ExtensionsTable.iconUrl]
val saveDir = "${Config.extensionsRoot}/icon"
val fileName = apkName
return getCachedResponse(saveDir, fileName) {
network.client.newCall(
GET(iconUrl)
).execute()
}
}
fun getExtensionIconUrl(apkName: String): String {
return "http://127.0.0.1:4567/api/v1/extension/icon/$apkName"
}
@@ -66,6 +66,8 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
}
}
}
} else {
println("used cached extension list")
}
return transaction {
@@ -78,7 +80,7 @@ fun getExtensionList(offline: Boolean = false): List<ExtensionDataClass> {
it[ExtensionsTable.lang],
it[ExtensionsTable.isNsfw],
it[ExtensionsTable.apkName],
it[ExtensionsTable.iconUrl],
getExtensionIconUrl(it[ExtensionsTable.apkName]),
it[ExtensionsTable.installed],
it[ExtensionsTable.classFQName]
)
@@ -4,6 +4,7 @@ package ir.armor.tachidesk.util
* 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 okhttp3.Response
import okio.BufferedSource
import okio.buffer
import okio.sink
@@ -15,15 +16,15 @@ import java.io.OutputStream
import java.nio.file.Files
import java.nio.file.Paths
fun writeStream(fileStream: InputStream, path: String) {
Files.newOutputStream(Paths.get(path)).use { os ->
val buffer = ByteArray(128 * 1024)
var len: Int
while (fileStream.read(buffer).also { len = it } > 0) {
os.write(buffer, 0, len)
}
}
}
// fun writeStream(fileStream: InputStream, path: String) {
// Files.newOutputStream(Paths.get(path)).use { os ->
// val buffer = ByteArray(128 * 1024)
// var len: Int
// while (fileStream.read(buffer).also { len = it } > 0) {
// os.write(buffer, 0, len)
// }
// }
// }
fun pathToInputStream(path: String): InputStream {
return BufferedInputStream(FileInputStream(path))
@@ -42,7 +43,7 @@ fun findFileNameStartingWith(directoryPath: String, fileName: String): String? {
*
* @param stream the stream where the source is copied.
*/
fun BufferedSource.saveTo(stream: OutputStream) {
private fun BufferedSource.saveTo(stream: OutputStream) {
use { input ->
stream.sink().buffer().use {
it.writeAll(input)
@@ -50,3 +51,32 @@ fun BufferedSource.saveTo(stream: OutputStream) {
}
}
}
fun getCachedResponse(saveDir: String, fileName: String, fetcher: () -> Response): Pair<InputStream, String> {
val cachedFile = findFileNameStartingWith(saveDir, fileName)
val filePath = "$saveDir/$fileName"
if (cachedFile != null) {
val fileType = cachedFile.substringAfter(filePath)
return Pair(
pathToInputStream(cachedFile),
"image/$fileType"
)
}
val response = fetcher()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
val fullPath = filePath + "." + contentType.substringAfter("image/")
Files.newOutputStream(Paths.get(fullPath)).use { os ->
response.body!!.source().saveTo(os)
}
return Pair(
pathToInputStream(fullPath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
}
}
@@ -10,11 +10,9 @@ import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.dataclass.MangaDataClass
import ir.armor.tachidesk.database.table.MangaStatus
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.io.File
import java.io.InputStream
fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
@@ -85,54 +83,19 @@ fun getManga(mangaId: Int, proxyThumbnail: Boolean = true): MangaDataClass {
fun getThumbnail(mangaId: Int): Pair<InputStream, String> {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
var filePath = "${Config.thumbnailsRoot}/$mangaId."
val saveDir = Config.thumbnailsRoot
val fileName = mangaId.toString()
val potentialCache = findFileNameStartingWith(Config.thumbnailsRoot, mangaId.toString())
if (potentialCache != null) {
println("using cached thumbnail file")
return Pair(
pathToInputStream(potentialCache),
"image/${potentialCache.substringAfter(filePath)}"
)
}
return getCachedResponse(saveDir, fileName) {
val sourceId = mangaEntry[MangaTable.sourceReference].value
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
val sourceId = mangaEntry[MangaTable.sourceReference].value
println("getting source for $mangaId")
val source = getHttpSource(sourceId)
var thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
if (thumbnailUrl == null || thumbnailUrl.isEmpty()) {
thumbnailUrl = getManga(mangaId, proxyThumbnail = false).thumbnailUrl!!
}
println(thumbnailUrl)
val response = source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
filePath += contentType.substringAfter("image/")
writeStream(response.body!!.byteStream(), filePath)
return Pair(
pathToInputStream(filePath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
source.client.newCall(
GET(thumbnailUrl, source.headers)
).execute()
}
}
fun getMangaDir(mangaId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference].value
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = sourceEntry[SourceTable.name]
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
}
@@ -6,17 +6,18 @@ package ir.armor.tachidesk.util
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.database.table.ChapterTable
import ir.armor.tachidesk.database.table.MangaTable
import ir.armor.tachidesk.database.table.PageTable
import ir.armor.tachidesk.database.table.SourceTable
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.io.File
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Paths
fun getTrueImageUrl(page: Page, source: HttpSource): String {
if (page.imageUrl == null) {
@@ -45,37 +46,32 @@ fun getPageImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, St
}
}
val saveDir = getMangaDir(mangaId) + "/" + chapterEntry[ChapterTable.chapter_number]
val saveDir = getChapterDir(mangaId, chapterId)
File(saveDir).mkdirs()
var filePath = "$saveDir/$index."
val fileName = index.toString()
val potentialCache = findFileNameStartingWith(saveDir, index.toString())
if (potentialCache != null) {
println("using cached page file for $index")
return Pair(
pathToInputStream(potentialCache),
"image/${potentialCache.substringAfter("$filePath")}"
)
}
val response = source.fetchImage(tachiPage).toBlocking().first()
if (response.code == 200) {
val contentType = response.headers["content-type"]!!
filePath += contentType.substringAfter("image/")
Files.newOutputStream(Paths.get(filePath)).use { os ->
response.body!!.source().saveTo(os)
}
// writeStream(response.body!!.source(), filePath)
return Pair(
pathToInputStream(filePath),
contentType
)
} else {
throw Exception("request error! ${response.code}")
return getCachedResponse(saveDir, fileName) {
source.fetchImage(tachiPage).toBlocking().first()
}
}
fun getChapterDir(mangaId: Int, chapterId: Int): String {
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.firstOrNull()!! }
val sourceId = mangaEntry[MangaTable.sourceReference].value
val source = getHttpSource(sourceId)
val sourceEntry = transaction { SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! }
val chapterEntry = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.firstOrNull()!! }
val chapterDir = when {
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
else -> chapterEntry[ChapterTable.name]
}
val mangaTitle = mangaEntry[MangaTable.title]
val sourceName = source.toString()
val mangaDir = "${Config.mangaRoot}/$sourceName/$mangaTitle/$chapterDir"
// make sure dirs exist
File(mangaDir).mkdirs()
return mangaDir
}
@@ -78,7 +78,7 @@ fun getSourceList(): List<SourceDataClass> {
it[SourceTable.id].value.toString(),
it[SourceTable.name],
Locale(it[SourceTable.lang]).getDisplayLanguage(Locale(it[SourceTable.lang])),
ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.iconUrl],
getExtensionIconUrl(ExtensionsTable.select { ExtensionsTable.id eq it[SourceTable.extension] }.first()[ExtensionsTable.apkName]),
getHttpSource(it[SourceTable.id].value).supportsLatest
)
}
@@ -4,15 +4,68 @@ package ir.armor.tachidesk.util
* 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 dorkbox.systemTray.MenuItem
import dorkbox.systemTray.SystemTray
import dorkbox.systemTray.SystemTray.TrayType
import dorkbox.util.CacheUtil
import dorkbox.util.Desktop
import ir.armor.tachidesk.Config
import ir.armor.tachidesk.Main
import ir.armor.tachidesk.database.makeDataBaseTables
import java.awt.event.ActionListener
import java.io.File
import java.io.IOException
fun applicationSetup() {
// make dirs we need
File(Config.dataRoot).mkdirs()
File(Config.extensionsRoot).mkdirs()
File("${Config.extensionsRoot}/icon").mkdirs()
File(Config.thumbnailsRoot).mkdirs()
makeDataBaseTables()
}
fun openInBrowser() {
Desktop.browseURL("http://127.0.0.1:4567")
}
val icon = Main::class.java.getResource("/icon/icon_round.png")
fun systemTray(): SystemTray? {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java
SystemTray.DEBUG = true; // for test apps, we always want to run in debug mode
if (System.getProperty("os.name").startsWith("Windows"))
SystemTray.FORCE_TRAY_TYPE = TrayType.Swing
CacheUtil.clear()
val systemTray = SystemTray.get() ?: return null
val mainMenu = systemTray.menu
mainMenu.add(
MenuItem(
"Open Tachidesk",
ActionListener {
try {
Desktop.browseURL("http://127.0.0.1:4567")
} catch (e1: IOException) {
e1.printStackTrace()
}
}
)
)
// systemTray.setTooltip("Tachidesk")
systemTray.setImage(icon)
// systemTray.status = "No Mail"
systemTray.getMenu().add(
MenuItem("Quit") {
systemTray.shutdown()
System.exit(0)
}
)
return systemTray
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+1 -1
View File
@@ -43,7 +43,7 @@ interface IProps {
export default function ExtensionCard(props: IProps) {
const {
extension: {
name, lang, versionName, iconUrl, installed, apkName,
name, lang, versionName, installed, apkName, iconUrl,
},
} = props;
const [installedState, setInstalledState] = useState<string>((installed ? 'uninstall' : 'install'));
+1 -1
View File
@@ -7,7 +7,7 @@ import React from 'react';
export default function Home() {
return (
<h1>
Home
Hint: Click Tn The Top Left Menu Button
</h1>
);
}
+1
View File
@@ -9,6 +9,7 @@ interface IExtension {
iconUrl: string
installed: boolean
apkName: string
pkgName: string
}
interface ISource {