Compare commits

...
21 Commits
Author SHA1 Message Date
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
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
13 changed files with 1230 additions and 119 deletions
+1 -1
View File
@@ -112,4 +112,4 @@ adb install my-tv.apk
## 赞赏 ## 赞赏
![image](./screenshots/appreciate.jpeg) ![image](./screenshots/zfb.jpg)
@@ -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()
}
}
}
@@ -139,7 +139,6 @@ class MainFragment : BrowseSupportFragment() {
val cardPresenter = CardPresenter(viewLifecycleOwner) val cardPresenter = CardPresenter(viewLifecycleOwner)
var idx: Long = 0 var idx: Long = 0
context?.let { TVList.init(it) }
for ((k, v) in TVList.list) { for ((k, v) in TVList.list) {
val listRowAdapter = ArrayObjectAdapter(cardPresenter) val listRowAdapter = ArrayObjectAdapter(cardPresenter)
for ((idx2, v1) in v.withIndex()) { for ((idx2, v1) in v.withIndex()) {
@@ -41,7 +41,7 @@ class Request {
private val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""") private val regex = Regex("""des_key = "([^"]+).+var des_iv = "([^"]+)""")
private val input = private val input =
"""{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,AppleM1Pro,OpenGL4.1)"}""".toByteArray() """{"mver":"1","subver":"1.2","host":"www.yangshipin.cn/#/tv/home?pid=","referer":"","canvas":"YSPANGLE(Apple,ANGLEMetalRenderer:AppleM1Pro,UnspecifiedVersion)"}""".toByteArray()
private var mapping = mapOf( private var mapping = mapOf(
"CCTV4K" to "CCTV4K 超高清", "CCTV4K" to "CCTV4K 超高清",
@@ -97,8 +97,6 @@ class Request {
fun initYSP(context: Context) { fun initYSP(context: Context) {
ysp = YSP(context) ysp = YSP(context)
//TODO 不确定在哪里初始化
TVList.init(context)
} }
var call: Call<LiveInfo>? = null var call: Call<LiveInfo>? = null
@@ -120,6 +118,7 @@ class Request {
override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) { override fun onResponse(call: Call<LiveInfo>, response: Response<LiveInfo>) {
if (response.isSuccessful) { if (response.isSuccessful) {
val liveInfo = response.body() val liveInfo = response.body()
if (liveInfo?.data?.playurl != null) { if (liveInfo?.data?.playurl != null) {
val chanll = liveInfo.data.chanll val chanll = liveInfo.data.chanll
val decodedBytes = Base64.decode( val decodedBytes = Base64.decode(
@@ -135,8 +134,8 @@ class Request {
val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES( val url = liveInfo.data.playurl + "&revoi=" + encryptTripleDES(
keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0), keyBytes + byteArrayOf(0, 0, 0, 0, 0, 0, 0, 0),
ivBytes ivBytes
).uppercase() ).uppercase() + liveInfo.data.extended_param
// Log.d(TAG, "$title url $url") Log.d(TAG, "$title url $url")
tvModel.addVideoUrl(url) tvModel.addVideoUrl(url)
tvModel.allReady() tvModel.allReady()
tvModel.retryTimes = 0 tvModel.retryTimes = 0
@@ -356,29 +355,21 @@ class Request {
) { ) {
if (response.isSuccessful) { if (response.isSuccessful) {
val body = response.body() val body = response.body()
if (body?.data?.feedModuleListCount == 1) { if (body?.data?.feedModuleListCount == 1) {
for (item in body.data?.feedModuleListList!![0]?.dataTvChannelListList!!) { for (item in body.data?.feedModuleListList!![0]?.dataTvChannelListList!!) {
if (item.isVip && !item.isLimitedFree) { Log.d(
continue
}
Log.i(
TAG, TAG,
"${item.channelName},${item.pid},${item.streamId}" "${item.channelName},${item.pid},${item.streamId}"
) )
var channelType = "央视频道"
if (item?.channelType === "weishi") { for ((_, v) in TVList.list) {
channelType = "地方频道" for (v2 in v) {
} Log.i(TAG, "$v2")
if (!mapping.containsKey(item.channelName)) { if (v2.title == item.channelName || v2.alias == item.channelName) {
continue v2.pid = item.pid
} v2.sid = item.streamId
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
} }
} }
} }
+4 -3
View File
@@ -5,6 +5,7 @@ import java.io.Serializable
data class TV( data class TV(
var id: Int = 0, var id: Int = 0,
var title: String, var title: String,
var alias: String = "",
var videoUrl: List<String>, var videoUrl: List<String>,
var videoIndex: Int = 0, var videoIndex: Int = 0,
var channel: String = "", var channel: String = "",
@@ -12,10 +13,10 @@ data class TV(
var pid: String = "", var pid: String = "",
var sid: String = "", var sid: String = "",
var programId: String = "", var programId: String = "",
var needToken:Boolean=false, var needToken: Boolean = false,
var mustToken:Boolean=false, var mustToken: Boolean = false,
) : Serializable { ) : Serializable {
override fun toString(): String { override fun toString(): String {
return "TV{" + return "TV{" +
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
package com.lizongying.mytv
class UpdateManager {
}
+39 -49
View File
@@ -1,12 +1,18 @@
package com.lizongying.mytv package com.lizongying.mytv
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.os.SystemClock
import android.util.Log
import android.util.TypedValue import android.util.TypedValue
import java.io.File import com.google.gson.Gson
import com.lizongying.mytv.api.TimeResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Date
import java.io.IOException import java.util.Locale
object Utils { object Utils {
fun getDateFormat(format: String): String { fun getDateFormat(format: String): String {
@@ -17,6 +23,35 @@ object Utils {
return Date().time / 1000 return Date().time / 1000
} }
init {
CoroutineScope(Dispatchers.Default).launch {
updateTimestampFromServer()
}
}
/**
* 从服务器获取时间戳
* @return Long 时间戳
*/
private suspend fun updateTimestampFromServer() {
val currentTimeMillis = 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("https://api.m.taobao.com/rest/api3.do?api=mtop.common.getTimestamp")
.build()
).execute().use { response ->
if (!response.isSuccessful) throw java.io.IOException("Unexpected code $response")
val string = response.body()?.string()
Gson().fromJson(string, TimeResponse::class.java).data.t.toLong()
}
}
SystemClock.setCurrentTimeMillis(currentTimeMillis)
}
fun dpToPx(dp: Float): Int { fun dpToPx(dp: Float): Int {
return TypedValue.applyDimension( return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().displayMetrics TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().displayMetrics
@@ -28,49 +63,4 @@ object Utils {
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), Resources.getSystem().displayMetrics
).toInt() ).toInt()
} }
/**
* 获取可读写的目录
*
* @param context 应用环境信息
*
* @return 可读写的目录
*
*/
fun getAppDirectory(context: Context): File {
return context.filesDir
}
/**
* 更新channels.json
*
* @param context 应用环境信息
*
* @return 无
*
* @throws IOException 网络请求失败
*/
fun updateChannel(context: Context) {
val client = okhttp3.OkHttpClient()
val request = okhttp3.Request.Builder().url(getServerUrl(context)).build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val body = response.body()
//覆盖channels.json
val file = File(getAppDirectory(context), "channels.json")
if (!file.exists()) {
file.createNewFile()
}
file.writeText(body!!.string())
}
}
/**
* 从res/values/server.xml获取服务器地址
* @param context 应用环境信息
* @return 服务器地址
*/
private fun getServerUrl(context: Context): String {
return context.resources.getString(R.string.server_url)
}
} }
@@ -9,3 +9,11 @@ data class Info(
data class InfoData( data class InfoData(
val token: String, val token: String,
) )
data class TimeResponse(
val data: Time
) {
data class Time(
val t: String
)
}
@@ -13,6 +13,7 @@ data class LiveInfo(
) )
data class LiveInfoData( data class LiveInfoData(
val extended_param: String,
val chanll: String, val chanll: String,
val playurl: String, val playurl: String,
val errinfo: String, val errinfo: String,
@@ -127,7 +127,6 @@ class YSP(var context: Context) {
private fun getSignature(): String { private fun getSignature(): String {
val e = 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() "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) } } return hashedData.let { it -> it.joinToString("") { "%02x".format(it) } }
} }
+3 -1
View File
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="server_url">http://localhost:8080</string> <integer name="local_channel_version">1</integer>
<string name="server_version_url">https://raw.githubusercontent.com/LeGend-wLw/my-tv-json-utils/main/version.txt</string>
<string name="server_url">https://github.com/LeGend-wLw/my-tv-json-utils/raw/main/channels.json</string>
</resources> </resources>
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB