From 3d0a361a707ce406705b2834f7c679dde15fe9e1 Mon Sep 17 00:00:00 2001
From: yaoxieyoulei <1622968661@qq.com>
Date: Thu, 18 Apr 2024 17:30:50 +0800
Subject: [PATCH] =?UTF-8?q?:art:=20=E5=AE=8C=E5=96=84=E5=BA=94=E7=94=A8?=
=?UTF-8?q?=E8=AE=BE=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.idea/appInsightsSettings.xml | 13 +
.idea/deploymentTargetDropDown.xml | 15 +-
README.md | 22 +-
.../java/top/yogiczy/mytv/MainActivity.kt | 11 +-
.../data/repositories/EpgRepositoryImpl.kt | 21 +-
.../data/repositories/IptvRepositoryImpl.kt | 13 +-
.../top/yogiczy/mytv/data/utils/Constants.kt | 20 +
.../ui/screens/home/HomeScreenViewModel.kt | 58 ++-
.../settings/components/SettingsList.kt | 74 ++--
.../top/yogiczy/mytv/ui/utils/HttpServer.kt | 145 ++++++-
.../main/java/top/yogiczy/mytv/ui/utils/SP.kt | 102 ++++-
app/src/main/res/raw/index.html | 393 ++++++++++++++----
12 files changed, 649 insertions(+), 238 deletions(-)
diff --git a/.idea/appInsightsSettings.xml b/.idea/appInsightsSettings.xml
index 23b2e1fe..cc5f8295 100644
--- a/.idea/appInsightsSettings.xml
+++ b/.idea/appInsightsSettings.xml
@@ -2,5 +2,18 @@
+
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
index 47e5b0bc..8cd3eb25 100644
--- a/.idea/deploymentTargetDropDown.xml
+++ b/.idea/deploymentTargetDropDown.xml
@@ -12,20 +12,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/README.md b/README.md
index f8ba0c77..711fe800 100644
--- a/README.md
+++ b/README.md
@@ -9,11 +9,10 @@
使用Android原生开发的电视直播软件
-
-
-
+
+
## 使用
@@ -26,13 +25,11 @@
- 频道选择:OK键;单击屏幕;
- 设置页面:菜单、帮助键、长按OK键;双击屏幕;
-### 自定义直播源
-
-1. 进入设置页面
-2. 请求网址:`http://<设备IP>:10481`
-3. 按界面提示操作
+### 自定义设置
-不支持多源,只会选择频道的第一个源,其他忽略
+- 访问以下网址:`http://<设备IP>:10481`
+- 支持自定义直播源、节目单等
+- 直播源不支持多源,只会选择频道的第一个源,其他忽略
## 下载
@@ -42,7 +39,7 @@
- 主要解决 [my_tv](https://github.com/yaoxieyoulei/my_tv)(flutter)在低端设备上播放(4k)视频卡顿掉帧
- 仅支持Android5及以上
-- 网络环境必须支持IPV6
+- 网络环境必须支持IPV6(默认直播源)
- 只在自家电视上测过,其他电视稳定性未知
## 功能
@@ -51,8 +48,9 @@
- [x] 数字选台
- [x] 节目单
- [x] 开机自启
-- [ ] 自动更新
-- [ ] 自定义直播源
+- [x] 自动更新
+- [x] 自定义直播源
+- [x] 自定义节目单
- [ ] 性能优化
## 更新日志
diff --git a/app/src/main/java/top/yogiczy/mytv/MainActivity.kt b/app/src/main/java/top/yogiczy/mytv/MainActivity.kt
index 2d2d42a1..3819ee14 100644
--- a/app/src/main/java/top/yogiczy/mytv/MainActivity.kt
+++ b/app/src/main/java/top/yogiczy/mytv/MainActivity.kt
@@ -21,6 +21,7 @@ import top.yogiczy.mytv.ui.App
import top.yogiczy.mytv.ui.theme.MyTVTheme
import top.yogiczy.mytv.ui.utils.HttpServer
import top.yogiczy.mytv.ui.utils.SP
+import kotlin.system.exitProcess
@AndroidEntryPoint
@@ -56,16 +57,14 @@ class MainActivity : ComponentActivity() {
LocalContentColor provides MaterialTheme.colorScheme.onSurface
) {
App(
- onBackPressed = { finishAffinity() }
+ onBackPressed = {
+ finish()
+ exitProcess(0)
+ }
)
}
}
}
}
}
-
- override fun onDestroy() {
- HttpServer.stop()
- super.onDestroy()
- }
}
diff --git a/app/src/main/java/top/yogiczy/mytv/data/repositories/EpgRepositoryImpl.kt b/app/src/main/java/top/yogiczy/mytv/data/repositories/EpgRepositoryImpl.kt
index fe1e1177..57e6bc09 100644
--- a/app/src/main/java/top/yogiczy/mytv/data/repositories/EpgRepositoryImpl.kt
+++ b/app/src/main/java/top/yogiczy/mytv/data/repositories/EpgRepositoryImpl.kt
@@ -14,7 +14,6 @@ import top.yogiczy.mytv.data.entities.Epg
import top.yogiczy.mytv.data.entities.EpgList
import top.yogiczy.mytv.data.entities.EpgProgramme
import top.yogiczy.mytv.data.entities.EpgProgrammeList
-import top.yogiczy.mytv.data.utils.Constants
import top.yogiczy.mytv.ui.utils.SP
import java.io.File
import java.io.StringReader
@@ -33,7 +32,7 @@ class EpgRepositoryImpl(private val context: Context) : EpgRepository {
val xml = getXml()
val hashCode = filteredChannels.hashCode()
- if (SP.epgCacheHash == hashCode) {
+ if (SP.epgCachedHash == hashCode) {
val cache = getCache()
if (cache != null) {
Log.d(TAG, "使用缓存epg")
@@ -43,16 +42,16 @@ class EpgRepositoryImpl(private val context: Context) : EpgRepository {
val epgList = parseFromXml(xml, filteredChannels)
setCache(epgList)
- SP.epgCacheHash = hashCode
+ SP.epgCachedHash = hashCode
return epgList
}
private suspend fun fetchXml(): String = withContext(Dispatchers.IO) {
- Log.d(TAG, "获取远程xml: ${Constants.EPG_XML_URL}")
+ Log.d(TAG, "获取远程xml: ${SP.epgXmlUrl}")
val client = OkHttpClient()
- val request = Request.Builder().url(Constants.EPG_XML_URL).build()
+ val request = Request.Builder().url(SP.epgXmlUrl).build()
try {
return@withContext with(client.newCall(request).execute()) {
@@ -84,23 +83,25 @@ class EpgRepositoryImpl(private val context: Context) : EpgRepository {
private suspend fun getXml(): String {
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
- if (dateFormat.format(System.currentTimeMillis()) == dateFormat.format(SP.epgXmlCacheTime)) {
+ if (dateFormat.format(System.currentTimeMillis()) == dateFormat.format(SP.epgXmlCachedAt)) {
val cache = getCacheXml()
if (cache.isNotBlank()) {
Log.d(TAG, "使用缓存xml")
return cache
}
} else {
- if (Calendar.getInstance().get(Calendar.HOUR_OF_DAY) < 1) {
- Log.d(TAG, "未到1点,不刷新epg")
+ if (Calendar.getInstance()
+ .get(Calendar.HOUR_OF_DAY) < SP.epgRefreshTimeThreshold
+ ) {
+ Log.d(TAG, "未到时间点,不刷新epg")
return ""
}
}
val xml = fetchXml()
setCacheXml(xml)
- SP.epgXmlCacheTime = System.currentTimeMillis()
- SP.epgCacheHash = 0
+ SP.epgXmlCachedAt = System.currentTimeMillis()
+ SP.epgCachedHash = 0
return xml
}
diff --git a/app/src/main/java/top/yogiczy/mytv/data/repositories/IptvRepositoryImpl.kt b/app/src/main/java/top/yogiczy/mytv/data/repositories/IptvRepositoryImpl.kt
index a983c60c..79e76c9a 100644
--- a/app/src/main/java/top/yogiczy/mytv/data/repositories/IptvRepositoryImpl.kt
+++ b/app/src/main/java/top/yogiczy/mytv/data/repositories/IptvRepositoryImpl.kt
@@ -11,7 +11,6 @@ import top.yogiczy.mytv.data.entities.IptvGroup
import top.yogiczy.mytv.data.entities.IptvGroupList
import top.yogiczy.mytv.data.entities.IptvList
import top.yogiczy.mytv.data.models.IptvResponseItem
-import top.yogiczy.mytv.data.utils.Constants
import top.yogiczy.mytv.ui.utils.SP
import java.io.File
import javax.inject.Singleton
@@ -21,7 +20,7 @@ class IptvRepositoryImpl(private val context: Context) : IptvRepository {
override suspend fun getIptvGroups(): IptvGroupList {
val now = System.currentTimeMillis()
- if (now - SP.iptvSourceCacheTime < 24 * 60 * 60 * 1000) {
+ if (now - SP.iptvSourceCachedAt < SP.iptvSourceCacheTime) {
val cache = getCache()
if (cache.isNotBlank()) {
Log.d(TAG, "使用缓存直播源")
@@ -31,22 +30,20 @@ class IptvRepositoryImpl(private val context: Context) : IptvRepository {
val data = fetchSource()
setCache(data)
- SP.iptvSourceCacheTime = now
+ SP.iptvSourceCachedAt = now
return parseSource(data)
}
- private fun getSource() = SP.iptvCustomSource.ifBlank { Constants.IPTV_SOURCE_URL }
-
private fun getSourceType(): SourceType {
- return if (getSource().endsWith(".m3u")) SourceType.M3U else SourceType.TVBOX
+ return if (SP.iptvSourceUrl.endsWith(".m3u")) SourceType.M3U else SourceType.TVBOX
}
private suspend fun fetchSource(): String = withContext(Dispatchers.IO) {
- Log.d(TAG, "获取远程直播源: ${getSource()}")
+ Log.d(TAG, "获取远程直播源: ${SP.iptvSourceUrl}")
val client = OkHttpClient()
- val request = Request.Builder().url(getSource()).build()
+ val request = Request.Builder().url(SP.iptvSourceUrl).build()
try {
return@withContext with(client.newCall(request).execute()) {
diff --git a/app/src/main/java/top/yogiczy/mytv/data/utils/Constants.kt b/app/src/main/java/top/yogiczy/mytv/data/utils/Constants.kt
index 53d94276..97bc8e99 100644
--- a/app/src/main/java/top/yogiczy/mytv/data/utils/Constants.kt
+++ b/app/src/main/java/top/yogiczy/mytv/data/utils/Constants.kt
@@ -10,11 +10,21 @@ object Constants {
const val IPTV_SOURCE_URL =
"https://mirror.ghproxy.com/https://raw.githubusercontent.com/zhumeng11/IPTV/main/IPTV.m3u"
+ /**
+ * IPTV源缓存时间(毫秒)
+ */
+ const val IPTV_SOURCE_CACHE_TIME = 1000 * 60 * 60 * 24L // 24小时
+
/**
* 节目单XML地址
*/
const val EPG_XML_URL = "https://live.fanmingming.com/e.xml"
+ /**
+ * 节目单刷新时间阈值(小时)
+ */
+ const val EPG_REFRESH_TIME_THRESHOLD = 6 // 不到6点不刷新
+
/**
* GitHub最新版本信息
*/
@@ -25,4 +35,14 @@ object Constants {
* GitHub加速代理地址
*/
const val GITHUB_PROXY = "https://mirror.ghproxy.com/"
+
+ /**
+ * HTTP请求重试次数
+ */
+ const val HTTP_RETRY_COUNT = 10L
+
+ /**
+ * HTTP请求重试间隔时间(毫秒)
+ */
+ const val HTTP_RETRY_INTERVAL = 3000L
}
\ No newline at end of file
diff --git a/app/src/main/java/top/yogiczy/mytv/ui/screens/home/HomeScreenViewModel.kt b/app/src/main/java/top/yogiczy/mytv/ui/screens/home/HomeScreenViewModel.kt
index 17f949ab..a8838da0 100644
--- a/app/src/main/java/top/yogiczy/mytv/ui/screens/home/HomeScreenViewModel.kt
+++ b/app/src/main/java/top/yogiczy/mytv/ui/screens/home/HomeScreenViewModel.kt
@@ -19,6 +19,7 @@ import top.yogiczy.mytv.data.entities.EpgProgrammeList
import top.yogiczy.mytv.data.entities.IptvGroupList
import top.yogiczy.mytv.data.repositories.EpgRepository
import top.yogiczy.mytv.data.repositories.IptvRepository
+import top.yogiczy.mytv.ui.utils.SP
import javax.inject.Inject
@HiltViewModel
@@ -31,47 +32,38 @@ class HomeScreeViewModel @Inject constructor(
init {
viewModelScope.launch {
- flow { emit(iptvRepository.getIptvGroups()) }
- .retryWhen { _, attempt ->
- if (attempt >= 10) return@retryWhen false
+ flow { emit(iptvRepository.getIptvGroups()) }.retryWhen { _, attempt ->
+ if (attempt >= SP.httpRetryCount) return@retryWhen false
- uiState.value =
- HomeScreenUiState.Loading("获取远程直播源(${attempt + 1}/10)...")
- delay(3000)
- true
- }
- .catch { uiState.value = HomeScreenUiState.Error(it.message) }
- .map {
- uiState.value = HomeScreenUiState.Ready(iptvGroupList = it)
- it
- }
+ uiState.value =
+ HomeScreenUiState.Loading("获取远程直播源(${attempt + 1}/${SP.httpRetryCount})...")
+ delay(SP.httpRetryInterval)
+ true
+ }.catch { uiState.value = HomeScreenUiState.Error(it.message) }.map {
+ uiState.value = HomeScreenUiState.Ready(iptvGroupList = it)
+ it
+ }
// 开始获取epg
.flatMapLatest { iptvGroupList ->
val channels =
iptvGroupList.flatMap { it.iptvs }.map { iptv -> iptv.channelName }
flow { emit(epgRepository.getEpgs(channels)) }
- }
- .retry(10) { delay(3000); true }
- .catch { emit(EpgList()) }
- .map { epgList ->
+ }.retry(SP.httpRetryCount) { delay(SP.httpRetryInterval); true }
+ .catch { emit(EpgList()) }.map { epgList ->
// 移除过期节目
- epgList.copy(
- value = epgList.map { epg ->
- epg.copy(
- programmes = EpgProgrammeList(
- epg.programmes.filter { programme ->
- System.currentTimeMillis() < programme.endAt
- },
- )
+ epgList.copy(value = epgList.map { epg ->
+ epg.copy(
+ programmes = EpgProgrammeList(
+ epg.programmes.filter { programme ->
+ System.currentTimeMillis() < programme.endAt
+ },
)
- }
- )
- }
- .map { epgList ->
- uiState.value = (uiState.value as HomeScreenUiState.Ready)
- .copy(epgList = epgList)
- }
- .collect()
+ )
+ })
+ }.map { epgList ->
+ uiState.value =
+ (uiState.value as HomeScreenUiState.Ready).copy(epgList = epgList)
+ }.collect()
}
}
}
diff --git a/app/src/main/java/top/yogiczy/mytv/ui/screens/settings/components/SettingsList.kt b/app/src/main/java/top/yogiczy/mytv/ui/screens/settings/components/SettingsList.kt
index 16f89cbc..c660f963 100644
--- a/app/src/main/java/top/yogiczy/mytv/ui/screens/settings/components/SettingsList.kt
+++ b/app/src/main/java/top/yogiczy/mytv/ui/screens/settings/components/SettingsList.kt
@@ -53,26 +53,23 @@ fun SettingsList(
val childPadding = rememberChildPadding()
var appBootLaunch by remember { mutableStateOf(SP.appBootLaunch) }
- var debugShowFps by remember { mutableStateOf(SP.debugShowFps) }
var iptvChannelChangeFlip by remember { mutableStateOf(SP.iptvChannelChangeFlip) }
var iptvSourceSimplify by remember { mutableStateOf(SP.iptvSourceSimplify) }
- val iptvCustomSource by remember { mutableStateOf(SP.iptvCustomSource) }
- var iptvSourceCacheTime by remember { mutableLongStateOf(SP.iptvSourceCacheTime) }
+ var iptvSourceCachedAt by remember { mutableLongStateOf(SP.iptvSourceCachedAt) }
+ val iptvSourceCacheTime by remember { mutableLongStateOf(SP.iptvSourceCacheTime) }
var epgEnable by remember { mutableStateOf(SP.epgEnable) }
- var epgXmlCacheTime by remember { mutableLongStateOf(SP.epgXmlCacheTime) }
- var epgCacheHash by remember { mutableIntStateOf(SP.epgCacheHash) }
+ var epgXmlCachedAt by remember { mutableLongStateOf(SP.epgXmlCachedAt) }
+ var epgCachedHash by remember { mutableIntStateOf(SP.epgCachedHash) }
DisposableEffect(Unit) {
onDispose {
SP.appBootLaunch = appBootLaunch
- SP.debugShowFps = debugShowFps
SP.iptvChannelChangeFlip = iptvChannelChangeFlip
SP.iptvSourceSimplify = iptvSourceSimplify
- SP.iptvCustomSource = iptvCustomSource
- SP.iptvSourceCacheTime = iptvSourceCacheTime
+ SP.iptvSourceCachedAt = iptvSourceCachedAt
SP.epgEnable = epgEnable
- SP.epgXmlCacheTime = epgXmlCacheTime
- SP.epgCacheHash = epgCacheHash
+ SP.epgXmlCachedAt = epgXmlCachedAt
+ SP.epgCachedHash = epgCachedHash
}
}
TvLazyRow(
@@ -141,32 +138,19 @@ fun SettingsList(
}
item {
- val serverUrl = "http://${HttpServer.getLocalIpAddress()}:${HttpServer.SERVER_PORT}"
- var showQrcode by remember { mutableStateOf(false) }
-
- SettingsItem(
- title = "自定义直播源",
- value = if (iptvCustomSource.isNotBlank()) "已启用" else "未启用",
- description = "访问以下网址进行配置:$serverUrl",
- onClick = {
- showQrcode = true
- },
- )
-
- if (showQrcode) {
- SettingsQrcodeDialog(
- onDismissRequest = { showQrcode = false },
- data = serverUrl,
- )
+ fun formatDuration(ms: Long): String {
+ return when (ms) {
+ in 0..<60_000 -> "${ms / 1000}秒"
+ in 60_000..<3_600_000 -> "${ms / 60_000}分钟"
+ else -> "${ms / 3_600_000}小时"
+ }
}
- }
- item {
SettingsItem(
title = "直播源缓存",
- value = "24小时",
- description = if (iptvSourceCacheTime > 0) "已缓存(点击清除缓存)" else "未缓存",
- onClick = { iptvSourceCacheTime = 0 },
+ value = formatDuration(iptvSourceCacheTime),
+ description = if (iptvSourceCachedAt > 0) "已缓存(点击清除缓存)" else "未缓存",
+ onClick = { iptvSourceCachedAt = 0 },
)
}
@@ -183,21 +167,33 @@ fun SettingsList(
SettingsItem(
title = "节目单缓存",
value = "当天",
- description = if (epgXmlCacheTime > 0) "已缓存(点击清除缓存)" else "未缓存",
+ description = if (epgXmlCachedAt > 0) "已缓存(点击清除缓存)" else "未缓存",
onClick = {
- epgXmlCacheTime = 0
- epgCacheHash = 0
+ epgXmlCachedAt = 0
+ epgCachedHash = 0
},
)
}
item {
+ val serverUrl = "http://${HttpServer.getLocalIpAddress()}:${HttpServer.SERVER_PORT}"
+ var showQrcode by remember { mutableStateOf(false) }
+
SettingsItem(
- title = "显示FPS",
- value = if (debugShowFps) "启用" else "禁用",
- description = "显示当前帧率",
- onClick = { debugShowFps = !debugShowFps },
+ title = "更多设置",
+ value = "",
+ description = "访问以下网址进行配置:$serverUrl",
+ onClick = {
+ showQrcode = true
+ },
)
+
+ if (showQrcode) {
+ SettingsQrcodeDialog(
+ onDismissRequest = { showQrcode = false },
+ data = serverUrl,
+ )
+ }
}
// item {
diff --git a/app/src/main/java/top/yogiczy/mytv/ui/utils/HttpServer.kt b/app/src/main/java/top/yogiczy/mytv/ui/utils/HttpServer.kt
index b059bbd7..f22444b5 100644
--- a/app/src/main/java/top/yogiczy/mytv/ui/utils/HttpServer.kt
+++ b/app/src/main/java/top/yogiczy/mytv/ui/utils/HttpServer.kt
@@ -7,8 +7,10 @@ import fi.iki.elonen.NanoHTTPD
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
import top.yogiczy.mytv.R
-import top.yogiczy.mytv.ui.screens.toast.ToastState
import java.net.Inet4Address
import java.net.NetworkInterface
import java.net.SocketException
@@ -35,10 +37,6 @@ object HttpServer {
}
}
- fun stop() {
- server.stop()
- }
-
fun getLocalIpAddress(): String {
val defaultIp = "0.0.0.0"
@@ -67,27 +65,130 @@ object HttpServer {
start(SOCKET_READ_TIMEOUT, false)
}
+ private fun isPreflightRequest(session: IHTTPSession): Boolean {
+ val headers = session.headers
+ return Method.OPTIONS == session.method && headers.contains("origin") && headers.containsKey(
+ "access-control-request-method"
+ ) && headers.containsKey("access-control-request-headers")
+ }
+
+ private fun responseCORS(session: IHTTPSession): Response {
+ val resp = wrapResponse(session, newFixedLengthResponse(""))
+ val headers = session.headers
+ resp.addHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS")
+ val requestHeaders = headers["access-control-request-headers"]
+ if (requestHeaders != null) {
+ resp.addHeader("Access-Control-Allow-Headers", requestHeaders)
+ }
+ resp.addHeader("Access-Control-Max-Age", "0")
+ return resp
+ }
+
+ private fun wrapResponse(session: IHTTPSession, resp: Response): Response {
+ val headers = session.headers
+ resp.addHeader("Access-Control-Allow-Credentials", "true")
+ resp.addHeader("Access-Control-Allow-Origin", headers.getOrElse("origin") { "*" })
+ val requestHeaders = headers["access-control-request-headers"]
+ if (requestHeaders != null) {
+ resp.addHeader("Access-Control-Allow-Headers", requestHeaders)
+ }
+ return resp
+ }
+
+ private inline fun responseJson(data: T): Response {
+ return newFixedLengthResponse(
+ Response.Status.OK, "application/json", Json.encodeToString(data)
+ )
+ }
+
+ private inline fun parseBody(session: IHTTPSession): T {
+ val files = mutableMapOf()
+ session.parseBody(files)
+ return Json.decodeFromString(files["postData"]!!)
+ }
+
override fun serve(session: IHTTPSession): Response {
- if (session.uri == "/") {
- return newFixedLengthResponse(
- context.resources.openRawResource(R.raw.index).readBytes().decodeToString(),
- )
- } else if (session.uri.startsWith("/api/settings/iptvCustomSource")) {
- val source = session.parameters["source"]?.firstOrNull() ?: ""
- Log.d(TAG, "设置直播源: $source")
-
- SP.iptvCustomSource = source
-
- if (source.isNotBlank()) {
- ToastState.I.showToast("直播源设置成功")
- } else {
- ToastState.I.showToast("直播源已恢复默认")
- }
+ if (isPreflightRequest(session)) {
+ return responseCORS(session)
+ }
- return newFixedLengthResponse("success")
+ if (session.uri.startsWith("/api/settings")) {
+ if (session.method == Method.GET) {
+ return wrapResponse(
+ session,
+ responseJson(
+ AllSettings(
+ appBootLaunch = SP.appBootLaunch,
+
+ iptvChannelChangeFlip = SP.iptvChannelChangeFlip,
+ iptvSourceSimplify = SP.iptvSourceSimplify,
+ iptvSourceCachedAt = SP.iptvSourceCachedAt,
+ iptvSourceUrl = SP.iptvSourceUrl,
+ iptvSourceCacheTime = SP.iptvSourceCacheTime,
+
+ epgEnable = SP.epgEnable,
+ epgXmlCachedAt = SP.epgXmlCachedAt,
+ epgCachedHash = SP.epgCachedHash,
+ epgXmlUrl = SP.epgXmlUrl,
+ epgRefreshTimeThreshold = SP.epgRefreshTimeThreshold,
+
+ httpRetryCount = SP.httpRetryCount,
+ httpRetryInterval = SP.httpRetryInterval,
+
+ debugShowFps = SP.debugShowFps,
+ )
+ ),
+ )
+ } else if (session.method == Method.POST) {
+ val data = parseBody(session)
+ SP.appBootLaunch = data.appBootLaunch
+
+ SP.iptvChannelChangeFlip = data.iptvChannelChangeFlip
+ SP.iptvSourceSimplify = data.iptvSourceSimplify
+ SP.iptvSourceCachedAt = data.iptvSourceCachedAt
+ SP.iptvSourceUrl = data.iptvSourceUrl
+ SP.iptvSourceCacheTime = data.iptvSourceCacheTime
+
+ SP.epgEnable = data.epgEnable
+ SP.epgXmlCachedAt = data.epgXmlCachedAt
+ SP.epgCachedHash = data.epgCachedHash
+ SP.epgXmlUrl = data.epgXmlUrl
+ SP.epgRefreshTimeThreshold = data.epgRefreshTimeThreshold
+
+ SP.httpRetryCount = data.httpRetryCount
+ SP.httpRetryInterval = data.httpRetryInterval
+
+ SP.debugShowFps = data.debugShowFps
+
+ return wrapResponse(session, newFixedLengthResponse("success"))
+ }
}
- return newFixedLengthResponse("not found")
+ return newFixedLengthResponse(
+ context.resources.openRawResource(R.raw.index).readBytes().decodeToString(),
+ )
}
}
}
+
+@Serializable
+private data class AllSettings(
+ val appBootLaunch: Boolean,
+
+ val iptvChannelChangeFlip: Boolean,
+ val iptvSourceSimplify: Boolean,
+ val iptvSourceCachedAt: Long,
+ val iptvSourceUrl: String,
+ val iptvSourceCacheTime: Long,
+
+ val epgEnable: Boolean,
+ val epgXmlCachedAt: Long,
+ val epgCachedHash: Int,
+ val epgXmlUrl: String,
+ val epgRefreshTimeThreshold: Int,
+
+ val httpRetryCount: Long,
+ val httpRetryInterval: Long,
+
+ val debugShowFps: Boolean,
+)
\ No newline at end of file
diff --git a/app/src/main/java/top/yogiczy/mytv/ui/utils/SP.kt b/app/src/main/java/top/yogiczy/mytv/ui/utils/SP.kt
index 3402bd4f..796f9ff6 100644
--- a/app/src/main/java/top/yogiczy/mytv/ui/utils/SP.kt
+++ b/app/src/main/java/top/yogiczy/mytv/ui/utils/SP.kt
@@ -2,6 +2,7 @@ package top.yogiczy.mytv.ui.utils
import android.content.Context
import android.content.SharedPreferences
+import top.yogiczy.mytv.data.utils.Constants
/**
* 应用配置存储
@@ -15,12 +16,15 @@ object SP {
}
enum class KEY {
+ /** ==================== 应用 ==================== */
/** 开机自启 */
APP_BOOT_LAUNCH,
+ /** ==================== 调式 ==================== */
/** 显示fps */
DEBUG_SHOW_FPS,
+ /** ==================== 直播源 ==================== */
/** 上一次直播源序号 */
IPTV_LAST_IPTV_IDX,
@@ -30,59 +34,117 @@ object SP {
/** 直播源精简 */
IPTV_SOURCE_SIMPLIFY,
- /** 直播源缓存时间 */
- IPTV_SOURCE_CACHE_TIME,
+ /** 直播源最近缓存时间 */
+ IPTV_SOURCE_CACHED_AT,
+
+ /** 直播源url */
+ IPTV_SOURCE_URL,
- /** 自定义直播源 */
- IPTV_CUSTOM_SOURCE,
+ /** 直播源缓存时间(毫秒) */
+ IPTV_SOURCE_CACHE_TIME,
+ /** ==================== 节目单 ==================== */
/** 启用epg */
EPG_ENABLE,
- /** epg缓存时间 */
- EPG_XLM_CACHE_TIME,
+ /** epg最近缓存时间 */
+ EPG_XLM_CACHED_AT,
+
+ /** epg解析最近缓存hash */
+ EPG_CACHED_HASH,
+
+ /** epg xml url */
+ EPG_XML_URL,
+
+ /** epg刷新时间阈值(小时) */
+ EPG_REFRESH_TIME_THRESHOLD,
- /** epg解析缓存hash */
- EPG_CACHE_HASH,
+ /** ==================== 网络请求 ==================== */
+ /** HTTP请求重试次数 */
+ HTTP_RETRY_COUNT,
+
+ /** HTTP请求重试间隔时间(毫秒) */
+ HTTP_RETRY_INTERVAL,
}
+ /** ==================== 应用 ==================== */
+ /** 开机自启 */
var appBootLaunch: Boolean
get() = sp.getBoolean(KEY.APP_BOOT_LAUNCH.name, false)
set(value) = sp.edit().putBoolean(KEY.APP_BOOT_LAUNCH.name, value).apply()
+ /** ==================== 调式 ==================== */
+ /** 显示fps */
var debugShowFps: Boolean
get() = sp.getBoolean(KEY.DEBUG_SHOW_FPS.name, false)
set(value) = sp.edit().putBoolean(KEY.DEBUG_SHOW_FPS.name, value).apply()
+ /** ==================== 直播源 ==================== */
+ /** 上一次直播源序号 */
var iptvLastIptvIdx: Int
get() = sp.getInt(KEY.IPTV_LAST_IPTV_IDX.name, 0)
set(value) = sp.edit().putInt(KEY.IPTV_LAST_IPTV_IDX.name, value).apply()
+ /** 换台反转 */
var iptvChannelChangeFlip: Boolean
get() = sp.getBoolean(KEY.IPTV_CHANNEL_CHANGE_FLIP.name, false)
set(value) = sp.edit().putBoolean(KEY.IPTV_CHANNEL_CHANGE_FLIP.name, value).apply()
+ /** 直播源精简 */
var iptvSourceSimplify: Boolean
get() = sp.getBoolean(KEY.IPTV_SOURCE_SIMPLIFY.name, false)
set(value) = sp.edit().putBoolean(KEY.IPTV_SOURCE_SIMPLIFY.name, value).apply()
+ /** 直播源最近缓存时间 */
+ var iptvSourceCachedAt: Long
+ get() = sp.getLong(KEY.IPTV_SOURCE_CACHED_AT.name, 0)
+ set(value) = sp.edit().putLong(KEY.IPTV_SOURCE_CACHED_AT.name, value).apply()
+
+ /** 直播源 url */
+ var iptvSourceUrl: String
+ get() = (sp.getString(KEY.IPTV_SOURCE_URL.name, "")
+ ?: "").ifBlank { Constants.IPTV_SOURCE_URL }
+ set(value) = sp.edit().putString(KEY.IPTV_SOURCE_URL.name, value).apply()
+
+ /** 直播源缓存时间(毫秒) */
var iptvSourceCacheTime: Long
- get() = sp.getLong(KEY.IPTV_SOURCE_CACHE_TIME.name, 0)
+ get() = sp.getLong(KEY.IPTV_SOURCE_CACHE_TIME.name, Constants.IPTV_SOURCE_CACHE_TIME)
set(value) = sp.edit().putLong(KEY.IPTV_SOURCE_CACHE_TIME.name, value).apply()
- var iptvCustomSource: String
- get() = sp.getString(KEY.IPTV_CUSTOM_SOURCE.name, "") ?: ""
- set(value) = sp.edit().putString(KEY.IPTV_CUSTOM_SOURCE.name, value).apply()
-
+ /** ==================== 节目单 ==================== */
+ /** 启用epg */
var epgEnable: Boolean
get() = sp.getBoolean(KEY.EPG_ENABLE.name, true)
set(value) = sp.edit().putBoolean(KEY.EPG_ENABLE.name, value).apply()
- var epgXmlCacheTime: Long
- get() = sp.getLong(KEY.EPG_XLM_CACHE_TIME.name, 0)
- set(value) = sp.edit().putLong(KEY.EPG_XLM_CACHE_TIME.name, value).apply()
-
- var epgCacheHash: Int
- get() = sp.getInt(KEY.EPG_CACHE_HASH.name, 0)
- set(value) = sp.edit().putInt(KEY.EPG_CACHE_HASH.name, value).apply()
+ /** epg最近缓存时间 */
+ var epgXmlCachedAt: Long
+ get() = sp.getLong(KEY.EPG_XLM_CACHED_AT.name, 0)
+ set(value) = sp.edit().putLong(KEY.EPG_XLM_CACHED_AT.name, value).apply()
+
+ /** epg解析最近缓存hash */
+ var epgCachedHash: Int
+ get() = sp.getInt(KEY.EPG_CACHED_HASH.name, 0)
+ set(value) = sp.edit().putInt(KEY.EPG_CACHED_HASH.name, value).apply()
+
+ /** epg xml url */
+ var epgXmlUrl: String
+ get() = (sp.getString(KEY.EPG_XML_URL.name, "") ?: "").ifBlank { Constants.EPG_XML_URL }
+ set(value) = sp.edit().putString(KEY.EPG_XML_URL.name, value).apply()
+
+ /** epg刷新时间阈值(小时) */
+ var epgRefreshTimeThreshold: Int
+ get() = sp.getInt(KEY.EPG_REFRESH_TIME_THRESHOLD.name, Constants.EPG_REFRESH_TIME_THRESHOLD)
+ set(value) = sp.edit().putInt(KEY.EPG_REFRESH_TIME_THRESHOLD.name, value).apply()
+
+ /** ==================== 网络请求 ==================== */
+ /** HTTP请求重试次数 */
+ var httpRetryCount: Long
+ get() = sp.getLong(KEY.HTTP_RETRY_COUNT.name, Constants.HTTP_RETRY_COUNT)
+ set(value) = sp.edit().putLong(KEY.HTTP_RETRY_COUNT.name, value).apply()
+
+ /** HTTP请求重试间隔时间(毫秒) */
+ var httpRetryInterval: Long
+ get() = sp.getLong(KEY.HTTP_RETRY_INTERVAL.name, Constants.HTTP_RETRY_INTERVAL)
+ set(value) = sp.edit().putLong(KEY.HTTP_RETRY_INTERVAL.name, value).apply()
}
\ No newline at end of file
diff --git a/app/src/main/res/raw/index.html b/app/src/main/res/raw/index.html
index d9f0391b..3264d662 100644
--- a/app/src/main/res/raw/index.html
+++ b/app/src/main/res/raw/index.html
@@ -7,90 +7,335 @@
content="width=device-width, initial-scale=1"
name="viewport">
我的电视
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
我的电视
+
+ https://github.com/yaoxieyoulei/mytv-android
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 支持m3u链接与tvbox链接
+ 不支持多源,若存在多源只会选择第一个播放地址,其他忽略
+
+
+
+
+
+ 推送链接
+
+
+
+
+
+
+
+ {{ settings.iptvSourceSimplify ? '显示精简直播源(仅央视、地方卫视)':
+ '显示完整直播源' }}
+
+ {{ settings.iptvSourceSimplify ? '精简规则:频道名称以·CCTV·开头或以·卫视·结尾'
+ : '' }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ settings.iptvSourceCachedAt > 0 ? `缓存于
+ ${dayjs(settings.iptvSourceCachedAt).fromNow()}` : '未缓存' }}
+
+
+
+ 清除缓存
+
+
+
+
+
+
+ {{ foramtDuration(settings.iptvSourceCacheTime) }}
+
+
+
+
+ 小时
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 推送链接
+
+
+
+
+
+
+ {{ settings.epgXmlCachedAt > 0 ? `缓存于
+ ${dayjs(settings.epgXmlCachedAt).fromNow()}` : '未缓存' }}
+
+
+
+
+ 清除缓存
+
+
+
+
+
+
+ 在{{ settings.epgRefreshTimeThreshold }}点之前,不会获取节目单
+
+
+
+
+ 小时
+
+
+
+
+
+
+
+
+
+ 当前重试支持:直播源获取、节目单获取
+
+
+
+
+
+
+
+
+
+ {{ foramtDuration(settings.httpRetryInterval) }}
+
+
+
+
+ 秒
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ try {
+ settings.value = await (await fetch(`${baseUrl}/api/settings`)).json()
+ loading.clear()
+ } catch (e) {
+ vant.Toast.fail('无法获取设置')
+ console.error(e)
+ }
+ }
-
-
-
-
-
-
-
支持m3u订阅链接与TVBOX链接
-
推送:将自定义直播源推送到设备
-
删除:删除设备自定义直播源
-
-
-
-
-
-
-
-
-
-
-
-
+ onMounted(async () => {
+ await refreshSettings()
+ })
+
+ function foramtDuration(ms) {
+ return humanizeDuration(ms, { language: 'zh_CN', round: true, largest: 2 })
+ }
+
+ return {
+ dayjs,
+ foramtDuration,
+ tabActive,
+ settings,
+ confirmSettings,
+ }
+ }
+ })
+ .use(vant)
+ .mount('#app')
+