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 +
+
+ + + + + +
+ + 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') +