Add id: prefix search for rest of third-party trackers (#3776)
* Add id: prefix search to AniList * Add id: prefix search to Bangumi * Add id: prefix search to Kitsu * Add id: prefix search to MangaUpdates * Add id: prefix search to Shikimori * Add id: prefix search to Hikka Since Hikka doesn't use integer IDs and instead relies solely on a slug system, users have to supply the slug to the `id:` search (so not `id:123` but `id:sousou-no-frieren-97e05f` to get the Frieren manga specifically) It's asymmetrical to the other trackers, but I thought keeping the prefix consistent was more important than to be true to the tracker's internal workings. * Parse MU string IDs as base36 Longs Turns out that's what the random strings in the URL are. Co-authored-by: BrutuZ <7104931+BrutuZ@users.noreply.github.com> * [skip ci] Add Changelog --------- Co-authored-by: BrutuZ <7104931+BrutuZ@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- `Other` - for technical stuff.
|
- `Other` - for technical stuff.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Added
|
||||||
|
- Add `id:` prefix search to remaining trackers (AniList, Bangumi, Kitsu, MangaUpdates, Shikimori, and Hikka) ([@MajorTanya](https://github.com/MajorTanya)) ([#3776](https://github.com/mihonapp/mihon/pull/3776))
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
- Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
|
- Show updates and upcoming filter icon as active for categories ([@Secozzi](https://github.com/Secozzi)) ([#3772](https://github.com/mihonapp/mihon/pull/3772))
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
const val POINT_10_DECIMAL = "POINT_10_DECIMAL"
|
||||||
const val POINT_5 = "POINT_5"
|
const val POINT_5 = "POINT_5"
|
||||||
const val POINT_3 = "POINT_3"
|
const val POINT_3 = "POINT_3"
|
||||||
|
|
||||||
|
private const val SEARCH_ID_PREFIX = "id:"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
@@ -196,6 +198,12 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
if (query.startsWith(SEARCH_ID_PREFIX)) {
|
||||||
|
query.substringAfter(SEARCH_ID_PREFIX).trim().toIntOrNull()?.let { id ->
|
||||||
|
return api.getMangaDetails(id)?.let { listOf(it) } ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -318,6 +318,73 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaDetails(id: Int): TrackSearch? {
|
||||||
|
return withIOContext {
|
||||||
|
val query = $$"""
|
||||||
|
|query Search($manga_id: Int) {
|
||||||
|
|Page (perPage: 1) {
|
||||||
|
|media(id: $manga_id, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
|
|id
|
||||||
|
|staff {
|
||||||
|
|edges {
|
||||||
|
|role
|
||||||
|
|id
|
||||||
|
|node {
|
||||||
|
|name {
|
||||||
|
|full
|
||||||
|
|userPreferred
|
||||||
|
|native
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|title {
|
||||||
|
|userPreferred
|
||||||
|
|}
|
||||||
|
|coverImage {
|
||||||
|
|large
|
||||||
|
|}
|
||||||
|
|format
|
||||||
|
|countryOfOrigin
|
||||||
|
|status
|
||||||
|
|chapters
|
||||||
|
|description
|
||||||
|
|startDate {
|
||||||
|
|year
|
||||||
|
|month
|
||||||
|
|day
|
||||||
|
|}
|
||||||
|
|averageScore
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|
|
||||||
|
""".trimMargin()
|
||||||
|
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
put("query", query)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("manga_id", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with(json) {
|
||||||
|
authClient.newCall(
|
||||||
|
POST(
|
||||||
|
API_URL,
|
||||||
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<ALSearchResult>()
|
||||||
|
.data.page.media
|
||||||
|
.firstOrNull()
|
||||||
|
?.toALManga()
|
||||||
|
?.toTrack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createDate(dateValue: Long): JsonObject {
|
private fun createDate(dateValue: Long): JsonObject {
|
||||||
if (dateValue == 0L) {
|
if (dateValue == 0L) {
|
||||||
return buildJsonObject {
|
return buildJsonObject {
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
if (query.startsWith(SEARCH_ID_PREFIX)) {
|
||||||
|
query.substringAfter(SEARCH_ID_PREFIX).trim().toIntOrNull()?.let { id ->
|
||||||
|
return api.getMangaDetails(id)?.let { listOf(it) } ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,5 +147,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
|
|||||||
|
|
||||||
private val SCORE_LIST = IntRange(0, 10)
|
private val SCORE_LIST = IntRange(0, 10)
|
||||||
.map(Int::toString)
|
.map(Int::toString)
|
||||||
|
|
||||||
|
private const val SEARCH_ID_PREFIX = "id:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMCollectionResponse
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSearchResult
|
||||||
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMSubject
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMUser
|
import eu.kanade.tachiyomi.data.track.bangumi.dto.BGMUser
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
@@ -110,6 +111,20 @@ class BangumiApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaDetails(id: Int): TrackSearch? {
|
||||||
|
return withIOContext {
|
||||||
|
val url = "$API_URL/v0/subjects/$id"
|
||||||
|
|
||||||
|
with(json) {
|
||||||
|
authClient.newCall(GET(url, headers = headersOf("Content-Type", APP_JSON)))
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<BGMSubject>()
|
||||||
|
.takeIf { it.platform == null || it.platform == "漫画" }
|
||||||
|
?.toTrackSearch(trackId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun statusLibManga(track: Track, username: String): Track? {
|
suspend fun statusLibManga(track: Track, username: String): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val url = "$API_URL/v0/users/$username/collections/${track.remote_id}"
|
val url = "$API_URL/v0/users/$username/collections/${track.remote_id}"
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker {
|
|||||||
private val SCORE_LIST = IntRange(0, 10)
|
private val SCORE_LIST = IntRange(0, 10)
|
||||||
.map(Int::toString)
|
.map(Int::toString)
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
|
|
||||||
|
private const val SEARCH_ID_PREFIX = "id:"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
@@ -118,7 +120,15 @@ class Hikka(id: Long) : BaseTracker(id, "Hikka"), DeletableTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> = api.searchManga(query)
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
if (query.startsWith(SEARCH_ID_PREFIX)) {
|
||||||
|
query.substringAfter(SEARCH_ID_PREFIX).trim().let { slug ->
|
||||||
|
return api.getMangaDetails(slug)?.let { listOf(it) } ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.searchManga(query)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val remoteTrack = api.getManga(track)
|
val remoteTrack = api.getManga(track)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.network.GET
|
|||||||
import eu.kanade.tachiyomi.network.HttpException
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
@@ -99,6 +100,25 @@ class HikkaApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaDetails(slug: String): TrackSearch? {
|
||||||
|
return withIOContext {
|
||||||
|
val url = "$BASE_API_URL/manga/$slug"
|
||||||
|
|
||||||
|
with(json) {
|
||||||
|
val response = authClient.newCall(GET(url))
|
||||||
|
.await()
|
||||||
|
|
||||||
|
if (response.code == 404) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
response
|
||||||
|
.parseAs<HKManga>()
|
||||||
|
.toTrack(trackId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getRead(track: Track): HKRead? {
|
suspend fun getRead(track: Track): HKRead? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val slug = track.tracking_url.split("/")[4]
|
val slug = track.tracking_url.split("/")[4]
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
|||||||
const val ON_HOLD = 3L
|
const val ON_HOLD = 3L
|
||||||
const val DROPPED = 4L
|
const val DROPPED = 4L
|
||||||
const val PLAN_TO_READ = 5L
|
const val PLAN_TO_READ = 5L
|
||||||
|
|
||||||
|
private const val SEARCH_ID_PREFIX = "id:"
|
||||||
}
|
}
|
||||||
|
|
||||||
override val supportsReadingDates: Boolean = true
|
override val supportsReadingDates: Boolean = true
|
||||||
@@ -114,6 +116,12 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
if (query.startsWith(SEARCH_ID_PREFIX)) {
|
||||||
|
query.substringAfter(SEARCH_ID_PREFIX).trim().toIntOrNull()?.let { id ->
|
||||||
|
return api.getMangaDetails(id)?.let { listOf(it) } ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentUserResult
|
|||||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuListSearchResult
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchResult
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSingleManga
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuUser
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuUser
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.DELETE
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.HttpException
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.jsonMime
|
import eu.kanade.tachiyomi.network.jsonMime
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
@@ -242,6 +245,26 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaDetails(id: Int): TrackSearch? {
|
||||||
|
return withIOContext {
|
||||||
|
val url = "$BASE_URL/manga/$id"
|
||||||
|
try {
|
||||||
|
with(json) {
|
||||||
|
authClient.newCall(GET(url))
|
||||||
|
.await()
|
||||||
|
.parseAs<KitsuSingleManga>()
|
||||||
|
.toTrackSearch()
|
||||||
|
}
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
if (e.code == 404) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||||
private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||||
|
|||||||
@@ -64,6 +64,26 @@ data class KitsuListSearchItemDataAttributes(
|
|||||||
val private: Boolean,
|
val private: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuSingleManga(
|
||||||
|
val data: KitsuListSearchItemIncluded,
|
||||||
|
) {
|
||||||
|
fun toTrackSearch(): TrackSearch {
|
||||||
|
return TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
|
remote_id = data.id
|
||||||
|
title = data.attributes.canonicalTitle
|
||||||
|
total_chapters = data.attributes.chapterCount ?: 0
|
||||||
|
cover_url = data.attributes.posterImage?.original ?: ""
|
||||||
|
summary = data.attributes.synopsis ?: ""
|
||||||
|
tracking_url = KitsuApi.mangaUrl(remote_id)
|
||||||
|
score = data.attributes.averageRating?.toDoubleOrNull() ?: -1.0
|
||||||
|
publishing_status = data.attributes.status
|
||||||
|
publishing_type = data.attributes.mangaType ?: ""
|
||||||
|
start_date = data.attributes.startDate ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class KitsuListSearchItemIncluded(
|
data class KitsuListSearchItemIncluded(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
@@ -79,4 +99,5 @@ data class KitsuListSearchItemIncludedAttributes(
|
|||||||
val synopsis: String?,
|
val synopsis: String?,
|
||||||
val startDate: String?,
|
val startDate: String?,
|
||||||
val status: String,
|
val status: String,
|
||||||
|
val averageRating: String?,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val SEARCH_ID_PREFIX = "id:"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val interceptor by lazy { MangaUpdatesInterceptor(this) }
|
private val interceptor by lazy { MangaUpdatesInterceptor(this) }
|
||||||
@@ -89,6 +91,17 @@ class MangaUpdates(id: Long) : BaseTracker(id, "MangaUpdates"), DeletableTracker
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
if (query.startsWith(SEARCH_ID_PREFIX)) {
|
||||||
|
return try {
|
||||||
|
val stringId = query.substringAfter(SEARCH_ID_PREFIX).trim()
|
||||||
|
val searchId = stringId.toLongOrNull() ?: stringId.toLong(36)
|
||||||
|
|
||||||
|
api.getSeriesDetails(searchId)?.let { listOf(it.toTrackSearch(id)) } ?: emptyList()
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
.map {
|
.map {
|
||||||
it.toTrackSearch(id)
|
it.toTrackSearch(id)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.network.DELETE
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
@@ -26,6 +27,7 @@ import kotlinx.serialization.json.putJsonObject
|
|||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||||
|
|
||||||
@@ -173,6 +175,23 @@ class MangaUpdatesApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getSeriesDetails(id: Long): MURecord? {
|
||||||
|
return withIOContext {
|
||||||
|
val url = "$BASE_URL/v1/series/$id"
|
||||||
|
|
||||||
|
with(json) {
|
||||||
|
val response = client.newCall(GET(url))
|
||||||
|
.await()
|
||||||
|
|
||||||
|
if (response.code == 404) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
response.parseAs<MURecord>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun authenticate(username: String, password: String): MUContext {
|
suspend fun authenticate(username: String, password: String): MUContext {
|
||||||
val body = buildJsonObject {
|
val body = buildJsonObject {
|
||||||
put("username", username)
|
put("username", username)
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
|||||||
|
|
||||||
private val SCORE_LIST = IntRange(0, 10)
|
private val SCORE_LIST = IntRange(0, 10)
|
||||||
.map(Int::toString)
|
.map(Int::toString)
|
||||||
|
|
||||||
|
private const val SEARCH_ID_PREFIX = "id:"
|
||||||
}
|
}
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
@@ -81,6 +83,12 @@ class Shikimori(id: Long) : BaseTracker(id, "Shikimori"), DeletableTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
if (query.startsWith(SEARCH_ID_PREFIX)) {
|
||||||
|
query.substringAfter(SEARCH_ID_PREFIX).trim().toIntOrNull()?.let { id ->
|
||||||
|
return api.getMangaDetails(id)?.let { listOf(it) } ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,57 @@ class ShikimoriApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getMangaDetails(id: Int): TrackSearch? {
|
||||||
|
return withIOContext {
|
||||||
|
val query = $$"""
|
||||||
|
|query($query: String) {
|
||||||
|
|mangas(ids: $query, limit: 1, kind:"!light_novel,!novel") {
|
||||||
|
|id
|
||||||
|
|name
|
||||||
|
|chapters
|
||||||
|
|kind
|
||||||
|
|poster {
|
||||||
|
|mainUrl
|
||||||
|
|}
|
||||||
|
|score
|
||||||
|
|url
|
||||||
|
|status
|
||||||
|
|airedOn {
|
||||||
|
|date
|
||||||
|
|}
|
||||||
|
|description
|
||||||
|
|personRoles {
|
||||||
|
|person {
|
||||||
|
|name
|
||||||
|
|}
|
||||||
|
|rolesEn
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
""".trimMargin()
|
||||||
|
val payload = buildJsonObject {
|
||||||
|
put("query", query)
|
||||||
|
putJsonObject("variables") {
|
||||||
|
put("query", "$id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with(json) {
|
||||||
|
authClient.newCall(
|
||||||
|
POST(
|
||||||
|
GRAPHQL_API_URL,
|
||||||
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<SMSearchResult>()
|
||||||
|
.data.mangas
|
||||||
|
.firstOrNull()
|
||||||
|
?.toTrack(trackId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun findLibManga(track: Track, isRefresh: Boolean = false): Track? {
|
suspend fun findLibManga(track: Track, isRefresh: Boolean = false): Track? {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = $$"""
|
val query = $$"""
|
||||||
|
|||||||
Reference in New Issue
Block a user