Skip to content

Commit b8b48b0

Browse files
authored
Implement seasons feature (#2095)
1 parent 65f3bf7 commit b8b48b0

File tree

77 files changed

+3539
-390
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+3539
-390
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
2121
### Added
2222

2323
- Add player settings to the main settings screen ([@jmir1](https://github.com/jmir1)) ([#2081](https://github.com/aniyomiorg/aniyomi/pull/2081))
24+
- Add seasons support ([@Secozzi](https://github.com/Secozzi)) ([#2095](https://github.com/aniyomiorg/aniyomi/pull/2095))
2425

2526
## [v0.18.0.1] - 2025-07-06
2627
### Fixed

app/src/main/java/eu/kanade/domain/DomainModule.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package eu.kanade.domain
33
import eu.kanade.domain.download.anime.interactor.DeleteEpisodeDownload
44
import eu.kanade.domain.download.manga.interactor.DeleteChapterDownload
55
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
6+
import eu.kanade.domain.entries.anime.interactor.SyncSeasonsWithSource
67
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
78
import eu.kanade.domain.entries.manga.interactor.GetExcludedScanlators
89
import eu.kanade.domain.entries.manga.interactor.SetExcludedScanlators
@@ -122,12 +123,13 @@ import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
122123
import tachiyomi.domain.entries.anime.interactor.GetAnime
123124
import tachiyomi.domain.entries.anime.interactor.GetAnimeByUrlAndSourceId
124125
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
125-
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
126+
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodesAndSeasons
126127
import tachiyomi.domain.entries.anime.interactor.GetDuplicateLibraryAnime
127128
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
128129
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
129130
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
130131
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
132+
import tachiyomi.domain.entries.anime.interactor.SetAnimeSeasonFlags
131133
import tachiyomi.domain.entries.anime.repository.AnimeRepository
132134
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
133135
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
@@ -165,6 +167,9 @@ import tachiyomi.domain.items.episode.interactor.SetAnimeDefaultEpisodeFlags
165167
import tachiyomi.domain.items.episode.interactor.ShouldUpdateDbEpisode
166168
import tachiyomi.domain.items.episode.interactor.UpdateEpisode
167169
import tachiyomi.domain.items.episode.repository.EpisodeRepository
170+
import tachiyomi.domain.items.season.interactor.GetAnimeSeasonsByParentId
171+
import tachiyomi.domain.items.season.interactor.SetAnimeDefaultSeasonFlags
172+
import tachiyomi.domain.items.season.interactor.ShouldUpdateDbSeason
168173
import tachiyomi.domain.release.interactor.GetApplicationRelease
169174
import tachiyomi.domain.release.service.ReleaseService
170175
import tachiyomi.domain.source.anime.interactor.GetAnimeSourcesWithNonLibraryAnime
@@ -228,19 +233,24 @@ class DomainModule : InjektModule {
228233
addFactory { GetDuplicateLibraryAnime(get()) }
229234
addFactory { GetAnimeFavorites(get()) }
230235
addFactory { GetLibraryAnime(get()) }
231-
addFactory { GetAnimeWithEpisodes(get(), get()) }
236+
addFactory { GetAnimeWithEpisodesAndSeasons(get(), get()) }
232237
addFactory { GetAnimeByUrlAndSourceId(get()) }
233238
addFactory { GetAnime(get()) }
239+
addFactory { GetAnimeSeasonsByParentId(get()) }
234240
addFactory { GetNextEpisodes(get(), get(), get()) }
235241
addFactory { GetUpcomingAnime(get()) }
236242
addFactory { ResetAnimeViewerFlags(get()) }
237243
addFactory { SetAnimeEpisodeFlags(get()) }
244+
addFactory { SetAnimeSeasonFlags(get()) }
238245
addFactory { AnimeFetchInterval(get()) }
239246
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
247+
addFactory { SetAnimeDefaultSeasonFlags(get(), get(), get()) }
240248
addFactory { SetAnimeViewerFlags(get()) }
241-
addFactory { NetworkToLocalAnime(get()) }
249+
addFactory { NetworkToLocalAnime(get(), get()) }
242250
addFactory { UpdateAnime(get(), get()) }
243251
addFactory { SetAnimeCategories(get()) }
252+
addFactory { ShouldUpdateDbSeason() }
253+
addFactory { SyncSeasonsWithSource(get(), get(), get(), get(), get()) }
244254

245255
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
246256
addFactory { GetDuplicateLibraryManga(get()) }
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package eu.kanade.domain.entries.anime.interactor
2+
3+
import eu.kanade.domain.entries.anime.model.toDomainAnime
4+
import eu.kanade.tachiyomi.animesource.AnimeSource
5+
import eu.kanade.tachiyomi.animesource.model.SAnime
6+
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
7+
import tachiyomi.domain.entries.anime.model.Anime
8+
import tachiyomi.domain.entries.anime.model.NoSeasonsException
9+
import tachiyomi.domain.entries.anime.model.toAnimeUpdate
10+
import tachiyomi.domain.entries.anime.repository.AnimeRepository
11+
import tachiyomi.domain.items.season.interactor.GetAnimeSeasonsByParentId
12+
import tachiyomi.domain.items.season.interactor.ShouldUpdateDbSeason
13+
import tachiyomi.domain.items.season.service.SeasonRecognition
14+
import tachiyomi.source.local.entries.anime.isLocal
15+
import java.time.ZonedDateTime
16+
17+
class SyncSeasonsWithSource(
18+
private val updateAnime: UpdateAnime,
19+
private val animeRepository: AnimeRepository,
20+
private val networkToLocalAnime: NetworkToLocalAnime,
21+
private val shouldUpdateDbSeason: ShouldUpdateDbSeason,
22+
private val getAnimeSeasonsByParentId: GetAnimeSeasonsByParentId,
23+
) {
24+
suspend fun await(
25+
rawSourceSeasons: List<SAnime>,
26+
anime: Anime,
27+
source: AnimeSource,
28+
manualFetch: Boolean = false,
29+
fetchWindow: Pair<Long, Long> = Pair(0, 0),
30+
): List<Anime> {
31+
if (rawSourceSeasons.isEmpty() && !source.isLocal()) {
32+
throw NoSeasonsException()
33+
}
34+
35+
val now = ZonedDateTime.now()
36+
37+
val sourceSeasons = rawSourceSeasons
38+
.distinctBy { it.url }
39+
.mapIndexed { i, sAnime ->
40+
networkToLocalAnime.await(sAnime.toDomainAnime(source.id))
41+
.copy(parentId = anime.id, seasonSourceOrder = i.toLong())
42+
}
43+
44+
val dbSeasons = getAnimeSeasonsByParentId.await(anime.id)
45+
46+
val newSeasons = mutableListOf<Anime>()
47+
val updatedSeasons = mutableListOf<Anime>()
48+
val removedSeasons = dbSeasons.filterNot { dbSeasons ->
49+
sourceSeasons.any { sourceSeason ->
50+
dbSeasons.anime.url == sourceSeason.url
51+
}
52+
}
53+
54+
for (sourceSeason in sourceSeasons) {
55+
var season = sourceSeason
56+
57+
// Recognize season number for the season
58+
val seasonNumber = SeasonRecognition.parseSeasonNumber(
59+
anime.title,
60+
season.title,
61+
season.seasonNumber,
62+
)
63+
season = season.copy(seasonNumber = seasonNumber)
64+
65+
val dbSeason = dbSeasons.find { it.anime.url == season.url }?.anime
66+
if (dbSeason == null) {
67+
newSeasons.add(season)
68+
} else {
69+
if (shouldUpdateDbSeason.await(dbSeason, season)) {
70+
val toChangeSeason = dbSeason.copy(
71+
title = season.title,
72+
seasonNumber = season.seasonNumber,
73+
seasonSourceOrder = season.seasonSourceOrder,
74+
)
75+
updatedSeasons.add(toChangeSeason)
76+
}
77+
}
78+
}
79+
80+
// Return if there's nothing to add, delete, or update to avoid unnecessary db transactions.
81+
if (newSeasons.isEmpty() && removedSeasons.isEmpty() && updatedSeasons.isEmpty()) {
82+
if (manualFetch || anime.fetchInterval == 0 || anime.nextUpdate < fetchWindow.first) {
83+
updateAnime.awaitUpdateFetchInterval(
84+
anime,
85+
now,
86+
fetchWindow,
87+
)
88+
}
89+
return sourceSeasons
90+
}
91+
92+
if (removedSeasons.isNotEmpty()) {
93+
val toDeleteIds = removedSeasons.map { it.id }
94+
animeRepository.removeParentIdByIds(toDeleteIds)
95+
}
96+
97+
val toUpdate = newSeasons.map { it.toAnimeUpdate() } +
98+
updatedSeasons.map { it.toAnimeUpdate() }
99+
100+
if (toUpdate.isNotEmpty()) {
101+
updateAnime.awaitAll(toUpdate)
102+
}
103+
104+
updateAnime.awaitUpdateLastUpdate(anime.id)
105+
106+
return sourceSeasons
107+
}
108+
}

app/src/main/java/eu/kanade/domain/entries/anime/model/Anime.kt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,31 @@ val Anime.downloadedFilter: TriState
1818
else -> TriState.DISABLED
1919
}
2020
}
21+
22+
val Anime.seasonDownloadedFilter: TriState
23+
get() {
24+
if (Injekt.get<BasePreferences>().downloadedOnly().get()) return TriState.ENABLED_IS
25+
return when (seasonDownloadedFilterRaw) {
26+
Anime.SEASON_SHOW_DOWNLOADED -> TriState.ENABLED_IS
27+
Anime.SEASON_SHOW_NOT_DOWNLOADED -> TriState.ENABLED_NOT
28+
else -> TriState.DISABLED
29+
}
30+
}
31+
2132
fun Anime.episodesFiltered(): Boolean {
2233
return unseenFilter != TriState.DISABLED ||
2334
downloadedFilter != TriState.DISABLED ||
2435
bookmarkedFilter != TriState.DISABLED
2536
}
2637

38+
fun Anime.seasonsFiltered(): Boolean {
39+
return seasonDownloadedFilter != TriState.DISABLED ||
40+
seasonUnseenFilter != TriState.DISABLED ||
41+
seasonStartedFilter != TriState.DISABLED ||
42+
seasonBookmarkedFilter != TriState.DISABLED ||
43+
seasonCompletedFilter != TriState.DISABLED
44+
}
45+
2746
fun Anime.toSAnime(): SAnime = SAnime.create().also {
2847
it.url = url
2948
it.title = title
@@ -33,6 +52,8 @@ fun Anime.toSAnime(): SAnime = SAnime.create().also {
3352
it.genre = genre.orEmpty().joinToString()
3453
it.status = status.toInt()
3554
it.thumbnail_url = thumbnailUrl
55+
it.fetch_type = fetchType
56+
it.season_number = seasonNumber
3657
it.initialized = initialized
3758
}
3859

@@ -54,6 +75,8 @@ fun Anime.copyFrom(other: SAnime): Anime {
5475
thumbnailUrl = thumbnailUrl,
5576
status = other.status.toLong(),
5677
updateStrategy = other.update_strategy,
78+
fetchType = other.fetch_type,
79+
seasonNumber = other.season_number,
5780
initialized = other.initialized && initialized,
5881
)
5982
}
@@ -69,6 +92,8 @@ fun SAnime.toDomainAnime(sourceId: Long): Anime {
6992
status = status.toLong(),
7093
thumbnailUrl = thumbnail_url,
7194
updateStrategy = update_strategy,
95+
fetchType = fetch_type,
96+
seasonNumber = season_number,
7297
initialized = initialized,
7398
source = sourceId,
7499
)

0 commit comments

Comments
 (0)