Merge remote-tracking branch 'origin/master'

This commit is contained in:
Blatzar 2022-04-18 14:07:05 +02:00
commit 2f69fffe87
18 changed files with 349 additions and 134 deletions

View file

@ -36,7 +36,7 @@ android {
targetSdkVersion 30 targetSdkVersion 30
versionCode 45 versionCode 45
versionName "2.9.18" versionName "2.9.19"
resValue "string", "app_version", resValue "string", "app_version",
"${defaultConfig.versionName}${versionNameSuffix ?: ""}" "${defaultConfig.versionName}${versionNameSuffix ?: ""}"
@ -96,8 +96,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0' implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha03' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.0-alpha04'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha03' implementation 'androidx.navigation:navigation-ui-ktx:2.5.0-alpha04'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
@ -126,6 +126,8 @@ dependencies {
implementation 'com.google.android.exoplayer:exoplayer:2.16.1' implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
implementation 'com.google.android.exoplayer:extension-cast:2.16.1' implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
implementation "com.google.android.exoplayer:extension-mediasession:2.16.1" implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0" //implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
// Bug reports // Bug reports
@ -154,7 +156,6 @@ dependencies {
// Networking // Networking
implementation "com.squareup.okhttp3:okhttp:4.9.2" implementation "com.squareup.okhttp3:okhttp:4.9.2"
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1" implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
// Util to skip the URI file fuckery 🙏 // Util to skip the URI file fuckery 🙏
implementation "com.github.tachiyomiorg:unifile:17bec43" implementation "com.github.tachiyomiorg:unifile:17bec43"

View file

@ -197,7 +197,8 @@ class NineAnimeProvider : MainAPI() {
) )
override suspend fun load(url: String): LoadResponse? { override suspend fun load(url: String): LoadResponse? {
val doc = app.get(url).document val validUrl = url.replace("https://9anime.to", mainUrl)
val doc = app.get(validUrl).document
val animeid = doc.selectFirst("div.player-wrapper.watchpage").attr("data-id") ?: return null val animeid = doc.selectFirst("div.player-wrapper.watchpage").attr("data-id") ?: return null
val animeidencoded = encode(getVrf(animeid) ?: return null) val animeidencoded = encode(getVrf(animeid) ?: return null)
val poster = doc.selectFirst("aside.main div.thumb div img").attr("src") val poster = doc.selectFirst("aside.main div.thumb div img").attr("src")
@ -233,7 +234,7 @@ class NineAnimeProvider : MainAPI() {
else null else null
val tags = doc.select("div.info .meta .col1 div:contains(Genre) a").map { it.text() } val tags = doc.select("div.info .meta .col1 div:contains(Genre) a").map { it.text() }
return newAnimeLoadResponse(title, url, tvType) { return newAnimeLoadResponse(title, validUrl, tvType) {
this.posterUrl = poster this.posterUrl = poster
this.plot = description this.plot = description
this.recommendations = recommendations this.recommendations = recommendations

View file

@ -0,0 +1,30 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil
object SyncRedirector {
val syncApis = SyncApis
suspend fun redirect(url: String, preferredUrl: String): String {
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(preferredUrl)
} ?: run {
throw ErrorLoadingException("Page does not exist on $preferredUrl")
}
}
}
return url
}
}

View file

@ -4,13 +4,26 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.utils.SyncUtil
// wont be implemented
class MultiAnimeProvider : MainAPI() { class MultiAnimeProvider : MainAPI() {
override var name = "MultiAnime" override var name = "MultiAnime"
override val lang = "en" override val lang = "en"
override val usesWebView = true override val usesWebView = true
override val supportedTypes = setOf(TvType.Anime) override val supportedTypes = setOf(TvType.Anime)
private val syncApi = OAuth2API.aniListApi private val syncApi: SyncAPI = OAuth2API.aniListApi
private val syncUtilType by lazy {
when (syncApi) {
is AniListApi -> "anilist"
is MALApi -> "myanimelist"
else -> throw ErrorLoadingException("Invalid Api")
}
}
private val validApis by lazy { private val validApis by lazy {
APIHolder.apis.filter { APIHolder.apis.filter {
@ -32,13 +45,25 @@ class MultiAnimeProvider : MainAPI() {
override suspend fun load(url: String): LoadResponse? { override suspend fun load(url: String): LoadResponse? {
return syncApi.getResult(url)?.let { res -> return syncApi.getResult(url)?.let { res ->
newAnimeLoadResponse(res.title!!, url, TvType.Anime) { val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).apmap { url ->
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
}.filterNotNull()
val type =
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
newAnimeLoadResponse(
res.title ?: throw ErrorLoadingException("No Title found"),
url,
type
) {
posterUrl = res.posterUrl posterUrl = res.posterUrl
plot = res.synopsis plot = res.synopsis
tags = res.genres tags = res.genres
rating = res.publicScore rating = res.publicScore
addTrailer(res.trailerUrl) addTrailer(res.trailerUrl)
addAniListId(res.id.toIntOrNull()) addAniListId(res.id.toIntOrNull())
recommendations = res.recommendations
} }
} }
} }

View file

@ -218,31 +218,26 @@ class NginxProvider : MainAPI() {
if (isMovieType) { if (isMovieType) {
val movieName = nfoContent.select("title").text() val movieName = nfoContent.select("title").text()
val posterUrl = mediaRootUrl + "poster.jpg" val posterUrl = mediaRootUrl + "poster.jpg"
return@mapNotNull newMovieSearchResponse(
return@mapNotNull MovieSearchResponse(
movieName, movieName,
mediaRootUrl, mediaRootUrl,
this.name,
TvType.Movie, TvType.Movie,
posterUrl, ) {
null, addPoster(posterUrl, authHeader)
) }
} else { // tv serie } else { // tv serie
val serieName = nfoContent.select("title").text() val serieName = nfoContent.select("title").text()
val posterUrl = mediaRootUrl + "poster.jpg" val posterUrl = mediaRootUrl + "poster.jpg"
TvSeriesSearchResponse( newTvSeriesSearchResponse(
serieName, serieName,
nfoPath, nfoPath,
this.name,
TvType.TvSeries, TvType.TvSeries,
posterUrl, ) {
null, addPoster(posterUrl, authHeader)
null, }
)
} }

View file

@ -605,14 +605,12 @@ open class SflixProvider : MainAPI() {
M3u8Helper().m3u8Generation(M3u8Helper.M3u8Stream(this.file, null), true) M3u8Helper().m3u8Generation(M3u8Helper.M3u8Stream(this.file, null), true)
.map { stream -> .map { stream ->
//println("stream: ${stream.quality} at ${stream.streamUrl}") //println("stream: ${stream.quality} at ${stream.streamUrl}")
val qualityString = if ((stream.quality ?: 0) == 0) label
?: "" else "${stream.quality}p"
ExtractorLink( ExtractorLink(
caller.name, caller.name,
"${caller.name} $qualityString $name", "${caller.name} $name",
stream.streamUrl, stream.streamUrl,
caller.mainUrl, caller.mainUrl,
getQualityFromName(stream.quality.toString()), getQualityFromName(stream.quality?.toString()),
true, true,
extractorData = extractorData extractorData = extractorData
) )

View file

@ -1,7 +1,6 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.ActorData import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ShowStatus
interface SyncAPI : OAuth2API { interface SyncAPI : OAuth2API {
val icon: Int val icon: Int
@ -24,13 +23,19 @@ interface SyncAPI : OAuth2API {
suspend fun search(name: String): List<SyncSearchResult>? suspend fun search(name: String): List<SyncSearchResult>?
fun getIdFromUrl(url : String) : String
data class SyncSearchResult( data class SyncSearchResult(
val name: String, override val name: String,
val syncApiName: String, override val apiName: String,
val id: String, var syncId: String,
val url: String, override val url: String,
val posterUrl: String?, override var posterUrl: String?,
) override var type: TvType? = null,
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null,
override var id: Int? = null,
) : SearchResponse
data class SyncNextAiring( data class SyncNextAiring(
val episode: Int, val episode: Int,

View file

@ -9,6 +9,7 @@ class SyncRepo(private val repo: SyncAPI) {
val idPrefix = repo.idPrefix val idPrefix = repo.idPrefix
val name = repo.name val name = repo.name
val icon = repo.icon val icon = repo.icon
val mainUrl = repo.mainUrl
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> { suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
return safeApiCall { repo.score(id, status) } return safeApiCall { repo.score(id, status) }
@ -29,4 +30,6 @@ class SyncRepo(private val repo: SyncAPI) {
fun hasAccount() : Boolean { fun hasAccount() : Boolean {
return normalSafeApiCall { repo.loginInfo() != null } ?: false return normalSafeApiCall { repo.loginInfo() != null } ?: false
} }
fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url)
} }

View file

@ -70,6 +70,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
return user != null return user != null
} }
override fun getIdFromUrl(url : String): String {
return url.removePrefix("$mainUrl/anime/").removeSuffix("/")
}
private fun getUrlFromId(id: Int): String {
return "$mainUrl/anime/$id"
}
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? { override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(name) ?: return null val data = searchShows(name) ?: return null
return data.data?.Page?.media?.map { return data.data?.Page?.media?.map {
@ -77,7 +85,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
it.title.romaji ?: return null, it.title.romaji ?: return null,
this.name, this.name,
it.id.toString(), it.id.toString(),
"$mainUrl/anime/${it.id}", getUrlFromId(it.id),
it.bannerImage it.bannerImage
) )
} }
@ -126,7 +134,16 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
) )
}, },
publicScore = season.averageScore?.times(100), publicScore = season.averageScore?.times(100),
//recommendations = season. recommendations = season.recommendations?.edges?.mapNotNull { rec ->
val recMedia = rec.node.mediaRecommendation
SyncAPI.SyncSearchResult(
name = recMedia.title?.userPreferred ?: return@mapNotNull null,
this.name,
recMedia.id?.toString() ?: return@mapNotNull null,
getUrlFromId(recMedia.id),
recMedia.coverImage?.large ?: recMedia.coverImage?.medium
)
}
//TODO REST //TODO REST
) )
} }
@ -335,10 +352,10 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
color color
} }
title { title {
romaji romaji
english english
native native
userPreferred userPreferred
} }
duration duration
episodes episodes
@ -382,23 +399,44 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
thumbnail thumbnail
} }
relations { relations {
edges { edges {
id id
relationType(version: 2) relationType(version: 2)
node { node {
id id
coverImage { coverImage {
extraLarge extraLarge
large large
medium medium
color color
} }
} }
} }
}
recommendations {
edges {
node {
mediaRecommendation {
id
coverImage {
extraLarge
large
medium
color
}
title {
romaji
english
native
userPreferred
}
}
}
}
} }
nextAiringEpisode { nextAiringEpisode {
timeUntilAiring timeUntilAiring
episode episode
} }
format format
} }
@ -772,6 +810,21 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("trailer") val trailer: MediaTrailer?, @JsonProperty("trailer") val trailer: MediaTrailer?,
@JsonProperty("description") val description: String?, @JsonProperty("description") val description: String?,
@JsonProperty("characters") val characters: CharacterConnection?, @JsonProperty("characters") val characters: CharacterConnection?,
@JsonProperty("recommendations") val recommendations: RecommendationConnection?,
)
data class RecommendationConnection(
@JsonProperty("edges") val edges: List<RecommendationEdge> = emptyList(),
@JsonProperty("nodes") val nodes: List<Recommendation> = emptyList(),
//@JsonProperty("pageInfo") val pageInfo: PageInfo,
)
data class RecommendationEdge(
//@JsonProperty("rating") val rating: Int,
@JsonProperty("node") val node: Recommendation,
)
data class Recommendation(
@JsonProperty("mediaRecommendation") val mediaRecommendation: SeasonMedia,
) )
data class CharacterName( data class CharacterName(
@ -955,13 +1008,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("data") val data: LikeData?, @JsonProperty("data") val data: LikeData?,
) )
data class Recommendation(
@JsonProperty("title") val title: String?,
@JsonProperty("idMal") val idMal: Int?,
@JsonProperty("poster") val poster: String?,
@JsonProperty("averageScore") val averageScore: Int?
)
data class AniListTitleHolder( data class AniListTitleHolder(
@JsonProperty("title") val title: Title?, @JsonProperty("title") val title: Title?,
@JsonProperty("isFavourite") val isFavourite: Boolean?, @JsonProperty("isFavourite") val isFavourite: Boolean?,

View file

@ -81,6 +81,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
override fun getIdFromUrl(url: String): String {
return Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first()
}
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean { override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
return setScoreRequest( return setScoreRequest(
id.toIntOrNull() ?: return false, id.toIntOrNull() ?: return false,
@ -173,8 +177,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private fun toSearchResult(node: Node?): SyncAPI.SyncSearchResult? { private fun toSearchResult(node: Node?): SyncAPI.SyncSearchResult? {
return SyncAPI.SyncSearchResult( return SyncAPI.SyncSearchResult(
name = node?.title ?: return null, name = node?.title ?: return null,
syncApiName = this.name, apiName = this.name,
id = node.id.toString(), syncId = node.id.toString(),
url = "https://myanimelist.net/anime/${node.id}", url = "https://myanimelist.net/anime/${node.id}",
posterUrl = node.main_picture?.large posterUrl = node.main_picture?.large
) )

View file

@ -93,6 +93,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadManager.getFileName
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import kotlinx.android.synthetic.main.fragment_result.* import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.result_recommendations.*
import kotlinx.android.synthetic.main.result_sync.* import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -572,14 +573,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f)) setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
} }
private fun setMalSync(id: Int?): Boolean {
return syncModel.setMalId(id?.toString())
}
private fun setAniListSync(id: Int?): Boolean {
return syncModel.setAniListId(id?.toString())
}
private fun setActors(actors: List<ActorData>?) { private fun setActors(actors: List<ActorData>?) {
if (actors.isNullOrEmpty()) { if (actors.isNullOrEmpty()) {
result_cast_text?.isVisible = false result_cast_text?.isVisible = false
@ -601,23 +594,47 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
} }
private fun setRecommendations(rec: List<SearchResponse>?) { private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
val isInvalid = rec.isNullOrEmpty() val isInvalid = rec.isNullOrEmpty()
result_recommendations?.isGone = isInvalid result_recommendations?.isGone = isInvalid
result_recommendations_btt?.isGone = isInvalid result_recommendations_btt?.isGone = isInvalid
result_recommendations_btt?.setOnClickListener { result_recommendations_btt?.setOnClickListener {
if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) { val nextFocusDown = if (result_overlapping_panels?.getSelectedPanel()?.ordinal == 1) {
result_recommendations_btt?.nextFocusDownId = R.id.result_recommendations
result_overlapping_panels?.openEndPanel() result_overlapping_panels?.openEndPanel()
R.id.result_recommendations
} else { } else {
result_recommendations_btt?.nextFocusDownId = R.id.result_description
result_overlapping_panels?.closePanels() result_overlapping_panels?.closePanels()
R.id.result_description
} }
result_recommendations_btt?.nextFocusDownId = nextFocusDown
result_search?.nextFocusDownId = nextFocusDown
result_open_in_browser?.nextFocusDownId = nextFocusDown
result_share?.nextFocusDownId = nextFocusDown
} }
result_overlapping_panels?.setEndPanelLockState(if (isInvalid) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED) result_overlapping_panels?.setEndPanelLockState(if (isInvalid) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED)
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
// very dirty selection
result_recommendations_filter_button?.isVisible = apiNames.size > 1
result_recommendations_filter_button?.text = matchAgainst
result_recommendations_filter_button?.setOnClickListener { _ ->
activity?.showBottomDialog(
apiNames,
apiNames.indexOf(matchAgainst),
getString(R.string.home_change_provider_img_des), false, {}
) {
setRecommendations(rec, apiNames[it])
}
}
} ?: run {
result_recommendations_filter_button?.isVisible = false
}
result_recommendations?.post { result_recommendations?.post {
rec?.let { list -> rec?.let { list ->
(result_recommendations?.adapter as SearchAdapter?)?.updateList(list) (result_recommendations?.adapter as SearchAdapter?)?.updateList(list.filter { it.apiName == matchAgainst })
} }
} }
} }
@ -1086,7 +1103,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
ACTION_PLAY_EPISODE_IN_PLAYER -> { ACTION_PLAY_EPISODE_IN_PLAYER -> {
viewModel.getGenerator(episodeClick.data) viewModel.getGenerator(episodeClick.data)
?.let { generator -> ?.let { generator ->
println("LANUCJ:::: $syncdata")
activity?.navigate( activity?.navigate(
R.id.global_to_navigation_player, R.id.global_to_navigation_player,
GeneratorPlayer.newInstance( GeneratorPlayer.newInstance(
@ -1641,7 +1657,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
setDuration(d.duration) setDuration(d.duration)
setYear(d.year) setYear(d.year)
setRating(d.rating) setRating(d.rating)
setRecommendations(d.recommendations) setRecommendations(d.recommendations, null)
setActors(d.actors) setActors(d.actors)
if (SettingsFragment.accountEnabled) { if (SettingsFragment.accountEnabled) {
@ -1950,7 +1966,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
} }
} }
result_recommendations.adapter = recAdapter result_recommendations?.adapter = recAdapter
context?.let { ctx -> context?.let { ctx ->
result_bookmark_button?.isVisible = ctx.isTvSettings() result_bookmark_button?.isVisible = ctx.isTvSettings()

View file

@ -11,6 +11,9 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromUrlNull
import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.animeproviders.GogoanimeProvider
import com.lagradost.cloudstream3.animeproviders.NineAnimeProvider
import com.lagradost.cloudstream3.metaproviders.SyncRedirector
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
@ -127,6 +130,17 @@ class ResultViewModel : ViewModel() {
addTrailer(meta.trailerUrl) addTrailer(meta.trailerUrl)
posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl posterUrl = posterUrl ?: meta.posterUrl ?: meta.backgroundPosterUrl
actors = actors ?: meta.actors actors = actors ?: meta.actors
val realRecommendations = ArrayList<SearchResponse>()
val apiNames = listOf(GogoanimeProvider().name, NineAnimeProvider().name)
meta.recommendations?.forEach { rec ->
apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name))
}
}
recommendations = recommendations?.union(realRecommendations)?.toList()
?: realRecommendations
} }
} }
@ -296,10 +310,9 @@ class ResultViewModel : ViewModel() {
} }
fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch { fun load(url: String, apiName: String, showFillers: Boolean) = viewModelScope.launch {
_resultResponse.postValue(Resource.Loading(url))
_publicEpisodes.postValue(Resource.Loading()) _publicEpisodes.postValue(Resource.Loading())
_resultResponse.postValue(Resource.Loading(url))
_apiName.postValue(apiName)
val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url) val api = getApiFromNameNull(apiName) ?: getApiFromUrlNull(url)
if (api == null) { if (api == null) {
_resultResponse.postValue( _resultResponse.postValue(
@ -312,9 +325,31 @@ class ResultViewModel : ViewModel() {
) )
return@launch return@launch
} }
val validUrlResource = safeApiCall {
SyncRedirector.redirect(
url,
api.mainUrl.replace(NineAnimeProvider().mainUrl, "9anime")
.replace(GogoanimeProvider().mainUrl, "gogoanime")
)
}
if (validUrlResource !is Resource.Success) {
if (validUrlResource is Resource.Failure) {
_resultResponse.postValue(validUrlResource)
}
return@launch
}
val validUrl = validUrlResource.value
_resultResponse.postValue(Resource.Loading(validUrl))
_apiName.postValue(apiName)
repo = APIRepository(api) repo = APIRepository(api)
val data = repo?.load(url) ?: return@launch val data = repo?.load(validUrl) ?: return@launch
_resultResponse.postValue(data) _resultResponse.postValue(data)
@ -331,7 +366,7 @@ class ResultViewModel : ViewModel() {
mainId.toString(), mainId.toString(),
VideoDownloadHelper.DownloadHeaderCached( VideoDownloadHelper.DownloadHeaderCached(
apiName, apiName,
url, validUrl,
d.type, d.type,
d.name, d.name,
d.posterUrl, d.posterUrl,

View file

@ -82,16 +82,16 @@ class SyncViewModel : ViewModel() {
var isValid = false var isValid = false
map?.forEach { (prefix, id) -> map?.forEach { (prefix, id) ->
isValid = isValid || addSync(prefix, id) isValid = addSync(prefix, id) || isValid
} }
return isValid return isValid
} }
fun setMalId(id: String?): Boolean { private fun setMalId(id: String?): Boolean {
return addSync(malApi.idPrefix, id ?: return false) return addSync(malApi.idPrefix, id ?: return false)
} }
fun setAniListId(id: String?): Boolean { private fun setAniListId(id: String?): Boolean {
return addSync(aniListApi.idPrefix, id ?: return false) return addSync(aniListApi.idPrefix, id ?: return false)
} }

View file

@ -4,7 +4,6 @@ import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.syncproviders.OAuth2API import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.SyncAPI
class SyncSearchViewModel { class SyncSearchViewModel {
private val repos = OAuth2API.SyncApis private val repos = OAuth2API.SyncApis
@ -20,15 +19,4 @@ class SyncSearchViewModel {
override var posterHeaders: Map<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
) : SearchResponse ) : SearchResponse
private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse {
return SyncSearchResultSearchResponse(
this.name,
this.url,
this.syncApiName,
null,
this.posterUrl,
null, //this.id.hashCode()
)
}
} }

View file

@ -7,6 +7,7 @@ import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.media.RemoteMediaClient import com.google.android.gms.cast.framework.media.RemoteMediaClient
import com.google.android.gms.common.api.PendingResult import com.google.android.gms.common.api.PendingResult
import com.google.android.gms.common.images.WebImage import com.google.android.gms.common.images.WebImage
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.MetadataHolder import com.lagradost.cloudstream3.ui.MetadataHolder
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultEpisode
@ -64,7 +65,10 @@ object CastHelper {
return builder.build() return builder.build()
} }
fun awaitLinks(pending: PendingResult<RemoteMediaClient.MediaChannelResult>?, callback: (Boolean) -> Unit) { fun awaitLinks(
pending: PendingResult<RemoteMediaClient.MediaChannelResult>?,
callback: (Boolean) -> Unit
) {
if (pending == null) return if (pending == null) return
main { main {
val res = withContext(Dispatchers.IO) { pending.await() } val res = withContext(Dispatchers.IO) { pending.await() }
@ -90,27 +94,15 @@ object CastHelper {
startIndex: Int? = null, startIndex: Int? = null,
startTime: Long? = null, startTime: Long? = null,
): Boolean { ): Boolean {
if (this == null) return false try {
if (episodes.isEmpty()) return false if (this == null) return false
if (currentEpisodeIndex >= episodes.size) return false if (episodes.isEmpty()) return false
if (currentEpisodeIndex >= episodes.size) return false
val epData = episodes[currentEpisodeIndex] val epData = episodes[currentEpisodeIndex]
val holder = val holder =
MetadataHolder(apiName, isMovie, title, poster, currentEpisodeIndex, episodes, currentLinks, subtitles) MetadataHolder(
val index = if (startIndex == null || startIndex < 0) 0 else startIndex
val mediaItem =
getMediaInfo(epData, holder, index, JSONObject(holder.toJson()), subtitles)
awaitLinks(
this.remoteMediaClient?.load(
MediaLoadRequestData.Builder().setMediaInfo(mediaItem).setCurrentTime(startTime ?: 0L).build()
)
) {
if (currentLinks.size > index + 1)
startCast(
apiName, apiName,
isMovie, isMovie,
title, title,
@ -118,11 +110,38 @@ object CastHelper {
currentEpisodeIndex, currentEpisodeIndex,
episodes, episodes,
currentLinks, currentLinks,
subtitles, subtitles
index + 1,
startTime
) )
val index = if (startIndex == null || startIndex < 0) 0 else startIndex
val mediaItem =
getMediaInfo(epData, holder, index, JSONObject(holder.toJson()), subtitles)
awaitLinks(
this.remoteMediaClient?.load(
MediaLoadRequestData.Builder().setMediaInfo(mediaItem)
.setCurrentTime(startTime ?: 0L).build()
)
) {
if (currentLinks.size > index + 1)
startCast(
apiName,
isMovie,
title,
poster,
currentEpisodeIndex,
episodes,
currentLinks,
subtitles,
index + 1,
startTime
)
}
return true
} catch (e: Exception) {
logError(e)
return false
} }
return true
} }
} }

View file

@ -53,7 +53,7 @@ object SyncUtil {
* valid sites are: Gogoanime, Twistmoe and 9anime*/ * valid sites are: Gogoanime, Twistmoe and 9anime*/
private suspend fun getIdsFromSlug( private suspend fun getIdsFromSlug(
slug: String, slug: String,
site: String = "GogoanimeGogoanime" site: String = "Gogoanime"
): Pair<String?, String?>? { ): Pair<String?, String?>? {
Log.i(TAG, "getIdsFromSlug $slug $site") Log.i(TAG, "getIdsFromSlug $slug $site")
try { try {
@ -76,6 +76,28 @@ object SyncUtil {
return null return null
} }
suspend fun getUrlsFromId(id: String, type: String = "anilist") : List<String> {
val url =
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/$type/anime/$id.json"
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).mapped<SyncPage>()
val pages = response.pages ?: return emptyList()
return pages.gogoanime.values.union(pages.nineanime.values).union(pages.twistmoe.values).mapNotNull { it.url }
}
data class SyncPage(
@JsonProperty("Pages") val pages: SyncPages?,
)
data class SyncPages(
@JsonProperty("9anime") val nineanime: Map<String, ProviderPage> = emptyMap(),
@JsonProperty("Gogoanime") val gogoanime: Map<String, ProviderPage> = emptyMap(),
@JsonProperty("Twistmoe") val twistmoe: Map<String, ProviderPage> = emptyMap(),
)
data class ProviderPage(
@JsonProperty("url") val url: String?,
)
data class MalSyncPage( data class MalSyncPage(
@JsonProperty("identifier") val identifier: String?, @JsonProperty("identifier") val identifier: String?,
@JsonProperty("type") val type: String?, @JsonProperty("type") val type: String?,

View file

@ -180,17 +180,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="end"> android:layout_gravity="end">
<com.lagradost.cloudstream3.ui.AutofitRecyclerView <include layout="@layout/result_recommendations" />
android:descendantFocusability="afterDescendants"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
app:spanCount="3"
android:id="@+id/result_recommendations"
tools:listitem="@layout/search_result_grid"
android:orientation="vertical" />
</FrameLayout> </FrameLayout>
</com.discord.panels.OverlappingPanelsLayout> </com.discord.panels.OverlappingPanelsLayout>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
style="@style/BlackButton"
android:layout_gravity="center_vertical|end"
android:text="GogoAnime"
android:id="@+id/result_recommendations_filter_button"
android:layout_width="match_parent"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
app:layout_constraintBottom_toTopOf="@id/result_recommendations" />
<com.lagradost.cloudstream3.ui.AutofitRecyclerView
android:descendantFocusability="afterDescendants"
android:background="?attr/primaryBlackBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
app:spanCount="3"
android:id="@+id/result_recommendations"
tools:listitem="@layout/search_result_grid"
android:orientation="vertical" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>