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}" sha256sum "${signed_apk}"
done 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 - name: Create or update release
id: release id: release
shell: bash shell: bash
@ -561,7 +534,7 @@ jobs:
"<!-->" \ "<!-->" \
"> [!TIP]" \ "> [!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}" } > "${release_body_file}"
# Build JSON — body from file only (jq --arg body "$huge" hits ARG_MAX). # 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()) newUpdates.add(manga to newChapters.toTypedArray())
} }
val libraryUpdateChapters = getChaptersForLibraryUpdateDownload.await(manga)
if (libraryUpdateChapters.isNotEmpty()) {
downloadChapters(manga, libraryUpdateChapters)
hasDownloads.store(true)
}
clearErrorFromDB(mangaId = manga.id) clearErrorFromDB(mangaId = manga.id)
} catch (e: Throwable) { } catch (e: Throwable) {
val errorMessage = when (e) { val errorMessage = when (e) {
@ -489,6 +484,17 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
writeErrorToDB(manga to errorMessage) writeErrorToDB(manga to errorMessage)
failedUpdates.add(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.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.history.interactor.GetCrossMangaPageRateHistorySamples import tachiyomi.domain.history.interactor.GetCrossMangaPageRateHistorySamples
import tachiyomi.domain.history.interactor.GetHistoryRevision import tachiyomi.domain.history.interactor.GetHistoryRevision
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.history.interactor.GetReadDurationEntriesByMangaIds import tachiyomi.domain.history.interactor.GetReadDurationEntriesByMangaIds
import tachiyomi.domain.history.interactor.GetTotalReadDuration import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.library.model.LibraryDisplayMode 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.LibrarySort
import tachiyomi.domain.library.model.sort import tachiyomi.domain.library.model.sort
import tachiyomi.domain.library.service.LibraryPreferences 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.GetIdsOfFavoriteMangaWithMetadata
import tachiyomi.domain.manga.interactor.GetLibraryManga import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetMergedMangaById 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.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.applyFilter 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.model.StubSource
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
@ -1028,7 +1028,7 @@ class LibraryScreenModel(
if (manga.source == MERGED_SOURCE_ID) { if (manga.source == MERGED_SOURCE_ID) {
val mergedMangas = getMergedMangaById.await(manga.id) val mergedMangas = getMergedMangaById.await(manga.id)
.associateBy { it.id } .associateBy { it.id }
getNextChapters.await(manga.id) getNextChapters.awaitFromLastRead(manga.id)
.let { if (amount != null) it.take(amount) else it } .let { if (amount != null) it.take(amount) else it }
.groupBy { it.mangaId } .groupBy { it.mangaId }
.forEach ab@{ (mangaId, chapters) -> .forEach ab@{ (mangaId, chapters) ->
@ -1051,7 +1051,7 @@ class LibraryScreenModel(
} }
// SY <-- // SY <--
val chapters = getNextChapters.await(manga.id) val chapters = getNextChapters.awaitFromLastRead(manga.id)
.fastFilterNot { chapter -> .fastFilterNot { chapter ->
downloadManager.getQueuedDownloadOrNull(chapter.id) != null || downloadManager.getQueuedDownloadOrNull(chapter.id) != null ||
downloadManager.isChapterDownloaded( 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) - Builds release APKs (`assembleRelease -Penable-updater -Pdisable-code-shrink`, no telemetry)
- Signs APKs with your release keystore - Signs APKs with your release keystore
- Creates or updates a Forgejo release and uploads APK assets - 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. The workflow uses shell + Forgejo API calls only (no GitHub-only release actions), so it runs on self-hosted Forgejo runners.
## Cut a release ## Cut a release
@ -90,7 +88,7 @@ export KOMIKKU_GRADLE_CACHE="$HOME/.cache/komikku-ci-gradle"
## Release assets ## Release assets
Versioned (one set per tag): One APK per architecture per tag:
- `Komikku-v1.13.6.apk` — universal - `Komikku-v1.13.6.apk` — universal
- `Komikku-arm64-v8a-v1.13.6.apk` - `Komikku-arm64-v8a-v1.13.6.apk`
@ -99,11 +97,6 @@ Versioned (one set per tag):
- `Komikku-x86_64-v1.13.6.apk` - `Komikku-x86_64-v1.13.6.apk`
- `Komikku-mapping-v1.13.6.zip` — ProGuard mapping (when present) - `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
Obtainium can install directly from Forgejo release assets. 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). 2. **Add app****Override source**: **Forgejo** (required for self-hosted instances).
3. Repository URL, e.g. `https://forgejo.example/youruser/komikku` (no `/releases` suffix). 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. 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`. 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. 7. Wait until the Forgejo release workflow has finished before adding or updating the app.
Direct download URL pattern (for reference): Direct download URL pattern (for reference):
```text ```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 ## Notes

View file

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

View file

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