extension settings

This commit is contained in:
Finnley Somdahl 2023-10-29 19:45:11 -05:00
parent 9c0ef7a788
commit 3368a1bc8d
76 changed files with 2320 additions and 131 deletions

View file

@ -0,0 +1,22 @@
package tachiyomi.domain.entries
enum class TriStateFilter {
DISABLED, // Disable filter
ENABLED_IS, // Enabled with "is" filter
ENABLED_NOT, // Enabled with "not" filter
;
fun next(): TriStateFilter {
return when (this) {
DISABLED -> ENABLED_IS
ENABLED_IS -> ENABLED_NOT
ENABLED_NOT -> DISABLED
}
}
}
inline fun applyFilter(filter: TriStateFilter, predicate: () -> Boolean): Boolean = when (filter) {
TriStateFilter.DISABLED -> true
TriStateFilter.ENABLED_IS -> predicate()
TriStateFilter.ENABLED_NOT -> !predicate()
}

View file

@ -0,0 +1,134 @@
package tachiyomi.domain.entries.anime.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import tachiyomi.domain.entries.TriStateFilter
import java.io.Serializable
import kotlin.math.pow
data class Anime(
val id: Long,
val source: Long,
val favorite: Boolean,
val lastUpdate: Long,
val nextUpdate: Long,
val calculateInterval: Int,
val dateAdded: Long,
val viewerFlags: Long,
val episodeFlags: Long,
val coverLastModified: Long,
val url: String,
val title: String,
val artist: String?,
val author: String?,
val description: String?,
val genre: List<String>?,
val status: Long,
val thumbnailUrl: String?,
val updateStrategy: UpdateStrategy,
val initialized: Boolean,
) : Serializable {
val sorting: Long
get() = episodeFlags and EPISODE_SORTING_MASK
val displayMode: Long
get() = episodeFlags and EPISODE_DISPLAY_MASK
val unseenFilterRaw: Long
get() = episodeFlags and EPISODE_UNSEEN_MASK
val downloadedFilterRaw: Long
get() = episodeFlags and EPISODE_DOWNLOADED_MASK
val bookmarkedFilterRaw: Long
get() = episodeFlags and EPISODE_BOOKMARKED_MASK
val skipIntroLength: Int
get() = (viewerFlags and ANIME_INTRO_MASK).toInt()
val nextEpisodeToAir: Int
get() = (viewerFlags and ANIME_AIRING_EPISODE_MASK).removeHexZeros(zeros = 2).toInt()
val nextEpisodeAiringAt: Long
get() = (viewerFlags and ANIME_AIRING_TIME_MASK).removeHexZeros(zeros = 6)
val unseenFilter: TriStateFilter
get() = when (unseenFilterRaw) {
EPISODE_SHOW_UNSEEN -> TriStateFilter.ENABLED_IS
EPISODE_SHOW_SEEN -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
val bookmarkedFilter: TriStateFilter
get() = when (bookmarkedFilterRaw) {
EPISODE_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
EPISODE_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
fun sortDescending(): Boolean {
return episodeFlags and EPISODE_SORT_DIR_MASK == EPISODE_SORT_DESC
}
private fun Long.removeHexZeros(zeros: Int): Long {
val hex = 16.0
return this.div(hex.pow(zeros)).toLong()
}
companion object {
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000L
const val EPISODE_SORT_DESC = 0x00000000L
const val EPISODE_SORT_ASC = 0x00000001L
const val EPISODE_SORT_DIR_MASK = 0x00000001L
const val EPISODE_SHOW_UNSEEN = 0x00000002L
const val EPISODE_SHOW_SEEN = 0x00000004L
const val EPISODE_UNSEEN_MASK = 0x00000006L
const val EPISODE_SHOW_DOWNLOADED = 0x00000008L
const val EPISODE_SHOW_NOT_DOWNLOADED = 0x00000010L
const val EPISODE_DOWNLOADED_MASK = 0x00000018L
const val EPISODE_SHOW_BOOKMARKED = 0x00000020L
const val EPISODE_SHOW_NOT_BOOKMARKED = 0x00000040L
const val EPISODE_BOOKMARKED_MASK = 0x00000060L
const val EPISODE_SORTING_SOURCE = 0x00000000L
const val EPISODE_SORTING_NUMBER = 0x00000100L
const val EPISODE_SORTING_UPLOAD_DATE = 0x00000200L
const val EPISODE_SORTING_MASK = 0x00000300L
const val EPISODE_DISPLAY_NAME = 0x00000000L
const val EPISODE_DISPLAY_NUMBER = 0x00100000L
const val EPISODE_DISPLAY_MASK = 0x00100000L
const val ANIME_INTRO_MASK = 0x000000000000FFL
const val ANIME_AIRING_EPISODE_MASK = 0x00000000FFFF00L
const val ANIME_AIRING_TIME_MASK = 0xFFFFFFFF000000L
fun create() = Anime(
id = -1L,
url = "",
title = "",
source = -1L,
favorite = false,
lastUpdate = 0L,
nextUpdate = 0L,
calculateInterval = 0,
dateAdded = 0L,
viewerFlags = 0L,
episodeFlags = 0L,
coverLastModified = 0L,
artist = null,
author = null,
description = null,
genre = null,
status = 0L,
thumbnailUrl = null,
updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
initialized = false,
)
}
}

View file

@ -0,0 +1,115 @@
package tachiyomi.domain.entries.manga.model
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import tachiyomi.domain.entries.TriStateFilter
import java.io.Serializable
data class Manga(
val id: Long,
val source: Long,
val favorite: Boolean,
val lastUpdate: Long,
val nextUpdate: Long,
val calculateInterval: Int,
val dateAdded: Long,
val viewerFlags: Long,
val chapterFlags: Long,
val coverLastModified: Long,
val url: String,
val title: String,
val artist: String?,
val author: String?,
val description: String?,
val genre: List<String>?,
val status: Long,
val thumbnailUrl: String?,
val updateStrategy: UpdateStrategy,
val initialized: Boolean,
) : Serializable {
val sorting: Long
get() = chapterFlags and CHAPTER_SORTING_MASK
val displayMode: Long
get() = chapterFlags and CHAPTER_DISPLAY_MASK
val unreadFilterRaw: Long
get() = chapterFlags and CHAPTER_UNREAD_MASK
val downloadedFilterRaw: Long
get() = chapterFlags and CHAPTER_DOWNLOADED_MASK
val bookmarkedFilterRaw: Long
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
val unreadFilter: TriStateFilter
get() = when (unreadFilterRaw) {
CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
CHAPTER_SHOW_READ -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
val bookmarkedFilter: TriStateFilter
get() = when (bookmarkedFilterRaw) {
CHAPTER_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
CHAPTER_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
else -> TriStateFilter.DISABLED
}
fun sortDescending(): Boolean {
return chapterFlags and CHAPTER_SORT_DIR_MASK == CHAPTER_SORT_DESC
}
companion object {
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000L
const val CHAPTER_SORT_DESC = 0x00000000L
const val CHAPTER_SORT_ASC = 0x00000001L
const val CHAPTER_SORT_DIR_MASK = 0x00000001L
const val CHAPTER_SHOW_UNREAD = 0x00000002L
const val CHAPTER_SHOW_READ = 0x00000004L
const val CHAPTER_UNREAD_MASK = 0x00000006L
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008L
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010L
const val CHAPTER_DOWNLOADED_MASK = 0x00000018L
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020L
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040L
const val CHAPTER_BOOKMARKED_MASK = 0x00000060L
const val CHAPTER_SORTING_SOURCE = 0x00000000L
const val CHAPTER_SORTING_NUMBER = 0x00000100L
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200L
const val CHAPTER_SORTING_MASK = 0x00000300L
const val CHAPTER_DISPLAY_NAME = 0x00000000L
const val CHAPTER_DISPLAY_NUMBER = 0x00100000L
const val CHAPTER_DISPLAY_MASK = 0x00100000L
fun create() = Manga(
id = -1L,
url = "",
title = "",
source = -1L,
favorite = false,
lastUpdate = 0L,
nextUpdate = 0L,
calculateInterval = 0,
dateAdded = 0L,
viewerFlags = 0L,
chapterFlags = 0L,
coverLastModified = 0L,
artist = null,
author = null,
description = null,
genre = null,
status = 0L,
thumbnailUrl = null,
updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
initialized = false,
)
}
}

View file

@ -0,0 +1,119 @@
package tachiyomi.domain.items.episode.service
/**
* -R> = regex conversion.
*/
object EpisodeRecognition {
private const val NUMBER_PATTERN = """([0-9]+)(\.[0-9]+)?(\.?[a-z]+)?"""
/**
* All cases with Ch.xx
* Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation -R> 4
*/
private val basic = Regex("""(?<=ep\.) *$NUMBER_PATTERN""")
/**
* Example: Bleach 567: Down With Snowwhite -R> 567
*/
private val number = Regex(NUMBER_PATTERN)
/**
* Regex used to remove unwanted tags
* Example Prison School 12 v.1 vol004 version1243 volume64 -R> Prison School 12
*/
private val unwanted = Regex("""\b(?:v|ver|vol|version|volume|season|s)[^a-z]?[0-9]+""")
/**
* Regex used to remove unwanted whitespace
* Example One Piece 12 special -R> One Piece 12special
*/
private val unwantedWhiteSpace = Regex("""\s(?=extra|special|omake)""")
fun parseEpisodeNumber(animeTitle: String, episodeName: String, episodeNumber: Float? = null): Float {
// If episode number is known return.
if (episodeNumber != null && (episodeNumber == -2f || episodeNumber > -1f)) {
return episodeNumber
}
// Get chapter title with lower case
var name = episodeName.lowercase()
// Remove anime title from episode title.
name = name.replace(animeTitle.lowercase(), "").trim()
// Remove comma's or hyphens.
name = name.replace(',', '.').replace('-', '.')
// Remove unwanted white spaces.
name = unwantedWhiteSpace.replace(name, "")
// Remove unwanted tags.
name = unwanted.replace(name, "")
// Check base case ch.xx
basic.find(name)?.let { return getEpisodeNumberFromMatch(it) }
// Take the first number encountered.
number.find(name)?.let { return getEpisodeNumberFromMatch(it) }
return episodeNumber ?: -1f
}
/**
* Check if episode number is found and return it
* @param match result of regex
* @return chapter number if found else null
*/
private fun getEpisodeNumberFromMatch(match: MatchResult): Float {
return match.let {
val initial = it.groups[1]?.value?.toFloat()!!
val subChapterDecimal = it.groups[2]?.value
val subChapterAlpha = it.groups[3]?.value
val addition = checkForDecimal(subChapterDecimal, subChapterAlpha)
initial.plus(addition)
}
}
/**
* Check for decimal in received strings
* @param decimal decimal value of regex
* @param alpha alpha value of regex
* @return decimal/alpha float value
*/
private fun checkForDecimal(decimal: String?, alpha: String?): Float {
if (!decimal.isNullOrEmpty()) {
return decimal.toFloat()
}
if (!alpha.isNullOrEmpty()) {
if (alpha.contains("extra")) {
return .99f
}
if (alpha.contains("omake")) {
return .98f
}
if (alpha.contains("special")) {
return .97f
}
val trimmedAlpha = alpha.trimStart('.')
if (trimmedAlpha.length == 1) {
return parseAlphaPostFix(trimmedAlpha[0])
}
}
return .0f
}
/**
* x.a -> x.1, x.b -> x.2, etc
*/
private fun parseAlphaPostFix(alpha: Char): Float {
val number = alpha.code - ('a'.code - 1)
if (number >= 10) return 0f
return number / 10f
}
}

View file

@ -0,0 +1,25 @@
package tachiyomi.domain.source.anime.model
data class AnimeSource(
val id: Long,
val lang: String,
val name: String,
val supportsLatest: Boolean,
val isStub: Boolean,
val pin: Pins = Pins.unpinned,
val isUsedLast: Boolean = false,
) {
val visualName: String
get() = when {
lang.isEmpty() -> name
else -> "$name (${lang.uppercase()})"
}
val key: () -> String = {
when {
isUsedLast -> "$id-lastused"
else -> "$id"
}
}
}

View file

@ -0,0 +1,13 @@
package tachiyomi.domain.source.anime.model
data class AnimeSourceWithCount(
val source: AnimeSource,
val count: Long,
) {
val id: Long
get() = source.id
val name: String
get() = source.name
}

View file

@ -0,0 +1,42 @@
package tachiyomi.domain.source.anime.model
sealed class Pin(val code: Int) {
object Unpinned : Pin(0b00)
object Pinned : Pin(0b01)
object Actual : Pin(0b10)
}
inline fun Pins(builder: Pins.PinsBuilder.() -> Unit = {}): Pins {
return Pins.PinsBuilder().apply(builder).flags()
}
fun Pins(vararg pins: Pin) = Pins {
pins.forEach { +it }
}
data class Pins(val code: Int = Pin.Unpinned.code) {
operator fun contains(pin: Pin): Boolean = pin.code and code == pin.code
operator fun plus(pin: Pin): Pins = Pins(code or pin.code)
operator fun minus(pin: Pin): Pins = Pins(code xor pin.code)
companion object {
val unpinned = Pins(Pin.Unpinned)
val pinned = Pins(Pin.Pinned, Pin.Actual)
}
class PinsBuilder(var code: Int = 0) {
operator fun Pin.unaryPlus() {
this@PinsBuilder.code = code or this@PinsBuilder.code
}
operator fun Pin.unaryMinus() {
this@PinsBuilder.code = code or this@PinsBuilder.code
}
fun flags(): Pins = Pins(code)
}
}

View file

@ -0,0 +1,33 @@
package tachiyomi.domain.source.anime.model
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Video
@Suppress("OverridingDeprecatedMember")
class StubAnimeSource(private val sourceData: AnimeSourceData) : AnimeSource {
override val id: Long = sourceData.id
override val name: String = sourceData.name.ifBlank { id.toString() }
override val lang: String = sourceData.lang
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
throw AnimeSourceNotInstalledException()
}
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
throw AnimeSourceNotInstalledException()
}
override suspend fun getVideoList(episode: SEpisode): List<Video> {
throw AnimeSourceNotInstalledException()
}
override fun toString(): String {
return if (sourceData.isMissingInfo.not()) "$name (${lang.uppercase()})" else id.toString()
}
}
class AnimeSourceNotInstalledException : Exception()

View file

@ -0,0 +1,27 @@
package tachiyomi.domain.source.anime.repository
import androidx.paging.PagingSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.SAnime
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.model.AnimeSourceWithCount
typealias AnimeSourcePagingSourceType = PagingSource<Long, SAnime>
interface AnimeSourceRepository {
fun getAnimeSources(): Flow<List<AnimeSource>>
fun getOnlineAnimeSources(): Flow<List<AnimeSource>>
fun getAnimeSourcesWithFavoriteCount(): Flow<List<Pair<AnimeSource, Long>>>
fun getSourcesWithNonLibraryAnime(): Flow<List<AnimeSourceWithCount>>
fun searchAnime(sourceId: Long, query: String, filterList: AnimeFilterList): AnimeSourcePagingSourceType
fun getPopularAnime(sourceId: Long): AnimeSourcePagingSourceType
fun getLatestAnime(sourceId: Long): AnimeSourcePagingSourceType
}

View file

@ -0,0 +1,22 @@
package tachiyomi.domain.source.anime.service
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.source.anime.model.StubAnimeSource
interface AnimeSourceManager {
val catalogueSources: Flow<List<AnimeCatalogueSource>>
fun get(sourceKey: Long): AnimeSource?
fun getOrStub(sourceKey: Long): AnimeSource
fun getOnlineSources(): List<AnimeHttpSource>
fun getCatalogueSources(): List<AnimeCatalogueSource>
fun getStubSources(): List<StubAnimeSource>
}

View file

@ -0,0 +1,50 @@
package tachiyomi.domain.source.manga.model
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import rx.Observable
@Suppress("OverridingDeprecatedMember")
class StubMangaSource(private val sourceData: MangaSourceData) : MangaSource {
override val id: Long = sourceData.id
override val name: String = sourceData.name.ifBlank { id.toString() }
override val lang: String = sourceData.lang
override suspend fun getMangaDetails(manga: SManga): SManga {
throw SourceNotInstalledException()
}
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return Observable.error(SourceNotInstalledException())
}
override suspend fun getChapterList(manga: SManga): List<SChapter> {
throw SourceNotInstalledException()
}
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return Observable.error(SourceNotInstalledException())
}
override suspend fun getPageList(chapter: SChapter): List<Page> {
throw SourceNotInstalledException()
}
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return Observable.error(SourceNotInstalledException())
}
override fun toString(): String {
return if (sourceData.isMissingInfo.not()) "$name (${lang.uppercase()})" else id.toString()
}
}
class SourceNotInstalledException : Exception()

View file

@ -0,0 +1,22 @@
package tachiyomi.domain.source.manga.service
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.flow.Flow
import tachiyomi.domain.source.manga.model.StubMangaSource
interface MangaSourceManager {
val catalogueSources: Flow<List<CatalogueSource>>
fun get(sourceKey: Long): MangaSource?
fun getOrStub(sourceKey: Long): MangaSource
fun getOnlineSources(): List<HttpSource>
fun getCatalogueSources(): List<CatalogueSource>
fun getStubSources(): List<StubMangaSource>
}