Compare commits

..

2 commits

Author SHA1 Message Date
littlecodedragon
7afd275eef ci(forgejo): drop stable APK aliases from releases
Publish only versioned release asset names and update Obtainium docs
to match the tag-suffixed APK filenames.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 15:16:16 +02:00
littlecodedragon
354877c6c1 fix(download): start unread downloads from last read position
Library update and manual unread downloads now use reading history as
the start point, and unread enqueueing survives chapter sync failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 15:05:44 +02:00
6 changed files with 49 additions and 63 deletions

View file

@ -413,33 +413,6 @@ jobs:
sha256sum "${signed_apk}"
done
- name: Create Obtainium-friendly aliases
shell: bash
run: |
set -euo pipefail
alias_asset() {
src="$1"
dst="$2"
if [ -e "$src" ]; then
cp -f "$src" "$dst"
fi
}
# Only publish signed APK aliases.
alias_asset "$(ls -1t Komikku-arm64-v8a-*.apk 2>/dev/null | head -n1)" "Komikku-arm64-v8a.apk"
for candidate in Komikku-*.apk; do
case "$candidate" in
*arm64-v8a*|*armeabi-v7a*|*x86_64*|*x86*|*-unsigned.apk)
;;
*)
alias_asset "$candidate" "Komikku-universal.apk"
break
;;
esac
done
- name: Create or update release
id: release
shell: bash
@ -561,7 +534,7 @@ jobs:
"<!-->" \
"> [!TIP]" \
">" \
"> ### If you are unsure which version to download then go with \`Komikku-universal.apk\`"
"> ### If you are unsure which version to download then go with \`Komikku-${VERSION_TAG}.apk\`"
} > "${release_body_file}"
# Build JSON — body from file only (jq --arg body "$huge" hits ARG_MAX).

View file

@ -468,11 +468,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
newUpdates.add(manga to newChapters.toTypedArray())
}
val libraryUpdateChapters = getChaptersForLibraryUpdateDownload.await(manga)
if (libraryUpdateChapters.isNotEmpty()) {
downloadChapters(manga, libraryUpdateChapters)
hasDownloads.store(true)
}
clearErrorFromDB(mangaId = manga.id)
} catch (e: Throwable) {
val errorMessage = when (e) {
@ -489,6 +484,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
writeErrorToDB(manga to errorMessage)
failedUpdates.add(manga to errorMessage)
}
try {
val libraryUpdateChapters = getChaptersForLibraryUpdateDownload.await(manga)
if (libraryUpdateChapters.isNotEmpty()) {
downloadChapters(manga, libraryUpdateChapters)
hasDownloads.store(true)
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
xLogE("Failed to queue library update downloads for ${manga.title}", e)
}
}
}
}

View file

@ -98,10 +98,10 @@ import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.history.interactor.GetCrossMangaPageRateHistorySamples
import tachiyomi.domain.history.interactor.GetHistoryRevision
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.history.interactor.GetReadDurationEntriesByMangaIds
import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -110,9 +110,6 @@ import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.model.sort
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.reading.ReadingEtaLimits
import tachiyomi.domain.reading.SeriesReadingEtaCalculator
import tachiyomi.domain.reading.service.ReadingEtaPreferences
import tachiyomi.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetMergedMangaById
@ -123,6 +120,9 @@ import tachiyomi.domain.manga.model.CustomMangaInfo
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.applyFilter
import tachiyomi.domain.reading.ReadingEtaLimits
import tachiyomi.domain.reading.SeriesReadingEtaCalculator
import tachiyomi.domain.reading.service.ReadingEtaPreferences
import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks
@ -1028,7 +1028,7 @@ class LibraryScreenModel(
if (manga.source == MERGED_SOURCE_ID) {
val mergedMangas = getMergedMangaById.await(manga.id)
.associateBy { it.id }
getNextChapters.await(manga.id)
getNextChapters.awaitFromLastRead(manga.id)
.let { if (amount != null) it.take(amount) else it }
.groupBy { it.mangaId }
.forEach ab@{ (mangaId, chapters) ->
@ -1051,7 +1051,7 @@ class LibraryScreenModel(
}
// SY <--
val chapters = getNextChapters.await(manga.id)
val chapters = getNextChapters.awaitFromLastRead(manga.id)
.fastFilterNot { chapter ->
downloadManager.getQueuedDownloadOrNull(chapter.id) != null ||
downloadManager.isChapterDownloaded(

View file

@ -8,8 +8,6 @@ Forgejo Actions workflow: [`.forgejo/workflows/build_release.yml`](../.forgejo/w
- Builds release APKs (`assembleRelease -Penable-updater -Pdisable-code-shrink`, no telemetry)
- Signs APKs with your release keystore
- Creates or updates a Forgejo release and uploads APK assets
- Publishes stable Obtainium-friendly asset names alongside versioned files
The workflow uses shell + Forgejo API calls only (no GitHub-only release actions), so it runs on self-hosted Forgejo runners.
## Cut a release
@ -90,7 +88,7 @@ export KOMIKKU_GRADLE_CACHE="$HOME/.cache/komikku-ci-gradle"
## Release assets
Versioned (one set per tag):
One APK per architecture per tag:
- `Komikku-v1.13.6.apk` — universal
- `Komikku-arm64-v8a-v1.13.6.apk`
@ -99,11 +97,6 @@ Versioned (one set per tag):
- `Komikku-x86_64-v1.13.6.apk`
- `Komikku-mapping-v1.13.6.zip` — ProGuard mapping (when present)
Stable aliases (same filename every release; best for Obtainium):
- `Komikku-universal.apk`
- `Komikku-arm64-v8a.apk`
## Obtainium
Obtainium can install directly from Forgejo release assets.
@ -112,14 +105,14 @@ Obtainium can install directly from Forgejo release assets.
2. **Add app****Override source**: **Forgejo** (required for self-hosted instances).
3. Repository URL, e.g. `https://forgejo.example/youruser/komikku` (no `/releases` suffix).
4. Enable **Allow insecure HTTP** or disable SSL verification if Forgejo uses a self-signed certificate.
5. **APK filter** (regex): `Komikku-arm64-v8a\.apk` (or `Komikku-universal\.apk`).
5. **APK filter** (regex): `Komikku-arm64-v8a-v.*\.apk` (or `Komikku-v.*\.apk` for universal).
6. **Trim version string (regex):** `^v?([0-9]+\.[0-9]+\.[0-9]+)` — needed for fork tags like `v1.13.6-forgejo.39`.
7. Wait until the Forgejo release workflow has finished before adding or updating the app.
Direct download URL pattern (for reference):
```text
https://forgejo.example/<owner>/<repo>/releases/download/<tag>/Komikku-arm64-v8a.apk
https://forgejo.example/<owner>/<repo>/releases/download/<tag>/Komikku-arm64-v8a-v1.13.6.apk
```
## Notes

View file

@ -17,7 +17,7 @@ class GetChaptersForLibraryUpdateDownload(
val mode = downloadPreferences.libraryUpdateDownloadMode().get()
if (mode == LibraryUpdateDownloadMode.DISABLED) return emptyList()
val chapters = getNextChapters.await(manga.id)
val chapters = getNextChapters.awaitFromLastRead(manga.id)
if (chapters.isEmpty()) return emptyList()
return when (mode) {

View file

@ -8,7 +8,6 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.history.repository.HistoryRepository
import tachiyomi.domain.manga.interactor.GetManga
import kotlin.math.max
class GetNextChapters(
private val getChaptersByMangaId: GetChaptersByMangaId,
@ -65,22 +64,37 @@ class GetNextChapters(
fromChapterId: Long,
onlyUnread: Boolean = true,
): List<Chapter> {
val chapters = await(mangaId, onlyUnread)
val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)
val allChapters = await(mangaId, onlyUnread = false)
val currChapterIndex = allChapters.indexOfFirst { it.id == fromChapterId }
if (onlyUnread) {
return nextChapters
if (currChapterIndex == -1) {
return if (onlyUnread) await(mangaId, onlyUnread = true) else allChapters
}
// The "next chapter" is either:
// - The current chapter if it isn't completely read
// - The chapters after the current chapter if the current one is completely read
val fromChapter = chapters.getOrNull(currChapterIndex)
return if (fromChapter != null && !fromChapter.read) {
nextChapters
val fromChapter = allChapters[currChapterIndex]
val startIndex = when {
fromChapter.read -> currChapterIndex + 1
else -> currChapterIndex
}.coerceAtMost(allChapters.size)
val fromPosition = allChapters.subList(startIndex, allChapters.size)
return if (onlyUnread) {
fromPosition.filterNot { it.read }
} else {
nextChapters.drop(1)
fromPosition
}
}
suspend fun awaitFromLastRead(mangaId: Long, onlyUnread: Boolean = true): List<Chapter> {
val lastReadChapterId = historyRepository.getHistoryByMangaId(mangaId)
.maxByOrNull { it.readAt?.time ?: 0L }
?.chapterId
return if (lastReadChapterId != null) {
await(mangaId, lastReadChapterId, onlyUnread)
} else {
await(mangaId, onlyUnread)
}
}
}