forked from recloudstream/cloudstream
Add internal support for subtitle headers + season names
This commit is contained in:
parent
c8cd6f921d
commit
53965b13fb
13 changed files with 71 additions and 30 deletions
|
@ -13,7 +13,8 @@ class AbstractSubtitleEntities {
|
||||||
var epNumber: Int? = null,
|
var epNumber: Int? = null,
|
||||||
var seasonNumber: Int? = null,
|
var seasonNumber: Int? = null,
|
||||||
var year: Int? = null,
|
var year: Int? = null,
|
||||||
var isHearingImpaired: Boolean = false
|
var isHearingImpaired: Boolean = false,
|
||||||
|
var headers: Map<String, String> = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SubtitleSearch(
|
data class SubtitleSearch(
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.util.Log
|
||||||
import com.lagradost.cloudstream3.TvType
|
import com.lagradost.cloudstream3.TvType
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.imdbUrlToIdNullable
|
import com.lagradost.cloudstream3.imdbUrlToIdNullable
|
||||||
|
import com.lagradost.cloudstream3.network.CloudflareKiller
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
@ -19,6 +20,8 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
|
|
||||||
override fun logOut() {}
|
override fun logOut() {}
|
||||||
|
|
||||||
|
private val interceptor = CloudflareKiller()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val host = "https://indexsubtitle.com"
|
const val host = "https://indexsubtitle.com"
|
||||||
const val TAG = "INDEXSUBS"
|
const val TAG = "INDEXSUBS"
|
||||||
|
@ -122,12 +125,13 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
|
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
|
||||||
epNumber = epNum,
|
epNumber = epNum,
|
||||||
seasonNumber = seasonNum,
|
seasonNumber = seasonNum,
|
||||||
year = yearNum
|
year = yearNum,
|
||||||
|
headers = interceptor.getCookieHeaders(link).toMap()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val document = app.get("$host/?search=$queryText").document
|
val document = app.get("$host/?search=$queryText", interceptor = interceptor).document
|
||||||
|
|
||||||
document.select("div.my-3.p-3 div.media").map { block ->
|
document.select("div.my-3.p-3 div.media").map { block ->
|
||||||
if (seasonNum > 0) {
|
if (seasonNum > 0) {
|
||||||
|
@ -159,7 +163,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
val urlItem = fixUrl(
|
val urlItem = fixUrl(
|
||||||
it.selectFirst("a")!!.attr("href")
|
it.selectFirst("a")!!.attr("href")
|
||||||
)
|
)
|
||||||
val itemDoc = app.get(urlItem).document
|
val itemDoc = app.get(urlItem, interceptor = interceptor).document
|
||||||
val id = imdbUrlToIdNullable(
|
val id = imdbUrlToIdNullable(
|
||||||
itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent()
|
itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent()
|
||||||
?.attr("href")
|
?.attr("href")
|
||||||
|
@ -198,7 +202,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
|
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
|
||||||
|
|
||||||
urlItems.forEach { url ->
|
urlItems.forEach { url ->
|
||||||
val request = app.get(url)
|
val request = app.get(url, interceptor = interceptor)
|
||||||
if (request.isSuccessful) {
|
if (request.isSuccessful) {
|
||||||
request.document.select("div.my-3.p-3 div.media").map { block ->
|
request.document.select("div.my-3.p-3 div.media").map { block ->
|
||||||
if (block.select("span.d-block span[data-original-title=Language]").text()
|
if (block.select("span.d-block span[data-original-title=Language]").text()
|
||||||
|
@ -231,7 +235,7 @@ class IndexSubtitleApi : AbstractSubApi {
|
||||||
val seasonNum = data.seasonNumber
|
val seasonNum = data.seasonNumber
|
||||||
val epNum = data.epNumber
|
val epNum = data.epNumber
|
||||||
|
|
||||||
val req = app.get(data.data)
|
val req = app.get(data.data, interceptor = interceptor)
|
||||||
|
|
||||||
if (req.isSuccessful) {
|
if (req.isSuccessful) {
|
||||||
val document = req.document
|
val document = req.document
|
||||||
|
|
|
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.ui.SubtitleView
|
||||||
import com.google.android.exoplayer2.upstream.DataSource
|
import com.google.android.exoplayer2.upstream.DataSource
|
||||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
|
||||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
||||||
|
import com.google.android.exoplayer2.upstream.HttpDataSource
|
||||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
||||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
|
||||||
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
||||||
|
@ -442,7 +443,14 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
var requestSubtitleUpdate: (() -> Unit)? = null
|
var requestSubtitleUpdate: (() -> Unit)? = null
|
||||||
|
|
||||||
private fun createOnlineSource(link: ExtractorLink): DataSource.Factory {
|
private fun createOnlineSource(headers: Map<String, String>): HttpDataSource.Factory {
|
||||||
|
val source = OkHttpDataSource.Factory(app.baseClient).setUserAgent(USER_AGENT)
|
||||||
|
return source.apply {
|
||||||
|
setDefaultRequestProperties(headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createOnlineSource(link: ExtractorLink): HttpDataSource.Factory {
|
||||||
val provider = getApiFromNameNull(link.source)
|
val provider = getApiFromNameNull(link.source)
|
||||||
val interceptor = provider?.getVideoInterceptor(link)
|
val interceptor = provider?.getVideoInterceptor(link)
|
||||||
|
|
||||||
|
@ -813,7 +821,8 @@ class CS3IPlayer : IPlayer {
|
||||||
// See setPreferredTextLanguage
|
// See setPreferredTextLanguage
|
||||||
it.language!!,
|
it.language!!,
|
||||||
SubtitleOrigin.EMBEDDED_IN_VIDEO,
|
SubtitleOrigin.EMBEDDED_IN_VIDEO,
|
||||||
it.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP
|
it.sampleMimeType ?: MimeTypes.APPLICATION_SUBRIP,
|
||||||
|
emptyMap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -981,9 +990,10 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
val mediaItem = getMediaItem(MimeTypes.VIDEO_MP4, data.uri)
|
val mediaItem = getMediaItem(MimeTypes.VIDEO_MP4, data.uri)
|
||||||
val offlineSourceFactory = context.createOfflineSource()
|
val offlineSourceFactory = context.createOfflineSource()
|
||||||
|
val onlineSourceFactory = createOnlineSource(emptyMap())
|
||||||
|
|
||||||
val (subSources, activeSubtitles) = getSubSources(
|
val (subSources, activeSubtitles) = getSubSources(
|
||||||
onlineSourceFactory = offlineSourceFactory,
|
onlineSourceFactory = onlineSourceFactory,
|
||||||
offlineSourceFactory = offlineSourceFactory,
|
offlineSourceFactory = offlineSourceFactory,
|
||||||
subtitleHelper,
|
subtitleHelper,
|
||||||
)
|
)
|
||||||
|
@ -997,7 +1007,7 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSubSources(
|
private fun getSubSources(
|
||||||
onlineSourceFactory: DataSource.Factory?,
|
onlineSourceFactory: HttpDataSource.Factory?,
|
||||||
offlineSourceFactory: DataSource.Factory?,
|
offlineSourceFactory: DataSource.Factory?,
|
||||||
subHelper: PlayerSubtitleHelper,
|
subHelper: PlayerSubtitleHelper,
|
||||||
): Pair<List<SingleSampleMediaSource>, List<SubtitleData>> {
|
): Pair<List<SingleSampleMediaSource>, List<SubtitleData>> {
|
||||||
|
@ -1021,7 +1031,10 @@ class CS3IPlayer : IPlayer {
|
||||||
SubtitleOrigin.URL -> {
|
SubtitleOrigin.URL -> {
|
||||||
if (onlineSourceFactory != null) {
|
if (onlineSourceFactory != null) {
|
||||||
activeSubtitles.add(sub)
|
activeSubtitles.add(sub)
|
||||||
SingleSampleMediaSource.Factory(onlineSourceFactory)
|
SingleSampleMediaSource.Factory(onlineSourceFactory.apply {
|
||||||
|
if (sub.headers.isNotEmpty())
|
||||||
|
this.setDefaultRequestProperties(sub.headers)
|
||||||
|
})
|
||||||
.createMediaSource(subConfig, C.TIME_UNSET)
|
.createMediaSource(subConfig, C.TIME_UNSET)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
@ -84,7 +84,8 @@ class DownloadFileGenerator(
|
||||||
realName.ifBlank { ctx.getString(R.string.default_subtitles) },
|
realName.ifBlank { ctx.getString(R.string.default_subtitles) },
|
||||||
file.second.toString(),
|
file.second.toString(),
|
||||||
SubtitleOrigin.DOWNLOADED_FILE,
|
SubtitleOrigin.DOWNLOADED_FILE,
|
||||||
name.toSubtitleMimeType()
|
name.toSubtitleMimeType(),
|
||||||
|
emptyMap()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -399,7 +399,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
name = getName(currentSubtitle, true),
|
name = getName(currentSubtitle, true),
|
||||||
url = url,
|
url = url,
|
||||||
origin = SubtitleOrigin.URL,
|
origin = SubtitleOrigin.URL,
|
||||||
mimeType = url.toSubtitleMimeType()
|
mimeType = url.toSubtitleMimeType(),
|
||||||
|
headers = currentSubtitle.headers
|
||||||
)
|
)
|
||||||
runOnMainThread {
|
runOnMainThread {
|
||||||
addAndSelectSubtitles(subtitle)
|
addAndSelectSubtitles(subtitle)
|
||||||
|
@ -483,7 +484,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
name,
|
name,
|
||||||
uri.toString(),
|
uri.toString(),
|
||||||
SubtitleOrigin.DOWNLOADED_FILE,
|
SubtitleOrigin.DOWNLOADED_FILE,
|
||||||
name.toSubtitleMimeType()
|
name.toSubtitleMimeType(),
|
||||||
|
emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
addAndSelectSubtitles(subtitleData)
|
addAndSelectSubtitles(subtitleData)
|
||||||
|
|
|
@ -29,12 +29,14 @@ enum class SubtitleOrigin {
|
||||||
/**
|
/**
|
||||||
* @param name To be displayed in the player
|
* @param name To be displayed in the player
|
||||||
* @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend language
|
* @param url Url for the subtitle, when EMBEDDED_IN_VIDEO this variable is used as the real backend language
|
||||||
|
* @param headers if empty it will use the base onlineDataSource headers else only the specified headers
|
||||||
* */
|
* */
|
||||||
data class SubtitleData(
|
data class SubtitleData(
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val origin: SubtitleOrigin,
|
val origin: SubtitleOrigin,
|
||||||
val mimeType: String,
|
val mimeType: String,
|
||||||
|
val headers: Map<String, String>
|
||||||
)
|
)
|
||||||
|
|
||||||
class PlayerSubtitleHelper {
|
class PlayerSubtitleHelper {
|
||||||
|
@ -71,7 +73,8 @@ class PlayerSubtitleHelper {
|
||||||
name = subtitleFile.lang,
|
name = subtitleFile.lang,
|
||||||
url = subtitleFile.url,
|
url = subtitleFile.url,
|
||||||
origin = SubtitleOrigin.URL,
|
origin = SubtitleOrigin.URL,
|
||||||
mimeType = subtitleFile.url.toSubtitleMimeType()
|
mimeType = subtitleFile.url.toSubtitleMimeType(),
|
||||||
|
headers = emptyMap()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -409,6 +409,11 @@ class ResultViewModel2 : ViewModel() {
|
||||||
private const val EPISODE_RANGE_SIZE = 20
|
private const val EPISODE_RANGE_SIZE = 20
|
||||||
private const val EPISODE_RANGE_OVERLOAD = 30
|
private const val EPISODE_RANGE_OVERLOAD = 30
|
||||||
|
|
||||||
|
private fun List<SeasonData>?.getSeason(season: Int?): SeasonData? {
|
||||||
|
if (season == null) return null
|
||||||
|
return this?.firstOrNull { it.season == season }
|
||||||
|
}
|
||||||
|
|
||||||
private fun filterName(name: String?): String? {
|
private fun filterName(name: String?): String? {
|
||||||
if (name == null) return null
|
if (name == null) return null
|
||||||
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
|
Regex("[eE]pisode [0-9]*(.*)").find(name)?.groupValues?.get(1)?.let {
|
||||||
|
@ -1476,11 +1481,18 @@ class ResultViewModel2 : ViewModel() {
|
||||||
if (isMovie || currentSeasons.size <= 1) null else
|
if (isMovie || currentSeasons.size <= 1) null else
|
||||||
when (indexer.season) {
|
when (indexer.season) {
|
||||||
0 -> txt(R.string.no_season)
|
0 -> txt(R.string.no_season)
|
||||||
else -> txt(
|
else -> {
|
||||||
R.string.season_format,
|
val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames
|
||||||
txt(R.string.season),
|
val seasonData =
|
||||||
indexer.season
|
seasonNames.getSeason(indexer.season)
|
||||||
) //TODO FIX DISPLAYNAME
|
val suffix = seasonData?.name?.let { " $it" } ?: ""
|
||||||
|
txt(
|
||||||
|
R.string.season_format,
|
||||||
|
txt(R.string.season),
|
||||||
|
seasonData?.displaySeason ?: indexer.season,
|
||||||
|
suffix
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1578,6 +1590,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
val id = mainId + episode + idIndex * 1000000
|
val id = mainId + episode + idIndex * 1000000
|
||||||
if (!existingEpisodes.contains(episode)) {
|
if (!existingEpisodes.contains(episode)) {
|
||||||
existingEpisodes.add(id)
|
existingEpisodes.add(id)
|
||||||
|
val seasonData = loadResponse.seasonNames.getSeason(i.season)
|
||||||
val eps =
|
val eps =
|
||||||
buildResultEpisode(
|
buildResultEpisode(
|
||||||
loadResponse.name,
|
loadResponse.name,
|
||||||
|
@ -1585,7 +1598,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
i.posterUrl,
|
i.posterUrl,
|
||||||
episode,
|
episode,
|
||||||
null,
|
null,
|
||||||
i.season,
|
seasonData?.displaySeason ?: i.season,
|
||||||
i.data,
|
i.data,
|
||||||
loadResponse.apiName,
|
loadResponse.apiName,
|
||||||
id,
|
id,
|
||||||
|
@ -1621,7 +1634,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
existingEpisodes.add(id)
|
existingEpisodes.add(id)
|
||||||
val seasonIndex = episode.season?.minus(1)
|
val seasonIndex = episode.season?.minus(1)
|
||||||
val currentSeason =
|
val currentSeason =
|
||||||
loadResponse.seasonNames?.getOrNull(seasonIndex ?: -1)
|
loadResponse.seasonNames.getSeason(episode.season)
|
||||||
|
|
||||||
val ep =
|
val ep =
|
||||||
buildResultEpisode(
|
buildResultEpisode(
|
||||||
|
@ -1630,7 +1643,7 @@ class ResultViewModel2 : ViewModel() {
|
||||||
episode.posterUrl,
|
episode.posterUrl,
|
||||||
episodeIndex,
|
episodeIndex,
|
||||||
seasonIndex,
|
seasonIndex,
|
||||||
currentSeason?.season ?: episode.season,
|
currentSeason?.displaySeason ?: episode.season,
|
||||||
episode.data,
|
episode.data,
|
||||||
loadResponse.apiName,
|
loadResponse.apiName,
|
||||||
id,
|
id,
|
||||||
|
@ -1731,10 +1744,19 @@ class ResultViewModel2 : ViewModel() {
|
||||||
_dubSubSelections.postValue(dubSelection.map { txt(it) to it })
|
_dubSubSelections.postValue(dubSelection.map { txt(it) to it })
|
||||||
if (loadResponse is EpisodeResponse) {
|
if (loadResponse is EpisodeResponse) {
|
||||||
_seasonSelections.postValue(seasonsSelection.map { seasonNumber ->
|
_seasonSelections.postValue(seasonsSelection.map { seasonNumber ->
|
||||||
|
val seasonData = loadResponse.seasonNames.getSeason(seasonNumber)
|
||||||
|
val fixedSeasonNumber = seasonData?.displaySeason ?: seasonNumber
|
||||||
|
val suffix = seasonData?.name?.let { " $it" } ?: ""
|
||||||
|
|
||||||
val name =
|
val name =
|
||||||
/*loadResponse.seasonNames?.firstOrNull { it.season == seasonNumber }?.name?.let { seasonData ->
|
/*loadResponse.seasonNames?.firstOrNull { it.season == seasonNumber }?.name?.let { seasonData ->
|
||||||
txt(seasonData)
|
txt(seasonData)
|
||||||
} ?:*/txt(R.string.season_format, txt(R.string.season), seasonNumber) //TODO FIX
|
} ?:*/txt(
|
||||||
|
R.string.season_format,
|
||||||
|
txt(R.string.season),
|
||||||
|
fixedSeasonNumber,
|
||||||
|
suffix
|
||||||
|
)
|
||||||
name to seasonNumber
|
name to seasonNumber
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,7 +210,6 @@
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="season">Staffel</string>
|
<string name="season">Staffel</string>
|
||||||
<string name="season_format">%s %d</string>
|
|
||||||
<string name="no_season">Keine Staffel</string>
|
<string name="no_season">Keine Staffel</string>
|
||||||
<string name="episode">Episode</string>
|
<string name="episode">Episode</string>
|
||||||
<string name="episodes">Episoden</string>
|
<string name="episodes">Episoden</string>
|
||||||
|
|
|
@ -228,7 +228,6 @@
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="season">Sezona</string>
|
<string name="season">Sezona</string>
|
||||||
<string name="season_format">%s %d</string>
|
|
||||||
<string name="no_season">Nema sezone</string>
|
<string name="no_season">Nema sezone</string>
|
||||||
<string name="episode">Epizoda</string>
|
<string name="episode">Epizoda</string>
|
||||||
<string name="episodes">Epizode</string>
|
<string name="episodes">Epizode</string>
|
||||||
|
|
|
@ -208,7 +208,6 @@
|
||||||
<string name="acra_report_toast">Sorry, de applicatie is gecrasht. Er wordt een anoniem bugrapport naar de ontwikkelaars gestuurd </string>
|
<string name="acra_report_toast">Sorry, de applicatie is gecrasht. Er wordt een anoniem bugrapport naar de ontwikkelaars gestuurd </string>
|
||||||
|
|
||||||
<string name="season">Seizoen</string>
|
<string name="season">Seizoen</string>
|
||||||
<string name="season_format">%s %d</string>
|
|
||||||
<string name="no_season">Geen seizoen</string>
|
<string name="no_season">Geen seizoen</string>
|
||||||
<string name="episode">Aflevering</string>
|
<string name="episode">Aflevering</string>
|
||||||
<string name="episodes">afleveringen</string>
|
<string name="episodes">afleveringen</string>
|
||||||
|
|
|
@ -221,7 +221,6 @@
|
||||||
|
|
||||||
|
|
||||||
<string name="season">Mùa</string>
|
<string name="season">Mùa</string>
|
||||||
<string name="season_format">%s %d</string>
|
|
||||||
<string name="no_season">Không có mùa nào</string>
|
<string name="no_season">Không có mùa nào</string>
|
||||||
<string name="episode">Tập</string>
|
<string name="episode">Tập</string>
|
||||||
<string name="episodes">Tập</string>
|
<string name="episodes">Tập</string>
|
||||||
|
|
|
@ -231,7 +231,6 @@
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="season">季</string>
|
<string name="season">季</string>
|
||||||
<string name="season_format">%s %d</string>
|
|
||||||
<string name="no_season">无季</string>
|
<string name="no_season">无季</string>
|
||||||
<string name="episode">集</string>
|
<string name="episode">集</string>
|
||||||
<string name="episodes">集</string>
|
<string name="episodes">集</string>
|
||||||
|
|
|
@ -290,7 +290,7 @@
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="season">Season</string>
|
<string name="season">Season</string>
|
||||||
<string name="season_format">%s %d</string>
|
<string name="season_format">%s %d%s</string>
|
||||||
<string name="no_season">No Season</string>
|
<string name="no_season">No Season</string>
|
||||||
<string name="episode">Episode</string>
|
<string name="episode">Episode</string>
|
||||||
<string name="episodes">Episodes</string>
|
<string name="episodes">Episodes</string>
|
||||||
|
|
Loading…
Reference in a new issue