Compare commits

..
5 Commits
Author SHA1 Message Date
Li ZongYing a340ab73b1 change dialog 2023-12-07 11:19:22 +08:00
Li ZongYing bc5a0cad71 fix exit on control 2023-12-07 09:47:31 +08:00
Li ZongYing 709cc2ccdf fix empty string bug 2023-12-07 01:05:26 +08:00
Li ZongYing 3b01debf7f switch source 2023-12-06 23:51:17 +08:00
Li ZongYing 20ddba6ad7 clean 2023-12-06 20:07:27 +08:00
10 changed files with 147 additions and 133 deletions
+1 -1
View File
@@ -52,5 +52,5 @@ jobs:
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ steps.sign_app.outputs.signedReleaseFile }} asset_path: ${{ steps.sign_app.outputs.signedReleaseFile }}
asset_name: my-tv.apk asset_name: my-tv-${{ github.ref_name }}.apk
asset_content_type: application/vnd.android.package-archive asset_content_type: application/vnd.android.package-archive
+5 -6
View File
@@ -11,8 +11,8 @@ android {
applicationId "com.lizongying.mytv" applicationId "com.lizongying.mytv"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode getVersionCode() versionCode VersionCode()
versionName getVersionName() versionName VersionName()
} }
buildTypes { buildTypes {
@@ -32,7 +32,7 @@ android {
} }
} }
static def getVersionCode() { static def VersionCode() {
try { try {
def process = 'git rev-list --count HEAD'.execute() def process = 'git rev-list --count HEAD'.execute()
process.waitFor() process.waitFor()
@@ -42,18 +42,17 @@ static def getVersionCode() {
} }
} }
static def getVersionName() { static def VersionName() {
try { try {
def process = 'git describe --tags --always'.execute() def process = 'git describe --tags --always'.execute()
process.waitFor() process.waitFor()
return process.text.trim() return process.text.trim() - "v"
} catch (ignored) { } catch (ignored) {
return "1.0.0" return "1.0.0"
} }
} }
dependencies { dependencies {
implementation 'androidx.core:core-ktx:1.11.0-beta02' implementation 'androidx.core:core-ktx:1.11.0-beta02'
implementation 'androidx.leanback:leanback:1.0.0' implementation 'androidx.leanback:leanback:1.0.0'
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation 'com.github.bumptech.glide:glide:4.11.0'
@@ -13,11 +13,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/**
* A CardPresenter is used to generate Views and bind Objects to them on demand.
* It contains an ImageCardView.
*/
class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Presenter() { class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
@@ -38,10 +33,9 @@ class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Prese
val tv = item as TV val tv = item as TV
val cardView = viewHolder.view as ImageCardView val cardView = viewHolder.view as ImageCardView
if (tv.videoUrl != null) { cardView.titleText = tv.title
cardView.titleText = tv.title cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT)
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT) cardView.tag = tv.videoUrl
cardView.tag = tv.videoUrl
// lifecycleScope.launch(Dispatchers.IO) { // lifecycleScope.launch(Dispatchers.IO) {
// val videoThumbnail = tv.videoUrl?.let { getVideoThumbnail(it) } // val videoThumbnail = tv.videoUrl?.let { getVideoThumbnail(it) }
@@ -50,7 +44,6 @@ class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Prese
// cardView.mainImageView.setImageBitmap(videoThumbnail) // cardView.mainImageView.setImageBitmap(videoThumbnail)
// } // }
// } // }
}
} }
override fun onUnbindViewHolder(viewHolder: ViewHolder) { override fun onUnbindViewHolder(viewHolder: ViewHolder) {
@@ -91,9 +84,7 @@ class CardPresenter(private val lifecycleScope: LifecycleCoroutineScope) : Prese
companion object { companion object {
private const val TAG = "CardPresenter" private const val TAG = "CardPresenter"
private const val CARD_WIDTH = 313 private const val CARD_WIDTH = 313
private const val CARD_HEIGHT = 176 private const val CARD_HEIGHT = 176
} }
} }
@@ -2,9 +2,8 @@ package com.lizongying.mytv
import java.io.Serializable import java.io.Serializable
data class Info( data class Info(
var rowPosition: Int = 0, var rowPosition: Int = 0,
var itemPosition: Int = 0, var itemPosition: Int = 0,
var item: TV? = null, var item: TV,
) : Serializable ) : Serializable
@@ -4,19 +4,14 @@ import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
/**
* Loads [MainFragment].
*/
class MainActivity : FragmentActivity() { class MainActivity : FragmentActivity() {
private val playbackFragment = PlaybackFragment() private val playbackFragment = PlaybackFragment()
@@ -48,6 +43,14 @@ class MainActivity : FragmentActivity() {
mainFragment.next() mainFragment.next()
} }
fun prevSource() {
mainFragment.prevSource()
}
fun nextSource() {
mainFragment.nextSource()
}
fun switchMainFragment() { fun switchMainFragment() {
val transaction = supportFragmentManager.beginTransaction() val transaction = supportFragmentManager.beginTransaction()
@@ -61,7 +64,7 @@ class MainActivity : FragmentActivity() {
transaction.commit() transaction.commit()
} }
fun focusMainFragment() { private fun focusMainFragment() {
mainFragment.focus() mainFragment.focus()
} }
@@ -69,7 +72,7 @@ class MainActivity : FragmentActivity() {
return mainFragment.isHidden return mainFragment.isHidden
} }
fun hideMainFragment() { private fun hideMainFragment() {
if (!mainFragment.isHidden) { if (!mainFragment.isHidden) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.hide(mainFragment) .hide(mainFragment)
@@ -100,31 +103,32 @@ class MainActivity : FragmentActivity() {
} }
KeyEvent.KEYCODE_MENU -> { KeyEvent.KEYCODE_MENU -> {
val packageInfo = packageManager.getPackageInfo(packageName, 0)
val versionName = packageInfo.versionName
val textView = TextView(this)
textView.text =
"当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/"
val imageView = ImageView(this) val imageView = ImageView(this)
val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate) val drawable = ContextCompat.getDrawable(this, R.drawable.appreciate)
imageView.setImageDrawable(drawable) imageView.setImageDrawable(drawable)
val parent = imageView.parent as? ViewGroup
parent?.removeView(imageView)
val linearLayout = LinearLayout(this) val linearLayout = LinearLayout(this)
linearLayout.orientation = LinearLayout.VERTICAL
linearLayout.addView(textView)
linearLayout.addView(imageView) linearLayout.addView(imageView)
val layoutParams = LinearLayout.LayoutParams( val layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT LinearLayout.LayoutParams.WRAP_CONTENT
) )
layoutParams.gravity = Gravity.BOTTOM
imageView.layoutParams = layoutParams imageView.layoutParams = layoutParams
textView.layoutParams = layoutParams
val packageInfo = packageManager.getPackageInfo(packageName, 0)
val versionName = packageInfo.versionName
val builder: AlertDialog.Builder = AlertDialog.Builder(this) val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder builder
.setTitle("当前版本: $versionName, 获取最新: https://github.com/lizongying/my-tv/releases/")
.setView(linearLayout) .setView(linearLayout)
val dialog: AlertDialog = builder.create() val dialog: AlertDialog = builder.create()
@@ -150,13 +154,13 @@ class MainActivity : FragmentActivity() {
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
prev() prevSource()
} }
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (mainFragment.isHidden) { if (mainFragment.isHidden) {
next() nextSource()
} }
} }
} }
@@ -2,9 +2,7 @@ package com.lizongying.mytv
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.leanback.app.BrowseSupportFragment import androidx.leanback.app.BrowseSupportFragment
import androidx.leanback.widget.ArrayObjectAdapter import androidx.leanback.widget.ArrayObjectAdapter
import androidx.leanback.widget.HeaderItem import androidx.leanback.widget.HeaderItem
@@ -18,9 +16,6 @@ import androidx.leanback.widget.Row
import androidx.leanback.widget.RowPresenter import androidx.leanback.widget.RowPresenter
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
/**
* Loads a grid of cards with movies to browse.
*/
class MainFragment : BrowseSupportFragment() { class MainFragment : BrowseSupportFragment() {
var itemPosition: Int = 0 var itemPosition: Int = 0
@@ -32,24 +27,17 @@ class MainFragment : BrowseSupportFragment() {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
setupUIElements() setupUIElements()
loadRows() loadRows()
setupEventListeners() setupEventListeners()
} }
fun show() {
if (!view?.isVisible!!) {
view?.visibility = View.VISIBLE
}
}
private fun setupUIElements() { private fun setupUIElements() {
// set fastLane (or headers) background color
brandColor = ContextCompat.getColor(context!!, R.color.fastlane_background) brandColor = ContextCompat.getColor(context!!, R.color.fastlane_background)
// headersState = HEADERS_DISABLED // headersState = HEADERS_DISABLED
} }
private var count: Int = 0
private fun loadRows() { private fun loadRows() {
val list = TVList.list val list = TVList.list
val rowsAdapter = ArrayObjectAdapter(ListRowPresenter()) val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
@@ -58,9 +46,21 @@ class MainFragment : BrowseSupportFragment() {
var idx: Long = 0 var idx: Long = 0
for ((k, v) in list) { for ((k, v) in list) {
val listRowAdapter = ArrayObjectAdapter(cardPresenter) val listRowAdapter = ArrayObjectAdapter(cardPresenter)
for ((idx2, v1) in v.withIndex()) { var idx2 = 0
listRowAdapter.add(v1) for ((k1, v1) in v) {
list2.add(Info(idx.toInt(), idx2, v1)) val tv = TV(
count,
k1,
v1.toList()
)
list2.add(
Info(
idx.toInt(), idx2, tv
)
)
listRowAdapter.add(tv)
count++
idx2++
} }
val header = HeaderItem(idx, k) val header = HeaderItem(idx, k)
rowsAdapter.add(ListRow(header, listRowAdapter)) rowsAdapter.add(ListRow(header, listRowAdapter))
@@ -69,7 +69,7 @@ class MainFragment : BrowseSupportFragment() {
adapter = rowsAdapter adapter = rowsAdapter
(activity as? MainActivity)?.play(list.values.first()[0]) (activity as? MainActivity)?.play(list2.first().item as TV)
(activity as? MainActivity)?.switchMainFragment() (activity as? MainActivity)?.switchMainFragment()
} }
@@ -94,7 +94,7 @@ class MainFragment : BrowseSupportFragment() {
) )
// Toast.makeText( // Toast.makeText(
// activity, // activity,
// "${l.title} $selectedPosition $itemPosition", // "${l.title} ${tv.videoIndex}",
// Toast.LENGTH_SHORT // Toast.LENGTH_SHORT
// ).show() // ).show()
} }
@@ -115,7 +115,45 @@ class MainFragment : BrowseSupportFragment() {
) )
// Toast.makeText( // Toast.makeText(
// activity, // activity,
// "${l.title} $selectedPosition $itemPosition", // "${l.title} ${tv.videoIndex}",
// Toast.LENGTH_SHORT
// ).show()
}
}
fun prevSource() {
view?.post {
val item = list2[itemPosition]
val tv = item.item as TV
tv.videoIndex--
if (tv.videoIndex == -1) {
tv.videoIndex = tv.videoUrl.size - 1
}
(activity as? MainActivity)?.play(tv)
// Toast.makeText(
// activity,
// "${l.title} ${tv.videoIndex}",
// Toast.LENGTH_SHORT
// ).show()
}
}
fun nextSource() {
view?.post {
val item = list2[itemPosition]
val tv = item.item as TV
tv.videoIndex++
if (tv.videoIndex == tv.videoUrl.size) {
tv.videoIndex = 0
}
(activity as? MainActivity)?.play(tv)
// Toast.makeText(
// activity,
// "${l.title} ${tv.videoIndex}",
// Toast.LENGTH_SHORT // Toast.LENGTH_SHORT
// ).show() // ).show()
} }
@@ -33,13 +33,13 @@ class PlaybackControlGlue(
KeyEvent.KEYCODE_DPAD_LEFT -> { KeyEvent.KEYCODE_DPAD_LEFT -> {
if ((context as? MainActivity)?.mainFragmentIsHidden() == true) { if ((context as? MainActivity)?.mainFragmentIsHidden() == true) {
(context as? MainActivity)?.prev() (context as? MainActivity)?.prevSource()
} }
} }
KeyEvent.KEYCODE_DPAD_RIGHT -> { KeyEvent.KEYCODE_DPAD_RIGHT -> {
if ((context as? MainActivity)?.mainFragmentIsHidden() == true) { if ((context as? MainActivity)?.mainFragmentIsHidden() == true) {
(context as? MainActivity)?.next() (context as? MainActivity)?.nextSource()
} }
} }
} }
@@ -10,7 +10,6 @@ import androidx.leanback.media.PlaybackTransportControlGlue
import androidx.leanback.widget.PlaybackControlsRow import androidx.leanback.widget.PlaybackControlsRow
import java.io.IOException import java.io.IOException
/** Handles video playback with media controls. */
class PlaybackFragment : VideoSupportFragment() { class PlaybackFragment : VideoSupportFragment() {
private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<MediaPlayerAdapter> private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<MediaPlayerAdapter>
@@ -28,21 +27,22 @@ class PlaybackFragment : VideoSupportFragment() {
} }
override fun showControlsOverlay(runAnimation: Boolean) { override fun showControlsOverlay(runAnimation: Boolean) {
// We will do nothing here, and thus controls will never be shown
} }
fun play(tv: TV) { fun play(tv: TV) {
if (tv.videoUrl.isNullOrBlank()) { val videoUrl = tv.videoUrl[tv.videoIndex]
if (videoUrl == "") {
Log.e(TAG, "videoUrl is empty") Log.e(TAG, "videoUrl is empty")
return return
} }
if (tv.videoUrl.equals(lastVideoUrl)) { if (videoUrl == lastVideoUrl) {
Log.e(TAG, "videoUrl is duplication") Log.e(TAG, "videoUrl is duplication")
return return
} }
lastVideoUrl = tv.videoUrl!! lastVideoUrl = videoUrl
playerAdapter?.reset() playerAdapter?.reset()
@@ -52,7 +52,7 @@ class PlaybackFragment : VideoSupportFragment() {
mTransportControlGlue.playWhenPrepared() mTransportControlGlue.playWhenPrepared()
try { try {
playerAdapter?.setDataSource(Uri.parse(tv.videoUrl)) playerAdapter?.setDataSource(Uri.parse(videoUrl))
} catch (e: IOException) { } catch (e: IOException) {
return return
} }
+4 -5
View File
@@ -2,13 +2,11 @@ package com.lizongying.mytv
import java.io.Serializable import java.io.Serializable
/**
* Movie class represents video entity with title, description, image thumbs and video url.
*/
data class TV( data class TV(
var id: Int = 0, var id: Int = 0,
var title: String? = null, var title: String,
var videoUrl: String? = null, var videoUrl: List<String>,
var videoIndex: Int = 0,
) : Serializable { ) : Serializable {
override fun toString(): String { override fun toString(): String {
@@ -16,6 +14,7 @@ data class TV(
"id=" + id + "id=" + id +
", title='" + title + '\'' + ", title='" + title + '\'' +
", videoUrl='" + videoUrl + '\'' + ", videoUrl='" + videoUrl + '\'' +
", videoIndex='" + videoIndex + '\'' +
'}' '}'
} }
+43 -59
View File
@@ -1,57 +1,45 @@
package com.lizongying.mytv package com.lizongying.mytv
object TVList { object TVList {
val list: Map<String, List<TV>> by lazy { val list: Map<String, Map<String, Set<String>>> by lazy {
setupTV() setupTV()
} }
private var count: Int = 0
private fun setupTV(): Map<String, List<TV>> { private fun setupTV(): Map<String, Map<String, Set<String>>> {
val tvs = """ val tvs = """
央视频道,CCTV1,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv1hd265/57/20191230/index.m3u8?&encrypt= 央视频道,CCTV1 综合,http://tvpull.dxhmt.cn/tv/11481-4.m3u8
央视频道,CCTV2,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv2hd265/55/20200407/index.m3u8?&encrypt= 央视频道,CCTV1 综合,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv1hd265/57/20191230/index.m3u8?&encrypt=
央视频道,CCTV3,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv3hd/3000/index.m3u8?&encrypt= 央视频道,CCTV2 财经,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv2hd265/55/20200407/index.m3u8?&encrypt=
央视频道,CCTV4,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv4hd/1500/index.m3u8?&encrypt= 央视频道,CCTV3 综艺,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv3hd/3000/index.m3u8?&encrypt=
央视频道,CCTV4美洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4meihd/57/index.m3u8?&encrypt=
央视频道,CCTV4欧洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4ouhd/51/index.m3u8?&encrypt=
央视频道,CCTV5,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv5hd265/57/20191230/index.m3u8?&encrypt=
央视频道,CCTV5+,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv5plusnew/2500/index.m3u8?&encrypt=
央视频道,CCTV6,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv6hd/3000/index.m3u8?&encrypt=
央视频道,CCTV7,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv7hd/51/index.m3u8?&encrypt=
央视频道,CCTV8,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv8hd/3000/index.m3u8?&encrypt=
央视频道,CCTV9,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv9hd/57/index.m3u8?&encrypt=
央视频道,CCTV10,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/cctv10hd/2000/index.m3u8?&encrypt=
央视频道,CCTV11,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv11hd/57/20200103/index.m3u8?&encrypt=
央视频道,CCTV12,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv12hd/57/index.m3u8?&encrypt=
央视频道,CCTV13,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctv13/2000/index.m3u8?&encrypt=
央视频道,CCTV14,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv14hd/3000/index.m3u8?&encrypt=
央视频道,CCTV15,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv15hd/51/index.m3u8?&encrypt=
央视频道,CCTV17,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv17hd/57/index.m3u8?&encrypt=
央视频道,CGTN,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctvnews/1000/index.m3u8?&encrypt=
央视频道,CCTV2,https://iptv.luas.edu.cn/liverespath/6b13fe5368d391761312a985ace065c0ecad2f5e/877097d2fa-0-0-b7736e6941fd5cb71f45ef9397b68092/index.m3u8
央视频道,CCTV3,https://iptv.luas.edu.cn/liverespath/f76f9947c68be18d7a456e25aa59a08c5747e6a5/0df24da9ec-0-0-dca40ddadd2a051ce1a83536d9310820/index.m3u8
央视频道,CCTV4 中文国际,http://39.134.24.161/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226191/index.m3u8 央视频道,CCTV4 中文国际,http://39.134.24.161/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226191/index.m3u8
央视频道,CCTV6,https://iptv.luas.edu.cn/liverespath/f2f39ee2105c85c32df375728a51b5d89d3afab4/113ed89f48-0-0-85216e55861329ec31ba1437a2ff37c9/index.m3u8 央视频道,CCTV4 中文国际,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv4hd/1500/index.m3u8?&encrypt=
央视频道,CCTV7,https://iptv.luas.edu.cn/liverespath/f116a0a5035935a3435155998163d8eaa60554c3/4ba629f762-0-0-0a66b18805ff859ce68ab1137157079e/index.m3u8 央视频道,CCTV4 中文国际美洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4meihd/57/index.m3u8?&encrypt=
央视频道,CCTV8,https://iptv.luas.edu.cn/liverespath/9e6e3b618b5dc902d992949f0c669bb674f6cde8/cae4471c68-0-0-316245c664c3311072c7279ec29672fe/index.m3u8 央视频道,CCTV4 中文国际欧洲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv4ouhd/51/index.m3u8?&encrypt=
央视频道,CCTV9,http://39.134.24.162/dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221226197/index.m3u8 央视频道,CCTV5 体育,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv5hd265/57/20191230/index.m3u8?&encrypt=
央视频道,CCTV10,https://iptv.luas.edu.cn/liverespath/924e6292a0e6f7440e8380075908fed9915b3c00/780499a193-0-0-c50d57fd93cf32a5144ec8d889eb3ed9/index.m3u8 央视频道,CCTV5+ 体育赛事,http://hlsbkmgsplive.miguvideo.com/wd_r2/cctv/cctv5plusnew/2500/index.m3u8?&encrypt=
央视频道,CCTV11,https://iptv.luas.edu.cn/liverespath/3d0aa0f1604f13f0ae90c2dc0590ca22a1bcaaa2/646e868153-0-0-a53121a1df5b93a74d1980270e652878/index.m3u8 央视频道,CCTV6 电影,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv6hd/3000/index.m3u8?&encrypt=
央视频道,CCTV12,https://iptv.luas.edu.cn/liverespath/fb84bde1de15cdb0308a7910cebc0497594ae94e/e2d8885f42-0-0-6e1f55986652ba786606a116c5cc0775/index.m3u8 央视频道,CCTV7 国防军事,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv7hd/51/index.m3u8?&encrypt=
央视频道,CCTV13,https://live-play.cctvnews.cctv.com/cctv/merge_cctv13.m3u8 央视频道,CCTV8 电视剧,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv8hd/3000/index.m3u8?&encrypt=
央视频道,CCTV14,https://iptv.luas.edu.cn/liverespath/0e0973e58d4835f4b872548164462930003f77b4/0ef01acfdb-0-0-3c10e95bcde07ba14f0f93a8d831b684/index.m3u8 央视频道,CCTV9 记录,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv9hd/57/index.m3u8?&encrypt=
央视频道,CCTV10 科教,http://hlsbkmgsplive.miguvideo.com/wd_r2/2018/ocn/cctv10hd/2000/index.m3u8?&encrypt=
央视频道,CCTV11 戏曲,http://hlsbkmgsplive.miguvideo.com/migu/kailu/cctv11hd/57/20200103/index.m3u8?&encrypt=
央视频道,CCTV12 社会与法,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv12hd/57/index.m3u8?&encrypt=
央视频道,CCTV13 新闻,https://live-play.cctvnews.cctv.com/cctv/merge_cctv13.m3u8
央视频道,CCTV13 新闻,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctv13/2000/index.m3u8?&encrypt=
央视频道,CCTV14 少儿,http://hlsbkmgsplive.miguvideo.com/wd_r2/ocn/cctv14hd/3000/index.m3u8?&encrypt=
央视频道,CCTV15 音乐,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225785/index.m3u8 央视频道,CCTV15 音乐,http://dbiptv.sn.chinamobile.com/PLTV/88888890/224/3221225785/index.m3u8
央视频道,CCTV15 音乐,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv15hd/51/index.m3u8?&encrypt=
央视频道,CCTV17 农业农村,http://hlsbkmgsplive.miguvideo.com/migu/kailu/20200324/cctv17hd/57/index.m3u8?&encrypt=
央视频道,CGTN,http://hlsbkmgsplive.miguvideo.com/envivo_x/2018/SD/cctvnews/1000/index.m3u8?&encrypt=
央视频道,CGTN 新闻频道,http://live.cgtn.com/1000/prog_index.m3u8
央视频道,CGTN 纪录频道,https://livedoc.cgtn.com/500d/prog_index.m3u8
央视频道,CGTN 法语频道,https://livefr.cgtn.com/1000f/prog_index.m3u8
央视频道,CGTN 俄语频道,http://liveru.cgtn.com/1000r/prog_index.m3u8
央视频道,CGTN 西班牙语频道,http://livees.cgtn.com/500e/prog_index.m3u8
央视频道,CGTN 阿拉伯语频道,http://livear.cgtn.com/1000a/prog_index.m3u8
央视频道,CGTN 拉丁美洲频道,http://livees.cgtn.com/1000e/prog_index.m3u8
央视频道,书画频道,http://211.103.180.178:8234/live_hls/hdmi.m3u8 央视频道,书画频道,http://211.103.180.178:8234/live_hls/hdmi.m3u8
CGTN,新闻频道,http://live.cgtn.com/1000/prog_index.m3u8
CGTN,纪录频道,https://livedoc.cgtn.com/500d/prog_index.m3u8
CGTN,法语频道,https://livefr.cgtn.com/1000f/prog_index.m3u8
CGTN,俄语频道,http://liveru.cgtn.com/1000r/prog_index.m3u8
CGTN,西班牙语频道,http://livees.cgtn.com/500e/prog_index.m3u8
CGTN,阿拉伯语频道,http://livear.cgtn.com/1000a/prog_index.m3u8
CGTN,拉丁美洲频道,http://livees.cgtn.com/1000e/prog_index.m3u8
地方频道,东方卫视,http://hlsbkmgsplive.miguvideo.com/wd_r4/dfl/dongfangwshd/3000/index.m3u8?&encrypt= 地方频道,东方卫视,http://hlsbkmgsplive.miguvideo.com/wd_r4/dfl/dongfangwshd/3000/index.m3u8?&encrypt=
地方频道,内蒙古卫视,http://hlsbkmgsplive.miguvideo.com/envivo_w/2018/SD/neimeng/1000/index.m3u8?&encrypt= 地方频道,内蒙古卫视,http://hlsbkmgsplive.miguvideo.com/envivo_w/2018/SD/neimeng/1000/index.m3u8?&encrypt=
地方频道,湖南卫视,http://hlsbkmgsplive.miguvideo.com/wd-hunanhd-2500/index.m3u8?&encrypt= 地方频道,湖南卫视,http://hlsbkmgsplive.miguvideo.com/wd-hunanhd-2500/index.m3u8?&encrypt=
@@ -107,26 +95,22 @@ CGTN,拉丁美洲频道,http://livees.cgtn.com/1000e/prog_index.m3u8
地方频道,延边卫视,http://live.ybtvyun.com/video/s10006-44f040627ca1/index.m3u8 地方频道,延边卫视,http://live.ybtvyun.com/video/s10006-44f040627ca1/index.m3u8
""".trimIndent() """.trimIndent()
val map: MutableMap<String, MutableList<TV>> = mutableMapOf() val map: MutableMap<String, MutableMap<String, LinkedHashSet<String>>> = mutableMapOf()
for (i in tvs.split("\n")) { for (i in tvs.split("\n")) {
val (channel, title, videoUrl) = i.split(",") if (!i.contains(",")) {
val videoList = map[channel] ?: mutableListOf() continue
videoList.add(buildTV(title, videoUrl)) }
map[channel] = videoList val (channel, title, url) = i.split(",")
val titleMap = map[channel] ?: mutableMapOf()
val urlSet = titleMap[title] ?: LinkedHashSet()
urlSet.add(url)
titleMap[title] = urlSet
map[channel] = titleMap
} }
return map return map
} }
private fun buildTV(
title: String,
videoUrl: String,
): TV {
val tv = TV()
tv.id = count++
tv.title = title
tv.videoUrl = videoUrl
return tv
}
} }