Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.lagradost.cloudstream3.TvSeriesLoadResponse
import com.lagradost.cloudstream3.plugins.BasePlugin
import com.lagradost.cloudstream3.plugins.Plugin
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import dalvik.system.PathClassLoader
import org.json.JSONArray
import org.json.JSONObject
Expand Down Expand Up @@ -431,9 +432,14 @@ class PluginHost(private val appContext: Context) {
}.toString()
}

// AudioFile is @Prerelease in the CloudStream library; the annotation is an
// IDE hint with BINARY retention, so reading the list a plugin may have filled
// costs nothing at runtime and is empty for every plugin that sets none.
@OptIn(com.lagradost.cloudstream3.Prerelease::class)
suspend fun loadLinksJson(providerName: String, data: String): String {
val api = apiByName(providerName)
val videoSources = JSONArray()
// (quality, source) pairs so the list can be ordered before it is emitted.
val collected = ArrayList<Pair<Int, JSONObject>>()
val subs = JSONArray()
val seenUrls = HashSet<String>()
val seenSubs = HashSet<String>()
Expand All @@ -446,35 +452,88 @@ class PluginHost(private val appContext: Context) {
if (sf.url.isNotEmpty() && seenSubs.add(sf.url)) {
subs.put(JSONObject().apply {
put("label", sf.lang); put("file", sf.url); put("default", false)
// A subtitle is fetched on its own, so it inherits none of
// the stream's headers. Plenty of CloudStream sources hand
// out tracks that 403 without the Referer the extractor
// set, and SubtitleFile has carried those headers since
// v4.7 — dropping them is why a provider could return
// subtitles and the player still show none.
sf.headers?.takeIf { it.isNotEmpty() }?.let {
put("headers", JSONObject(it as Map<*, *>))
}
})
}
},
callback = { link: ExtractorLink ->
if (link.url.isNotEmpty() && seenUrls.add(link.url)) {
val headers = JSONObject(link.headers as Map<*, *>)
if (link.referer.isNotEmpty()) headers.put("Referer", link.referer)
// Torrents and magnets are not streams. Passed through as an
// ordinary source they reached the player as a bare "Source
// error"; the honest thing is to not offer them. Relative urls
// ("dl.php?id=…") go for the same reason — ExoPlayer resolves
// those as a local file path.
val streamable = link.type != ExtractorLinkType.TORRENT &&
link.type != ExtractorLinkType.MAGNET
if (streamable && link.url.startsWith("http", ignoreCase = true) &&
seenUrls.add(link.url)
) {
// getAllHeaders() folds in the referer exactly the way
// CloudStream's own player does, instead of us overwriting a
// Referer an extractor had deliberately set.
val headers = JSONObject(link.getAllHeaders() as Map<*, *>)
// quality is a resolution int (e.g. 1080) or a Qualities
// sentinel; build a readable, distinct "<host> · <res>p".
val q = link.quality
val res = if (q in 144..4320) "${q}p" else null
val nm = link.name.ifBlank { "Source" }
val label = if (res != null) "$nm · $res" else nm
videoSources.put(JSONObject().apply {
collected.add((if (q in 144..4320) q else 0) to JSONObject().apply {
put("quality", label)
put("videoUrl", link.url)
put("type", if (link.isM3u8) "hls" else "http")
// DASH used to fall into the `else` branch and be handed
// over as a plain progressive file, so every .mpd source
// failed to open.
put("type", when (link.type) {
ExtractorLinkType.M3U8 -> "hls"
ExtractorLinkType.DASH -> "dash"
else -> "http"
})
put("host", link.name)
put("isDefault", videoSources.length() == 0)
put("accessible", true)
put("headers", headers)
// Separate audio renditions the extractor says belong with
// this video. A dual-audio release puts its dub here rather
// than in the manifest, so discarding them left the player
// with one track and nothing to switch to.
if (link.audioTracks.isNotEmpty()) {
put("audioTracks", JSONArray().apply {
link.audioTracks.forEach { a ->
put(JSONObject().apply {
put("url", a.url)
a.headers?.takeIf { it.isNotEmpty() }?.let {
put("headers", JSONObject(it as Map<*, *>))
}
})
}
})
}
})
}
}
)
} catch (t: Throwable) {
Log.e(TAG, "loadLinks ${api.name}: ${t.javaClass.simpleName}: ${t.message}")
}
Log.i(TAG, "loadLinks ${api.name}: ${videoSources.length()} source(s), ${subs.length()} sub(s)")
Log.i(TAG, "loadLinks ${api.name}: ${collected.size} source(s), ${subs.length()} sub(s)")
}
// Best first. Extractors call back in whatever order they finish, so the
// default source used to be a race: a 360p mirror that resolved quickly
// won over a 1080p one that took a moment longer. CloudStream's own
// player orders by quality for the same reason. The sort is stable, so
// sources of equal quality keep the order the provider produced them in.
collected.sortByDescending { it.first }
val videoSources = JSONArray()
collected.forEachIndexed { i, entry ->
entry.second.put("isDefault", i == 0)
videoSources.put(entry.second)
}
// Shape matches MediaResolveModel.fromJson (videoUrl/type/headers + videoSources + subtitles).
val first = if (videoSources.length() > 0) videoSources.getJSONObject(0) else null
Expand Down
68 changes: 61 additions & 7 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@
"ai_translate_remaining": "{}/{} translations left today",
"ai_translate_used_up": "You've used today's translation limit",
"ai_translate_menu": "AI translate",
"ai_translate_menu_desc": "Translates a subtitle to {}"
"ai_translate_menu_desc": "Translates a subtitle to {}",
"ready_translations": "Ready translations",
"lines": "lines"
},
"profile": {
"title": "Profile",
Expand Down Expand Up @@ -272,6 +274,9 @@
"sign_out": "Sign Out",
"sign_out_confirm": "Are you sure you want to sign out?",
"section_providers": "PROVIDERS",
"section_content": "CONTENT",
"sources_row": "Extension sources",
"sources_row_subtitle": "CloudStream, Aniyomi, Mangayomi",
"provider": "Provider",
"choose_provider": "Choose Provider",
"search_providers_hint": "Search providers…",
Expand Down Expand Up @@ -350,6 +355,47 @@
"remove_photo": "Remove photo",
"photo_pick_failed": "Could not pick a photo"
},
"appearance": {
"title": "Appearance",
"entry_subtitle": "Accent colour, pure black",
"section_preview": "PREVIEW",
"section_accent": "ACCENT COLOUR",
"accent_footnote": "The accent colours buttons, highlights, progress bars and the active tab — everywhere in the app.",
"accent_custom": "Custom",
"accent_red": "Sozo Red",
"accent_ember": "Ember",
"accent_amber": "Amber",
"accent_emerald": "Emerald",
"accent_teal": "Teal",
"accent_sky": "Sky",
"accent_ocean": "Ocean",
"accent_indigo": "Indigo",
"accent_violet": "Violet",
"accent_magenta": "Magenta",
"accent_rose": "Rose",
"accent_slate": "Slate",
"section_darkness": "BACKGROUND",
"darkness_dark": "Dark",
"darkness_dark_desc": "The standard dark greys",
"darkness_black": "Pure black",
"darkness_black_desc": "True black backgrounds",
"amoled_footnote": "On an OLED screen a black pixel is switched off, so pure black gives deeper contrast and uses less battery.",
"reset": "Reset to default",
"reset_tooltip": "Back to the default look",
"custom_title": "Custom colour",
"custom_hue": "HUE",
"custom_saturation": "SATURATION",
"custom_lightness": "LIGHTNESS",
"custom_apply": "Use this colour",
"custom_adjusted": "Darkened a little so white text stays readable on it.",
"section_library": "FROM YOUR LIBRARY",
"library_footnote": "Colours taken from the posters of what you have been watching.",
"section_extras": "MORE",
"tint_nav": "Colour the tab bar",
"tint_nav_desc": "Paint the selected tab in your accent instead of white",
"shuffle": "Try the next colour",
"shuffle_desc": "Step through the palette one accent at a time"
},
"search": {
"title": "Search",
"hint": "Search movies, series...",
Expand Down Expand Up @@ -1161,17 +1207,25 @@
},
"live_tv": {
"title": "Live TV",
"all": "All",
"favourites": "FAVOURITES",
"all_channels": "ALL CHANNELS",
"favourites": "Favourites",
"recent": "Recently watched",
"categories": "Categories",
"countries": "Countries",
"results": "Search results",
"search_hint": "Channel name…",
"search_in": "Search in {folder}…",
"no_match": "No channel matches that.",
"empty": "No channels yet.",
"load_failed": "Couldn't load the channels.",
"retry": "Try again",
"recent": "RECENTLY WATCHED",
"folders": "FOLDERS",
"search_in": "Search in {folder}…",
"now": "Now",
"next": "Next",
"guide": "TV guide",
"no_guide": "No guide for this channel.",
"watch": "Watch",
"favourite_add": "Add to favourites",
"favourite_remove": "Remove from favourites",
"hint_long_press": "Long-press a channel for the guide and favourites.",
"channel_count": {
"one": "{} channel",
"other": "{} channels"
Expand Down
68 changes: 61 additions & 7 deletions assets/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@
"ai_translate_remaining": "Сегодня осталось {}/{}",
"ai_translate_used_up": "Дневной лимит перевода исчерпан",
"ai_translate_menu": "ИИ перевод",
"ai_translate_menu_desc": "Переводит субтитры на {}"
"ai_translate_menu_desc": "Переводит субтитры на {}",
"ready_translations": "Готовые переводы",
"lines": "строк"
},
"profile": {
"title": "Профиль",
Expand Down Expand Up @@ -272,6 +274,9 @@
"sign_out": "Выйти",
"sign_out_confirm": "Вы уверены, что хотите выйти?",
"section_providers": "ИСТОЧНИКИ",
"section_content": "КОНТЕНТ",
"sources_row": "Источники расширений",
"sources_row_subtitle": "CloudStream, Aniyomi, Mangayomi",
"provider": "Источник",
"choose_provider": "Выберите источник",
"search_providers_hint": "Поиск источников…",
Expand Down Expand Up @@ -350,6 +355,47 @@
"remove_photo": "Удалить фото",
"photo_pick_failed": "Не удалось выбрать фото"
},
"appearance": {
"title": "Внешний вид",
"entry_subtitle": "Акцентный цвет, чёрный фон",
"section_preview": "ПРЕДПРОСМОТР",
"section_accent": "АКЦЕНТНЫЙ ЦВЕТ",
"accent_footnote": "Акцентный цвет применяется к кнопкам, выделениям, полосам прогресса и активной вкладке — во всём приложении.",
"accent_custom": "Свой цвет",
"accent_red": "Красный Sozo",
"accent_ember": "Уголь",
"accent_amber": "Янтарь",
"accent_emerald": "Изумруд",
"accent_teal": "Бирюза",
"accent_sky": "Небо",
"accent_ocean": "Океан",
"accent_indigo": "Индиго",
"accent_violet": "Фиолетовый",
"accent_magenta": "Пурпурный",
"accent_rose": "Розовый",
"accent_slate": "Серый",
"section_darkness": "ФОН",
"darkness_dark": "Тёмный",
"darkness_dark_desc": "Обычный тёмно-серый фон",
"darkness_black": "Чистый чёрный",
"darkness_black_desc": "Полностью чёрный фон",
"amoled_footnote": "На OLED-экране чёрный пиксель просто не горит, поэтому чистый чёрный даёт более глубокий контраст и экономит заряд.",
"reset": "Сбросить по умолчанию",
"reset_tooltip": "Вернуть стандартный вид",
"custom_title": "Свой цвет",
"custom_hue": "ОТТЕНОК",
"custom_saturation": "НАСЫЩЕННОСТЬ",
"custom_lightness": "СВЕТЛОТА",
"custom_apply": "Применить цвет",
"custom_adjusted": "Цвет слегка затемнён, чтобы белый текст на нём оставался читаемым.",
"section_library": "ИЗ ВАШЕЙ БИБЛИОТЕКИ",
"library_footnote": "Цвета, взятые с постеров того, что вы смотрите.",
"section_extras": "ДОПОЛНИТЕЛЬНО",
"tint_nav": "Раскрасить панель вкладок",
"tint_nav_desc": "Активная вкладка в вашем цвете вместо белого",
"shuffle": "Попробовать следующий цвет",
"shuffle_desc": "Перебирать палитру по одному цвету"
},
"search": {
"title": "Поиск",
"hint": "Поиск фильмов, сериалов...",
Expand Down Expand Up @@ -1161,17 +1207,25 @@
},
"live_tv": {
"title": "Эфир",
"all": "Все",
"favourites": "ИЗБРАННОЕ",
"all_channels": "ВСЕ КАНАЛЫ",
"favourites": "Избранное",
"recent": "Недавно смотрели",
"categories": "Категории",
"countries": "Страны",
"results": "Результаты поиска",
"search_hint": "Название канала…",
"search_in": "Поиск в {folder}…",
"no_match": "Ничего не найдено.",
"empty": "Каналов пока нет.",
"load_failed": "Не удалось загрузить каналы.",
"retry": "Повторить",
"recent": "НЕДАВНО СМОТРЕЛИ",
"folders": "ПАПКИ",
"search_in": "Поиск в {folder}…",
"now": "Сейчас",
"next": "Далее",
"guide": "Телепрограмма",
"no_guide": "Для этого канала нет телепрограммы.",
"watch": "Смотреть",
"favourite_add": "Добавить в избранное",
"favourite_remove": "Убрать из избранного",
"hint_long_press": "Нажмите и удерживайте канал — телепрограмма и избранное.",
"channel_count": {
"one": "{} канал",
"few": "{} канала",
Expand Down
Loading