Compare commits

...
87 Commits
Author SHA1 Message Date
Li ZongYing 06b8df5f82 request optimize 2024-03-15 21:51:27 +08:00
Li ZongYing 7bed18ee6d phoenix EPG 2024-03-15 17:16:12 +08:00
Li ZongYing 0c2beb936a phoenix retry 2024-03-15 17:16:12 +08:00
WoooKong 51ab79b5ca Optimize SettingFragment (#536) 2024-03-15 17:04:50 +08:00
Li ZongYing c8de039d05 logo 2024-03-11 10:26:19 +08:00
Li ZongYing c9494f7bf3 cetv1 logo 2024-03-09 21:12:56 +08:00
Sky 79b6cfcd11 fix typo (#541) 2024-03-09 18:59:04 +08:00
Li ZongYing 31988d0d86 fh fhd 2024-03-09 18:08:53 +08:00
Li ZongYing e55e8ceb18 delayHideMain 10s 2024-03-09 08:50:27 +08:00
Li ZongYing 0b62742350 token-v2 2024-03-08 22:50:47 +08:00
Li ZongYing 558fe7e4e7 cetv1 2024-03-08 21:17:13 +08:00
Li ZongYing e2145f8d0d bootStartup->false 2024-03-06 01:27:13 +08:00
Li ZongYing 46b4df1306 readme 2024-03-06 01:25:19 +08:00
李宗英 358539f9c3 Merge pull request #455 from WoooKong/main
创建 SP 工具类,管理所有(除 ChannelUtils 以外)用户偏好设置
2024-03-06 01:13:10 +08:00
WoooKong 17d8d4cb46 还原 VersionCode 2024-03-04 18:07:15 +08:00
WoooKong dc81112a3d Merge branch 'main' into main 2024-03-04 17:52:02 +08:00
Li ZongYing 95ea83e45c add phoenix tv 2024-02-25 16:08:49 +08:00
Li ZongYing a6078e75e1 got 2024-02-25 11:16:09 +08:00
Li ZongYing 8a3720049f update mannager 2024-02-25 10:19:35 +08:00
WoooKong ede420a190 调整注释 2024-02-19 16:55:34 +08:00
WoooKong f94d136a03 创建 SP 工具类,管理所有(除 ChannelUtils 以外)用户偏好设置 2024-02-19 12:23:05 +08:00
李宗英 e7780434d7 Merge pull request #442 from JohnsonRan/main
fix: LibChecker -> my-tv
2024-02-18 18:30:28 +08:00
旋律旋律你在干什么 67edb2830c fix: LibChecker -> my-tv 2024-02-18 13:06:12 +08:00
Li ZongYing 2869164368 readme 2024-02-17 09:15:16 +08:00
Li ZongYing 83a70291d4 fix tianmao-mohe 2024-02-16 22:14:11 +08:00
李宗英 872f668c59 Merge pull request #427 from DavidDengHui/patch-3
修复远程服务地址
2024-02-16 22:02:37 +08:00
李宗英 426839c747 Merge pull request #425 from DavidDengHui/patch-1
Fix MainActivity.kt
2024-02-16 21:59:33 +08:00
邓晖 3e513d5adf Fix server.xml
原远程服务CDN地址`https://cdn.jsdelivr.net/gh/LeGend-wLw/my-tv-json-utils@raw/main/channels.json`有误已修复。
另外GitHub镜像服务`fastly.jsdelivr.net`要比`cdn.jsdelivr.net`快一些已修改。
2024-02-14 16:06:07 +08:00
邓晖 86d4081d03 Fix MainActivity.kt
不知道理解的对不对,第244行好像是打错了?
2024-02-14 14:37:07 +08:00
李宗英 f76aa37169 Merge pull request #402 from fanmingming/patch-1
Resolve the issue of inaccessible resources in mainland China.
2024-02-11 11:00:15 +08:00
李宗英 d944777fc6 Merge pull request #400 from tuuzed/main
feat: 新增开机自启配置项,默认启用
2024-02-11 10:55:22 +08:00
范明明 a45244c552 Resolve the issue of inaccessible resources in mainland China. 2024-02-11 10:52:54 +08:00
Li ZongYing 809ec920a5 get time online 2024-02-11 10:42:00 +08:00
Li ZongYing 8e4f7df8d4 Merge remote-tracking branch 'refs/remotes/origin/main' 2024-02-11 10:30:30 +08:00
Li ZongYing fe209183ea fix token bug 2024-02-11 10:26:59 +08:00
Li ZongYing b7ec58ac5b readme 2024-02-11 10:20:58 +08:00
Li ZongYing 096acdf55e readme 2024-02-11 10:17:15 +08:00
liuyonghui c8d59d5aab feat: 新增开机自启配置项,默认启用 2024-02-11 09:58:50 +08:00
李宗英 2b4b823946 Merge pull request #304 from 2926295173/patch-1
[GitHub] 创建 issue 模板,分类 issue
2024-02-08 19:01:16 +08:00
AOIAN 127ceeb0b0 Create fr.yml 2024-02-07 13:01:09 +08:00
AOIAN 9dd6a2155f Create bug.yml 2024-02-07 12:58:05 +08:00
Li ZongYing 526a7c5c40 fix ysp 2024-02-07 02:57:36 +08:00
Li ZongYing 960f890ddd readme 2024-02-06 23:03:36 +08:00
李宗英 0a7837c577 Merge pull request #223 from LeGend-wLw/main
完成服务器有关所有功能,并优化相关代码
2024-02-06 15:59:12 +08:00
LeGend-wLw d8342e8c6a Add:从淘宝服务器获取当前时间 2024-02-05 14:25:18 +08:00
LeGend-wLw 6387f48409 Add:淘宝服务器返回时间戳格式 2024-02-05 14:24:46 +08:00
LeGend-wLw 807300918c Fix:网络请求超时时间修改为500ms 2024-02-05 14:15:52 +08:00
LeGend-wLw 36eacb7a57 Fix:添加1s超时 2024-02-05 14:14:37 +08:00
LeGend-wLw b4abe0ccc6 Merge remote-tracking branch 'origin/main' 2024-02-05 13:24:25 +08:00
LeGend-wLw fd1dc8d3c7 Fix:更改服务器地址为Github地址 2024-02-05 13:21:44 +08:00
LeGend-wLw ea56bf1b80 Merge branch 'main' into main 2024-02-05 13:10:07 +08:00
Li ZongYing 4fb27ba502 fix channel number did not disappear 2024-02-05 06:12:01 +08:00
LeGend-wLw a746fe6c65 Add:添加log信息 2024-02-04 23:58:11 +08:00
LeGend-wLw 594d5e3499 Merge branch 'main' of https://github.com/LeGend-wLw/my-tv 2024-02-04 23:52:44 +08:00
LeGend-wLw 6061187bc8 Merge branch 'lizongying:main' into main 2024-02-04 23:52:04 +08:00
LeGend-wLw cd8b37e63c Fix:优化从服务器获取逻辑 2024-02-04 23:49:19 +08:00
LeGend-wLw aa3ba1d41f Fix:移除ChannelUtils有关代码 2024-02-04 23:48:58 +08:00
LeGend-wLw c778ac07ba Add:添加本地文件版本,服务器版本地址 2024-02-04 23:48:39 +08:00
LeGend-wLw 700c8b4c04 Fix:移除TVList的初始化 2024-02-04 23:48:03 +08:00
LeGend-wLw 825a872a09 Fix:移除TVList的初始化 2024-02-04 23:47:54 +08:00
LeGend-wLw 54b43664ec add:注释 2024-02-04 23:47:10 +08:00
LeGend-wLw a93cfb455c 在应用初始化时初始化TVlist 2024-02-04 23:46:57 +08:00
LeGend-wLw 8535689e95 完成专门的ChannelUtils 2024-02-04 23:41:51 +08:00
李宗英 1714633cb7 Merge pull request #205 from LeGend-wLw/main
移除硬编码,更改为json格式,并支持从服务器上动态更新channels。
2024-02-04 22:46:08 +08:00
LeGend-wLw f7b024f6da 防止多次初始化 2024-02-04 16:42:56 +08:00
LeGend-wLw dd8df63b01 添加初始化TVList行为 2024-02-04 16:42:38 +08:00
LeGend-wLw a4c95efd71 添加从服务器上获取channel.json逻辑,但未启用 2024-02-04 16:37:59 +08:00
LeGend-wLw 9f32fa3673 移除MustToken和needToken,移动到TV类中 2024-02-04 16:37:20 +08:00
LeGend-wLw b3d0093ab0 修改为从json文件中读取channel信息 2024-02-04 16:36:34 +08:00
LeGend-wLw 5798d3a5fe 更改TV类中成员,使能和json对应 2024-02-04 16:36:05 +08:00
LeGend-wLw 4eb90de79d 当前转换成json的channel信息 2024-02-04 16:35:38 +08:00
LeGend-wLw b8232971e2 服务器地址,未更新 2024-02-04 16:35:20 +08:00
LeGend-wLw bc3681b0a6 将从tvModel中获取needToken和MustToken改成从TV类中获取needToken和MustToken 2024-02-04 16:34:59 +08:00
Li ZongYing 088808c3fd fix retry bug 2024-02-03 20:07:08 +08:00
Li ZongYing 73239a6677 replay 2024-02-01 17:04:02 +08:00
Li ZongYing cc2514a81a performance optimization 2024-01-28 16:00:33 +08:00
Li ZongYing bcf9bab25f add channel 2024-01-27 19:52:36 +08:00
Li ZongYing f7eebb3f7a channel allow one num 2024-01-25 12:21:16 +08:00
Li ZongYing 0f4a33a3fc change channel order 2024-01-25 11:37:06 +08:00
Li ZongYing c17d62c698 rename to setting 2024-01-19 13:49:43 +08:00
Li ZongYing 33d3da97e5 fix 13 2024-01-18 22:19:04 +08:00
Li ZongYing ba95ce5374 number selection 2024-01-18 21:42:06 +08:00
Li ZongYing e1ee909071 optimize memu hiding 2024-01-17 10:19:50 +08:00
Li ZongYing 1fd0951f92 channel reversal 2024-01-16 19:04:43 +08:00
Li ZongYing 42b072f5ec support KEYCODE_CHANNEL_UP & KEYCODE_CHANNEL_DOWN 2024-01-14 12:09:12 +08:00
Li ZongYing a23b1e11ce fix the menu did not appear 2024-01-14 10:15:45 +08:00
Li ZongYing 1ac6d87c3c clean log 2024-01-12 10:13:29 +08:00
57 changed files with 4559 additions and 873 deletions
+83
View File
@@ -0,0 +1,83 @@
name: Bug 反馈
description: 反馈一个 Bug
labels: [ "bug" ]
title: "[BUG] "
body:
- type: checkboxes
id: checklist
attributes:
label: 检查清单
description: 确保我们的错误报告表单适合您。
options:
- label: 之前没有人提交过类似或相同的 bug report。
required: true
- label: 我正在使用本软件的最新版本。
required: true
- type: dropdown
id: version
attributes:
label: my-ty 版本
description: 请选择正在使用的版本
options:
- 通用版
- 专用版
validations:
required: true
- type: textarea
id: bug
attributes:
label: Bug 描述
description: 请描述 bug 详情
placeholder: |
e.g. Crashed when generating snapshot.
validations:
required: true
- type: textarea
id: expected
attributes:
label: 预期行为
description: 你预期会发生什么?
placeholder: |
e.g. A New snapshot!
validations:
required: true
- type: textarea
id: actual
attributes:
label: 实际行为
description: 反而发生了什么?
placeholder: |
e.g. Crashed.
validations:
required: true
- type: textarea
id: steps
attributes:
label: 复现步骤
description: 如何复现这个 bug。
placeholder: |
1. Open the app
2. Crashed
What an app.
- type: input
id: ui
attributes:
label: UI / OS
description: 你的电视系统 UI 或 OS 或 品牌
placeholder: TCL / XIAOMI / PHONE / etc.
validations:
required: true
- type: input
id: android
attributes:
label: Android 版本
description: 你的 Android 版本
placeholder: "12"
validations:
required: true
- type: textarea
id: additional
attributes:
label: 额外信息
description: 任何你觉得值得说的。
+36
View File
@@ -0,0 +1,36 @@
name: 功能(新频道)请求
description: 提出一个建议
labels: [ "enhancement" ]
title: "[FR] "
body:
- type: checkboxes
id: checklist
attributes:
label: 检查清单
description: 确保我们的错误报告表单适合您。
options:
- label: 之前没有人提交过类似或相同的功能请求。
required: true
- label: 这个建议不会背离 my-tv 的初衷。
required: true
- type: textarea
id: propose
attributes:
label: 改进目的
description: 改进有什么用
placeholder: |
Show your idea here.
validations:
required: true
- type: textarea
id: solution
attributes:
label: 解决方案
description: 你会怎么完成这个改进?
placeholder: |
How to do it on your opinion? Or left this blank
- type: textarea
id: addition
attributes:
label: 额外信息
description: 任何你觉得值得说的。
+171 -2
View File
@@ -1,12 +1,12 @@
# 我的电视
安卓电视直播软件,内置直播源
电视直播软件,安装即可使用
## 使用
下载安装 [releases](https://github.com/lizongying/my-tv/releases/)
其他地址 [my-tv](https://lyrics.run/my-tv.html)
更多地址 [my-tv](https://lyrics.run/my-tv.html)
![image](./screenshots/img_3.png)
![image](./screenshots/img_2.png)
@@ -14,6 +14,133 @@
## 更新日志
### v1.6.4(通用)
* 增加CETV1
* 增加凤凰卫视
* 默认关闭开机启动
### v1.6.3(安卓5及以上专用)
* 增加CETV1
* 凤凰卫视增强画质
* 默认关闭开机启动
* 延迟菜单自动关闭时间
* 解决一些可能导致首次打开时黑屏的问题
### v1.6.2(通用)
* 修复按键无效的问题
* 新的频道列表样式
### v1.6.1(安卓5及以上专用)
* 增加凤凰卫视
### v1.6.0(通用)
* 通用(春晚緊急修復)
### v1.5.9(安卓5及以上专用)
* 解决天猫魔盒闪退问题
### v1.5.8(通用)
* 修复央视6画质差的问题
* 增加兵团卫视
* 播放失败重试
### v1.5.7(安卓5及以上专用)
* 修复播放失败的问题
### v1.5.6(通用)
* 解决部分设备系统时间不对导致播放失败的问题
### v1.5.5(安卓5及以上专用)
* 修复播放失败的问题
* 修复APP恢复后频道号、频道列表不自动消失的问题
### v1.5.4(通用)
* 修复播放失败的问题
### 以下已不可用
### v1.5.3(安卓5及以上专用)
* 修复部分情况下APP切换后无法继续播放的问题
* 优化重试逻辑
### v1.5.2(通用)
* 修复APP恢复后频道号、频道列表不自动消失的问题
### v1.5.1(安卓5及以上专用)
* 性能优化
### v1.5.0(通用)
* 修复部分情况下APP切换后无法继续播放的问题
### v1.4.9(安卓5及以上专用)
* 同步v1.4.8
### v1.4.8(通用)
* 频道号从1开始,CCTV5+为18
* 提高CCTV6清晰度
* 增加天津卫视、新疆卫视
### v1.4.7(安卓5及以上专用)
* 修复部分用户CCTV13播放过程中卡住的问题
* 调整CCTV的频道顺序
### v1.4.6(通用)
* 10以下频道不再需要先按0
### v1.4.5(安卓5及以上专用)
* 数字选台配置
### v1.4.4(通用)
* 优化图标显示
* 增加换台反转
### v1.4.3(安卓5及以上专用)
* 支持频道反转配置
### v1.4.2(通用)
* 支持安卓4
### v1.4.1
* 解决部分用户无法打开菜单的问题
### v1.4.0
* 解决极个别高版本机型黑屏问题
### v1.3.9
* 提高稳定性
* 提高连接速度
### v1.3.4
* 部分错误会提示用户
* 菜单3秒钟后自动关闭
### v1.3.3
* 部分错误会提示用户
@@ -24,6 +151,27 @@
* 增加重试,减少因网络问题导致的播放失败
* 优化横幅banner
### v1.3.1
* 增加CCTV 8K 超高清频道
* 增加国际频道栏目
### v1.3.0
* 处理368可能失败的情形
### v1.2.9
* 支持3、6、8
### v1.2.8
* 兼容16:10等分辨率
### v1.2.7
* 此版本是为了测试安卓6.0以下版本,如之前可以正常运行,请勿安装。
### v1.2.6
* 支持安卓4.2
@@ -44,6 +192,14 @@
* 播放相同的频道,不再重复加载
* 暂时移除部分频道
### v1.2.3
* 固定视频比列为16:9
* 隐藏全面屏底部的小横条
* 移除移动专区
* 修复一个闪退问题
* 更换图标和应用名
## 其他
小米电视可以使用小米电视助手进行安装
@@ -54,6 +210,19 @@
adb install my-tv.apk
```
## TODO
* 音量不同
* 大湾区卫视、广东4k超高清、广东珠江、三沙卫视
* CHC高清三个电影频道
* 地方频道
* 收藏夹
* 自定义源
* 海外
* 节目增加预告
* 频道列表优化
* 自动更新
## 赞赏
![image](./screenshots/appreciate.jpeg)
+6 -2
View File
@@ -5,7 +5,7 @@ plugins {
android {
namespace 'com.lizongying.mytv'
compileSdk 33
compileSdk 34
viewBinding {
enabled = true
@@ -90,7 +90,7 @@ static def VersionName() {
}
dependencies {
def media3_version = "1.1.1"
def media3_version = "1.2.1"
implementation "androidx.media3:media3-ui:$media3_version"
@@ -113,4 +113,8 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0-RC")
implementation 'com.google.android.exoplayer:exoplayer-ui:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-core:2.13.3'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.13.3'
}
+19 -4
View File
@@ -7,19 +7,22 @@
android:name="android.software.leanback"
android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:largeHeap="true"
android:icon="@drawable/logo"
android:logo="@drawable/logo"
android:banner="@drawable/banner"
android:label="@string/app_name"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network"
android:usesCleartextTraffic="true"
android:theme="@style/Theme.MyTV">
<activity
android:keepScreenOn="true"
android:name=".MainActivity"
android:banner="@drawable/banner"
android:exported="true"
android:icon="@drawable/logo"
android:logo="@drawable/logo"
android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -27,5 +30,17 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<provider
android:name=".InitializerProvider"
android:authorities="${applicationId}.InitializerProvider"
android:exported="false"/>
</application>
</manifest>
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,18 @@
package com.lizongying.mytv
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (SP.bootStartup) {
context.startActivity(
Intent(context, MainActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
}
}
}
@@ -31,28 +31,19 @@ class CardPresenter(
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT)
cardView.tag = tvViewModel.videoUrl.value
if (tvViewModel.logo.value != null) {
if (tvViewModel.title.value == "CCTV8K 超高清") {
Glide.with(viewHolder.view.context)
.load(R.drawable.cctv8k)
.centerInside()
.into(cardView.mainImageView)
} else {
Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
.into(cardView.mainImageView)
}
Glide.with(viewHolder.view.context)
.load(tvViewModel.logo.value)
.centerInside()
.into(cardView.mainImageView)
cardView.setBackgroundColor(Color.WHITE)
cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE)
}
cardView.setBackgroundColor(Color.WHITE)
cardView.setMainImageScaleType(ImageView.ScaleType.CENTER_INSIDE)
tvViewModel.program.observe(owner) { _ ->
val program = tvViewModel.getProgramOne()
if (program != null) {
cardView.contentText = program.name
}
val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
if (!epg.isNullOrEmpty()) {
cardView.contentText = epg.last().title
} else {
cardView.contentText = ""
}
}
@@ -0,0 +1,83 @@
package com.lizongying.mytv
import android.os.Bundle
import android.os.Handler
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.lizongying.mytv.databinding.ChannelBinding
import com.lizongying.mytv.models.TVViewModel
class ChannelFragment : Fragment() {
private var _binding: ChannelBinding? = null
private val binding get() = _binding!!
private val handler = Handler()
private val delay: Long = 3000
private var channel = 0
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = ChannelBinding.inflate(inflater, container, false)
_binding!!.root.visibility = View.GONE
(activity as MainActivity).fragmentReady()
return binding.root
}
fun show(tvViewModel: TVViewModel) {
handler.removeCallbacks(hideRunnable)
handler.removeCallbacks(playRunnable)
binding.channelContent.text = (tvViewModel.id.value?.plus(1)).toString()
view?.visibility = View.VISIBLE
handler.postDelayed(hideRunnable, delay)
}
fun show(channel: String) {
this.channel = "${binding.channelContent.text}$channel".toInt()
handler.removeCallbacks(hideRunnable)
handler.removeCallbacks(playRunnable)
if (binding.channelContent.text == "") {
binding.channelContent.text = channel
view?.visibility = View.VISIBLE
handler.postDelayed(playRunnable, delay)
} else {
handler.postDelayed(playRunnable, 0)
}
}
override fun onResume() {
super.onResume()
if (view?.visibility == View.VISIBLE) {
handler.postDelayed(hideRunnable, delay)
}
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(hideRunnable)
handler.removeCallbacks(playRunnable)
}
private val hideRunnable = Runnable {
binding.channelContent.text = ""
view?.visibility = View.GONE
}
private val playRunnable = Runnable {
(activity as MainActivity).play(channel - 1)
binding.channelContent.text = ""
view?.visibility = View.GONE
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "ChannelFragment"
}
}
@@ -0,0 +1,157 @@
package com.lizongying.mytv
import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
/**
*@author LeGend
*@date 2024/2/4 22:42
*/
object ChannelUtils {
/**
* 获取服务器channel版本
*
* @param context Context
*
* @return 服务器channel版本
*/
suspend fun getServerVersion(context: Context): Int {
return withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder().connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
client.newCall(okhttp3.Request.Builder().url(getServerVersionUrl(context)).build()).execute()
.use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val body = response.body()
body?.string()?.toInt() ?: 0
}
}
}
/**
* 获取服务器channel
*
* @param url String 服务器地址
*
* @return Array<TV> 服务器channel
*
* @throws java.io.IOException 网络请求失败
*/
suspend fun getServerChannel(url: String): List<TV> {
val result = withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder().connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
val request = okhttp3.Request.Builder().url(url).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val body = response.body()
body?.string() ?: ""
}
}
return withContext(Dispatchers.Default) {
val type = object : com.google.gson.reflect.TypeToken<List<TV>>() {}.type
com.google.gson.Gson().fromJson(result, type)
}
}
/**
* 获取服务器地址
*
* @param context Context
*
* @return 服务器地址
*/
fun getServerUrl(context: Context): String {
return context.resources.getString(R.string.server_url)
}
/**
* 获取serverVersion的URL
*
* @param context Context
*
* @return serverVersionURL 服务器版本地址
*/
suspend fun getServerVersionUrl(context: Context): String {
return withContext(Dispatchers.IO) {
context.resources.getString(R.string.server_version_url)
}
}
/**
* 获取本地channel版本
*
* @param context Context
*
* @return 本地channel
*/
suspend fun getLocalVersion(context: Context): Int {
return withContext(Dispatchers.IO) {
val file = File(getAppDirectory(context), "channels")
//检查本地是否已经有保存的channels.json,若无保存的Channel.json则从读取assert中文件
val savedVersion =
context.getSharedPreferences("saved_version", Context.MODE_PRIVATE).getInt("version", Integer.MIN_VALUE)
if (!file.exists() || savedVersion == Integer.MIN_VALUE) {
context.resources.getInteger(R.integer.local_channel_version)
} else {
savedVersion
}
}
}
/**
* 获取本地可读取的目录
* @param context Context
*
* @return 可读取的目录
*/
private fun getAppDirectory(context: Context): File {
return context.filesDir
}
/**
* 获取本地channel
*
* @param context Context
*
* @return Array<TV> 本地channel
*/
suspend fun getLocalChannel(context: Context): List<TV> {
val str = withContext(Dispatchers.IO) {
if (File(getAppDirectory(context), "channels").exists()) {
File(getAppDirectory(context), "channels").readText()
} else {
context.resources.openRawResource(R.raw.channels).bufferedReader().use { it.readText() }
}
}
return withContext(Dispatchers.Default) {
val type = object : com.google.gson.reflect.TypeToken<List<TV>>() {}.type
com.google.gson.Gson().fromJson(str, type)
}
}
/**
* 更新channels.json
*
* @param context Context
*
* @return 无
*
* @throws java.io.IOException 写入失败
*/
suspend fun updateLocalChannel(context: Context, version: Int, channels: List<TV>) {
withContext(Dispatchers.IO) {
val file = File(getAppDirectory(context), "channels")
if (!file.exists()) {
file.createNewFile()
}
file.writeText(com.google.gson.Gson().toJson(channels))
context.getSharedPreferences("saved_version", Context.MODE_PRIVATE).edit().putInt(
"version", version
).apply()
}
}
}
@@ -0,0 +1,36 @@
package com.lizongying.mytv
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
class ConfirmationDialogFragment(private val listener: ConfirmationDialogListener) :
DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val builder = AlertDialog.Builder(it)
builder.setTitle("确认")
.setMessage("确认更新吗?")
.setPositiveButton(
"确定"
) { _, _ ->
listener.onConfirm()
}
.setNegativeButton(
"取消"
) { _, _ ->
listener.onCancel()
}
// 创建并返回 AlertDialog 对象
builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
interface ConfirmationDialogListener {
fun onConfirm()
fun onCancel()
}
}
@@ -9,6 +9,8 @@ class Encryptor {
external fun hash(data: ByteArray): ByteArray?
external fun hash2(data: ByteArray): ByteArray?
companion object {
init {
System.loadLibrary("native")
@@ -0,0 +1,84 @@
@file:Suppress("DEPRECATION")
package com.lizongying.mytv
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.content.pm.SigningInfo
import android.os.Build
import android.util.Log
import java.security.MessageDigest
private const val TAG = "Extensions"
private val Context.packageInfo: PackageInfo
get() {
val flag = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
PackageManager.GET_SIGNATURES
} else {
PackageManager.GET_SIGNING_CERTIFICATES
}
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(packageName, flag)
} else {
packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES.toLong())
)
}
}
/**
* Return the version code of the app which is defined in build.gradle.
* eg:100
*/
val Context.appVersionCode: Long
get() {
val packageInfo = this.packageInfo
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode
} else {
packageInfo.versionCode.toLong()
}
}
/**
* Return the version name of the app which is defined in build.gradle.
* eg:1.0.0
*/
val Context.appVersionName: String get() = packageInfo.versionName
val Context.appSignature: String
get() {
val packageInfo = this.packageInfo
var sign: Signature? = null
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
val signatures: Array<out Signature>? = packageInfo.signatures
if (signatures != null) {
sign = signatures[0]
}
} else {
val signingInfo: SigningInfo? = packageInfo.signingInfo
if (signingInfo != null) {
sign = signingInfo.apkContentsSigners[0]
}
}
if (sign == null) {
return ""
}
return hashSignature(sign)
}
private fun hashSignature(signature: Signature): String {
return try {
val md = MessageDigest.getInstance("MD5")
md.update(signature.toByteArray())
val digest = md.digest()
digest.let { it -> it.joinToString("") { "%02x".format(it) } }
} catch (e: Exception) {
Log.e(TAG, "Error hashing signature", e)
""
}
}
@@ -23,32 +23,24 @@ class InfoFragment : Fragment() {
savedInstanceState: Bundle?
): View {
_binding = InfoBinding.inflate(inflater, container, false)
_binding!!.root.visibility = View.GONE
(activity as MainActivity).fragmentReady()
return binding.root
}
override fun onResume() {
super.onResume()
// Use a Handler to delay the fragment transaction
handler.postDelayed(removeRunnable, delay)
}
fun show(tvViewModel: TVViewModel) {
binding.textView.text = tvViewModel.title.value
if (tvViewModel.title.value == "CCTV8K 超高清") {
Glide.with(this)
.load(R.drawable.cctv8k)
.into(binding.infoLogo)
} else {
Glide.with(this)
.load(tvViewModel.logo.value)
.into(binding.infoLogo)
}
val program = tvViewModel.getProgramOne()
if (program != null) {
binding.infoDesc.text = program.name
Glide.with(this)
.load(tvViewModel.logo.value)
.into(binding.infoLogo)
Log.i(TAG, "${tvViewModel.title.value} ${tvViewModel.epg.value}")
val epg = tvViewModel.epg.value?.filter { it.beginTime < Utils.getDateTimestamp() }
if (!epg.isNullOrEmpty()) {
binding.infoDesc.text = epg.last().title
} else {
binding.infoDesc.text = ""
}
handler.removeCallbacks(removeRunnable)
@@ -56,10 +48,13 @@ class InfoFragment : Fragment() {
handler.postDelayed(removeRunnable, delay)
}
override fun onResume() {
super.onResume()
handler.postDelayed(removeRunnable, delay)
}
override fun onPause() {
super.onPause()
Log.i(TAG, "onPause")
// Cancel the delayed task when the fragment is paused
handler.removeCallbacks(removeRunnable)
}
@@ -0,0 +1,39 @@
package com.lizongying.mytv
import android.content.ContentProvider
import android.content.ContentValues
import android.net.Uri
internal class InitializerProvider : ContentProvider() {
// Happens before Application#onCreate.It's fine to init something here
override fun onCreate(): Boolean {
SP.init(context!!)
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?,
) = unsupported()
override fun getType(uri: Uri) = unsupported()
override fun insert(uri: Uri, values: ContentValues?) = unsupported()
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) =
unsupported()
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?,
) = unsupported()
private fun unsupported(errorMessage: String? = null): Nothing =
throw UnsupportedOperationException(errorMessage)
}
@@ -1,12 +1,5 @@
package com.lizongying.mytv
import android.app.AlertDialog
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.content.pm.SigningInfo
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -16,33 +9,41 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.lizongying.mytv.models.TVViewModel
import java.security.MessageDigest
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
class MainActivity : FragmentActivity() {
class MainActivity : FragmentActivity(), Request.RequestListener {
var playerFragment = PlayerFragment()
private var ready = 0
private val playerFragment = PlayerFragment()
private val mainFragment = MainFragment()
private val infoFragment = InfoFragment()
private val channelFragment = ChannelFragment()
private val settingFragment = SettingFragment()
private var doubleBackToExitPressedOnce = false
private lateinit var gestureDetector: GestureDetector
private val handler = Handler()
private val delay: Long = 4000
private val delayHideMain: Long = 10000
private val delayHideSetting: Long = 10000
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "onCreate")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Request.onCreate()
Request.setRequestListener(this)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.decorView.systemUiVisibility = SYSTEM_UI_FLAG_HIDE_NAVIGATION
@@ -51,24 +52,44 @@ class MainActivity : FragmentActivity() {
supportFragmentManager.beginTransaction()
.add(R.id.main_browse_fragment, playerFragment)
.add(R.id.main_browse_fragment, infoFragment)
.add(R.id.main_browse_fragment, channelFragment)
.add(R.id.main_browse_fragment, mainFragment)
.hide(infoFragment)
.hide(mainFragment)
.commit()
mainFragment.view?.requestFocus()
}
gestureDetector = GestureDetector(this, GestureListener())
}
fun showInfoFragment(tvViewModel: TVViewModel) {
infoFragment.show(tvViewModel)
if (SP.channelNum) {
channelFragment.show(tvViewModel)
}
}
private fun showChannel(channel: String) {
if (!mainFragment.isHidden) {
return
}
if (settingFragment.isVisible) {
return
}
if (SP.channelNum) {
channelFragment.show(channel)
}
}
fun play(tvViewModel: TVViewModel) {
Log.i(TAG, "play: ${tvViewModel.getTV()}")
playerFragment.play(tvViewModel)
mainFragment.view?.requestFocus()
}
fun play(itemPosition: Int) {
mainFragment.play(itemPosition)
}
fun prev() {
mainFragment.prev()
}
@@ -78,11 +99,11 @@ class MainActivity : FragmentActivity() {
}
private fun prevSource() {
mainFragment.prevSource()
// mainFragment.prevSource()
}
private fun nextSource() {
mainFragment.nextSource()
// mainFragment.nextSource()
}
fun switchMainFragment() {
@@ -90,7 +111,7 @@ class MainActivity : FragmentActivity() {
if (mainFragment.isHidden) {
transaction.show(mainFragment)
keepRunnable()
mainActive()
} else {
transaction.hide(mainFragment)
}
@@ -98,13 +119,20 @@ class MainActivity : FragmentActivity() {
transaction.commit()
}
fun keepRunnable() {
handler.removeCallbacks(hideRunnable)
handler.postDelayed(hideRunnable, delay)
fun mainActive() {
handler.removeCallbacks(hideMain)
handler.postDelayed(hideMain, delayHideMain)
}
private val hideRunnable = Runnable {
supportFragmentManager.beginTransaction().hide(mainFragment).commit()
fun settingActive() {
handler.removeCallbacks(hideSetting)
handler.postDelayed(hideSetting, delayHideSetting)
}
private val hideMain = Runnable {
if (!mainFragment.isHidden) {
supportFragmentManager.beginTransaction().hide(mainFragment).commit()
}
}
private fun mainFragmentIsHidden(): Boolean {
@@ -120,7 +148,11 @@ class MainActivity : FragmentActivity() {
}
fun fragmentReady() {
mainFragment.fragmentReady()
ready++
Log.i(TAG, "ready $ready")
if (ready == 5) {
mainFragment.fragmentReady()
}
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
@@ -139,7 +171,7 @@ class MainActivity : FragmentActivity() {
}
override fun onFling(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
@@ -171,100 +203,188 @@ class MainActivity : FragmentActivity() {
}
}
private fun showHelp() {
val versionName = getPackageInfo().versionName
private fun showSetting() {
if (!mainFragment.isHidden) {
return
}
val textView = TextView(this)
textView.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
textView.setBackgroundColor(0xFF263238.toInt())
textView.setPadding(20, 50, 20, 20)
val imageView = ImageView(this)
val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate)
imageView.setImageDrawable(drawable)
imageView.setBackgroundColor(Color.WHITE)
val linearLayout = LinearLayout(this)
linearLayout.orientation = LinearLayout.VERTICAL
linearLayout.addView(textView)
linearLayout.addView(imageView)
val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
imageView.layoutParams = layoutParams
textView.layoutParams = layoutParams
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder
.setView(linearLayout)
val dialog: AlertDialog = builder.create()
dialog.show()
Log.i(TAG, "settingFragment ${settingFragment.isVisible}")
if (!settingFragment.isVisible) {
settingFragment.show(supportFragmentManager, "setting")
settingActive()
} else {
handler.removeCallbacks(hideSetting)
settingFragment.dismiss()
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_BACK -> {
if (!mainFragmentIsHidden()) {
hideMainFragment()
return true
}
private val hideSetting = Runnable {
if (settingFragment.isVisible) {
settingFragment.dismiss()
}
}
if (doubleBackToExitPressedOnce) {
super.onBackPressed()
return true
}
this.doubleBackToExitPressedOnce = true
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show()
Handler(Looper.getMainLooper()).postDelayed({
doubleBackToExitPressedOnce = false
}, 2000)
return true
private fun channelUp() {
if (mainFragment.isHidden) {
if (SP.channelReversal) {
next()
return
}
KeyEvent.KEYCODE_SETTINGS -> {
showHelp()
return true
}
KeyEvent.KEYCODE_MENU -> {
showHelp()
return true
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
Log.i(TAG, "KEYCODE_DPAD_CENTER")
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (mainFragment.isHidden) {
prev()
} else {
prev()
} else {
// if (mainFragment.selectedPosition == 0) {
// mainFragment.setSelectedPosition(
// mainFragment.tvListViewModel.maxNum.size - 1,
// false
// )
// }
}
}
}
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (mainFragment.isHidden) {
next()
} else {
private fun channelDown() {
if (mainFragment.isHidden) {
if (SP.channelReversal) {
prev()
return
}
next()
} else {
// if (mainFragment.selectedPosition == mainFragment.tvListViewModel.maxNum.size - 1) {
//// mainFragment.setSelectedPosition(0, false)
// hideMainFragment()
// return false
// }
}
}
}
private fun back() {
if (!mainFragmentIsHidden()) {
hideMainFragment()
return
}
if (doubleBackToExitPressedOnce) {
super.onBackPressed()
return
}
doubleBackToExitPressedOnce = true
Toast.makeText(this, "再按一次退出", Toast.LENGTH_SHORT).show()
Handler(Looper.getMainLooper()).postDelayed({
doubleBackToExitPressedOnce = false
}, 2000)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_0 -> {
showChannel("0")
return true
}
KeyEvent.KEYCODE_1 -> {
showChannel("1")
return true
}
KeyEvent.KEYCODE_2 -> {
showChannel("2")
return true
}
KeyEvent.KEYCODE_3 -> {
showChannel("3")
return true
}
KeyEvent.KEYCODE_4 -> {
showChannel("4")
return true
}
KeyEvent.KEYCODE_5 -> {
showChannel("5")
return true
}
KeyEvent.KEYCODE_6 -> {
showChannel("6")
return true
}
KeyEvent.KEYCODE_7 -> {
showChannel("7")
return true
}
KeyEvent.KEYCODE_8 -> {
showChannel("8")
return true
}
KeyEvent.KEYCODE_9 -> {
showChannel("9")
return true
}
KeyEvent.KEYCODE_ESCAPE -> {
back()
return true
}
KeyEvent.KEYCODE_BACK -> {
back()
return true
}
KeyEvent.KEYCODE_BOOKMARK -> {
showSetting()
return true
}
KeyEvent.KEYCODE_UNKNOWN -> {
showSetting()
return true
}
KeyEvent.KEYCODE_HELP -> {
showSetting()
return true
}
KeyEvent.KEYCODE_SETTINGS -> {
showSetting()
return true
}
KeyEvent.KEYCODE_MENU -> {
showSetting()
return true
}
KeyEvent.KEYCODE_ENTER -> {
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
switchMainFragment()
}
KeyEvent.KEYCODE_DPAD_UP -> {
channelUp()
}
KeyEvent.KEYCODE_CHANNEL_UP -> {
channelUp()
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
channelDown()
}
KeyEvent.KEYCODE_CHANNEL_DOWN -> {
channelDown()
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
@@ -296,58 +416,37 @@ class MainActivity : FragmentActivity() {
return super.onKeyDown(keyCode, event)
}
private fun getPackageInfo(): PackageInfo {
val flag = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
PackageManager.GET_SIGNATURES
} else {
PackageManager.GET_SIGNING_CERTIFICATES
}
private fun getAppSignature() = this.appSignature
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
packageManager.getPackageInfo(packageName, flag)
} else {
packageManager.getPackageInfo(
packageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_SIGNING_CERTIFICATES.toLong())
)
override fun onStart() {
Log.i(TAG, "onStart")
super.onStart()
}
override fun onResume() {
Log.i(TAG, "onResume")
super.onResume()
if (!mainFragment.isHidden) {
handler.postDelayed(hideMain, delayHideMain)
}
}
private fun getAppSignature(): String {
val packageInfo = getPackageInfo()
var sign: Signature? = null
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
val signatures: Array<out Signature>? = packageInfo.signatures
if (signatures != null) {
sign = signatures[0]
}
} else {
val signingInfo: SigningInfo? = packageInfo.signingInfo
if (signingInfo != null) {
sign = signingInfo.apkContentsSigners[0]
}
}
if (sign == null) {
return ""
}
return hashSignature(sign)
override fun onPause() {
Log.i(TAG, "onPause")
super.onPause()
handler.removeCallbacks(hideMain)
}
private fun hashSignature(signature: Signature): String {
return try {
val md = MessageDigest.getInstance("SHA-256")
md.update(signature.toByteArray())
val digest = md.digest()
digest.let { it -> it.joinToString("") { "%02x".format(it) } }
} catch (e: Exception) {
Log.e(TAG, "Error hashing signature", e)
""
}
override fun onDestroy() {
super.onDestroy()
Request.onDestroy()
}
companion object {
private const val TAG = "MainActivity"
override fun onRequestFinished() {
fragmentReady()
}
private companion object {
const val TAG = "MainActivity"
}
}
@@ -1,7 +1,5 @@
package com.lizongying.mytv
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -20,50 +18,43 @@ import androidx.leanback.widget.Row
import androidx.leanback.widget.RowPresenter
import androidx.lifecycle.lifecycleScope
import com.lizongying.mytv.Utils.getDateTimestamp
import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.models.TVListViewModel
import com.lizongying.mytv.models.TVViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainFragment : BrowseSupportFragment() {
var itemPosition: Int = 0
private var request: Request = Request()
private var itemPosition = 0
private var rowsAdapter: ArrayObjectAdapter? = null
var tvListViewModel = TVListViewModel()
private var sharedPref: SharedPreferences? = null
private var lastVideoUrl: String = ""
private val handler = Handler(Looper.getMainLooper())
private lateinit var mUpdateProgramRunnable: UpdateProgramRunnable
private var ready = 0
private var lastVideoUrl = ""
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(TAG, "onCreate")
super.onCreate(savedInstanceState)
headersState = HEADERS_DISABLED
}
override fun onStart() {
Log.i(TAG, "onStart")
super.onStart()
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
activity?.let { request.initYSP(it) }
sharedPref = activity?.getPreferences(Context.MODE_PRIVATE)
activity?.let { YSP.init(it) }
loadRows()
setupEventListeners()
mUpdateProgramRunnable = UpdateProgramRunnable()
handler.post(mUpdateProgramRunnable)
tvListViewModel.getTVListViewModel().value?.forEach { tvViewModel ->
tvListViewModel.tvListViewModel.value?.forEach { tvViewModel ->
tvViewModel.errInfo.observe(viewLifecycleOwner) { _ ->
if (tvViewModel.errInfo.value != null
&& tvViewModel.id.value == itemPosition
@@ -87,9 +78,9 @@ class MainFragment : BrowseSupportFragment() {
val title = tvViewModel.title.value
Log.i(TAG, "switch $title")
if (tvViewModel.pid.value != "") {
Log.i(TAG, "request $title ${tvViewModel.pid.value}")
Log.i(TAG, "request $title")
lifecycleScope.launch(Dispatchers.IO) {
tvViewModel.let { request.fetchData(it) }
tvViewModel.let { Request.fetchData(it) }
}
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition(
@@ -100,7 +91,6 @@ class MainFragment : BrowseSupportFragment() {
if (check(tvViewModel)) {
(activity as? MainActivity)?.play(tvViewModel)
(activity as? MainActivity)?.showInfoFragment(tvViewModel)
setSelectedPosition(
tvViewModel.getRowPosition(), true,
SelectItemViewHolderTask(tvViewModel.getItemPosition())
@@ -111,59 +101,7 @@ class MainFragment : BrowseSupportFragment() {
}
}
fragmentReady()
}
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacks(mUpdateProgramRunnable)
with(sharedPref!!.edit()) {
putInt("position", itemPosition)
apply()
}
}
fun updateProgram(tvViewModel: TVViewModel) {
val timestamp = getDateTimestamp()
if (timestamp - tvViewModel.programUpdateTime > 60) {
if (tvViewModel.program.value!!.isEmpty()) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
} else {
if (timestamp - tvViewModel.program.value!!.last().et < 600) {
tvViewModel.programUpdateTime = timestamp
request.fetchProgram(tvViewModel)
}
}
}
}
inner class UpdateProgramRunnable : Runnable {
override fun run() {
tvListViewModel.getTVListViewModel().value?.filter { it.programId.value != null }
?.forEach { tvViewModel ->
updateProgram(
tvViewModel
)
}
handler.postDelayed(this, 60000)
}
}
fun check(tvViewModel: TVViewModel): Boolean {
val title = tvViewModel.title.value
val videoUrl = tvViewModel.videoIndex.value?.let { tvViewModel.videoUrl.value?.get(it) }
if (videoUrl == null || videoUrl == "") {
Log.e(TAG, "$title videoUrl is empty")
return false
}
if (videoUrl == lastVideoUrl) {
Log.e(TAG, "$title videoUrl is duplication")
return false
}
return true
(activity as MainActivity).fragmentReady()
}
fun toLastPosition() {
@@ -197,6 +135,8 @@ class MainFragment : BrowseSupportFragment() {
tvViewModel.setItemPosition(idx2)
tvListViewModel.addTVViewModel(tvViewModel)
listRowAdapter.add(tvViewModel)
updateEPG(tvViewModel)
}
tvListViewModel.maxNum.add(v.size)
val header = HeaderItem(idx, k)
@@ -206,49 +146,11 @@ class MainFragment : BrowseSupportFragment() {
adapter = rowsAdapter
itemPosition = sharedPref?.getInt("position", 0)!!
itemPosition = SP.itemPosition
if (itemPosition >= tvListViewModel.size()) {
itemPosition = 0
tvListViewModel.setItemPosition(itemPosition)
}
}
fun fragmentReady() {
ready++
Log.i(TAG, "ready $ready")
if (ready == 3) {
// request.fetchPage()
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
(activity as? MainActivity)?.switchMainFragment()
}
}
fun prev() {
view?.post {
itemPosition--
if (itemPosition == -1) {
itemPosition = tvListViewModel.size() - 1
}
tvListViewModel.setItemPosition(itemPosition)
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
}
}
fun next() {
view?.post {
itemPosition++
if (itemPosition == tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
}
tvListViewModel.setItemPosition(itemPosition)
}
fun prevSource() {
@@ -297,9 +199,7 @@ class MainFragment : BrowseSupportFragment() {
if (itemPosition != item.id.value!!) {
itemPosition = item.id.value!!
tvListViewModel.setItemPosition(itemPosition)
val tvViewModel = tvListViewModel.getTVViewModel(itemPosition)
tvViewModel?.changed()
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
(activity as? MainActivity)?.switchMainFragment()
}
@@ -313,17 +213,93 @@ class MainFragment : BrowseSupportFragment() {
) {
if (item is TVViewModel) {
tvListViewModel.setItemPositionCurrent(item.id.value!!)
(activity as MainActivity).keepRunnable()
(activity as MainActivity).mainActive()
}
}
}
fun check(tvViewModel: TVViewModel): Boolean {
val title = tvViewModel.title.value
val videoUrl = tvViewModel.videoIndex.value?.let { tvViewModel.videoUrl.value?.get(it) }
if (videoUrl == null || videoUrl == "") {
Log.e(TAG, "$title videoUrl is empty")
return false
}
if (videoUrl == lastVideoUrl) {
Log.e(TAG, "$title videoUrl is duplication")
return false
}
return true
}
fun fragmentReady() {
// request.fetchPage()
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
fun play(itemPosition: Int) {
view?.post {
if (itemPosition > -1 && itemPosition < tvListViewModel.size()) {
this.itemPosition = itemPosition
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
} else {
Toast.makeText(context, "频道不存在", Toast.LENGTH_SHORT).show()
}
}
}
fun prev() {
view?.post {
itemPosition--
if (itemPosition == -1) {
itemPosition = tvListViewModel.size() - 1
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
fun next() {
view?.post {
itemPosition++
if (itemPosition == tvListViewModel.size()) {
itemPosition = 0
}
tvListViewModel.setItemPosition(itemPosition)
tvListViewModel.getTVViewModel(itemPosition)?.changed()
}
}
private fun updateEPG(tvViewModel: TVViewModel) {
if (tvViewModel.getTV().channel == "港澳台") {
Request.fetchFEPG(tvViewModel)
} else {
Request.fetchYEPG(tvViewModel)
}
}
override fun onResume() {
Log.i(TAG, "onResume")
super.onResume()
view!!.requestFocus()
}
override fun onStop() {
Log.i(TAG, "onStop")
super.onStop()
SP.itemPosition = itemPosition
Log.i(TAG, "$POSITION $itemPosition saved")
}
override fun onDestroy() {
Log.i(TAG, "onDestroy")
super.onDestroy()
}
companion object {
private const val TAG = "MainFragment"
private const val POSITION = "position"
}
}
@@ -3,6 +3,8 @@ package com.lizongying.mytv
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
@@ -15,24 +17,39 @@ import androidx.media3.common.VideoSize
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.google.android.exoplayer2.SimpleExoPlayer
import com.lizongying.mytv.databinding.PlayerBinding
import com.lizongying.mytv.models.TVViewModel
class PlayerFragment : Fragment() {
class PlayerFragment : Fragment(), SurfaceHolder.Callback {
private var _binding: PlayerBinding? = null
private var playerView: PlayerView? = null
private var tvViewModel: TVViewModel? = null
private val aspectRatio = 16f / 9f
private lateinit var surfaceView: SurfaceView
private lateinit var surfaceHolder: SurfaceHolder
private var exoPlayer: SimpleExoPlayer? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = PlayerBinding.inflate(inflater, container, false)
playerView = _binding!!.playerView
(activity as MainActivity).playerFragment = this
if (Utils.isTmallDevice()) {
_binding!!.playerView.visibility = View.GONE
surfaceView = _binding!!.surfaceView
surfaceHolder = surfaceView.holder
surfaceHolder.addCallback(this)
} else {
_binding!!.surfaceView.visibility = View.GONE
playerView = _binding!!.playerView
}
playerView?.viewTreeObserver?.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
@@ -63,6 +80,7 @@ class PlayerFragment : Fragment() {
super.onPlayerError(error)
Log.e(TAG, "PlaybackException $error")
tvViewModel?.changed()
}
})
}
@@ -74,26 +92,44 @@ class PlayerFragment : Fragment() {
@OptIn(UnstableApi::class)
fun play(tvViewModel: TVViewModel) {
this.tvViewModel = tvViewModel
val videoUrlCurrent =
tvViewModel.videoIndex.value?.let { tvViewModel.videoUrl.value?.get(it) }
playerView?.player?.run {
videoUrlCurrent?.let { setMediaItem(MediaItem.fromUri(it)) }
setMediaItem(MediaItem.fromUri(tvViewModel.getVideoUrlCurrent()))
prepare()
}
exoPlayer?.run {
setMediaItem(com.google.android.exoplayer2.MediaItem.fromUri(tvViewModel.getVideoUrlCurrent()))
prepare()
}
}
override fun onStart() {
Log.i(TAG, "onStart")
super.onStart()
if (playerView != null) {
if (playerView != null && playerView!!.player?.isPlaying == false) {
Log.i(TAG, "replay")
playerView!!.player?.prepare()
playerView!!.player?.play()
}
if (exoPlayer?.isPlaying == false) {
Log.i(TAG, "replay")
exoPlayer?.prepare()
exoPlayer?.play()
}
}
override fun onStop() {
super.onStop()
if (playerView != null) {
override fun onResume() {
Log.i(TAG, "onResume")
super.onResume()
}
override fun onPause() {
super.onPause()
if (playerView != null && playerView!!.player?.isPlaying == true) {
playerView!!.player?.stop()
}
if (exoPlayer?.isPlaying == true) {
exoPlayer?.stop()
}
}
override fun onDestroy() {
@@ -101,9 +137,27 @@ class PlayerFragment : Fragment() {
if (playerView != null) {
playerView!!.player?.release()
}
exoPlayer?.release()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "PlaybackVideoFragment"
}
override fun surfaceCreated(holder: SurfaceHolder) {
exoPlayer = SimpleExoPlayer.Builder(requireContext()).build()
exoPlayer?.setVideoSurfaceHolder(surfaceHolder)
exoPlayer?.playWhenReady = true
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
}
+516 -119
View File
@@ -1,15 +1,21 @@
package com.lizongying.mytv
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Base64
import android.util.Log
import com.lizongying.mytv.Utils.getDateFormat
import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.Auth
import com.lizongying.mytv.api.AuthRequest
import com.lizongying.mytv.api.FAuth
import com.lizongying.mytv.api.FAuthService
import com.lizongying.mytv.api.FEPG
import com.lizongying.mytv.api.Info
import com.lizongying.mytv.api.InfoV2
import com.lizongying.mytv.api.LiveInfo
import com.lizongying.mytv.api.LiveInfoRequest
import com.lizongying.mytv.api.Token
import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.api.YSPApiService
import com.lizongying.mytv.api.YSPBtraceService
@@ -24,88 +30,161 @@ import retrofit2.Response
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.math.max
class Request {
object Request {
private const val TAG = "Request"
private var yspTokenService: YSPTokenService = ApiClient().yspTokenService
private var yspApiService: YSPApiService = ApiClient().yspApiService
private var yspBtraceService: YSPBtraceService = ApiClient().yspBtraceService
private var yspProtoService: YSPProtoService = ApiClient().yspProtoService
private var ysp: YSP? = null
private var fAuthService: FAuthService = ApiClient().fAuthService
private var token = ""
private var tokenFH = ""
private var needAuth = false
// TODO onDestroy
private val handler = Handler(Looper.getMainLooper())
private lateinit var myRunnable: MyRunnable
private lateinit var btraceRunnable: BtraceRunnable
private var tokenRunnable: TokenRunnable = TokenRunnable()
private var mapping = mapOf(
"CCTV4K" to "CCTV4K 超高清",
"CCTV1" to "CCTV1 综合",
"CCTV2" to "CCTV2 财经",
"CCTV4" to "CCTV4 中文国际",
"CCTV5" to "CCTV5 体育",
"CCTV5+" to "CCTV5+ 体育赛事",
"CCTV7" to "CCTV7 国防军事",
"CCTV9" to "CCTV9 记录",
"CCTV10" to "CCTV10 科教",
"CCTV11" to "CCTV11 戏曲",
"CCTV12" to "CCTV12 社会与法",
"CCTV13" to "CCTV13 新闻",
"CCTV14" to "CCTV14 少儿",
"CCTV15" to "CCTV15 音乐",
"CCTV16-HD" to "CCTV16 奥林匹克",
"CCTV17" to "CCTV17 农业农村",
"CGTN" to "CGTN",
"CGTN法语频道" to "CGTN 法语频道",
"CGTN俄语频道" to "CGTN 俄语频道",
"CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道",
"CGTN西班牙语频道" to "CGTN 西班牙语频道",
"CGTN外语纪录频道" to "CGTN 纪录频道",
"东方卫视" to "东方卫视",
"湖南卫视" to "湖南卫视",
"湖北卫视" to "湖北卫视",
"辽宁卫视" to "辽宁卫视",
"江苏卫视" to "江苏卫视",
"江西卫视" to "江西卫视",
"山东卫视" to "山东卫视",
"广东卫视" to "广东卫视",
"广西卫视" to "广西卫视",
"重庆卫视" to "重庆卫视",
"河南卫视" to "河南卫视",
"河北卫视" to "河北卫视",
"贵州卫视" to "贵州卫视",
"北京卫视" to "北京卫视",
"黑龙江卫视" to "黑龙江卫视",
"浙江卫视" to "浙江卫视",
"安徽卫视" to "安徽卫视",
"深圳卫视" to "深圳卫视",
"四川卫视" to "四川卫视",
"福建东南卫视" to "东南卫视",
"海南卫视" to "海南卫视",
)
private val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
private val input =
"""{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,ANGLEMetalRenderer:AppleM1Pro,UnspecifiedVersion)"}""".toByteArray()
fun initYSP(context: Context) {
ysp = YSP(context)
private var listener: RequestListener? = null
private var initRetryTimes = 0
private var initRetryMaxTimes = 1
fun onCreate() {
Log.i(TAG, "onCreate")
fetchInfoV2()
}
var call: Call<LiveInfo>? = null
fun onDestroy() {
Log.i(TAG, "onDestroy")
handler.removeCallbacks(tokenRunnable)
}
fun fetchVideo(tvModel: TVViewModel, cookie: String) {
private var call: Call<LiveInfo>? = null
private var callAuth: Call<Auth>? = null
private var callInfo: Call<Info>? = null
private var fAuth: Call<FAuth>? = null
private var callPage: Call<pageModel.Response>? = null
private fun cancelCall() {
call?.cancel()
if (::myRunnable.isInitialized) {
handler.removeCallbacks(myRunnable)
callAuth?.cancel()
callInfo?.cancel()
fAuth?.cancel()
callPage?.cancel()
}
private fun fetchAuth(tvModel: TVViewModel, cookie: String) {
cancelCall()
val title = tvModel.title.value
val data = YSP.getAuthData(tvModel)
val request = AuthRequest(data)
callAuth = request.let { yspApiService.getAuth("guid=${YSP.getGuid()}; $cookie", it) }
callAuth?.enqueue(object : Callback<Auth> {
override fun onResponse(call: Call<Auth>, response: Response<Auth>) {
if (response.isSuccessful) {
val liveInfo = response.body()
if (liveInfo?.data?.token != null) {
Log.i(TAG, "token ${liveInfo.data.token}")
YSP.token = liveInfo.data.token
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "$title token error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
} else {
Log.e(TAG, "$title auth status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<Auth>, t: Throwable) {
Log.e(TAG, "$title auth request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchAuth(tvModel)
}
} else {
fetchAuth(tvModel, cookie)
}
}
}
})
}
private fun fetchVideo(tvModel: TVViewModel, cookie: String) {
cancelCall()
if (::btraceRunnable.isInitialized) {
handler.removeCallbacks(btraceRunnable)
}
val title = tvModel.title.value
tvModel.seq = 0
val data = ysp?.switch(tvModel)
val request = data?.let { LiveInfoRequest(it) }
call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", it) }
val data = YSP.switch(tvModel)
val request = LiveInfoRequest(data)
call = request.let {
yspApiService.getLiveInfo(
"guid=${YSP.getGuid()}; $cookie",
YSP.token,
it
)
}
call?.enqueue(object : Callback<LiveInfo> {
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
if (response.isSuccessful) {
val liveInfo = response.body()
if (liveInfo?.data?.playurl != null) {
val chanll = liveInfo.data.chanll
val decodedBytes = Base64.decode(
@@ -113,7 +192,6 @@ class Request {
Base64.DEFAULT
)
val decodedString = String(decodedBytes)
val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
val matchResult = regex.find(decodedString)
if (matchResult != null) {
val (key, iv) = matchResult.destructured
@@ -122,18 +200,32 @@ class Request {
val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES(
keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0),
ivBytes
).uppercase()
Log.i(TAG, "$title url $url")
).uppercase() + liveInfo.data.extended_param
Log.d(TAG, "$title url $url")
tvModel.addVideoUrl(url)
tvModel.allReady()
tvModel.retryTimes = 0
myRunnable = MyRunnable(tvModel)
handler.post(myRunnable)
btraceRunnable = BtraceRunnable(tvModel)
handler.post(btraceRunnable)
} else {
Log.e(TAG, "$title key error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchVideo(tvModel)
// fetchAuth(tvModel)
}
} else {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
}
}
}
} else {
@@ -144,7 +236,21 @@ class Request {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchVideo(tvModel)
// fetchAuth(tvModel)
}
} else {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
}
}
}
}
@@ -152,60 +258,331 @@ class Request {
Log.e(TAG, "$title status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
}
} else {
token = ""
fetchVideo(tvModel)
// fetchAuth(tvModel)
}
} else {
fetchVideo(tvModel, cookie)
// fetchAuth(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
Log.e(TAG, "$title fetchVideo request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
if (tvModel.getTV().needToken) {
if (tvModel.tokenRetryTimes == tvModel.tokenRetryMaxTimes) {
if (!tvModel.getTV().mustToken) {
fetchVideo(tvModel, cookie)
}
} else {
token = ""
fetchVideo(tvModel)
}
} else {
fetchVideo(tvModel, cookie)
}
}
}
})
}
fun fetchVideo(tvModel: TVViewModel) {
yspTokenService.getInfo()
.enqueue(object : Callback<Info> {
private fun fetchAuth(tvModel: TVViewModel) {
cancelCall()
if (token == "") {
callInfo = yspTokenService.getInfo("")
callInfo?.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
val token = response.body()?.data?.token
token = response.body()?.data?.token!!
Log.i(TAG, "info success $token")
val cookie =
"vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchAuth(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel)
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchAuth(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchAuth(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchVideo(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchAuth(tvModel, cookie)
}
}
}
})
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109;yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchAuth(tvModel, cookie)
}
}
private fun fetchVideo(tvModel: TVViewModel) {
cancelCall()
Log.d(TAG, "fetchVideo")
if (token == "") {
callInfo = yspTokenService.getInfo("")
callInfo?.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful && response.body()?.data?.token != null) {
token = response.body()?.data?.token!!
Log.i(TAG, "info success $token")
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchVideo(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchVideo(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<Info>, t: Throwable) {
Log.e(TAG, "info request error $t")
if (tvModel.tokenRetryTimes < tvModel.tokenRetryMaxTimes) {
tvModel.tokenRetryTimes++
fetchVideo(tvModel)
} else {
if (!tvModel.getTV().mustToken) {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109"
fetchVideo(tvModel, cookie)
}
}
}
})
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205; yspappid=519748109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
}
}
private fun fetchFAuth(tvModel: TVViewModel) {
cancelCall()
val title = tvModel.title.value
var qa = "HD"
if (tokenFH != "") {
qa = "FHD"
}
fAuth = fAuthService.getAuth(tokenFH, tvModel.getTV().pid, qa)
fAuth?.enqueue(object : Callback<FAuth> {
override fun onResponse(call: Call<FAuth>, response: Response<FAuth>) {
if (response.isSuccessful && response.body()?.data?.live_url != null) {
val url = response.body()?.data?.live_url!!
// Log.d(TAG, "$title url $url")
tvModel.addVideoUrl(url)
tvModel.allReady()
tvModel.tokenFHRetryTimes = 0
} else {
Log.e(TAG, "auth status error ${response.code()}")
if (tvModel.tokenFHRetryTimes < tvModel.tokenFHRetryMaxTimes) {
tvModel.tokenFHRetryTimes++
fetchFAuth(tvModel)
}
}
}
override fun onFailure(call: Call<FAuth>, t: Throwable) {
Log.e(TAG, "auth request error $t")
if (tvModel.tokenFHRetryTimes < tvModel.tokenFHRetryMaxTimes) {
tvModel.tokenFHRetryTimes++
fetchFAuth(tvModel)
}
}
})
}
fun fetchData(tvModel: TVViewModel) {
if (tvModel.getTV().channel == "港澳台") {
fetchFAuth(tvModel)
return
}
if (tvModel.getTV().needToken) {
if (needAuth) {
fetchAuth(tvModel)
} else {
fetchVideo(tvModel)
}
} else {
val cookie =
"versionName=99.99.99; versionCode=999999; vplatform=109; platformVersion=Chrome; deviceModel=120; appid=1400421205"
if (needAuth) {
fetchAuth(tvModel, cookie)
} else {
fetchVideo(tvModel, cookie)
}
}
}
class TokenRunnable : Runnable {
override fun run() {
// fetchToken()
}
}
private fun fetchInfoV2() {
yspTokenService.getInfoV2()
.enqueue(object : Callback<InfoV2> {
override fun onResponse(call: Call<InfoV2>, response: Response<InfoV2>) {
if (response.isSuccessful) {
val t = response.body()?.t
val f = response.body()?.f
val e = response.body()?.e
val c = response.body()?.c
if (!t.isNullOrEmpty()) {
token = t
Log.i(TAG, "token success $token")
if (e != null) {
handler.postDelayed(
tokenRunnable,
max(30 * 60 * 1000, (e - 1500) * 1000).toLong()
)
} else {
Log.e(TAG, "e empty")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
} else {
Log.e(TAG, "token empty")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
if (!f.isNullOrEmpty()) {
tokenFH = f
Log.i(TAG, "tokenFH success $tokenFH")
}
if (c != null) {
Utils.setBetween(c * 1000L)
Log.i(TAG, "current time $c")
}
listener?.onRequestFinished()
} else {
Log.e(TAG, "token status error")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
if (initRetryTimes < initRetryMaxTimes) {
initRetryTimes++
fetchInfoV2()
} else {
listener?.onRequestFinished()
}
}
}
override fun onFailure(call: Call<InfoV2>, t: Throwable) {
Log.e(TAG, "token request error $t")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
if (initRetryTimes < initRetryMaxTimes) {
initRetryTimes++
fetchInfoV2()
} else {
listener?.onRequestFinished()
}
}
})
}
fun fetchData(tvModel: TVViewModel) {
if (tvModel.needToken) {
fetchVideo(tvModel)
} else {
val cookie = "vplatform=109"
fetchVideo(tvModel, cookie)
}
fun fetchToken() {
yspTokenService.getToken(token)
.enqueue(object : Callback<Token> {
override fun onResponse(call: Call<Token>, response: Response<Token>) {
if (response.isSuccessful) {
val t = response.body()?.t
val e = response.body()?.e
if (!t.isNullOrEmpty()) {
token = t
Log.e(TAG, "token success $token")
if (e != null) {
handler.postDelayed(
tokenRunnable,
max(30 * 60 * 1000, (e - 1500) * 1000).toLong()
)
} else {
Log.e(TAG, "e empty")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
} else {
Log.e(TAG, "token empty")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
} else {
Log.e(TAG, "token status error")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
}
override fun onFailure(call: Call<Token>, t: Throwable) {
Log.e(TAG, "token request error $t")
handler.postDelayed(
tokenRunnable,
30 * 60 * 1000
)
}
})
}
inner class MyRunnable(private val tvModel: TVViewModel) : Runnable {
class BtraceRunnable(private val tvModel: TVViewModel) : Runnable {
override fun run() {
fetchBtrace(tvModel)
handler.postDelayed(this, 60000)
@@ -215,11 +592,11 @@ class Request {
fun fetchBtrace(tvModel: TVViewModel) {
val title = tvModel.title.value
val guid = ysp?.getGuid()!!
val guid = YSP.getGuid()
val pid = tvModel.pid.value!!
val sid = tvModel.sid.value!!
yspBtraceService.kvcollect(
c_timestamp = ysp?.generateGuid()!!,
c_timestamp = YSP.generateGuid(),
guid = guid,
c_guid = guid,
prog = sid,
@@ -227,7 +604,7 @@ class Request {
fpid = pid,
livepid = pid,
sUrl = "https://www.yangshipin.cn/#/tv/home?pid=$pid",
playno = ysp?.getRand()!!,
playno = YSP.getRand(),
ftime = getDateFormat("yyyy-MM-dd HH:mm:ss"),
seq = tvModel.seq.toString(),
)
@@ -248,36 +625,28 @@ class Request {
}
fun fetchPage() {
yspProtoService.getPage().enqueue(object : Callback<pageModel.Response> {
callPage = yspProtoService.getPage()
callPage?.enqueue(object : Callback<pageModel.Response> {
override fun onResponse(
call: Call<pageModel.Response>,
response: Response<pageModel.Response>
) {
if (response.isSuccessful) {
val body = response.body()
if (body?.data?.feedModuleListCount == 1) {
for (item in body.data?.feedModuleListList!![0]?.dataTvChannelListList!!) {
if (item.isVip && !item.isLimitedFree) {
continue
}
Log.i(
Log.d(
TAG,
"${item.channelName},${item.pid},${item.streamId}"
)
var channelType = "央视频道"
if (item?.channelType === "weishi") {
channelType = "地方频道"
}
if (!mapping.containsKey(item.channelName)) {
continue
}
val tv =
TVList.list[channelType]?.find { it.title == mapping[item.channelName] }
if (tv != null) {
tv.logo = item.tvLogo
tv.pid = item.pid
tv.sid = item.streamId
for ((_, v) in TVList.list) {
for (v2 in v) {
if (v2.title == item.channelName || v2.alias == item.channelName) {
v2.pid = item.pid
v2.sid = item.streamId
}
}
}
}
}
@@ -290,7 +659,7 @@ class Request {
})
}
fun fetchProgram(tvViewModel: TVViewModel) {
fun fetchYEPG(tvViewModel: TVViewModel) {
val title = tvViewModel.title.value
yspProtoService.getProgram(tvViewModel.programId.value!!, getDateFormat("yyyyMMdd"))
.enqueue(object : Callback<epgProgramModel.Response> {
@@ -301,9 +670,11 @@ class Request {
if (response.isSuccessful) {
val program = response.body()
if (program != null) {
tvViewModel.addProgram(program.dataListList)
Log.i(TAG, "$title program ${program.dataListList.size}")
tvViewModel.addYEPG(program.dataListList)
Log.d(TAG, "$title program ${program.dataListList.size}")
}
} else {
Log.w(TAG, "$title program error")
}
}
@@ -313,23 +684,49 @@ class Request {
})
}
fun fetchFEPG(tvViewModel: TVViewModel) {
val title = tvViewModel.title.value
fAuthService.getEPG(tvViewModel.pid.value!!, getDateFormat("yyyyMMdd"))
.enqueue(object : Callback<List<FEPG>> {
override fun onResponse(
call: Call<List<FEPG>>,
response: Response<List<FEPG>>
) {
if (response.isSuccessful) {
val program = response.body()
if (program != null) {
tvViewModel.addFEPG(program)
Log.d(TAG, "$title program ${program.size}")
}
} else {
Log.w(TAG, "$title program error")
}
}
override fun onFailure(call: Call<List<FEPG>>, t: Throwable) {
Log.e(TAG, "$title program request failed $t")
}
})
}
private fun encryptTripleDES(key: ByteArray, iv: ByteArray): String {
val plaintext =
"""{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,AppleM1Pro,OpenGL4.1)"}"""
return try {
val keySpec = SecretKeySpec(key, "DESede")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val encryptedBytes = cipher.doFinal(plaintext.toByteArray())
return encryptedBytes.let { it -> it.joinToString("") { "%02x".format(it) } }
return cipher.doFinal(input).let { it -> it.joinToString("") { "%02x".format(it) } }
} catch (e: Exception) {
e.printStackTrace()
""
}
}
companion object {
private const val TAG = "Request"
interface RequestListener {
fun onRequestFinished()
}
fun setRequestListener(listener: RequestListener) {
this.listener = listener
}
}
@@ -0,0 +1,48 @@
package com.lizongying.mytv
import android.content.Context
import android.content.SharedPreferences
object SP {
// Name of the sp file TODO Should use a meaningful name and do migrations
private const val SP_FILE_NAME = "MainActivity"
// If Change channel with up and down in reversed order or not
private const val KEY_CHANNEL_REVERSAL = "channel_reversal"
// If use channel num to select channel or not
private const val KEY_CHANNEL_NUM = "channel_num"
// If start app on device boot or not
private const val KEY_BOOT_STARTUP = "boot_startup"
// Position in list of the selected channel item
private const val KEY_POSITION = "position"
// guid
private const val KEY_GUID = "guid"
private lateinit var sp: SharedPreferences
/**
* The method must be invoked as early as possible(At least before using the keys)
*/
fun init(context: Context) {
sp = context.getSharedPreferences(SP_FILE_NAME, Context.MODE_PRIVATE)
}
var channelReversal: Boolean
get() = sp.getBoolean(KEY_CHANNEL_REVERSAL, false)
set(value) = sp.edit().putBoolean(KEY_CHANNEL_REVERSAL, value).apply()
var channelNum: Boolean
get() = sp.getBoolean(KEY_CHANNEL_NUM, true)
set(value) = sp.edit().putBoolean(KEY_CHANNEL_NUM, value).apply()
var bootStartup: Boolean
get() = sp.getBoolean(KEY_BOOT_STARTUP, false)
set(value) = sp.edit().putBoolean(KEY_BOOT_STARTUP, value).apply()
var itemPosition: Int
get() = sp.getInt(KEY_POSITION, 0)
set(value) = sp.edit().putInt(KEY_POSITION, value).apply()
var guid: String
get() = sp.getString(KEY_GUID, "") ?: ""
set(value) = sp.edit().putString(KEY_GUID, value).apply()
}
@@ -0,0 +1,88 @@
package com.lizongying.mytv
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.lizongying.mytv.databinding.DialogBinding
class SettingFragment : DialogFragment() {
private var _binding: DialogBinding? = null
private val binding get() = _binding!!
private lateinit var updateManager: UpdateManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val context = requireContext() // It‘s safe to get context here.
_binding = DialogBinding.inflate(inflater, container, false)
binding.version.text =
"当前版本: ${context.appVersionName}\n获取最新: https://github.com/lizongying/my-tv/releases/"
binding.switchChannelReversal.run {
isChecked = SP.channelReversal
setOnCheckedChangeListener { _, isChecked ->
SP.channelReversal = isChecked
(activity as MainActivity).settingActive()
}
}
binding.switchChannelNum.run {
isChecked = SP.channelNum
setOnCheckedChangeListener { _, isChecked ->
SP.channelNum = isChecked
(activity as MainActivity).settingActive()
}
}
binding.switchBootStartup.run {
isChecked = SP.bootStartup
setOnCheckedChangeListener { _, isChecked ->
SP.bootStartup = isChecked
(activity as MainActivity).settingActive()
}
}
updateManager = UpdateManager(context, this, context.appVersionCode)
binding.checkVersion.setOnClickListener(OnClickListenerCheckVersion(updateManager))
return binding.root
}
fun setVersionName(versionName: String) {
binding.versionName.text = versionName
}
internal class OnClickListenerCheckVersion(private val updateManager: UpdateManager) :
View.OnClickListener {
override fun onClick(view: View?) {
updateManager.checkAndUpdate()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onDestroy() {
super.onDestroy()
// updateManager.destroy()
}
companion object {
const val TAG = "SettingFragment"
}
}
+7 -2
View File
@@ -5,13 +5,18 @@ import java.io.Serializable
data class TV(
var id: Int = 0,
var title: String,
var alias: String = "",
var videoUrl: List<String>,
var videoIndex: Int = 0,
var logo: String = "",
var channel: String = "",
var logo: Any = "",
var pid: String = "",
var sid: String = "",
var programId: String = "",
) : Serializable {
var needToken: Boolean = false,
var mustToken: Boolean = false,
) : Serializable {
override fun toString(): String {
return "TV{" +
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,189 @@
package com.lizongying.mytv
import android.app.DownloadManager
import android.app.DownloadManager.Request
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import com.lizongying.mytv.api.Release
import com.lizongying.mytv.requests.MyRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
class UpdateManager(
private var context: Context,
private var settingFragment: SettingFragment,
private var versionCode: Long
) :
ConfirmationDialogFragment.ConfirmationDialogListener {
private var myRequest = MyRequest()
private var release: Release? = null
private var downloadReceiver: DownloadReceiver? = null
fun checkAndUpdate() {
CoroutineScope(Dispatchers.Main).launch {
try {
release = myRequest.getRelease()
updateUI(release)
Log.i(TAG, "versionCode $versionCode ${release?.data?.versionCode}")
if (release != null) {
if (release?.data?.versionCode!! >= versionCode) {
val dialog = ConfirmationDialogFragment(this@UpdateManager)
dialog.show(settingFragment.fragmentManager, "ConfirmationDialogFragment")
} else {
Toast.makeText(context, "不需要更新", Toast.LENGTH_LONG)
.show()
}
}
} catch (e: Exception) {
Log.e(TAG, "Error occurred: ${e.message}", e)
}
}
}
private fun updateUI(release: Release?) {
if (release?.data?.versionName.isNullOrEmpty()) {
settingFragment.setVersionName("版本获取失败")
} else {
settingFragment.setVersionName("最新版本:${release?.data?.versionName!!}")
}
}
private fun startDownload(release: Release) {
val apkFileName = "my-tv-${release.data.versionName}.apk"
Log.i(TAG, "apkFileName $apkFileName")
val downloadManager =
context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = Request(Uri.parse(release.data.downloadUrl))
Log.i(TAG, "url ${Uri.parse(release.data.downloadUrl)}")
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, apkFileName)
request.setTitle("New Version Download")
request.setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
// 获取下载任务的引用
val downloadReference = downloadManager.enqueue(request)
downloadReceiver = DownloadReceiver(context, apkFileName, downloadReference)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.registerReceiver(
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
Context.RECEIVER_NOT_EXPORTED,
)
} else {
context.registerReceiver(
downloadReceiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
}
getDownloadProgress(context, downloadReference) { progress ->
println("Download progress: $progress%")
}
}
private fun getDownloadProgress(context: Context, downloadId: Long, progressListener: (Int) -> Unit) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val handler = Handler(Looper.getMainLooper())
val intervalMillis: Long = 1000
handler.post(object : Runnable {
override fun run() {
Log.i(TAG, "search")
val query = DownloadManager.Query().setFilterById(downloadId)
val cursor: Cursor = downloadManager.query(query)
cursor.use {
if (it.moveToFirst()) {
val bytesDownloadedIndex =
it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val bytesTotalIndex =
it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
// 检查列名是否存在
if (bytesDownloadedIndex != -1 && bytesTotalIndex != -1) {
val bytesDownloaded = it.getInt(bytesDownloadedIndex)
val bytesTotal = it.getInt(bytesTotalIndex)
if (bytesTotal != -1) {
val progress = (bytesDownloaded * 100L / bytesTotal).toInt()
progressListener(progress)
if (progress == 100) {
return
}
}
}
}
}
// handler.postDelayed(this, intervalMillis)
}
})
}
private class DownloadReceiver(
private val context: Context,
private val apkFileName: String,
private val downloadReference: Long
) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val reference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
Log.i(TAG, "reference $reference")
val progress = intent.getIntExtra("progress", 0)
Log.i(TAG, "progress $progress")
// 检查是否是我们发起的下载
if (reference == downloadReference) {
// 下载完成,触发安装
installNewVersion()
}
}
private fun installNewVersion() {
val installIntent = Intent(Intent.ACTION_VIEW)
val apkUri = Uri.fromFile(
File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
apkFileName
)
)
installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive")
installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(installIntent)
}
}
companion object {
private const val TAG = "UpdateManager"
}
override fun onConfirm() {
Log.i(TAG, "onConfirm $release")
release?.let { startDownload(it) }
}
override fun onCancel() {
}
fun destroy() {
if (downloadReceiver != null) {
context.unregisterReceiver(downloadReceiver)
Log.i(TAG, "destroy downloadReceiver")
}
}
}
+68 -2
View File
@@ -1,15 +1,81 @@
package com.lizongying.mytv
import android.content.res.Resources
import android.os.Build
import android.util.TypedValue
import com.google.gson.Gson
import com.lizongying.mytv.api.TimeResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object Utils {
private var between: Long = 0
fun getDateFormat(format: String): String {
return SimpleDateFormat(format, Locale.CHINA).format(Date())
return SimpleDateFormat(
format,
Locale.CHINA
).format(Date(System.currentTimeMillis() - between))
}
fun getDateTimestamp(): Long {
return Date().time / 1000
return (System.currentTimeMillis() - between) / 1000
}
fun setBetween(currentTimeMillis: Long) {
between = System.currentTimeMillis() - currentTimeMillis
}
suspend fun init() {
var currentTimeMillis: Long = 0
try {
currentTimeMillis = getTimestampFromServer()
} catch (e: Exception) {
println("Failed to retrieve timestamp from server: ${e.message}")
}
between = System.currentTimeMillis() - currentTimeMillis
}
/**
* 从服务器获取时间戳
* @return Long 时间戳
*/
private suspend fun getTimestampFromServer(): Long {
return withContext(Dispatchers.IO) {
val client = okhttp3.OkHttpClient.Builder()
.connectTimeout(500, java.util.concurrent.TimeUnit.MILLISECONDS)
.readTimeout(1, java.util.concurrent.TimeUnit.SECONDS).build()
val request = okhttp3.Request.Builder()
.url("https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp")
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val string = response.body()?.string()
Gson().fromJson(string, TimeResponse::class.java).data.t.toLong()
}
} catch (e: IOException) {
// Handle network errors
throw IOException("Error during network request", e)
}
}
}
fun dpToPx(dp: Float): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().displayMetrics
).toInt()
}
fun dpToPx(dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics
).toInt()
}
fun isTmallDevice() = Build.MANUFACTURER.equals("Tmall", ignoreCase = true)
}
@@ -13,8 +13,10 @@ import javax.net.ssl.X509TrustManager
class ApiClient {
private val yspUrl = "https://player-api.yangshipin.cn/"
private val myUrl = "https://lyrics.run/"
private val devUrl = "http://10.0.2.2:8081/"
private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/"
private val fUrl = "https://m.fengshows.com/"
private var okHttpClient = getUnsafeOkHttpClient()
@@ -34,6 +36,14 @@ class ApiClient {
.build().create(YSPTokenService::class.java)
}
val releaseService: ReleaseService by lazy {
Retrofit.Builder()
.baseUrl(myUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(ReleaseService::class.java)
}
val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder()
.baseUrl(protoUrl)
@@ -50,6 +60,14 @@ class ApiClient {
.build().create(YSPBtraceService::class.java)
}
val fAuthService: FAuthService by lazy {
Retrofit.Builder()
.baseUrl(fUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(FAuthService::class.java)
}
private fun getUnsafeOkHttpClient(): OkHttpClient {
try {
val trustAllCerts: Array<TrustManager> = arrayOf(
@@ -0,0 +1,37 @@
package com.lizongying.mytv.api
import android.util.Log
import okhttp3.MediaType
import okhttp3.RequestBody
import okio.BufferedSink
import java.io.IOException
data class Auth(
val code: Int,
val msg: String,
val data: AuthData,
)
data class AuthData(
val token: String,
)
data class AuthRequest(
var data: String,
) : RequestBody() {
override fun contentType(): MediaType? {
return MediaType.parse("application/x-www-form-urlencoded;charset=UTF-8")
}
override fun writeTo(sink: BufferedSink) {
try {
sink.writeUtf8(data)
} catch (e: IOException) {
Log.e(TAG, "$e")
}
}
companion object {
private const val TAG = "AuthRequest"
}
}
@@ -0,0 +1,10 @@
package com.lizongying.mytv.api
data class FAuth(
val data: Data,
) {
data class Data(
val live_url: String,
)
}
@@ -0,0 +1,23 @@
package com.lizongying.mytv.api
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
import retrofit2.http.Query
interface FAuthService {
@GET("api/v3/hub/live/auth-url")
fun getAuth(
@Header("token") token: String,
@Query("live_id") live_id: String = "",
@Query("live_qa") live_qa: String = "",
): Call<FAuth>
@GET("api/v3/live/{liveId}/resources")
fun getEPG(
@Path("liveId") liveId: String,
@Query("date") date: String,
@Query("dir") dir: String = "asc"
): Call<List<FEPG>>
}
@@ -0,0 +1,7 @@
package com.lizongying.mytv.api
data class FEPG(
val title: String,
val event_time: String,
)
@@ -3,9 +3,42 @@ package com.lizongying.mytv.api
data class Info(
val code: Int?,
val msg: String?,
val data: InfoData,
val data: Data,
) {
data class Data(
val token: String,
)
}
data class Token(
val e: Int?,
val t: String?,
)
data class InfoData(
val token: String,
data class InfoV2(
val f: String?,
val t: String?,
val e: Int?,
val c: Int?,
)
data class Release(
val code: Int?,
val msg: String?,
val data: Data,
) {
data class Data(
val versionName: String,
val versionCode: Int,
val downloadUrl: String,
val updateTime: Int,
)
}
data class TimeResponse(
val data: Time
) {
data class Time(
val t: String
)
}
@@ -13,6 +13,7 @@ data class LiveInfo(
)
data class LiveInfoData(
val extended_param: String,
val chanll: String,
val playurl: String,
val errinfo: String,
@@ -0,0 +1,11 @@
package com.lizongying.mytv.api
import retrofit2.Call
import retrofit2.http.GET
interface ReleaseService {
@GET("my-tv/v1/release")
fun getRelease(
): Call<Release>
}
@@ -1,15 +1,17 @@
package com.lizongying.mytv.api
import android.content.Context
import android.content.SharedPreferences
import com.lizongying.mytv.Encryptor
import com.lizongying.mytv.MainActivity
import com.lizongying.mytv.SP
import com.lizongying.mytv.Utils.getDateTimestamp
import com.lizongying.mytv.models.TVViewModel
import kotlin.math.floor
import kotlin.random.Random
class YSP(var context: Context) {
object YSP {
private const val TAG = "YSP"
private var cnlid = ""
private var livepid = ""
@@ -50,17 +52,13 @@ class YSP(var context: Context) {
private var signature = ""
private var encryptor: Encryptor? = null
private var sharedPref: SharedPreferences? = null
private var appid = "ysp_pc"
var token = ""
init {
if (context is MainActivity) {
encryptor = Encryptor()
encryptor!!.init(context)
sharedPref = (context as MainActivity).getPreferences(Context.MODE_PRIVATE)
}
private var encryptor = Encryptor()
fun init(context: Context) {
encryptor.init(context)
guid = getGuid()
}
@@ -77,15 +75,23 @@ class YSP(var context: Context) {
timeStr = getTimeStr()
// guid = "lq3oqitm_1e15dnzgjnb"
// randStr = "BfcCPQp8Hq"
// timeStr = "1702166501"
cKey =
encryptor!!.encrypt(cnlid, timeStr, appVer, guid, platform)
encryptor.encrypt(cnlid, timeStr, appVer, guid, platform)
signature = getSignature()
return """{"cnlid":"$cnlid","livepid":"$livepid","stream":"$stream","guid":"$guid","cKey":"$cKey","adjust":$adjust,"sphttps":"$sphttps","platform":"$platform","cmd":"$cmd","encryptVer":"$encryptVer","dtype":"$dtype","devid":"$devid","otype":"$otype","appVer":"$appVer","app_version":"$appVersion","rand_str":"$randStr","channel":"$channel","defn":"$defn","signature":"$signature"}"""
// return """{"cnlid":"2000203803","livepid":"600001801","stream":"2","guid":"lq1y36mb_ccfwmja9zan","cKey":"--01A9F5E89BB86A0C61F4025BEDE15309B6913A79FD1AF1EE7F5EC9C7605F377D1D2281488385C32DEB9E7D0DD3559CB700BD7AF44DD5C9DE0AE14D94B8214027B5D664C108AEE23532348DCC61B86F7C8FBB6CF14D588E6093A25E97DF6D66F4882AB28F17016472DD43D45EF076B7F505176A5E8DEDF2662E5F9AB12B69CB20BCE1579BE724091F3AF6826AE34B713906F3FE139C3783F80EECBD08416DC525E1","adjust":1,"sphttps":"1","platform":"5910204","cmd":"2","encryptVer":"8.1","dtype":"1","devid":"devid","otype":"ojson","appVer":"V1.0.0","app_version":"V1.0.0","rand_str":"IOS2soOw44","channel":"ysp_tx","defn":"fhd","signature":"6ca945f651817de8c6e6910457ceafd6"}"""
}
fun getAuthData(tvModel: TVViewModel): String {
livepid = tvModel.pid.value!!
randStr = getRand()
if (tvModel.retryTimes > 0) {
guid = newGuid()
}
signature = getAuthSignature()
return """pid=$livepid&guid=$guid&appid=$appid&rand_str=$randStr&signature=$signature"""
}
private fun getTimeStr(): String {
@@ -99,23 +105,17 @@ class YSP(var context: Context) {
}
fun getGuid(): String {
var guid = sharedPref?.getString("guid", "")
if (guid == null || guid.length < 18) {
var guid = SP.guid
if (guid.length < 18) {
guid = generateGuid()
with(sharedPref!!.edit()) {
putString("guid", guid)
apply()
}
SP.guid = guid
}
return guid
}
private fun newGuid(): String {
guid = generateGuid()
with(sharedPref!!.edit()) {
putString("guid", guid)
apply()
}
SP.guid = guid
return guid
}
@@ -132,12 +132,14 @@ class YSP(var context: Context) {
private fun getSignature(): String {
val e =
"adjust=${adjust}&appVer=${appVer}&app_version=$appVersion&cKey=$cKey&channel=$channel&cmd=$cmd&cnlid=$cnlid&defn=${defn}&devid=${devid}&dtype=${dtype}&encryptVer=${encryptVer}&guid=${guid}&livepid=${livepid}&otype=${otype}&platform=${platform}&rand_str=${randStr}&sphttps=${sphttps}&stream=${stream}".toByteArray()
val hashedData = encryptor?.hash(e) ?: return ""
val hashedData = encryptor.hash(e) ?: return ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
}
companion object {
private const val TAG = "YSP"
private fun getAuthSignature(): String {
val e =
"appid=${appid}&guid=${guid}&pid=${livepid}&rand_str=${randStr}".toByteArray()
val hashedData = encryptor.hash2(e) ?: return ""
return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
}
}
@@ -13,9 +13,23 @@ interface YSPApiService {
"content-type: application/json;charset=UTF-8",
"referer: https://www.yangshipin.cn/",
"yspappid: 519748109",
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
fun getLiveInfo(
@Header("cookie") cookie: String,
@Header("Yspplayertoken") token: String,
@Body request: LiveInfoRequest,
): Call<LiveInfo>
@POST("v1/player/auth")
@Headers(
"content-type: application/x-www-form-urlencoded;charset=UTF-8",
"referer: https://www.yangshipin.cn/",
"yspappid: 519748109",
"user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
fun getAuth(
@Header("cookie") cookie: String,
@Body request: AuthRequest,
): Call<Auth>
}
@@ -2,10 +2,21 @@ package com.lizongying.mytv.api
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface YSPTokenService {
@GET("my-tv/v1/info")
fun getInfo(
@Query("token") token: String = "",
): Call<Info>
@GET("my-tv/v2/token")
fun getToken(
@Query("token") token: String = "",
): Call<Token>
@GET("my-tv/v2/info")
fun getInfoV2(
): Call<InfoV2>
}
@@ -0,0 +1,7 @@
package com.lizongying.mytv.models
data class EPG(
val title: String,
val beginTime: Int,
)
@@ -8,7 +8,9 @@ class TVListViewModel : ViewModel() {
var maxNum = mutableListOf<Int>()
private val tvListViewModel = MutableLiveData<MutableList<TVViewModel>>()
private val _tvListViewModel = MutableLiveData<MutableList<TVViewModel>>()
val tvListViewModel: LiveData<MutableList<TVViewModel>>
get() = _tvListViewModel
private val _itemPosition = MutableLiveData<Int>()
val itemPosition: LiveData<Int>
@@ -18,22 +20,20 @@ class TVListViewModel : ViewModel() {
val itemPositionCurrent: LiveData<Int>
get() = _itemPositionCurrent
fun getTVListViewModel(): MutableLiveData<MutableList<TVViewModel>> {
return tvListViewModel
}
fun addTVViewModel(tvViewModel: TVViewModel) {
val currentTVModelList = tvListViewModel.value ?: mutableListOf()
currentTVModelList.add(tvViewModel)
tvListViewModel.value = currentTVModelList
if (_tvListViewModel.value == null) {
_tvListViewModel.value = mutableListOf(tvViewModel)
} else {
_tvListViewModel.value?.add(tvViewModel)
}
}
fun getTVViewModel(id: Int): TVViewModel? {
return tvListViewModel.value?.get(id)
return _tvListViewModel.value?.get(id)
}
fun getTVViewModelCurrent(): TVViewModel? {
return _itemPositionCurrent.value?.let { tvListViewModel.value?.get(it) }
return _itemPositionCurrent.value?.let { _tvListViewModel.value?.get(it) }
}
fun setItemPosition(position: Int) {
@@ -46,6 +46,9 @@ class TVListViewModel : ViewModel() {
}
fun size(): Int {
return tvListViewModel.value!!.size
if (_tvListViewModel.value == null) {
return 0
}
return _tvListViewModel.value!!.size
}
}
@@ -11,17 +11,22 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.hls.HlsMediaSource
import com.lizongying.mytv.TV
import com.lizongying.mytv.Utils.getDateTimestamp
import com.lizongying.mytv.api.FEPG
import com.lizongying.mytv.proto.Ysp.cn.yangshipin.omstv.common.proto.programModel.Program
import java.text.SimpleDateFormat
import java.util.TimeZone
class TVViewModel(private var tv: TV) : ViewModel() {
private var rowPosition: Int = 0
private var itemPosition: Int = 0
var retryTimes: Int = 0
var retryMaxTimes: Int = 5
var programUpdateTime: Long = 0
var retryTimes = 0
var retryMaxTimes = 8
var tokenRetryTimes = 0
var tokenRetryMaxTimes = 0
var tokenFHRetryTimes = 0
var tokenFHRetryMaxTimes = 2
private val _errInfo = MutableLiveData<String>()
val errInfo: LiveData<String>
@@ -31,9 +36,9 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val programId: LiveData<String>
get() = _programId
private var _program = MutableLiveData<MutableList<Program>>()
val program: LiveData<MutableList<Program>>
get() = _program
private var _epg = MutableLiveData<MutableList<EPG>>()
val epg: LiveData<MutableList<EPG>>
get() = _epg
private val _id = MutableLiveData<Int>()
val id: LiveData<Int>
@@ -51,8 +56,8 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val videoIndex: LiveData<Int>
get() = _videoIndex
private val _logo = MutableLiveData<String>()
val logo: LiveData<String>
private val _logo = MutableLiveData<Any>()
val logo: LiveData<Any>
get() = _logo
private val _pid = MutableLiveData<String>()
@@ -71,31 +76,8 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val ready: LiveData<Boolean>
get() = _ready
private var mMinimumLoadableRetryCount = 5
var seq = 0
var needToken = false
private val channelsNeedToken = arrayOf(
"CCTV3 综艺",
"CCTV6 电影",
"CCTV8 电视剧",
"风云剧场",
"第一剧场",
"怀旧剧场",
"世界地理",
"风云音乐",
"兵器科技",
"风云足球",
"高尔夫网球",
"女性时尚",
"央视文化精品",
"央视台球",
"电视指南",
"卫生健康",
)
fun addVideoUrl(url: String) {
if (_videoUrl.value?.isNotEmpty() == true) {
if (_videoUrl.value!!.last().contains("cctv.cn")) {
@@ -132,10 +114,6 @@ class TVViewModel(private var tv: TV) : ViewModel() {
_videoIndex.value = videoIndex
}
fun setLogo(url: String) {
_logo.value = url
}
init {
_id.value = tv.id
_title.value = tv.title
@@ -145,11 +123,6 @@ class TVViewModel(private var tv: TV) : ViewModel() {
_programId.value = tv.programId
_pid.value = tv.pid
_sid.value = tv.sid
_program.value = mutableListOf()
if (tv.title in channelsNeedToken) {
needToken = true
}
}
fun getRowPosition(): Int {
@@ -180,37 +153,23 @@ class TVViewModel(private var tv: TV) : ViewModel() {
return tv
}
fun getProgramOne(): Program? {
val programNew =
(_program.value?.filter { it.et > getDateTimestamp() })?.toMutableList()
if (programNew != null && _program.value != programNew) {
_program.value = programNew
}
if (_program.value!!.isEmpty()) {
return null
}
return _program.value?.first()
fun addYEPG(p: MutableList<Program>) {
_epg.value = p.map { EPG(it.name, it.st.toInt()) }.toMutableList()
}
fun addProgram(p: MutableList<Program>) {
val timestamp = getDateTimestamp()
// after now & not empty & different
val p1 = (p.filter { it.et > timestamp }).toMutableList()
if (p1.isEmpty() || _program.value == p1) {
return
}
if (_program.value!!.isEmpty()) {
_program.value = p1
} else {
_program.value =
((_program.value?.filter { it.et > timestamp && it.st < p1.first().st })?.plus(
p1
))?.toMutableList()
private fun formatFTime(s: String): Int {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val date = dateFormat.parse(s.substring(0, 19))
if (date != null) {
return (date.time / 1000).toInt()
}
return 0
}
fun addFEPG(p: List<FEPG>) {
_epg.value = p.map { EPG(it.title, formatFTime(it.event_time)) }.toMutableList()
}
private var mHeaders: Map<String, String>? = mapOf()
@@ -218,22 +177,25 @@ class TVViewModel(private var tv: TV) : ViewModel() {
mHeaders = headers
}
fun setMinimumLoadableRetryCount(minimumLoadableRetryCount: Int) {
mMinimumLoadableRetryCount = minimumLoadableRetryCount
}
/**
* (playerView?.player as ExoPlayer).setMediaSource(tvViewModel.buildSource())
*/
@OptIn(UnstableApi::class)
fun buildSource(videoUrl: String, mHeaders: Map<String, String>?): HlsMediaSource {
fun buildSource(): HlsMediaSource {
val httpDataSource = DefaultHttpDataSource.Factory()
mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) }
return HlsMediaSource.Factory(httpDataSource).createMediaSource(
MediaItem.fromUri(
Uri.parse(videoUrl)
Uri.parse(getVideoUrlCurrent())
)
)
}
fun getVideoUrlCurrent(): String {
return _videoUrl.value!![_videoIndex.value!!]
}
companion object {
private const val TAG = "TVViewModel"
}
@@ -0,0 +1,44 @@
package com.lizongying.mytv.requests
import com.lizongying.mytv.api.ApiClient
import com.lizongying.mytv.api.Release
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class MyRequest {
private var releaseService = ApiClient().releaseService
suspend fun getRelease(): Release? {
return withContext(Dispatchers.IO) {
fetchRelease()
}
}
private suspend fun fetchRelease(): Release? {
return suspendCoroutine { continuation ->
releaseService.getRelease()
.enqueue(object : Callback<Release> {
override fun onResponse(call: Call<Release>, response: Response<Release>) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
continuation.resume(null)
}
}
override fun onFailure(call: Call<Release>, t: Throwable) {
continuation.resume(null)
}
})
}
}
companion object {
private const val TAG = "MyRequest"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/channel_fragment"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_gravity="end|top"
android:layout_marginTop="20dp"
android:layout_marginEnd="50dp">
<TextView
android:id="@+id/channel_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="0dp"
android:textColor="#FFEEEEEE"
android:textStyle="bold"
android:textSize="50sp" />
</FrameLayout>
+80
View File
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingTop="20dp"
android:paddingLeft="7dp"
android:paddingRight="7dp"
android:background="#FF263238"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="16sp" />
<TextView
android:id="@+id/version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
/>
<LinearLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<Button
android:id="@+id/check_version"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@string/check_version"
/>
<ProgressBar
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/version_name"
android:text=""
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<Switch
android:id="@+id/switch_channel_reversal"
android:text="@string/title_channel_reversal"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/switch_channel_num"
android:text="@string/title_channel_num"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Switch
android:id="@+id/switch_boot_startup"
android:text="@string/title_boot_startup"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<ImageView
android:layout_width="300dp"
android:layout_height="wrap_content"
android:src="@drawable/appreciate"
android:background="@color/white" />
</LinearLayout>
</FrameLayout>
-1
View File
@@ -12,7 +12,6 @@
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:contentDescription="@string/logo"
android:padding="10dp" />
<LinearLayout
+8
View File
@@ -7,6 +7,14 @@
android:background="@color/black"
android:keepScreenOn="true">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:resize_mode="fill"
app:use_controller="false" />
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
+954
View File
@@ -0,0 +1,954 @@
[
{
"id": 0,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d57905b93540bd15f0c48230dbbbff7ee0d645ff539e38866e2d15c8b9f7dfcd.png?imageMogr2/format/webp",
"pid": "600001859",
"sid": "2000210103",
"programId": "600001859",
"needToken": false,
"mustToken": false,
"title": "CCTV1 综合",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226231/index.m3u8"
]
},
{
"id": 1,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/20115388de0207131af17eac86c33049b95d69eaff064e55653a1b941810a006.png?imageMogr2/format/webp",
"pid": "600001800",
"sid": "2000203603",
"programId": "600001800",
"needToken": true,
"mustToken": false,
"title": "CCTV2 财经",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226195/index.m3u8"
]
},
{
"id": 2,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/7b7a65c712450da3deb6ca66fbacf4f9aee00d3f20bd80eafb5ada01ec63eb3a.png?imageMogr2/format/webp",
"pid": "600001801",
"sid": "2000203803",
"programId": "600001801",
"needToken": true,
"mustToken": true,
"title": "CCTV3 综艺",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226397/index.m3u8"
]
},
{
"id": 3,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f357e58fdbcc076a3d65e1f958c942b2e14f14342c60736ceed98b092d35356a.png?imageMogr2/format/webp",
"pid": "600001814",
"sid": "2000204803",
"programId": "600001814",
"needToken": false,
"mustToken": false,
"title": "CCTV4 中文国际",
"videoUrl": [
"http://39.134.24.161/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226191/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226191/index.m3u8"
]
},
{
"id": 4,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/0a6a7138952675983a3d854df7688557b286d59aa06166edae51506f9204d655.png?imageMogr2/format/webp",
"pid": "600001818",
"sid": "2000205103",
"programId": "600001818",
"needToken": true,
"mustToken": false,
"title": "CCTV5 体育",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226395/index.m3u8"
]
},
{
"id": 5,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/741515efda91f03f455df8a7da4ee11fa9329139c276435cf0a9e2af398d5bf2.png?imageMogr2/format/webp",
"pid": "600001802",
"sid": "2013693901",
"programId": "600001802",
"needToken": true,
"mustToken": true,
"title": "CCTV6 电影",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226393/index.m3u8"
]
},
{
"id": 6,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/b29af94e295ebdf646cefb68122c429b9cd921f498ca20d2d8070252536f9ff9.png?imageMogr2/format/webp",
"pid": "600004092",
"sid": "2000510003",
"programId": "600004092",
"needToken": true,
"mustToken": false,
"title": "CCTV7 国防军事",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226192/index.m3u8"
]
},
{
"id": 7,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ad51de94426a0ba039e6dd6a8534ea98ecc813a6176bde87b4f18cc34d6d7590.png?imageMogr2/format/webp",
"pid": "600001803",
"sid": "2000203903",
"programId": "600001803",
"needToken": true,
"mustToken": true,
"title": "CCTV8 电视剧",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226391/index.m3u8"
]
},
{
"id": 8,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2ed1b4deeca179d5db806bb941790f82eb92a1b7299c1c38fe027f95a5caee5e.png?imageMogr2/format/webp",
"pid": "600004078",
"sid": "2000499403",
"programId": "600004078",
"needToken": true,
"mustToken": false,
"title": "CCTV9 纪录",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226197/index.m3u8"
]
},
{
"id": 9,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/aa6157ec65188cd41826e5a2f088c3d6d153205f5f6428258d12c59999e221aa.png?imageMogr2/format/webp",
"pid": "600001805",
"sid": "2000203503",
"programId": "600001805",
"needToken": true,
"mustToken": false,
"title": "CCTV10 科教",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226189/index.m3u8"
]
},
{
"id": 10,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ed12ed7c7a1034dae4350011fe039284c5d5a836506b28c9e32e3c75299625c0.png?imageMogr2/format/webp",
"pid": "600001806",
"sid": "2000204103",
"programId": "600001806",
"needToken": true,
"mustToken": false,
"title": "CCTV11 戏曲",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226240/index.m3u8"
]
},
{
"id": 11,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/484083cffaa40df7e659565e8cb4d1cc740158a185512114167aa21fa0c59240.png?imageMogr2/format/webp",
"pid": "600001807",
"sid": "2000202603",
"programId": "600001807",
"needToken": true,
"mustToken": false,
"title": "CCTV12 社会与法",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226190/index.m3u8"
]
},
{
"id": 12,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/266da7b43c03e2312186b4a999e0f060e8f15b10d2cc2c9aa32171819254cf1a.png?imageMogr2/format/webp",
"pid": "600001811",
"sid": "2000204603",
"programId": "600001811",
"needToken": false,
"mustToken": false,
"title": "CCTV13 新闻",
"videoUrl": [
"https://live-play.cctvnews.cctv.com/cctv/merge_cctv13.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226233/index.m3u8"
]
},
{
"id": 13,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/af6b603896938dc346fbb16abfc63c12cba54b0ec9d18770a15d347d115f12d5.png?imageMogr2/format/webp",
"pid": "600001809",
"sid": "2000204403",
"programId": "600001809",
"needToken": true,
"mustToken": false,
"title": "CCTV14 少儿",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226193/index.m3u8"
]
},
{
"id": 14,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2ceee92188ef684efe0d8b90839c4f3ad450d179dc64d59beff417059453af47.png?imageMogr2/format/webp",
"pid": "600001815",
"sid": "2000205003",
"programId": "600001815",
"needToken": true,
"mustToken": false,
"title": "CCTV15 音乐",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225785/index.m3u8"
]
},
{
"id": 15,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/53793fa7bacd3a93ff6dc5d2758418985e1f952a316c335d663b572d8bdcd74d.png?imageMogr2/format/webp",
"pid": "600098637",
"sid": "2012375003",
"programId": "600098637",
"needToken": true,
"mustToken": false,
"title": "CCTV16 奥林匹克",
"videoUrl": [
"http://39.134.24.162/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226921/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226921/index.m3u8"
]
},
{
"id": 16,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ddef563072f8bad2bea5b9e52674cb7b4ed50efb20c26e61994dfbdf05c1e3c0.png?imageMogr2/format/webp",
"pid": "600001810",
"sid": "2000204203",
"programId": "600001810",
"needToken": true,
"mustToken": false,
"title": "CCTV17 农业农村",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226198/index.m3u8"
]
},
{
"id": 17,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/649ad76a90bfef55b05db9fe52e006487280f619089099d5dc971e387fc6eff0.png?imageMogr2/format/webp",
"pid": "600001817",
"sid": "2000204503",
"programId": "600001817",
"needToken": true,
"mustToken": false,
"title": "CCTV5+ 体育赛事",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226221/index.m3u8"
]
},
{
"id": 18,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3e9d06fd7244d950df5838750f1c6ac3456e172b51caca2c16d2282125b111e8.png?imageMogr2/format/webp",
"pid": "600002264",
"sid": "2000266303",
"programId": "600002264",
"needToken": true,
"mustToken": false,
"title": "CCTV4K 超高清",
"videoUrl": [
""
]
},
{
"id": 19,
"videoIndex": 0,
"channel": "央视",
"logo": "",
"pid": "600156816",
"sid": "2020603421",
"programId": "",
"needToken": false,
"mustToken": false,
"title": "CCTV8K 超高清",
"videoUrl": [
""
]
},
{
"id": 20,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4d549e53e6d0f632d5a633d1945280797b153e588f919221a07faa869812cc89.png?imageMogr2/format/webp",
"pid": "600099658",
"sid": "2012513603",
"programId": "600099658",
"needToken": true,
"mustToken": true,
"title": "风云剧场",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226950/index.m3u8"
]
},
{
"id": 21,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a556bd7d93ce65e18f243a8892b5604f4faa994a4897315914216a710a706208.png?imageMogr2/format/webp",
"pid": "600099655",
"sid": "2012514403",
"programId": "600099655",
"needToken": true,
"mustToken": true,
"title": "第一剧场",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226959/index.m3u8"
]
},
{
"id": 22,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/5661bd04fecdb6e899f801147a22ab5d3a475bf2b62e30aec2c0023190ebc9b1.png?imageMogr2/format/webp",
"pid": "600099620",
"sid": "2012511203",
"programId": "600099620",
"needToken": true,
"mustToken": true,
"title": "怀旧剧场",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226972/index.m3u8"
]
},
{
"id": 23,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bb3c6c9e145d698137f5bb64a582021a01b51344b929003630eb769ea65832a9.png?imageMogr2/format/webp",
"pid": "600099637",
"sid": "2012513303",
"programId": "600099637",
"needToken": true,
"mustToken": true,
"title": "世界地理",
"videoUrl": [
""
]
},
{
"id": 24,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bbf1d024c5228b8dd128b0e3cb1717d173fab4ee84c3a4c8a57b1a215362ca3b.png?imageMogr2/format/webp",
"pid": "600099660",
"sid": "2012514103",
"programId": "600099660",
"needToken": true,
"mustToken": true,
"title": "风云音乐",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226953/index.m3u8"
]
},
{
"id": 25,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4c6b6a6d3839889f34d33db3c2f80233b26b74d3489b393487635f8704e70796.png?imageMogr2/format/webp",
"pid": "600099649",
"sid": "2012513403",
"programId": "600099649",
"needToken": true,
"mustToken": true,
"title": "兵器科技",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226975/index.m3u8"
]
},
{
"id": 26,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/cd1e2bb52b06a991de168733e5ff0f1d85adc8042d40c8f393f723543e5dd08a.png?imageMogr2/format/webp",
"pid": "600099636",
"sid": "2012514203",
"programId": "600099636",
"needToken": true,
"mustToken": true,
"title": "风云足球",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226984/index.m3u8"
]
},
{
"id": 27,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/cdd1b31ede7a5ad049ed53d9a072422f829e72dd062ed2c19e077fdd01699071.png?imageMogr2/format/webp",
"pid": "600099659",
"sid": "2012512503",
"programId": "600099659",
"needToken": true,
"mustToken": true,
"title": "高尔夫网球",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226978/index.m3u8"
]
},
{
"id": 28,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/fa28955ce8b2539d728bf4c6a13a46ff57ad76eae46627f7bcfb1ed8a613d3fc.png?imageMogr2/format/webp",
"pid": "600099650",
"sid": "2012513903",
"programId": "600099650",
"needToken": true,
"mustToken": true,
"title": "女性时尚",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226969/index.m3u8"
]
},
{
"id": 29,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/14ac5ce40482cacd3d4b37435222bfe86af2b452a2f04ecbfc1d13d76edd7c57.png?imageMogr2/format/webp",
"pid": "600099653",
"sid": "2012513803",
"programId": "600099653",
"needToken": true,
"mustToken": true,
"title": "央视文化精品",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226981/index.m3u8"
]
},
{
"id": 30,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/10e14a92478011aa6c3c8562e62127f3b1908e29fcd78e4b2b24b9e6d3ec2fbc.png?imageMogr2/format/webp",
"pid": "600099652",
"sid": "2012513703",
"programId": "600099652",
"needToken": true,
"mustToken": true,
"title": "央视台球",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226956/index.m3u8"
]
},
{
"id": 31,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/244d72c0eb1615ed7d51c2f5db5a67f306aa3f58c05bc2d34de3aa7e956dc8c9.png?imageMogr2/format/webp",
"pid": "600099656",
"sid": "2012514003",
"programId": "600099656",
"needToken": true,
"mustToken": true,
"title": "电视指南",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888893/224/3221226987/index.m3u8"
]
},
{
"id": 32,
"videoIndex": 0,
"channel": "央视",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/54a6863656fdfd8f803be193ddf22441c5000a108833889816fd2d8911715ce8.png?imageMogr2/format/webp",
"pid": "600099651",
"sid": "2012513503",
"programId": "600099651",
"needToken": true,
"mustToken": true,
"title": "卫生健康",
"videoUrl": [
""
]
},
{
"id": 33,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/9bd372ca292a82ce3aa08772b07efc4af1f85c21d1f268ea33440c49e9a0a488.png?imageMogr2/format/webp",
"pid": "600002483",
"sid": "2000292403",
"programId": "600002483",
"needToken": true,
"mustToken": false,
"title": "东方卫视",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226217/index.m3u8"
]
},
{
"id": 34,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4120e89d3079d08aa17d382f69a2308ec70839b278367763c34a34666c75cb88.png?imageMogr2/format/webp",
"pid": "600002475",
"sid": "2000296203",
"programId": "600002475",
"needToken": true,
"mustToken": false,
"title": "湖南卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226307/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226211/index.m3u8"
]
},
{
"id": 35,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/7a6be5a2bb1dc53a945c016ff1f525dc4a84c51db371c15c89aa55404b0ba784.png?imageMogr2/format/webp",
"pid": "600002508",
"sid": "2000294503",
"programId": "600002508",
"needToken": true,
"mustToken": false,
"title": "湖北卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226477/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226194/index.m3u8"
]
},
{
"id": 36,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/ac4ed6058a87c101ae7147ebc38905d0cae047fb73fd277ee5049b84f52bda36.png?imageMogr2/format/webp",
"pid": "600002505",
"sid": "2000281303",
"programId": "600002505",
"needToken": true,
"mustToken": false,
"title": "辽宁卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226546/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226201/index.m3u8"
]
},
{
"id": 37,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/380ad685c0c1d5b2c902246b8d2df6d3f9b45e2837abcfe493075bbded597a31.png?imageMogr2/format/webp",
"pid": "600002521",
"sid": "2000295603",
"programId": "600002521",
"needToken": true,
"mustToken": false,
"title": "江苏卫视",
"videoUrl": [
"http://39.134.24.166/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226200/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226200/index.m3u8"
]
},
{
"id": 38,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3c760d0d00463855890e8a1864ea4a6b6dd66b90c29b4ac714a4b17c16519871.png?imageMogr2/format/webp",
"pid": "600002503",
"sid": "2000294103",
"programId": "600002503",
"needToken": true,
"mustToken": false,
"title": "江西卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226344/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225764/index.m3u8"
]
},
{
"id": 39,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/22d403f07a7cf5410b3ad3ddb65a11aa229a32475fac213f5344c9f0ec330ca1.png?imageMogr2/format/webp",
"pid": "600002513",
"sid": "2000294803",
"programId": "600002513",
"needToken": true,
"mustToken": false,
"title": "山东卫视",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226209/index.m3u8"
]
},
{
"id": 40,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/28886880a4dc0f06fb7e0a528a1def0591d61a65870e29176ede0cc92033bbfd.png?imageMogr2/format/webp",
"pid": "600002485",
"sid": "2000292703",
"programId": "600002485",
"needToken": true,
"mustToken": false,
"title": "广东卫视",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226216/index.m3u8"
]
},
{
"id": 41,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/54b7e97cb816bb223fe05f3fc44da2c7820eb66e8550c19d23100f2c414ecc38.png?imageMogr2/format/webp",
"pid": "600002509",
"sid": "2000294203",
"programId": "600002509",
"needToken": true,
"mustToken": false,
"title": "广西卫视",
"videoUrl": [
"http://live.gxrb.com.cn/tv/gxtvlive03/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225770/index.m3u8"
]
},
{
"id": 42,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/657651f411de2673d1770d9a78b44c1265704f7468cc41d4be7f51d630768494.png?imageMogr2/format/webp",
"pid": "600002531",
"sid": "2000297803",
"programId": "600002531",
"needToken": true,
"mustToken": false,
"title": "重庆卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226409/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226202/index.m3u8"
]
},
{
"id": 43,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/74925962148a6d31c85808b6cd4e444c2a54bab393d2c5fc85e960b50e22fa86.png?imageMogr2/format/webp",
"pid": "600002525",
"sid": "2000296103",
"programId": "600002525",
"needToken": true,
"mustToken": false,
"title": "河南卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226480/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225767/index.m3u8"
]
},
{
"id": 44,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d545becdc81c60197b08c7f47380705e4665ed3fe55efc8b855e486f6e655378.png?imageMogr2/format/webp",
"pid": "600002493",
"sid": "2000293403",
"programId": "600002493",
"needToken": true,
"mustToken": false,
"title": "河北卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226406/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225750/index.m3u8"
]
},
{
"id": 45,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/4eb45f4781d33d872af027dc01c941559aab55667dd99cc5c22bef7037807b13.png?imageMogr2/format/webp",
"pid": "600002490",
"sid": "2000293303",
"programId": "600002490",
"needToken": true,
"mustToken": false,
"title": "贵州卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226474/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225793/index.m3u8"
]
},
{
"id": 46,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f4f23633c578beea49a3841d88d3490100f029ee349059fa532869db889872c5.png?imageMogr2/format/webp",
"pid": "600002309",
"sid": "2000272103",
"programId": "600002309",
"needToken": true,
"mustToken": false,
"title": "北京卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221225728/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226222/index.m3u8"
]
},
{
"id": 47,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d8273ae9be698ce2db21f5b886ecac95a73429593f93713c60ed8c12c38bf0d3.png?imageMogr2/format/webp",
"pid": "600002498",
"sid": "2000293903",
"programId": "600002498",
"needToken": true,
"mustToken": false,
"title": "黑龙江卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226327/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226215/index.m3u8"
]
},
{
"id": 48,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a66c836bd98ba3e41a2e9a570d4b9c50dedc6839e9de333e2e78212ad505f37e.png?imageMogr2/format/webp",
"pid": "600002520",
"sid": "2000295503",
"programId": "600002520",
"needToken": true,
"mustToken": false,
"title": "浙江卫视",
"videoUrl": [
"http://hw-m-l.cztv.com/channels/lantian/channel01/1080p.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226199/index.m3u8"
]
},
{
"id": 49,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/f35fa04b51b1ee4984b03578b65403570868ebca03c6c01e11b097f999a58d9b.png?imageMogr2/format/webp",
"pid": "600002532",
"sid": "2000298003",
"programId": "600002532",
"needToken": true,
"mustToken": false,
"title": "安徽卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226391/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226203/index.m3u8"
]
},
{
"id": 50,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/d59fec04c902e3581c617136d02d4b9b8c4cbe64272781ddd3525e80c823edb7.png?imageMogr2/format/webp",
"pid": "600002481",
"sid": "2000292203",
"programId": "600002481",
"needToken": true,
"mustToken": false,
"title": "深圳卫视",
"videoUrl": [
"http://39.134.24.166/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226205/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226205/index.m3u8"
]
},
{
"id": 51,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3276a414ae0eaa0f116f2045cd913367967d0c7c1e978e8621ac3879436c6ed7.png?imageMogr2/format/webp",
"pid": "600002516",
"sid": "2000295003",
"programId": "600002516",
"needToken": true,
"mustToken": false,
"title": "四川卫视",
"videoUrl": [
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225768/index.m3u8",
"http://39.134.24.166/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225768/index.m3u8"
]
},
{
"id": 52,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/3208fe6564a293c21b711333fb3edb05bb5b406cff840573c9a8d839680a1579.png?imageMogr2/format/webp",
"pid": "600002484",
"sid": "2000292503",
"programId": "600002484",
"needToken": true,
"mustToken": false,
"title": "东南卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226341/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225766/index.m3u8"
]
},
{
"id": 53,
"videoIndex": 0,
"channel": "地方",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/6e060391fde0469801fc3d84dbf204b4f8d650d251f17d7595a6964c0bb99e81.png?imageMogr2/format/webp",
"pid": "600002506",
"sid": "2000291503",
"programId": "600002506",
"needToken": true,
"mustToken": false,
"title": "海南卫视",
"videoUrl": [
"http://ottrrs.hl.chinamobile.com/PLTV/88888888/224/3221226465/index.m3u8",
"http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225769/index.m3u8"
]
},
{
"id": 54,
"videoIndex": 0,
"channel": "地方",
"logo": "",
"pid": "600152137",
"sid": "2019927003",
"programId": "",
"needToken": true,
"mustToken": false,
"title": "天津卫视",
"videoUrl": [
""
]
},
{
"id": 55,
"videoIndex": 0,
"channel": "地方",
"logo": "",
"pid": "600152138",
"sid": "2019927403",
"programId": "",
"needToken": true,
"mustToken": false,
"title": "新疆卫视",
"videoUrl": [
""
]
},
{
"id": 56,
"videoIndex": 0,
"channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a72dff758ca1c17cd0ecc8cedc11b893d208f409d5e6302faa0e9d298848abc3.png?imageMogr2/format/webp",
"pid": "600014550",
"sid": "2001656803",
"programId": "600014550",
"needToken": false,
"mustToken": false,
"title": "CGTN",
"videoUrl": [
"http://live.cgtn.com/1000/prog_index.m3u8"
]
},
{
"id": 57,
"videoIndex": 0,
"channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/a8d0046a47433d952bf6ed17062deb8bd2184ba9aec0f7781df6bf9487a3ffcf.png?imageMogr2/format/webp",
"pid": "600084704",
"sid": "2010153503",
"programId": "600084704",
"needToken": false,
"mustToken": false,
"title": "CGTN 法语频道",
"videoUrl": [
"https://livefr.cgtn.com/1000f/prog_index.m3u8"
]
},
{
"id": 58,
"videoIndex": 0,
"channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/bf0a820893cbaf20dd0333e27042e1ef9c8806e5b602b6a8c95af399db0bc77a.png?imageMogr2/format/webp",
"pid": "600084758",
"sid": "2010152603",
"programId": "600084758",
"needToken": false,
"mustToken": false,
"title": "CGTN 俄语频道",
"videoUrl": [
"http://liveru.cgtn.com/1000r/prog_index.m3u8"
]
},
{
"id": 59,
"videoIndex": 0,
"channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202306/2e44e2aa3e7a1cedf07fd0ae59fe69e86a60a2632660a006e3e9e7397b2d107e.png?imageMogr2/format/webp",
"pid": "600084782",
"sid": "2010155203",
"programId": "600084782",
"needToken": false,
"mustToken": false,
"title": "CGTN 阿拉伯语频道",
"videoUrl": [
"http://livear.cgtn.com/1000a/prog_index.m3u8"
]
},
{
"id": 60,
"videoIndex": 0,
"channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202309/7c337e3dbe64402ec7e4678a619a4a6d95144e42f35161181ff78e143b7cf67a.png?imageMogr2/format/webp",
"pid": "600084744",
"sid": "2010152503",
"programId": "600084744",
"needToken": false,
"mustToken": false,
"title": "CGTN 西班牙语频道",
"videoUrl": [
"http://livees.cgtn.com/500e/prog_index.m3u8",
"http://livees.cgtn.com/1000e/prog_index.m3u8"
]
},
{
"id": 61,
"videoIndex": 0,
"channel": "国际",
"logo": "https://resources.yangshipin.cn/assets/oms/image/202309/74d3ac436a7e374879578de1d87a941fbf566d39d5632b027c5097891ed32bd5.png?imageMogr2/format/webp",
"pid": "600084781",
"sid": "2010155403",
"programId": "600084781",
"needToken": false,
"mustToken": false,
"title": "CGTN 纪录频道",
"videoUrl": [
"https://livedoc.cgtn.com/500d/prog_index.m3u8"
]
}
]
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="local_channel_version">1</integer>
<string name="server_version_url">https://fastly.jsdelivr.net/gh/LeGend-wLw/my-tv-json-utils@main/version.txt</string>
<string name="server_url">https://fastly.jsdelivr.net/gh/LeGend-wLw/my-tv-json-utils@main/channels.json</string>
</resources>
+4 -1
View File
@@ -1,4 +1,7 @@
<resources>
<string name="app_name">我的电视</string>
<string name="logo">logo</string>
<string name="title_channel_reversal">换台反转</string>
<string name="title_channel_num">数字选台</string>
<string name="check_version">检查更新</string>
<string name="title_boot_startup">开机自启</string>
</resources>
-1
View File
@@ -1,6 +1,5 @@
<resources>
<style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle">
<item name="lbImageCardViewType">Title|Content</item>
</style>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
+3 -3
View File
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '8.2.0' apply false
id 'com.android.library' version '8.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.9.21' apply false
id 'com.android.application' version '8.2.2' apply false
id 'com.android.library' version '8.2.2' apply false
id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB