mirror of
https://github.com/lizongying/my-tv.git
synced 2026-11-11 14:06:28 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
526a7c5c40 | ||
|
|
960f890ddd | ||
|
|
0a7837c577 | ||
|
|
d8342e8c6a | ||
|
|
6387f48409 | ||
|
|
807300918c | ||
|
|
36eacb7a57 | ||
|
|
b4abe0ccc6 | ||
|
|
fd1dc8d3c7 | ||
|
|
ea56bf1b80 | ||
|
|
a746fe6c65 | ||
|
|
594d5e3499 | ||
|
|
6061187bc8 | ||
|
|
cd8b37e63c | ||
|
|
aa3ba1d41f | ||
|
|
c778ac07ba | ||
|
|
700c8b4c04 | ||
|
|
825a872a09 | ||
|
|
54b43664ec | ||
|
|
a93cfb455c | ||
|
|
8535689e95 |
@@ -112,4 +112,4 @@ adb install my-tv.apk
|
|||||||
|
|
||||||
## 赞赏
|
## 赞赏
|
||||||
|
|
||||||

|

|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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) } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
Reference in New Issue
Block a user