Compare commits

...
8 Commits
Author SHA1 Message Date
Li ZongYing b075a7db81 trust all certs 2024-01-11 19:33:31 +08:00
Li ZongYing 7821e8fa58 optimize 2024-01-11 15:19:57 +08:00
Li ZongYing 3109a61bcf action sign 2024-01-10 23:19:21 +08:00
Li ZongYing ce6e5b6546 action sign 2024-01-10 23:18:16 +08:00
Li ZongYing 230e4fea2b action sign 2024-01-10 23:14:49 +08:00
Li ZongYing 0d26912321 action sign 2024-01-10 23:12:35 +08:00
Li ZongYing af9ff5c8dd action sign 2024-01-10 22:39:49 +08:00
Li ZongYing f0f43e0fda dns cache 2024-01-10 22:11:13 +08:00
14 changed files with 216 additions and 133 deletions
+3
View File
@@ -33,6 +33,9 @@ jobs:
signingKeyBase64: ${{ secrets.KEYSTORE }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.ALIAS_PASSWORD }}
env:
# override default build-tools version (29.0.3) -- optional
BUILD_TOOLS_VERSION: "34.0.0"
- name: Create Release
id: create_release
+6 -3
View File
@@ -100,10 +100,13 @@ dependencies {
// For HLS playback support with ExoPlayer
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
// 21:2.9.0 17:2.6.4
def retrofit2_version = "2.9.0"
implementation 'com.google.protobuf:protobuf-kotlin:3.25.1'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:converter-protobuf:2.9.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
implementation "com.squareup.retrofit2:converter-protobuf:$retrofit2_version"
implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2"
implementation 'androidx.core:core-ktx:1.11.0-beta02'
implementation 'androidx.leanback:leanback:1.2.0-alpha02'
Binary file not shown.
Binary file not shown.
@@ -5,6 +5,7 @@ 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
@@ -36,7 +37,7 @@ class MainActivity : FragmentActivity() {
private lateinit var gestureDetector: GestureDetector
private val handler = Handler()
private val delay: Long = 3000
private val delay: Long = 4000
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -170,6 +171,40 @@ class MainActivity : FragmentActivity() {
}
}
private fun showHelp() {
val versionName = getPackageInfo().versionName
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()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_BACK -> {
@@ -192,35 +227,13 @@ class MainActivity : FragmentActivity() {
return true
}
KeyEvent.KEYCODE_SETTINGS -> {
showHelp()
return true
}
KeyEvent.KEYCODE_MENU -> {
val versionName = getPackageInfo().versionName
val textView = TextView(this)
textView.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
val imageView = ImageView(this)
val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate)
imageView.setImageDrawable(drawable)
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()
showHelp()
return true
}
@@ -24,6 +24,7 @@ class PlayerFragment : Fragment() {
private var _binding: PlayerBinding? = null
private var playerView: PlayerView? = null
private var tvViewModel: TVViewModel? = null
private val aspectRatio = 16f / 9f
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@@ -43,10 +44,9 @@ class PlayerFragment : Fragment() {
playerView!!.player?.playWhenReady = true
playerView!!.player?.addListener(object : Player.Listener {
override fun onVideoSizeChanged(videoSize: VideoSize) {
val aspectRatio = 16f / 9f
val layoutParams = playerView?.layoutParams
val ratio = playerView?.measuredWidth?.div(playerView?.measuredHeight!!)
if (ratio != null) {
val layoutParams = playerView?.layoutParams
if (ratio < aspectRatio) {
layoutParams?.height =
(playerView?.measuredWidth?.div(aspectRatio))?.toInt()
@@ -7,11 +7,9 @@ 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.BtraceClient
import com.lizongying.mytv.api.Info
import com.lizongying.mytv.api.LiveInfo
import com.lizongying.mytv.api.LiveInfoRequest
import com.lizongying.mytv.api.ProtoClient
import com.lizongying.mytv.api.YSP
import com.lizongying.mytv.api.YSPApiService
import com.lizongying.mytv.api.YSPBtraceService
@@ -31,8 +29,8 @@ import javax.crypto.spec.SecretKeySpec
class Request {
private var yspTokenService: YSPTokenService = ApiClient().yspTokenService
private var yspApiService: YSPApiService = ApiClient().yspApiService
private var yspBtraceService: YSPBtraceService = BtraceClient().yspBtraceService
private var yspProtoService: YSPProtoService = ProtoClient().yspProtoService
private var yspBtraceService: YSPBtraceService = ApiClient().yspBtraceService
private var yspProtoService: YSPProtoService = ApiClient().yspProtoService
private var ysp: YSP? = null
// TODO onDestroy
@@ -62,7 +60,6 @@ class Request {
"CGTN阿拉伯语频道" to "CGTN 阿拉伯语频道",
"CGTN西班牙语频道" to "CGTN 西班牙语频道",
"CGTN外语纪录频道" to "CGTN 纪录频道",
"东方卫视" to "东方卫视",
"湖南卫视" to "湖南卫视",
"湖北卫视" to "湖北卫视",
@@ -90,7 +87,10 @@ class Request {
ysp = YSP(context)
}
var call: Call<LiveInfo>? = null
fun fetchVideo(tvModel: TVViewModel, cookie: String) {
call?.cancel()
if (::myRunnable.isInitialized) {
handler.removeCallbacks(myRunnable)
}
@@ -100,71 +100,71 @@ class Request {
tvModel.seq = 0
val data = ysp?.switch(tvModel)
val request = data?.let { LiveInfoRequest(it) }
call = request?.let { yspApiService.getLiveInfo("guid=${ysp?.getGuid()}; $cookie", it) }
request?.let { yspApiService.getLiveInfo(cookie, it) }
?.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(
chanll.substring(9, chanll.length - 3),
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
val keyBytes = Base64.decode(key, Base64.DEFAULT)
val ivBytes = Base64.decode(iv, Base64.DEFAULT)
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")
tvModel.addVideoUrl(url)
tvModel.allReady()
tvModel.retryTimes = 0
myRunnable = MyRunnable(tvModel)
handler.post(myRunnable)
} else {
Log.e(TAG, "$title key error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
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(
chanll.substring(9, chanll.length - 3),
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
val keyBytes = Base64.decode(key, Base64.DEFAULT)
val ivBytes = Base64.decode(iv, Base64.DEFAULT)
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")
tvModel.addVideoUrl(url)
tvModel.allReady()
tvModel.retryTimes = 0
myRunnable = MyRunnable(tvModel)
handler.post(myRunnable)
} else {
if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo != "success!") {
Log.e(TAG, "$title url error ${liveInfo.data.errinfo}")
tvModel.setErrInfo(liveInfo.data.errinfo)
} else {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
Log.e(TAG, "$title key error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
} else {
Log.e(TAG, "$title status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
if (liveInfo?.data?.errinfo != null && liveInfo.data.errinfo == "应版权方要求,暂停提供直播信号,请点击观看其他精彩节目") {
Log.e(TAG, "$title error ${liveInfo.data.errinfo}")
tvModel.setErrInfo(liveInfo.data.errinfo)
} else {
Log.e(TAG, "$title url error $request $liveInfo")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
}
}
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
} else {
Log.e(TAG, "$title status error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
})
}
override fun onFailure(call: Call<LiveInfo>, t: Throwable) {
Log.e(TAG, "$title request error")
if (tvModel.retryTimes < tvModel.retryMaxTimes) {
tvModel.retryTimes++
fetchVideo(tvModel, cookie)
}
}
})
}
fun fetchVideo(tvModel: TVViewModel) {
@@ -172,11 +172,10 @@ class Request {
.enqueue(object : Callback<Info> {
override fun onResponse(call: Call<Info>, response: Response<Info>) {
if (response.isSuccessful) {
val info = response.body()
val token = info?.data?.token
val token = response.body()?.data?.token
Log.i(TAG, "info success $token")
val cookie =
"guid=1; vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
"vplatform=109; yspopenid=vu0-8lgGV2LW9QjDeuBFsX8yMnzs37Q3_HZF6XyVDpGR_I; vusession=$token"
fetchVideo(tvModel, cookie)
} else {
Log.e(TAG, "info status error")
@@ -201,7 +200,7 @@ class Request {
if (tvModel.needToken) {
fetchVideo(tvModel)
} else {
val cookie = "guid=1; vplatform=109"
val cookie = "vplatform=109"
fetchVideo(tvModel, cookie)
}
}
@@ -1,18 +1,27 @@
package com.lizongying.mytv.api
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.protobuf.ProtoConverterFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class ApiClient {
private val yspUrl = "https://player-api.yangshipin.cn/"
private val myUrl = "https://lyrics.run/"
// private val myUrl = "http://10.0.2.2:8081/"
private val protoUrl = "https://capi.yangshipin.cn/"
private val traceUrl = "https://btrace.yangshipin.cn/"
private var okHttpClient = getUnsafeOkHttpClient()
val yspApiService: YSPApiService by lazy {
Retrofit.Builder()
.baseUrl(yspUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(YSPApiService::class.java)
}
@@ -20,7 +29,60 @@ class ApiClient {
val yspTokenService: YSPTokenService by lazy {
Retrofit.Builder()
.baseUrl(myUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(YSPTokenService::class.java)
}
val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder()
.baseUrl(protoUrl)
.client(okHttpClient)
.addConverterFactory(ProtoConverterFactory.create())
.build().create(YSPProtoService::class.java)
}
val yspBtraceService: YSPBtraceService by lazy {
Retrofit.Builder()
.baseUrl(traceUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build().create(YSPBtraceService::class.java)
}
private fun getUnsafeOkHttpClient(): OkHttpClient {
try {
val trustAllCerts: Array<TrustManager> = arrayOf(
object : X509TrustManager {
override fun checkClientTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}
override fun checkServerTrusted(
chain: Array<out java.security.cert.X509Certificate>?,
authType: String?
) {
}
override fun getAcceptedIssuers(): Array<java.security.cert.X509Certificate> {
return emptyArray()
}
}
)
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, java.security.SecureRandom())
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
.hostnameVerifier { _, _ -> true }
.dns(DnsCache())
.build()
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
@@ -1,17 +0,0 @@
package com.lizongying.mytv.api
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class BtraceClient {
private val yspUrl = "https://btrace.yangshipin.cn/"
val yspBtraceService: YSPBtraceService by lazy {
Retrofit.Builder()
.baseUrl(yspUrl)
.addConverterFactory(GsonConverterFactory.create())
.build().create(YSPBtraceService::class.java)
}
}
@@ -0,0 +1,23 @@
package com.lizongying.mytv.api
import okhttp3.Dns
import java.net.InetAddress
import java.util.concurrent.ConcurrentHashMap
class DnsCache : Dns {
private val dnsCache: MutableMap<String, List<InetAddress>> = ConcurrentHashMap()
override fun lookup(hostname: String): List<InetAddress> {
dnsCache[hostname]?.let {
return it
}
val addresses = InetAddress.getAllByName(hostname).toList()
if (addresses.isNotEmpty()) {
dnsCache[hostname] = addresses
}
return addresses
}
}
@@ -1,16 +0,0 @@
package com.lizongying.mytv.api
import retrofit2.Retrofit
import retrofit2.converter.protobuf.ProtoConverterFactory
class ProtoClient {
private val protoUrl = "https://capi.yangshipin.cn/"
val yspProtoService: YSPProtoService by lazy {
Retrofit.Builder()
.baseUrl(protoUrl)
.addConverterFactory(ProtoConverterFactory.create())
.build().create(YSPProtoService::class.java)
}
}
@@ -70,6 +70,11 @@ class YSP(var context: Context) {
defn = "fhd"
randStr = getRand()
if (tvModel.retryTimes > 0) {
guid = newGuid()
}
timeStr = getTimeStr()
// guid = "lq3oqitm_1e15dnzgjnb"
@@ -105,6 +110,15 @@ class YSP(var context: Context) {
return guid
}
private fun newGuid(): String {
guid = generateGuid()
with(sharedPref!!.edit()) {
putString("guid", guid)
apply()
}
return guid
}
fun getRand(): String {
var n = ""
val e = "ABCDEFGHIJKlMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
@@ -1,6 +1,5 @@
package com.lizongying.mytv.models
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@@ -20,7 +20,7 @@ class TVViewModel(private var tv: TV) : ViewModel() {
private var itemPosition: Int = 0
var retryTimes: Int = 0
var retryMaxTimes: Int = 3
var retryMaxTimes: Int = 5
var programUpdateTime: Long = 0
private val _errInfo = MutableLiveData<String>()
@@ -71,7 +71,7 @@ class TVViewModel(private var tv: TV) : ViewModel() {
val ready: LiveData<Boolean>
get() = _ready
private var mMinimumLoadableRetryCount = 3
private var mMinimumLoadableRetryCount = 5
var seq = 0