commit
0a7837c577
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,7 @@ class MainActivity : FragmentActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
Log.i(TAG, "onCreate")
|
Log.i(TAG, "onCreate")
|
||||||
|
TVList.init(this)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,8 +139,7 @@ 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()) {
|
||||||
val tvViewModel = TVViewModel(v1)
|
val tvViewModel = TVViewModel(v1)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -374,7 +372,7 @@ class Request {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val tv =
|
val tv =
|
||||||
TVList.list[channelType]?.find { it.title == mapping[item.channelName] }
|
TVList.list?.get(channelType)?.find { it.title == mapping[item.channelName] }
|
||||||
if (tv != null) {
|
if (tv != null) {
|
||||||
tv.logo = item.tvLogo
|
tv.logo = item.tvLogo
|
||||||
tv.pid = item.pid
|
tv.pid = item.pid
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,81 @@
|
||||||
package com.lizongying.mytv
|
package com.lizongying.mytv
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.google.gson.Gson
|
import android.util.Log
|
||||||
import com.google.gson.reflect.TypeToken
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import java.io.File
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.math.log
|
||||||
|
|
||||||
object TVList {
|
object TVList {
|
||||||
lateinit var list: Map<String, List<TV>>
|
@Volatile
|
||||||
private val channels = "channels.json"
|
var list: Map<String, List<TV>>? = null
|
||||||
|
get():Map<String, List<TV>>? {
|
||||||
|
//等待初始化完成
|
||||||
|
while (this.list === null) {
|
||||||
|
Thread.sleep(10)
|
||||||
|
}
|
||||||
|
return this.list
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
*/
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
if (::list.isInitialized) {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
return
|
//获取本地版本号
|
||||||
|
val localVersion = ChannelUtils.getLocalVersion(context)
|
||||||
|
//获取服务器版本号
|
||||||
|
val serverVersion = try {
|
||||||
|
ChannelUtils.getServerVersion(context)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("TVList", "无法从服务器获取版本信息", e)
|
||||||
|
Integer.MIN_VALUE
|
||||||
}
|
}
|
||||||
synchronized(this) {
|
//频道列表
|
||||||
if (::list.isInitialized) {
|
val channelTVMap: MutableMap<String, MutableList<TV>> = mutableMapOf()
|
||||||
return
|
//是否从服务器更新
|
||||||
|
var updateFromServer = false
|
||||||
|
//获取频道列表
|
||||||
|
val tvList: List<TV> = if (localVersion < serverVersion) {
|
||||||
|
//获取服务器地址
|
||||||
|
val url = ChannelUtils.getServerUrl(context)
|
||||||
|
//是否从服务器更新
|
||||||
|
updateFromServer = true
|
||||||
|
Log.i("TVList", "从服务器获取频道信息")
|
||||||
|
try {
|
||||||
|
ChannelUtils.getServerChannel(url)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("TVList", "无法从服务器获取频道信息", e)
|
||||||
|
updateFromServer = false
|
||||||
|
ChannelUtils.getLocalChannel(context)
|
||||||
}
|
}
|
||||||
list = setupTV(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun setupTV(context: Context): Map<String, List<TV>> {
|
|
||||||
val map: MutableMap<String, MutableList<TV>> = mutableMapOf()
|
|
||||||
val appDirectory = Utils.getAppDirectory(context)
|
|
||||||
|
|
||||||
//检查当前目录下是否存在channels.json
|
|
||||||
val file = File(appDirectory, channels)
|
|
||||||
if (!file.exists()) {
|
|
||||||
//不存在则从assets中拷贝
|
|
||||||
file.createNewFile()
|
|
||||||
context.resources.openRawResource(R.raw.channels).use { input ->
|
|
||||||
file.outputStream().use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//读取channels.json,并转换为Map<String,LIst<TV>>
|
|
||||||
val json = file.readText()
|
|
||||||
//防止类型擦除
|
|
||||||
val type = object : TypeToken<Array<TV>>() {}.type
|
|
||||||
Gson().fromJson<Array<TV>>(json, type)?.forEach {
|
|
||||||
if (map.containsKey(it.channel)) {
|
|
||||||
map[it.channel]?.add(it)
|
|
||||||
} else {
|
} else {
|
||||||
map[it.channel] = mutableListOf(it)
|
Log.i("TVList", "从本地获取频道信息")
|
||||||
|
//获取本地频道
|
||||||
|
ChannelUtils.getLocalChannel(context)
|
||||||
|
}
|
||||||
|
//按频道分类
|
||||||
|
for (tv in tvList) {
|
||||||
|
val key = tv.channel
|
||||||
|
if (channelTVMap.containsKey(key)) {
|
||||||
|
val list = channelTVMap[key]!!
|
||||||
|
list.add(tv)
|
||||||
|
channelTVMap[key] = list
|
||||||
|
} else {
|
||||||
|
channelTVMap[key] = mutableListOf(tv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//保存频道列表
|
||||||
|
list = channelTVMap
|
||||||
|
//保存版本号
|
||||||
|
if (updateFromServer) {
|
||||||
|
ChannelUtils.updateLocalChannel(context, serverVersion, tvList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,20 +1,41 @@
|
||||||
package com.lizongying.mytv
|
package com.lizongying.mytv
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
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.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
object Utils {
|
object Utils {
|
||||||
fun getDateFormat(format: String): String {
|
fun getDateFormat(format: String): String {
|
||||||
return SimpleDateFormat(format, Locale.CHINA).format(Date())
|
return SimpleDateFormat(format, Locale.CHINA).format(Date())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDateTimestamp(): Long {
|
suspend fun getDateTimestamp(): Long {
|
||||||
return Date().time / 1000
|
return getTimestampFromServer() / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从服务器获取时间戳
|
||||||
|
* @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()
|
||||||
|
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 body = response.body()
|
||||||
|
val string = body?.toString()
|
||||||
|
Gson().fromJson(string, TimeResponse::class.java).data.t.toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dpToPx(dp: Float): Int {
|
fun dpToPx(dp: Float): Int {
|
||||||
|
|
@ -28,49 +49,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 api: String, val v: String, val ret: List<String>, val data: Time
|
||||||
|
) {
|
||||||
|
data class Time(
|
||||||
|
val t: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue