港澳台直播 v8.3.0 源码快照(含全部历史功能)
No files matched your search
@@ -0,0 +1,5 @@
|
||||
build/
|
||||
.gradle/
|
||||
local.properties
|
||||
*.apk
|
||||
.idea/
|
||||
@@ -0,0 +1,42 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.iptv807.tv"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.iptv807.tv"
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 830
|
||||
versionName = "8.3.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.webkit:webkit:1.9.0")
|
||||
// ExoPlayer (Media3) 播放 FLV/HLS 直播流
|
||||
implementation("androidx.media3:media3-exoplayer:1.3.1")
|
||||
implementation("androidx.media3:media3-exoplayer-hls:1.3.1")
|
||||
implementation("androidx.media3:media3-ui:1.3.1")
|
||||
implementation("androidx.media3:media3-datasource:1.3.1")
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
<application android:name=".IptvApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.IPTVTV"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:banner="@drawable/banner"
|
||||
android:exported="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:theme="@style/Theme.IPTVTV">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".LoginActivity" android:exported="false" />
|
||||
<activity android:name=".TrialExpiredActivity" android:exported="false" />
|
||||
<activity android:name=".ShopActivity" android:exported="false" />
|
||||
<receiver android:name=".BootReceiver" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<activity android:name=".SettingsActivity" android:exported="false" />
|
||||
<activity android:name=".AiSettingsActivity" android:exported="false" />
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
|
||||
/** AI 助手 —— 超保守实现:静态布局,异常完全不崩 */
|
||||
class AiChatActivity : Activity() {
|
||||
|
||||
private lateinit var scroll: ScrollView
|
||||
private lateinit var chatBox: LinearLayout
|
||||
private lateinit var input: EditText
|
||||
private lateinit var statusText: TextView
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var channelsText = ""
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
try { init() } catch (e: Exception) {
|
||||
val r = LinearLayout(this)
|
||||
r.orientation = LinearLayout.VERTICAL
|
||||
val tv = TextView(this)
|
||||
tv.text = "AI 加载失败(已恢复)。回按下返回重启"
|
||||
tv.setTextColor(Color.RED); tv.textSize = 16f
|
||||
r.addView(tv)
|
||||
setContentView(r)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun init() {
|
||||
val root = LinearLayout(this)
|
||||
root.orientation = LinearLayout.VERTICAL
|
||||
root.setBackgroundColor(Color.parseColor("#12141A"))
|
||||
root.setPadding(48, 40, 48, 40)
|
||||
setContentView(root)
|
||||
|
||||
val title = TextView(this)
|
||||
title.text = "🤖 AI 助手"
|
||||
title.textSize = 24f; title.setTextColor(Color.WHITE); title.gravity = Gravity.CENTER
|
||||
root.addView(title, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { bottomMargin = 20 })
|
||||
|
||||
// 检测配置
|
||||
if (!AiClient.isConfigured(this)) {
|
||||
val hint = TextView(this)
|
||||
hint.text = "尚未配置 AI。\n请先到 设置 → AI 配置 填写 API 地址 / Key / 模型名。"
|
||||
hint.textSize = 15f; hint.setTextColor(Color.LTGRAY); hint.gravity = Gravity.CENTER
|
||||
root.addView(hint, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = 200 })
|
||||
val go = Button(this)
|
||||
go.text = "去配置 AI"; go.textSize = 16f
|
||||
go.setOnClickListener {
|
||||
try { startActivity(Intent(this, AiSettingsActivity::class.java)) } catch (e: Exception) { }
|
||||
}
|
||||
root.addView(go, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = 40 })
|
||||
return
|
||||
}
|
||||
|
||||
// 拼频道清单
|
||||
try {
|
||||
channelsText = ChannelData.load().take(160).joinToString("\n") { c -> "${c.num}. ${c.name} [${c.group}]" }
|
||||
} catch (e: Exception) { channelsText = "" }
|
||||
|
||||
scroll = ScrollView(this)
|
||||
scroll.setBackgroundColor(Color.parseColor("#171A20"))
|
||||
chatBox = LinearLayout(this)
|
||||
chatBox.orientation = LinearLayout.VERTICAL
|
||||
chatBox.setPadding(24, 24, 24, 24)
|
||||
scroll.addView(chatBox)
|
||||
root.addView(scroll, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f
|
||||
).apply { bottomMargin = 16 })
|
||||
|
||||
// 快捷键按钮(功能入口)
|
||||
val quickActions = listOf(
|
||||
"📺 当前在播什么" to "@now",
|
||||
"🎯 推荐值得看" to "@rec",
|
||||
"📊 我的收视报告" to "@report",
|
||||
)
|
||||
val row = LinearLayout(this)
|
||||
row.orientation = LinearLayout.HORIZONTAL
|
||||
for ((label, action) in quickActions) {
|
||||
val b = Button(this)
|
||||
b.text = label.take(3); b.textSize = 11f
|
||||
b.setBackgroundColor(Color.parseColor("#2A2F3A"))
|
||||
b.setOnClickListener {
|
||||
try { ask(action, label) } catch (e: Exception) { statusText.text = "动作失败" }
|
||||
}
|
||||
row.addView(b, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { rightMargin = 8 })
|
||||
}
|
||||
root.addView(row, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = 16 })
|
||||
|
||||
input = EditText(this)
|
||||
input.hint = "问点啥…"
|
||||
input.textSize = 16f
|
||||
input.setTextColor(Color.WHITE); input.setHintTextColor(Color.GRAY)
|
||||
input.setBackgroundColor(Color.parseColor("#23262F"))
|
||||
input.setPadding(24, 20, 24, 20)
|
||||
input.setSingleLine(true)
|
||||
root.addView(input, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = 16 })
|
||||
|
||||
statusText = TextView(this)
|
||||
statusText.textSize = 13f; statusText.setTextColor(Color.LTGRAY); statusText.gravity = Gravity.CENTER
|
||||
root.addView(statusText, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = 12 })
|
||||
|
||||
val send = Button(this)
|
||||
send.text = "发送"; send.textSize = 17f
|
||||
send.setOnClickListener {
|
||||
try {
|
||||
val q = input.text?.toString()?.trim().orEmpty()
|
||||
if (q.isNotEmpty()) ask(q, null)
|
||||
input.setText("")
|
||||
} catch (e: Exception) { statusText.text = "发送失败" }
|
||||
}
|
||||
root.addView(send, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||
|
||||
addBubble("AI 已就绪。可以直接说话,或用上面的快捷键。", Color.parseColor("#2A3038"))
|
||||
}
|
||||
|
||||
private fun addBubble(text: String, bg: Int) {
|
||||
try {
|
||||
val v = TextView(this)
|
||||
v.text = text
|
||||
v.textSize = 15f; v.setTextColor(Color.WHITE)
|
||||
v.setBackgroundColor(bg)
|
||||
v.setPadding(20, 16, 20, 16)
|
||||
chatBox.addView(v, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = 12 })
|
||||
scroll.post { scroll.fullScroll(android.view.View.FOCUS_DOWN) }
|
||||
} catch (e: Exception) { }
|
||||
}
|
||||
|
||||
private fun ask(action: String, label: String?) {
|
||||
val q = when (action) {
|
||||
"@now" -> "当前时段值得看的频道有哪些节目"
|
||||
"@rec" -> "推荐我现在时期值得看的3-5个频道"
|
||||
"@report" -> "请基于我的收视记录给报告"
|
||||
else -> action
|
||||
}
|
||||
addBubble(label ?: q, Color.parseColor("#1B3A5C"))
|
||||
statusText.text = "思考中…"
|
||||
val ctx = this
|
||||
Thread {
|
||||
var reply = try {
|
||||
val sys = "你是Android TV IPTV应用的AI助手。频道清单(编号. 名称 [组]):\n$channelsText\n简洁回复,直接给频道编号和名字。"
|
||||
AiClient.chat(ctx, sys, q, 600)
|
||||
} catch (e: Exception) { "AI 调用失败: ${e.message}" }
|
||||
handler.post {
|
||||
try { statusText.text = "" } catch (e: Exception) { }
|
||||
try {
|
||||
// SWITCH:台号 切台
|
||||
if (reply.contains("SWITCH:")) {
|
||||
val target = reply.substringAfter("SWITCH:").substringBefore("\n").trim()
|
||||
val found = ChannelData.load().firstOrNull { ch -> ch.name.contains(target, true) }
|
||||
if (found != null) {
|
||||
MainActivity.playRequested = found.num
|
||||
addBubble("已切到:${found.num} ${found.name}", Color.parseColor("#2A4030"))
|
||||
handler.postDelayed({ try { finish() } catch (e: Exception) { } }, 800)
|
||||
return@post
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { }
|
||||
addBubble(reply, Color.parseColor("#2A3038"))
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
try { finish() } catch (e: Exception) { }
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.orFallback(): String = this ?: ""
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.content.Context
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* 通用 AI 客户端 —— 兼容 OpenAI /api/chat/completions 格式
|
||||
* 用户在设置里填: BASE_URL / API_KEY / MODEL(任何一个 OpenAI 兼容服务都可用,
|
||||
* 如 new-api 一站式、DeepSeek、通义、Kimi、OpenAI、本地Ollama等)
|
||||
*/
|
||||
object AiClient {
|
||||
private const val PREF = "iptv_ai"
|
||||
|
||||
fun baseUrl(ctx: Context) = ctx.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString("base_url", "https://ai.88531.cn/v1") ?: "https://ai.88531.cn/v1"
|
||||
fun apiKey(ctx: Context) = ctx.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString("api_key", "") ?: ""
|
||||
fun model(ctx: Context) = ctx.getSharedPreferences(PREF, Context.MODE_PRIVATE).getString("model", "deepseek-v4.1-flash") ?: "deepseek-v4.1-flash"
|
||||
|
||||
fun isConfigured(ctx: Context): Boolean =
|
||||
baseUrl(ctx).isNotEmpty() && apiKey(ctx).isNotEmpty() && model(ctx).isNotEmpty()
|
||||
|
||||
fun save(ctx: Context, baseUrl: String, apiKey: String, model: String) {
|
||||
ctx.getSharedPreferences(PREF, Context.MODE_PRIVATE).edit()
|
||||
.putString("base_url", baseUrl.trim().removeSuffix("/"))
|
||||
.putString("api_key", apiKey.trim())
|
||||
.putString("model", model.trim()).apply()
|
||||
}
|
||||
|
||||
/** chat 请求(同步,调用方自行放子线程)返回回复文本,失败抛异常 */
|
||||
fun chat(ctx: Context, systemPrompt: String, userPrompt: String, maxTokens: Int = 500): String {
|
||||
val url = URL(baseUrl(ctx) + "/chat/completions")
|
||||
val body = JSONObject().apply {
|
||||
put("model", model(ctx))
|
||||
put("max_tokens", maxTokens)
|
||||
put("temperature", 0.6)
|
||||
put("messages", JSONArray()
|
||||
.put(JSONObject().put("role", "system").put("content", systemPrompt))
|
||||
.put(JSONObject().put("role", "user").put("content", userPrompt)))
|
||||
}
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
connectTimeout = 10000
|
||||
readTimeout = 60000
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
setRequestProperty("Authorization", "Bearer ${apiKey(ctx)}")
|
||||
doOutput = true
|
||||
outputStream.use { it.write(body.toString().toByteArray(StandardCharsets.UTF_8)) }
|
||||
}
|
||||
val code = conn.responseCode
|
||||
val text = (if (code in 200..299) conn.inputStream else conn.errorStream)
|
||||
?.let { BufferedReader(InputStreamReader(it, StandardCharsets.UTF_8)).readText() } ?: ""
|
||||
conn.disconnect()
|
||||
if (code !in 200..299) throw RuntimeException("HTTP $code: ${text.take(180)}")
|
||||
val j = JSONObject(text)
|
||||
return j.optJSONArray("choices")?.optJSONObject(0)?.optJSONObject("message")?.optString("content") ?: ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
|
||||
/** 用户自定义 AI 服务配置(OpenAI兼容格式) */
|
||||
class AiSettingsActivity : Activity() {
|
||||
|
||||
private lateinit var baseEdit: EditText
|
||||
private lateinit var keyEdit: EditText
|
||||
private lateinit var modelEdit: EditText
|
||||
private lateinit var statusText: TextView
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setBackgroundColor(Color.parseColor("#12141A"))
|
||||
setPadding(80, 56, 80, 56)
|
||||
}
|
||||
setContentView(root)
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "AI 功能配置"
|
||||
textSize = 26f; setTextColor(Color.WHITE); gravity = android.view.Gravity.CENTER
|
||||
setPadding(0, 0, 0, 12)
|
||||
})
|
||||
root.addView(TextView(this).apply {
|
||||
text = "兼容 OpenAI 格式。支持:DeepSeek、通义、Kimi、OpenAI、本地Ollama、NewAPI 中转等。填你的 key 即可。"
|
||||
textSize = 13f; setTextColor(Color.LTGRAY); gravity = android.view.Gravity.CENTER
|
||||
setPadding(0, 0, 0, 32)
|
||||
})
|
||||
|
||||
fun mkEdit(hint: String, v: String = "") = EditText(this).apply {
|
||||
this.hint = hint
|
||||
setText(v)
|
||||
setSingleLine(true)
|
||||
textSize = 15f
|
||||
setTextColor(Color.WHITE); setHintTextColor(Color.GRAY)
|
||||
setBackgroundColor(Color.parseColor("#23262F"))
|
||||
setPadding(24, 20, 24, 20)
|
||||
}
|
||||
|
||||
baseEdit = mkEdit("https://ai.88531.cn/v1(可改自己的)", AiClient.baseUrl(this))
|
||||
keyEdit = mkEdit("API Key(sk-…)", AiClient.apiKey(this))
|
||||
modelEdit = mkEdit("deepseek-v4.1-flash(可改)", AiClient.model(this))
|
||||
listOf(baseEdit, keyEdit, modelEdit).forEach {
|
||||
root.addView(it, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply { bottomMargin = 24 })
|
||||
}
|
||||
|
||||
val saveBtn = Button(this).apply { text = "保存"; textSize = 17f }
|
||||
val testBtn = Button(this).apply { text = "测试连接"; textSize = 17f }
|
||||
val row = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL }
|
||||
row.addView(saveBtn, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { rightMargin = 16 })
|
||||
row.addView(testBtn, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
root.addView(row)
|
||||
|
||||
statusText = TextView(this).apply {
|
||||
textSize = 14f; setTextColor(Color.LTGRAY); gravity = android.view.Gravity.CENTER
|
||||
setPadding(0, 32, 0, 0)
|
||||
text = "AI 功能:语音搜台 · 节目解读 · 故障诊断 · 智能推荐 · 找片 · 收视报告"
|
||||
}
|
||||
root.addView(statusText)
|
||||
|
||||
saveBtn.setOnClickListener {
|
||||
if (baseEdit.text.trim().isEmpty() || keyEdit.text.trim().isEmpty() || modelEdit.text.trim().isEmpty()) {
|
||||
statusText.text = "三项都要填"; return@setOnClickListener
|
||||
}
|
||||
AiClient.save(this, baseEdit.text.toString(), keyEdit.text.toString(), modelEdit.text.toString())
|
||||
statusText.setTextColor(Color.parseColor("#4CAF50"))
|
||||
statusText.text = "✓ 已保存"
|
||||
}
|
||||
testBtn.setOnClickListener {
|
||||
if (!AiClient.isConfigured(this)) {
|
||||
statusText.setTextColor(Color.RED); statusText.text = "先填写并保存三项"; return@setOnClickListener
|
||||
}
|
||||
statusText.setTextColor(Color.LTGRAY)
|
||||
statusText.text = "测试中…"
|
||||
Thread {
|
||||
try {
|
||||
val r = AiClient.chat(this, "你是测试助手", "回复:OK")
|
||||
runOnUiThread { statusText.setTextColor(Color.parseColor("#4CAF50")); statusText.text = "✓ 连通:$r" }
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread { statusText.setTextColor(Color.RED); statusText.text = "✗ ${e.message}" }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
/** 开机自动启动(用户在设置里打开开关才启用) */
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
val i = Intent(context, LoginActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/** CardAuth 公开API的轻量封装 */
|
||||
object CardAuthApi {
|
||||
private const val BASE = "http://app.88531.cn:8881"
|
||||
private const val API_KEY = "ak_563f4768d5d411d381e58bc3f76ba447"
|
||||
|
||||
fun get(path: String): JSONObject = request("GET", path, null)
|
||||
fun post(path: String, body: JSONObject): JSONObject = request("POST", path, body.toString())
|
||||
|
||||
private fun request(method: String, path: String, body: String?): JSONObject {
|
||||
val conn = (URL(BASE + path).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = method
|
||||
connectTimeout = 8000; readTimeout = 10000
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
setRequestProperty("X-API-Key", API_KEY)
|
||||
if (body != null && method == "POST") {
|
||||
doOutput = true
|
||||
outputStream.use { it.write(body.toByteArray(StandardCharsets.UTF_8)) }
|
||||
}
|
||||
}
|
||||
val code = conn.responseCode
|
||||
val text = (if (code in 200..299) conn.inputStream else conn.errorStream)
|
||||
?.let { BufferedReader(InputStreamReader(it, StandardCharsets.UTF_8)).readText() } ?: ""
|
||||
conn.disconnect()
|
||||
if (text.trimStart().startsWith("<")) {
|
||||
throw RuntimeException("服务端返回了HTML(路径错误/网关拦截),HTTP $code")
|
||||
}
|
||||
return JSONObject(text)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
/**
|
||||
* 港澳台专项清单(55台)—— 3大子分组(香港 / 澳门 / 台湾)
|
||||
* 存 tid+id,播放Url每次实时解密(token 每次都换)
|
||||
*/
|
||||
object ChannelData {
|
||||
data class Channel(val num: Int, val name: String, val group: String, val tid: String, val id: String)
|
||||
data class Group(val name: String, val startNum: Int, val count: Int)
|
||||
|
||||
fun load(): List<Channel> = listOf(
|
||||
Channel(1, "HOY TV", "🌏 香港", "tv", "43"),
|
||||
Channel(2, "HOY国际财经台", "🌏 香港", "tv", "41"),
|
||||
Channel(3, "HOY资讯台", "🌏 香港", "tv", "42"),
|
||||
Channel(4, "NOW新闻台", "🌏 香港", "tv", "36"),
|
||||
Channel(5, "NOW财经台", "🌏 香港", "tv", "37"),
|
||||
Channel(6, "TVB Plus", "🌏 香港", "tv", "26"),
|
||||
Channel(7, "TVB无线新闻台", "🌏 香港", "tv", "25"),
|
||||
Channel(8, "TVB明珠台", "🌏 香港", "tv", "27"),
|
||||
Channel(9, "TVB翡翠台", "🌏 香港", "tv", "24"),
|
||||
Channel(10, "ViuTV", "🌏 香港", "tv", "38"),
|
||||
Channel(11, "ViuTV6", "🌏 香港", "tv", "39"),
|
||||
Channel(12, "凤凰卫视中文台", "🌏 香港", "tv", "20"),
|
||||
Channel(13, "凤凰卫视资讯台", "🌏 香港", "tv", "21"),
|
||||
Channel(14, "凤凰卫视香港台", "🌏 香港", "tv", "22"),
|
||||
Channel(15, "港台电视31", "🌏 香港", "tv", "45"),
|
||||
Channel(16, "港台电视32", "🌏 香港", "gt", "27"),
|
||||
Channel(17, "香港卫视HKS", "🌏 香港", "gt", "30"),
|
||||
Channel(18, "香港有线18台", "🌏 香港", "gt", "12"),
|
||||
Channel(19, "香港有线18台(备)", "🌏 香港", "gt", "13"),
|
||||
Channel(20, "香港有线新闻台", "🌏 香港", "tv", "29"),
|
||||
Channel(21, "澳視澳門", "🌏 澳门", "gt", "38"),
|
||||
Channel(22, "澳视综艺", "🌏 澳门", "gt", "40"),
|
||||
Channel(23, "澳视资讯", "🌏 澳门", "gt", "37"),
|
||||
Channel(24, "澳門Macau", "🌏 澳门", "gt", "39"),
|
||||
Channel(25, "澳門卫视", "🌏 澳门", "gt", "36"),
|
||||
Channel(26, "澳门莲花", "🌏 澳门", "gt", "35"),
|
||||
Channel(27, "DAZN 1", "🌏 台湾", "tv", "80"),
|
||||
Channel(28, "DAZN 2", "🌏 台湾", "tv", "81"),
|
||||
Channel(29, "ELEVEN 1(DAZN 1)", "🌏 台湾", "ty", "24"),
|
||||
Channel(30, "ELEVEN 2(DAZN 2)", "🌏 台湾", "ty", "25"),
|
||||
Channel(31, "TVBS HD", "🌏 台湾", "tv", "65"),
|
||||
Channel(32, "TVBS新闻台", "🌏 台湾", "tv", "63"),
|
||||
Channel(33, "三立新闻台", "🌏 台湾", "tv", "54"),
|
||||
Channel(34, "东森新闻台", "🌏 台湾", "tv", "50"),
|
||||
Channel(35, "东森财经新闻台", "🌏 台湾", "gt", "45"),
|
||||
Channel(36, "东森超视", "🌏 台湾", "tv", "98"),
|
||||
Channel(37, "中天新闻台", "🌏 台湾", "tv", "49"),
|
||||
Channel(38, "中视HD", "🌏 台湾", "tv", "68"),
|
||||
Channel(39, "中视新闻台", "🌏 台湾", "tv", "52"),
|
||||
Channel(40, "华视HD", "🌏 台湾", "tv", "71"),
|
||||
Channel(41, "华视新闻台", "🌏 台湾", "tv", "57"),
|
||||
Channel(42, "台视HD", "🌏 台湾", "tv", "69"),
|
||||
Channel(43, "台视新闻台", "🌏 台湾", "tv", "53"),
|
||||
Channel(44, "寰宇新闻台", "🌏 台湾", "tv", "59"),
|
||||
Channel(45, "寰宇新闻台湾台", "🌏 台湾", "tv", "60"),
|
||||
Channel(46, "民视HD", "🌏 台湾", "tv", "70"),
|
||||
Channel(47, "民视新闻台", "🌏 台湾", "tv", "56"),
|
||||
Channel(48, "爱尔达体育1台", "🌏 台湾", "tv", "83"),
|
||||
Channel(49, "爱尔达体育2台", "🌏 台湾", "tv", "84"),
|
||||
Channel(50, "緯來體育台", "🌏 台湾", "tv", "77"),
|
||||
Channel(51, "纬来日本台", "🌏 台湾", "tv", "78"),
|
||||
Channel(52, "纬来育乐台", "🌏 台湾", "tv", "79"),
|
||||
Channel(53, "镜电视新闻台", "🌏 台湾", "tv", "58"),
|
||||
Channel(54, "非凡新闻", "🌏 台湾", "tv", "62"),
|
||||
Channel(55, "华丽翡翠台", "🌏 其他", "tv", "28")
|
||||
)
|
||||
|
||||
fun groups(): List<Group> = listOf(
|
||||
Group("🌏 香港", 1, 20),
|
||||
Group("🌏 澳门", 21, 6),
|
||||
Group("🌏 台湾", 27, 28),
|
||||
Group("🌏 其他", 55, 1)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.content.Context
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/** 收视历史(供 AI 推荐/报告用) */
|
||||
object HistoryRecorder {
|
||||
private fun prefs(ctx: Context) = ctx.getSharedPreferences("iptv_history", Context.MODE_PRIVATE)
|
||||
private val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.CHINA)
|
||||
|
||||
fun log(ctx: Context, chanNum: Int, name: String) {
|
||||
val now = fmt.format(Date())
|
||||
val p = prefs(ctx)
|
||||
var count = p.getInt("count_${chanNum}", 0)
|
||||
count++
|
||||
p.edit().putInt("count_${chanNum}", count)
|
||||
.putString("last_" + chanNum, "$now $name").apply()
|
||||
// 只保留最近 90 days(清理老数据)
|
||||
if (p.all.size > 200) {
|
||||
val sorted = p.all.keys.sorted()
|
||||
p.edit().remove(sorted.take(50).toString()).apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun topChannels(ctx: Context, n: Int = 5): List<Pair<Int, String>> {
|
||||
val p = prefs(ctx)
|
||||
return p.all.entries
|
||||
.filter { it.key.startsWith("count_") }
|
||||
.sortedByDescending { it.value as Int }
|
||||
.take(n)
|
||||
.map { it.key.removePrefix("count_").toInt() }
|
||||
.mapNotNull { num -> ChannelData.load().find { it.num == num }?.let { num to it.name } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.app.Application
|
||||
|
||||
/** 应用级 crash 防护:把异常写标记后照常崩溃(不吞什),下次启动 reset license 缓存 */
|
||||
class IptvApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val default = Thread.getDefaultUncaughtExceptionHandler()
|
||||
Thread.setDefaultUncaughtExceptionHandler { t, e ->
|
||||
try {
|
||||
getSharedPreferences("iptv_crash", MODE_PRIVATE)
|
||||
.edit().putBoolean("crashed", true)
|
||||
.putString("msg", "${e.javaClass.simpleName}: ${e.message?.take(200)}").commit()
|
||||
} catch (ex: Exception) { }
|
||||
default?.uncaughtException(t, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* CardAuth 授权客户端(app.88531.cn:8881)
|
||||
*
|
||||
* 协议(已实测):
|
||||
* POST /api/public/verify
|
||||
* Header: X-API-Key: ak_...(项目API密钥)
|
||||
* Body: {"card_key":"...","machine_id":"TV-XXXX","device_info":"AndroidTV"}
|
||||
* 未用卡 → 自动激活绑定;已绑 → 校验machine_id一致 + 未过期
|
||||
* 响应 data: {valid, type, duration_days, expire_time, is_permanent, remaining_days}
|
||||
* 免费用户:无卡/无效卡 → 每天最多1小时(本地计时 + 服务器日期同步)
|
||||
*/
|
||||
object LicenseClient {
|
||||
private const val BASE = "http://app.88531.cn:8881"
|
||||
private const val API_KEY = "ak_563f4768d5d411d381e58bc3f76ba447"
|
||||
|
||||
data class Result(
|
||||
val ok: Boolean, // 通过授权(VIP)
|
||||
val message: String,
|
||||
val expireTime: String? = null,
|
||||
val isPermanent: Boolean = false,
|
||||
val remainingDays: Int? = null,
|
||||
val trial: Boolean = false // 走免费1小时
|
||||
)
|
||||
|
||||
/** 卡密验证/激活 */
|
||||
fun verify(cardKey: String, machineId: String): Result {
|
||||
return try {
|
||||
val body = JSONObject().apply {
|
||||
put("card_key", cardKey)
|
||||
put("machine_id", machineId)
|
||||
put("device_info", "AndroidTV ${Build.MODEL}")
|
||||
}
|
||||
val r = post("/api/public/verify", body)
|
||||
val data = r.optJSONObject("data") ?: JSONObject()
|
||||
when {
|
||||
r.optInt("code") == 200 && data.optBoolean("valid") ->
|
||||
Result(true, data.optString("message", "授权有效"),
|
||||
data.optString("expire_time", ""), data.optBoolean("is_permanent"),
|
||||
if (data.has("remaining_days") && !data.isNull("remaining_days")) data.optInt("remaining_days") else null)
|
||||
else -> Result(false, data.optString("message", r.optString("message", "授权无效")).ifEmpty { "卡密无效" })
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result(false, "网络错误: ${e.message ?: e.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun post(path: String, body: JSONObject): JSONObject {
|
||||
val url = URL(BASE + path)
|
||||
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
connectTimeout = 8000
|
||||
readTimeout = 10000
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
setRequestProperty("X-API-Key", API_KEY)
|
||||
doOutput = true
|
||||
outputStream.use { it.write(body.toString().toByteArray(StandardCharsets.UTF_8)) }
|
||||
}
|
||||
val code = conn.responseCode
|
||||
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
|
||||
val text = stream?.let { BufferedReader(InputStreamReader(it, StandardCharsets.UTF_8)).readText() } ?: ""
|
||||
conn.disconnect()
|
||||
return JSONObject(text)
|
||||
}
|
||||
|
||||
/** 稳定机器码(安装期持久化+硬件融合) */
|
||||
fun machineCode(ctx: Context): String {
|
||||
val prefs = ctx.getSharedPreferences("iptv_license", Context.MODE_PRIVATE)
|
||||
prefs.getString("machine_code", null)?.let { return it }
|
||||
val raw = listOf(Build.BOARD, Build.BRAND, Build.DEVICE, Build.MODEL, Build.PRODUCT).joinToString("|")
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val code = "TV-" + md.digest(raw.toByteArray(StandardCharsets.UTF_8))
|
||||
.joinToString("") { "%02X".format(it) }.take(12)
|
||||
prefs.edit().putString("machine_code", code).apply()
|
||||
return code
|
||||
}
|
||||
|
||||
// ===== 免费时长控制(每天1小时)=====
|
||||
const val FREE_DAILY_MS = 60 * 60 * 1000L
|
||||
|
||||
/** 今天已用免费毫秒数 */
|
||||
fun freeUsedTodayMs(ctx: Context): Long {
|
||||
val prefs = ctx.getSharedPreferences("iptv_license", Context.MODE_PRIVATE)
|
||||
val today = todayKey()
|
||||
if (prefs.getString("free_date", "") != today) return 0
|
||||
return prefs.getLong("free_used_ms", 0)
|
||||
}
|
||||
|
||||
fun addFreeUsedMs(ctx: Context, delta: Long) {
|
||||
val prefs = ctx.getSharedPreferences("iptv_license", Context.MODE_PRIVATE)
|
||||
val today = todayKey()
|
||||
val used = if (prefs.getString("free_date", "") == today) prefs.getLong("free_used_ms", 0) else 0
|
||||
prefs.edit().putString("free_date", today).putLong("free_used_ms", used + delta).apply()
|
||||
}
|
||||
|
||||
fun freeRemainingMs(ctx: Context): Long =
|
||||
(FREE_DAILY_MS - freeUsedTodayMs(ctx)).coerceAtLeast(0)
|
||||
|
||||
/** 日期键北京时间 */
|
||||
private fun todayKey(): String {
|
||||
val f = java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.CHINA)
|
||||
f.timeZone = java.util.TimeZone.getTimeZone("GMT+8")
|
||||
return f.format(java.util.Date())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
|
||||
/**
|
||||
* CardAuth 授权登录页 —— 应用第一屏:
|
||||
* 有效卡本地缓存 → 直接进播放;否则显示卡密输入
|
||||
* CardAuth 无用户体系:一卡一机绑定,验证即在服务端注册到期时间
|
||||
*/
|
||||
class LoginActivity : Activity() {
|
||||
|
||||
private lateinit var cardEdit: EditText
|
||||
private lateinit var statusText: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
try {
|
||||
// crashGuard: 若上次有崩溃标记,先清before检查
|
||||
getSharedPreferences("iptv_crash", MODE_PRIVATE).let {
|
||||
if (it.getBoolean("crashed", false)) {
|
||||
it.edit().putBoolean("crashed", false).apply()
|
||||
// 清理license cache以防被污染
|
||||
getSharedPreferences("iptv_license", MODE_PRIVATE).edit().remove("token").remove("card_key").apply()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { }
|
||||
|
||||
val prefs = getSharedPreferences("iptv_license", MODE_PRIVATE)
|
||||
val machine = LicenseClient.machineCode(this)
|
||||
|
||||
// 本地已授权缓存 → 快速放行(在线重验在主界面做,避免卡启动)
|
||||
if (prefs.getBoolean("is_vip", false)) {
|
||||
startMain(); return
|
||||
}
|
||||
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = android.view.Gravity.CENTER
|
||||
setBackgroundColor(Color.parseColor("#12141A"))
|
||||
setPadding(96, 64, 96, 64)
|
||||
}
|
||||
setContentView(root)
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "港澳台直播"
|
||||
textSize = 30f; setTextColor(Color.WHITE)
|
||||
gravity = android.view.Gravity.CENTER; setPadding(0, 0, 0, 16)
|
||||
})
|
||||
root.addView(TextView(this).apply {
|
||||
text = "免费用户每天可看 1 小时 · 输入卡密解锁全天观看"
|
||||
textSize = 15f; setTextColor(Color.LTGRAY)
|
||||
gravity = android.view.Gravity.CENTER; setPadding(0, 0, 0, 48)
|
||||
})
|
||||
|
||||
cardEdit = EditText(this).apply {
|
||||
hint = "卡密(如 8914-XXXX-XXXX)"
|
||||
setSingleLine(true)
|
||||
textSize = 18f
|
||||
setTextColor(Color.WHITE); setHintTextColor(Color.GRAY)
|
||||
setBackgroundColor(Color.parseColor("#23262F"))
|
||||
setPadding(24, 24, 24, 24)
|
||||
imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
}
|
||||
root.addView(cardEdit, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { bottomMargin = 32 })
|
||||
|
||||
val btn = Button(this).apply {
|
||||
text = "激活 / 验证"; textSize = 19f
|
||||
setOnClickListener { doVerify() }
|
||||
}
|
||||
root.addView(btn, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||
|
||||
statusText = TextView(this).apply {
|
||||
textSize = 14f; setTextColor(Color.LTGRAY)
|
||||
gravity = android.view.Gravity.CENTER; setPadding(0, 40, 0, 0)
|
||||
text = "已有卡密可直接激活;或购买套餐自动绑定本机"
|
||||
}
|
||||
root.addView(statusText)
|
||||
|
||||
val shopBtn = Button(this).apply {
|
||||
text = "购买套餐(支付宝/微信)"; textSize = 15f
|
||||
setBackgroundColor(Color.parseColor("#1B6EF3"))
|
||||
setOnClickListener { startActivity(android.content.Intent(this@LoginActivity, ShopActivity::class.java)) }
|
||||
}
|
||||
root.addView(shopBtn, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = 24 })
|
||||
|
||||
val freeBtn = Button(this).apply {
|
||||
text = "先看一会(免费)"; textSize = 14f
|
||||
setBackgroundColor(Color.parseColor("#2A3344"))
|
||||
setOnClickListener {
|
||||
getSharedPreferences("iptv_license", MODE_PRIVATE).edit().putBoolean("is_vip", false).apply()
|
||||
startMain()
|
||||
}
|
||||
}
|
||||
root.addView(freeBtn, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = 24 })
|
||||
|
||||
cardEdit.setOnEditorActionListener { _,_,_ -> doVerify(); true }
|
||||
}
|
||||
|
||||
private fun doVerify() {
|
||||
val card = cardEdit.text.toString().trim()
|
||||
if (card.isEmpty()) { statusText.text = "请输入卡密"; return }
|
||||
statusText.text = "验证中…"
|
||||
val machine = LicenseClient.machineCode(this)
|
||||
Thread {
|
||||
val r = LicenseClient.verify(card, machine)
|
||||
runOnUiThread {
|
||||
if (r.ok) {
|
||||
getSharedPreferences("iptv_license", MODE_PRIVATE).edit()
|
||||
.putBoolean("is_vip", true)
|
||||
.putString("card_key", card)
|
||||
.putString("vip_expire", r.expireTime)
|
||||
.putBoolean("is_permanent", r.isPermanent)
|
||||
.apply()
|
||||
Toast.makeText(this, "✓ ${r.message}${if (!r.isPermanent && r.expireTime != null) "\n到期: ${r.expireTime}" else ""}", Toast.LENGTH_LONG).show()
|
||||
startMain()
|
||||
} else {
|
||||
statusText.text = r.message
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun startMain() {
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,742 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AbsListView
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.ui.PlayerView
|
||||
|
||||
/**
|
||||
* 港澳台直播 v7.0 —— 纯原生播放(无网页兜底)
|
||||
*
|
||||
* · App 本地解密获取新鲜播放源 → ExoPlayer 直接播放(带 Referer/302)
|
||||
* (纯原生播放,无网页兜底)
|
||||
*/
|
||||
class MainActivity : Activity() {
|
||||
|
||||
private val channels: List<ChannelData.Channel> = ChannelData.load()
|
||||
private val groups: List<ChannelData.Group> = ChannelData.groups()
|
||||
private val favGroup = ChannelData.Group("❤ 收藏", 0, 0)
|
||||
|
||||
private var player: ExoPlayer? = null
|
||||
private lateinit var playerView: PlayerView
|
||||
|
||||
private lateinit var statusText: TextView
|
||||
private lateinit var trialText: TextView
|
||||
private lateinit var digitText: TextView
|
||||
private lateinit var loadingText: TextView
|
||||
private lateinit var loadingMask: FrameLayout
|
||||
private lateinit var root: FrameLayout
|
||||
private var panelLayout: LinearLayout? = null
|
||||
private var groupList: ListView? = null
|
||||
private var channelList: ListView? = null
|
||||
private var curGroupPos = 0
|
||||
private var currentChannel = -1
|
||||
private var currentLine = 0
|
||||
private var mode = MODE_NATIVE
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val hidePanelTask = Runnable { hidePanel() }
|
||||
private val uiTick = Runnable { onUiTick() }
|
||||
private var loadStart = 0L
|
||||
private val loadTick = object : Runnable {
|
||||
override fun run() {
|
||||
if (loadingMask.visibility != View.VISIBLE) return
|
||||
val s = ((System.currentTimeMillis() - loadStart) / 1000 + 1).toInt()
|
||||
loadingText.text = "正在加载 $s 秒,精彩继续…"
|
||||
handler.postDelayed(this, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
private var digitBuf = ""
|
||||
private val digitTask = object : CountDownTimer(2000, 2000) {
|
||||
override fun onTick(m: Long) {}
|
||||
override fun onFinish() { if (digitBuf.isNotEmpty()) { commitDigit(); digitBuf = "" } }
|
||||
}
|
||||
|
||||
private var isVip = false
|
||||
private var lastPlayError = ""
|
||||
private var lastSelfTest = ""
|
||||
private var failureReport = ""
|
||||
private var wasStopped = false
|
||||
private var resumeBtn: TextView? = null
|
||||
// 连续滑动两次退出
|
||||
private var lastSwipeTs = 0L
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
private var downT = 0L
|
||||
private var exitHint: TextView? = null
|
||||
|
||||
companion object {
|
||||
const val MODE_NATIVE = 0
|
||||
@JvmStatic var playRequested: Int = -1
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "SetTextI18n", "ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
try { init(savedInstanceState) } catch (e: Exception) {
|
||||
// 保守恢复:显示错误不闪退
|
||||
val r = FrameLayout(this)
|
||||
r.setBackgroundColor(Color.BLACK)
|
||||
val tv = TextView(this)
|
||||
tv.text = "启动异常已恢复 (${e.javaClass.simpleName})\n请按返回键重启"
|
||||
tv.setTextColor(Color.WHITE); tv.textSize = 18f; tv.gravity = Gravity.CENTER
|
||||
r.addView(tv)
|
||||
setContentView(r)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility")
|
||||
private fun init(savedInstanceState: Bundle?) {
|
||||
hideSystemUi()
|
||||
root = FrameLayout(this)
|
||||
setContentView(root)
|
||||
|
||||
// 1) 原生播放层
|
||||
playerView = PlayerView(this).apply { useController = false }
|
||||
root.addView(playerView, FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
|
||||
// 3) 左上状态
|
||||
statusText = TextView(this).apply {
|
||||
textSize = 18f; setTextColor(Color.WHITE)
|
||||
setShadowLayer(4f, 2f, 2f, Color.BLACK)
|
||||
setPadding(32, 24, 32, 24)
|
||||
}
|
||||
root.addView(statusText, FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||
|
||||
// 4) 右上:免费时长 + 模式
|
||||
val rightCol = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL }
|
||||
trialText = TextView(this).apply {
|
||||
textSize = 14f; setTextColor(Color.WHITE)
|
||||
setBackgroundColor(0x88000000u.toInt())
|
||||
setPadding(20, 10, 20, 10)
|
||||
}
|
||||
rightCol.addView(trialText)
|
||||
val rcLP = FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP or Gravity.END)
|
||||
root.addView(rightCol, rcLP)
|
||||
|
||||
// 5) 数字键大字
|
||||
digitText = TextView(this).apply {
|
||||
textSize = 56f; setTextColor(Color.WHITE)
|
||||
setShadowLayer(6f, 3f, 3f, Color.BLACK)
|
||||
setPadding(24, 64, 48, 24)
|
||||
visibility = View.GONE
|
||||
}
|
||||
root.addView(digitText, FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP or Gravity.END))
|
||||
|
||||
// 6) 加载罩
|
||||
loadingMask = FrameLayout(this).apply { setBackgroundColor(0xB2000000u.toInt()); visibility = View.GONE }
|
||||
loadingText = TextView(this).apply {
|
||||
textSize = 28f; setTextColor(Color.WHITE); gravity = Gravity.CENTER
|
||||
setShadowLayer(6f, 3f, 3f, Color.BLACK)
|
||||
}
|
||||
loadingMask.addView(loadingText, FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
root.addView(loadingMask, FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
|
||||
// 8) 「继续播放」按钮(从桌面回来画面卡住时点它重连)
|
||||
resumeBtn = TextView(this).apply {
|
||||
text = "▶ 继续播放"
|
||||
textSize = 22f
|
||||
setTextColor(Color.WHITE)
|
||||
setBackgroundColor(0xE61B6EF3u.toInt())
|
||||
setPadding(48, 24, 48, 24)
|
||||
gravity = Gravity.CENTER
|
||||
isFocusable = true
|
||||
visibility = View.GONE
|
||||
setOnClickListener { resumePlayback() }
|
||||
}
|
||||
root.addView(resumeBtn, FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER))
|
||||
|
||||
// 7) ExoPlayer 初始化
|
||||
val loadControl = androidx.media3.exoplayer.DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(1500, 12000, 500, 1000)
|
||||
.build()
|
||||
player = ExoPlayer.Builder(this).setLoadControl(loadControl).setMediaSourceFactory(
|
||||
DefaultMediaSourceFactory(
|
||||
DefaultDataSource.Factory(this,
|
||||
DefaultHttpDataSource.Factory()
|
||||
.setUserAgent("Mozilla/5.0 (Linux; Android 12; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36")
|
||||
.setDefaultRequestProperties(mapOf(
|
||||
"Referer" to "https://m.iptv807.com/",
|
||||
"Origin" to "https://m.iptv807.com"
|
||||
))
|
||||
.setAllowCrossProtocolRedirects(true)
|
||||
.setConnectTimeoutMs(8000)
|
||||
.setReadTimeoutMs(12000)
|
||||
)
|
||||
)).build()
|
||||
playerView.player = player
|
||||
|
||||
player?.addListener(object : Player.Listener {
|
||||
override fun onPlayerError(err: androidx.media3.common.PlaybackException) {
|
||||
lastPlayError = "播放错误 code=${err.errorCode}: ${err.message?.take(60) ?: ""}"
|
||||
autoSwitchLine()
|
||||
}
|
||||
override fun onPlaybackStateChanged(state: Int) {
|
||||
when (state) {
|
||||
Player.STATE_READY -> { setMode(MODE_NATIVE); hideLoading() }
|
||||
Player.STATE_BUFFERING -> { /* 缓冲中,保持计时继续 */ }
|
||||
}
|
||||
}
|
||||
override fun onIsPlayingChanged(p: Boolean) {
|
||||
if (p) { hideLoading(); resumeBtn?.visibility = View.GONE }
|
||||
}
|
||||
})
|
||||
|
||||
// 触屏/遥控 OK 弹面板
|
||||
playerView.setOnClickListener { if (!isPanelVisible()) showPanel() else hidePanel() }
|
||||
|
||||
SourceResolver.init(this)
|
||||
// 播放期间保持屏幕常亮
|
||||
window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
checkLicense()
|
||||
if (savedInstanceState == null && channels.isNotEmpty()) {
|
||||
val defIdx = channels.indexOfFirst { it.name.contains("TVB翡翠台") }.let { if (it >= 0) it else 0 }
|
||||
playChannel(defIdx)
|
||||
// 启动后台预取默认频道所在分组,之后切台秒开
|
||||
val g = channels.getOrNull(defIdx)?.group
|
||||
val gi = ChannelData.groups().indexOfFirst { it.name == g }
|
||||
if (gi >= 0) handler.postDelayed({ prefetchGroup(gi) }, 2500)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMode(m: Int) { mode = m }
|
||||
|
||||
/** 启动自检:解析源 + 实测第一线路连通性,结果在状态栏显示 10 秒 */
|
||||
private fun runSelfTest(idx: Int) {
|
||||
val ch = channels.getOrNull(idx) ?: return
|
||||
Thread {
|
||||
val t0 = System.currentTimeMillis()
|
||||
val urls = try { SourceResolver.resolve(ch.tid, ch.id) } catch (e: Exception) { null }
|
||||
val resolveMs = System.currentTimeMillis() - t0
|
||||
val info = StringBuilder()
|
||||
if (urls.isNullOrEmpty()) {
|
||||
info.append("自检: 解析失败(${SourceResolver.lastError})")
|
||||
} else {
|
||||
info.append("自检: 解析${urls.size}线/${resolveMs}ms")
|
||||
// 实测第一条能否连上
|
||||
info.append(" | ").append(SourceResolver.verifyChain(ch.tid, ch.id))
|
||||
}
|
||||
runOnUiThread {
|
||||
statusText.text = info.toString()
|
||||
handler.postDelayed({
|
||||
// 10 秒后恢复正常显示
|
||||
val c = channels.getOrNull(currentChannel)
|
||||
if (c != null) statusText.text = "▶ ${c.num} ${c.name}"
|
||||
}, 10000)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun freeRemainingMs(): Long = LicenseClient.freeRemainingMs(this)
|
||||
private fun checkLicense() {
|
||||
val prefs = getSharedPreferences("iptv_license", MODE_PRIVATE)
|
||||
val card = prefs.getString("card_key", "") ?: ""
|
||||
Thread {
|
||||
val r = if (card.isNotEmpty()) LicenseClient.verify(card, LicenseClient.machineCode(this)) else LicenseClient.Result(false, "免费")
|
||||
runOnUiThread {
|
||||
isVip = r.ok
|
||||
prefs.edit().putBoolean("is_vip", r.ok).apply()
|
||||
if (r.ok) { trialText.visibility = View.GONE; trialText.text = "VIP" } else enterFreeMode()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun enterFreeMode() {
|
||||
trialText.visibility = View.VISIBLE
|
||||
handler.removeCallbacks(uiTick)
|
||||
handler.post(uiTick)
|
||||
}
|
||||
|
||||
private fun onUiTick() {
|
||||
if (isVip) return
|
||||
// 每秒累计一次免费用量,这样右上角倒计时才会真正走动
|
||||
if (player?.isPlaying == true) LicenseClient.addFreeUsedMs(this, 1000)
|
||||
val r = freeRemainingMs()
|
||||
trialText.visibility = View.VISIBLE
|
||||
trialText.text = if (r <= 0) "⏳ 今日免費已用完"
|
||||
else "⏳ 今日免費 ${r / 60000}分${((r % 60000) / 1000).toString().padStart(2, '0')}秒"
|
||||
handler.postDelayed(uiTick, 1000)
|
||||
}
|
||||
|
||||
private fun showTrialExpired() {
|
||||
player?.pause()
|
||||
trialText.visibility = View.GONE
|
||||
statusText.text = "今日免费时长已用完"
|
||||
startActivity(Intent(this, TrialExpiredActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun isPanelVisible(): Boolean = panelLayout?.visibility == View.VISIBLE
|
||||
|
||||
private fun panelGroups(): List<ChannelData.Group> =
|
||||
if (favSet().isNotEmpty()) listOf(favGroup) + groups else groups
|
||||
|
||||
private fun favSet(): MutableSet<String> =
|
||||
getSharedPreferences("iptv_fav", MODE_PRIVATE).getStringSet("favs", HashSet<String>())!!.toMutableSet()
|
||||
|
||||
private fun toggleFav(ch: ChannelData.Channel) {
|
||||
val s = favSet()
|
||||
val n = ch.num.toString()
|
||||
if (n in s) s.remove(n) else s.add(n)
|
||||
getSharedPreferences("iptv_fav", MODE_PRIVATE).edit().putStringSet("favs", s).apply()
|
||||
}
|
||||
|
||||
private fun channelsOfGroup(gpos: Int): List<ChannelData.Channel> {
|
||||
val g = panelGroups()[gpos]
|
||||
return if (g.name == favGroup.name) {
|
||||
favSet().mapNotNull { n -> channels.find { it.num == n.toInt() } }.sortedBy { it.num }
|
||||
} else channels.filter { it.group == g.name }
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun showPanel() {
|
||||
if (panelLayout == null) {
|
||||
panelLayout = buildPanel()
|
||||
root.addView(panelLayout, FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
}
|
||||
panelLayout?.visibility = View.VISIBLE
|
||||
val gpos = panelGroups().indexOfFirst { g ->
|
||||
val num = channels.getOrNull(currentChannel)?.num
|
||||
if (g.name == favGroup.name) num?.toString() in favSet()
|
||||
else g.name == channels.getOrNull(currentChannel)?.group
|
||||
}
|
||||
if (gpos >= 0) { curGroupPos = gpos; groupList?.setSelection(gpos + 1); updateChannelListOfGroup(gpos) }
|
||||
resetHideTimer()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun buildPanel(): LinearLayout = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
setBackgroundColor(0xE8141418u.toInt())
|
||||
setOnClickListener { hidePanel() }
|
||||
|
||||
groupList = ListView(this@MainActivity).apply {
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
adapter = ArrayAdapter(this@MainActivity, android.R.layout.simple_list_item_1,
|
||||
listOf("⚙ 设置") + panelGroups().map { "${it.name} (${it.count})" })
|
||||
onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ ->
|
||||
if (pos == 0) {
|
||||
startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
|
||||
hidePanel(); return@OnItemClickListener
|
||||
}
|
||||
curGroupPos = pos - 1
|
||||
groupList?.setSelection(pos)
|
||||
updateChannelListOfGroup(curGroupPos)
|
||||
}
|
||||
setPadding(8, 24, 8, 24)
|
||||
setOnTouchListener { v, ev ->
|
||||
if (ev.action == MotionEvent.ACTION_DOWN || ev.action == MotionEvent.ACTION_MOVE) resetHideTimer()
|
||||
v.onTouchEvent(ev)
|
||||
}
|
||||
}
|
||||
addView(groupList, LinearLayout.LayoutParams(
|
||||
(resources.displayMetrics.widthPixels * 0.2f).toInt(), ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
|
||||
channelList = ListView(this@MainActivity).apply {
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ ->
|
||||
val list = channelsOfGroup(curGroupPos)
|
||||
if (pos < list.size) { playChannel(channels.indexOf(list[pos])); hidePanel() }
|
||||
}
|
||||
setPadding(8, 24, 8, 24)
|
||||
setOnTouchListener { v, ev ->
|
||||
if (ev.action == MotionEvent.ACTION_DOWN || ev.action == MotionEvent.ACTION_MOVE) resetHideTimer()
|
||||
v.onTouchEvent(ev)
|
||||
}
|
||||
setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(v: AbsListView, s: Int) { resetHideTimer() }
|
||||
override fun onScroll(v: AbsListView, a: Int, b: Int, c: Int) { resetHideTimer() }
|
||||
})
|
||||
}
|
||||
addView(channelList, LinearLayout.LayoutParams(
|
||||
(resources.displayMetrics.widthPixels * 0.38f).toInt(), ViewGroup.LayoutParams.MATCH_PARENT))
|
||||
}
|
||||
|
||||
private fun updateChannelListOfGroup(pos: Int) {
|
||||
prefetchGroup(pos)
|
||||
val list = channelsOfGroup(pos)
|
||||
channelList?.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1,
|
||||
list.map { "%3d %s".format(it.num, it.name) })
|
||||
}
|
||||
|
||||
private fun hidePanel() { handler.removeCallbacks(hidePanelTask); panelLayout?.visibility = View.GONE }
|
||||
|
||||
/** 后台预取某分组所有频道的源,让切台变秒切(每次运行只做一轮) */
|
||||
private var prefetchedGroups = HashSet<Int>()
|
||||
private fun prefetchGroup(gpos: Int) {
|
||||
if (prefetchedGroups.contains(gpos)) return
|
||||
prefetchedGroups.add(gpos)
|
||||
val list = channelsOfGroup(gpos)
|
||||
Thread {
|
||||
for (ch in list) {
|
||||
try { SourceResolver.resolveLines(ch.tid, ch.id) } catch (e: Exception) { }
|
||||
try { Thread.sleep(250) } catch (ie: InterruptedException) { }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
private fun resetHideTimer() {
|
||||
handler.removeCallbacks(hidePanelTask)
|
||||
handler.postDelayed(hidePanelTask, 5000)
|
||||
}
|
||||
|
||||
private fun hideLoading() { loadingMask.visibility = View.GONE; handler.removeCallbacks(loadTick) }
|
||||
|
||||
/** 开始一次新的换台计时(只在切台时调用) */
|
||||
private fun beginLoading() {
|
||||
loadStart = System.currentTimeMillis()
|
||||
loadingText.text = "正在加载 1 秒,精彩继续…"
|
||||
loadingMask.visibility = View.VISIBLE
|
||||
handler.removeCallbacks(loadTick)
|
||||
handler.postDelayed(loadTick, 1000)
|
||||
}
|
||||
|
||||
private fun playChannel(idx: Int, line: Int = 0) {
|
||||
if (idx >= channels.size) return
|
||||
val isSameChannel = (idx == currentChannel)
|
||||
currentChannel = idx; currentLine = line
|
||||
val ch = channels[idx]
|
||||
if (!isSameChannel || loadingMask.visibility != View.VISIBLE) {
|
||||
beginLoading() // 只有换台才重置计时;同台换线路时继续计时
|
||||
}
|
||||
Thread {
|
||||
val urls = try { SourceResolver.resolve(ch.tid, ch.id) } catch (e: Exception) { null }
|
||||
if (urls.isNullOrEmpty()) {
|
||||
// 自动重试 3 次(间隔 4 秒),期间给友好提示
|
||||
var got: List<String>? = null
|
||||
for (r in 1..3) {
|
||||
runOnUiThread { statusText.text = "网络波动,正在重试 ($r/3)…"; loadingText.text = "正在加载,精彩继续…" }
|
||||
Thread.sleep(4000)
|
||||
got = try { SourceResolver.resolveLines(ch.tid, ch.id) } catch (e: Exception) { null }
|
||||
if (!got.isNullOrEmpty()) break
|
||||
}
|
||||
if (got.isNullOrEmpty()) {
|
||||
runOnUiThread {
|
||||
statusText.text = "⚠ ${ch.num} ${ch.name} 源解析失败\n解析: ${SourceResolver.lastError}"
|
||||
failureReport = "解析失败 ${SourceResolver.lastError}"
|
||||
hideLoading()
|
||||
}
|
||||
return@Thread
|
||||
}
|
||||
val u2 = got!!
|
||||
runOnUiThread { playUrl(u2[0], ch) }
|
||||
return@Thread
|
||||
}
|
||||
// 不预探测(那是切台慢的主因):直接播对应线路,失败由 onPlayerError 自动换下一条
|
||||
var chosen = line % urls.size
|
||||
val probe = StringBuilder("skip")
|
||||
val tok = Regex("token=([^&]+)").find(urls[0])?.groupValues?.get(1)?.take(14) ?: "?"
|
||||
failureReport = "探测: $probe\ntoken: $tok"
|
||||
lastSelfTest = failureReport
|
||||
if (chosen < 0) {
|
||||
// 用的是缓存线路且全失效 → 强制刷新一次再试
|
||||
runOnUiThread { statusText.text = "线路失效,正在刷新源…" }
|
||||
Thread.sleep(2500)
|
||||
val fresh = try { SourceResolver.resolveFresh(ch.tid, ch.id) } catch (e: Exception) { null }
|
||||
if (!fresh.isNullOrEmpty() && fresh != urls) {
|
||||
for (i in fresh.indices) {
|
||||
val st = probeLine(fresh[i])
|
||||
if (st == "200" || st == "206" || st == "302") { chosen = i; break }
|
||||
}
|
||||
if (chosen >= 0) {
|
||||
runOnUiThread { playUrl(fresh[chosen], ch) }
|
||||
return@Thread
|
||||
}
|
||||
}
|
||||
}
|
||||
runOnUiThread {
|
||||
if (chosen < 0) {
|
||||
statusText.text = "⚠ ${ch.num} ${ch.name} 源站拒绝(全部线路)\n$failureReport"
|
||||
hideLoading()
|
||||
} else {
|
||||
playUrl(urls[chosen], ch)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
/** 轻量探测一条线路:跟 302 后取 HTTP 状态码 */
|
||||
private fun probeLine(url: String): String = try {
|
||||
val conn = (java.net.URL(url).openConnection() as java.net.HttpURLConnection).apply {
|
||||
requestMethod = "GET"; connectTimeout = 4000; readTimeout = 4000
|
||||
setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 12; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36")
|
||||
setRequestProperty("Referer", "https://m.iptv807.com/")
|
||||
setRequestProperty("Origin", "https://m.iptv807.com")
|
||||
setRequestProperty("Range", "bytes=0-1")
|
||||
instanceFollowRedirects = true
|
||||
}
|
||||
val code = conn.responseCode
|
||||
runCatching { conn.inputStream?.close() }
|
||||
conn.disconnect()
|
||||
code.toString()
|
||||
} catch (e: Exception) { "E:${e.javaClass.simpleName}" }
|
||||
|
||||
private fun playUrl(url: String, ch: ChannelData.Channel) {
|
||||
val isFlv = url.contains("type=.flv") || url.endsWith(".flv")
|
||||
val item = MediaItem.Builder().setUri(Uri.parse(url))
|
||||
.setMimeType(if (isFlv) MimeTypes.VIDEO_FLV else MimeTypes.APPLICATION_M3U8)
|
||||
.build()
|
||||
player?.setMediaItem(item)
|
||||
player?.prepare()
|
||||
player?.playWhenReady = true
|
||||
statusText.text = "… ${ch.num} ${ch.name}"
|
||||
HistoryRecorder.log(this, ch.num, ch.name)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun autoSwitchLine() {
|
||||
val ch = channels.getOrNull(currentChannel) ?: return
|
||||
currentLine++
|
||||
if (currentLine > 12) {
|
||||
val vn = try { packageManager.getPackageInfo(packageName, 0).versionName } catch (e: Exception) { "?" }
|
||||
statusText.text = "⚠ ${ch.num} ${ch.name} 全部线路失败\n$lastPlayError\n解析: ${SourceResolver.lastError}\n$failureReport\n版本: v$vn"
|
||||
hideLoading()
|
||||
return
|
||||
}
|
||||
playChannel(currentChannel, currentLine)
|
||||
}
|
||||
|
||||
private fun onDigit(d: Int) {
|
||||
digitBuf += d.toString()
|
||||
digitText.text = digitBuf
|
||||
digitText.visibility = View.VISIBLE
|
||||
val t = digitBuf.toIntOrNull() ?: return
|
||||
val idx = channels.indexOfFirst { it.num == t }
|
||||
if (idx >= 0) {
|
||||
playChannel(idx); digitBuf = ""; digitText.visibility = View.GONE; digitTask.cancel()
|
||||
} else { digitTask.cancel(); digitTask.start() }
|
||||
}
|
||||
private fun commitDigit() {
|
||||
val t = digitBuf.toIntOrNull() ?: return
|
||||
val idx = channels.indexOfFirst { it.num == t }
|
||||
if (idx >= 0) playChannel(idx)
|
||||
digitText.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
// 「继续播放」按钮可见时,遥控 OK/确认 直接触发
|
||||
if (resumeBtn?.visibility == View.VISIBLE &&
|
||||
(keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER ||
|
||||
keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER || keyCode == KeyEvent.KEYCODE_SPACE)) {
|
||||
resumePlayback(); return true
|
||||
}
|
||||
if (isPanelVisible()) resetHideTimer()
|
||||
if (keyCode in KeyEvent.KEYCODE_0..KeyEvent.KEYCODE_9) { onDigit(keyCode - KeyEvent.KEYCODE_0); return true }
|
||||
if (keyCode == KeyEvent.KEYCODE_NUMPAD_0) { onDigit(0); return true }
|
||||
if (keyCode in KeyEvent.KEYCODE_NUMPAD_1..KeyEvent.KEYCODE_NUMPAD_9) { onDigit(keyCode - KeyEvent.KEYCODE_NUMPAD_0); return true }
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_MENU -> {
|
||||
if (isPanelVisible()) {
|
||||
val list = channelsOfGroup(curGroupPos)
|
||||
val pos = channelList?.checkedItemPosition ?: -1
|
||||
if (pos in 0 until list.size) {
|
||||
toggleFav(list[pos])
|
||||
updateChannelListOfGroup(curGroupPos)
|
||||
Toast.makeText(this, if (favSet().contains(list[pos].num.toString())) "★ 已收藏 ${list[pos].name}" else "☆ 已取消 ${list[pos].name}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return true
|
||||
} else { autoSwitchLine(); return true }
|
||||
}
|
||||
KeyEvent.KEYCODE_CHANNEL_UP -> { playChannel((currentChannel - 1 + channels.size) % channels.size); return true }
|
||||
KeyEvent.KEYCODE_CHANNEL_DOWN -> { playChannel((currentChannel + 1) % channels.size); return true }
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
if (isPanelVisible()) {
|
||||
curGroupPos = (curGroupPos - 1 + groups.size) % groups.size
|
||||
groupList?.setSelection(curGroupPos + 1); updateChannelListOfGroup(curGroupPos)
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
if (isPanelVisible()) {
|
||||
curGroupPos = (curGroupPos + 1) % groups.size
|
||||
groupList?.setSelection(curGroupPos + 1); updateChannelListOfGroup(curGroupPos)
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||
if (isPanelVisible()) return super.onKeyDown(keyCode, event)
|
||||
playChannel((currentChannel + (if (keyCode == KeyEvent.KEYCODE_DPAD_UP) -1 else 1) + channels.size) % channels.size); return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER -> {
|
||||
if (isPanelVisible()) {
|
||||
val list = channelsOfGroup(curGroupPos)
|
||||
val pos = channelList?.checkedItemPosition ?: -1
|
||||
if (pos >= 0 && pos < list.size) { playChannel(channels.indexOf(list[pos])); hidePanel(); return true }
|
||||
}
|
||||
showPanel(); return true
|
||||
}
|
||||
KeyEvent.KEYCODE_BACK -> { if (isPanelVisible()) { hidePanel(); return true } }
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
/** 全局手势:连续滑动两次 → 完全退出 App */
|
||||
override fun dispatchTouchEvent(ev: android.view.MotionEvent?): Boolean {
|
||||
try {
|
||||
if (ev != null) {
|
||||
when (ev.actionMasked) {
|
||||
android.view.MotionEvent.ACTION_DOWN -> {
|
||||
downX = ev.rawX; downY = ev.rawY; downT = System.currentTimeMillis()
|
||||
}
|
||||
android.view.MotionEvent.ACTION_UP -> {
|
||||
val dx = ev.rawX - downX
|
||||
val dy = ev.rawY - downY
|
||||
val dist = kotlin.math.hypot(dx.toDouble(), dy.toDouble()).toFloat()
|
||||
val dt = System.currentTimeMillis() - downT
|
||||
// 一次有效滑动:位移够大、时间够短
|
||||
if (dist > 120f && dt < 800) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastSwipeTs < 1500) { exitApp(); return true }
|
||||
lastSwipeTs = now
|
||||
showExitHint()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { }
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
/** 第一次滑动后提示"再滑一次退出" */
|
||||
private fun showExitHint() {
|
||||
runOnUiThread {
|
||||
if (exitHint == null) {
|
||||
exitHint = TextView(this).apply {
|
||||
textSize = 18f; setTextColor(Color.WHITE)
|
||||
setBackgroundColor(0xB3000000u.toInt())
|
||||
setPadding(40, 20, 40, 20); gravity = Gravity.CENTER
|
||||
}
|
||||
(window.decorView as android.view.ViewGroup).addView(exitHint,
|
||||
android.view.ViewGroup.LayoutParams(
|
||||
android.view.ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
android.view.ViewGroup.LayoutParams.WRAP_CONTENT).also {
|
||||
(it as android.widget.FrameLayout.LayoutParams).gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
|
||||
it.bottomMargin = 80
|
||||
})
|
||||
}
|
||||
exitHint?.text = "再滑动一次退出应用"
|
||||
exitHint?.visibility = View.VISIBLE
|
||||
handler.removeCallbacks(hideExitHintTask)
|
||||
handler.postDelayed(hideExitHintTask, 2000)
|
||||
}
|
||||
}
|
||||
private val hideExitHintTask = Runnable { exitHint?.visibility = View.GONE }
|
||||
|
||||
/** 退出前先弹窗确认 */
|
||||
private fun exitApp() {
|
||||
runOnUiThread {
|
||||
try {
|
||||
exitHint?.visibility = View.GONE
|
||||
android.app.AlertDialog.Builder(this)
|
||||
.setTitle("退出应用")
|
||||
.setMessage("确定要退出「港澳台直播」吗?")
|
||||
.setPositiveButton("退出") { _, _ -> doExit() }
|
||||
.setNegativeButton("取消", null)
|
||||
.setCancelable(true)
|
||||
.show()
|
||||
} catch (e: Exception) {
|
||||
doExit() // 弹窗异常时直接退出,别卡住
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 真正完全退出 App */
|
||||
private fun doExit() {
|
||||
try { handler.removeCallbacksAndMessages(null) } catch (e: Exception) { }
|
||||
try { player?.release() } catch (e: Exception) { }
|
||||
try { finishAndRemoveTask() } catch (e: Exception) { finishAffinity() }
|
||||
// 彻底结束进程(电视盒子上保证真的退出)
|
||||
handler.postDelayed({ android.os.Process.killProcess(android.os.Process.myPid()) }, 250)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// 从桌面/设置返回:先尝试续播,失败则显示「继续播放」按钮
|
||||
if (wasStopped) {
|
||||
wasStopped = false
|
||||
try { player?.play() } catch (e: Exception) { }
|
||||
handler.postDelayed({
|
||||
// 直播流暂停后常常定格无法续播 → 给用户一个明确的按钮
|
||||
if (player?.isPlaying != true) resumeBtn?.visibility = View.VISIBLE
|
||||
else resumeBtn?.visibility = View.GONE
|
||||
}, 1800)
|
||||
}
|
||||
if (playRequested > 0) {
|
||||
val idx = channels.indexOfFirst { it.num == playRequested }
|
||||
playRequested = -1
|
||||
if (idx >= 0) playChannel(idx)
|
||||
}
|
||||
}
|
||||
|
||||
/** 点「继续播放」→ 重新连接当前频道(直播流最稳的恢复方式) */
|
||||
private fun resumePlayback() {
|
||||
resumeBtn?.visibility = View.GONE
|
||||
val idx = if (currentChannel in channels.indices) currentChannel else 0
|
||||
try { player?.play() } catch (e: Exception) { }
|
||||
// 等 1.2 秒看是否已恢复,没恢复就重连
|
||||
handler.postDelayed({
|
||||
if (player?.isPlaying != true) playChannel(idx)
|
||||
}, 1200)
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) hideSystemUi()
|
||||
}
|
||||
private fun hideSystemUi() {
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
wasStopped = true
|
||||
super.onStop()
|
||||
player?.pause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
digitTask.cancel()
|
||||
handler.removeCallbacksAndMessages(null)
|
||||
playerView.player = null
|
||||
player?.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
|
||||
/**
|
||||
* 设置页:
|
||||
* - 版本号显示
|
||||
* - 开机自启动开关(BOOT_COMPLETED receiver)
|
||||
* - 会员状态显示(已是会员显示有效期;非会员显示成为会员按钮→跳购买)
|
||||
*/
|
||||
class SettingsActivity : Activity() {
|
||||
|
||||
private lateinit var statusText: TextView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
try {
|
||||
// crashGuard: 若上次有崩溃标记,先清before检查
|
||||
getSharedPreferences("iptv_crash", MODE_PRIVATE).let {
|
||||
if (it.getBoolean("crashed", false)) {
|
||||
it.edit().putBoolean("crashed", false).apply()
|
||||
// 清理license cache以防被污染
|
||||
getSharedPreferences("iptv_license", MODE_PRIVATE).edit().remove("token").remove("card_key").apply()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { }
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setBackgroundColor(Color.parseColor("#12141A"))
|
||||
setPadding(96, 64, 96, 64)
|
||||
}
|
||||
setContentView(root)
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "设置"
|
||||
textSize = 28f; setTextColor(Color.WHITE)
|
||||
gravity = Gravity.CENTER; setPadding(0, 0, 0, 48)
|
||||
})
|
||||
|
||||
// ===== 版本信息 =====
|
||||
val versionName = try {
|
||||
packageManager.getPackageInfo(packageName, 0).versionName ?: "1.0"
|
||||
} catch (e: Exception) { "1.0" }
|
||||
|
||||
val prefs = getSharedPreferences("iptv_license", MODE_PRIVATE)
|
||||
val isVip = prefs.getBoolean("is_vip", false)
|
||||
val expire = prefs.getString("vip_expire", "")
|
||||
val permanent = prefs.getBoolean("is_permanent", false)
|
||||
val cardKey = prefs.getString("card_key", "")
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "版本信息"
|
||||
textSize = 15f; setTextColor(Color.parseColor("#7AA7D9"))
|
||||
setPadding(0, 0, 0, 8)
|
||||
})
|
||||
root.addView(TextView(this).apply {
|
||||
text = "App 版本:$versionName\n授权系统:CardAuth(app.88531.cn)\n机器码:${LicenseClient.machineCode(this@SettingsActivity)}"
|
||||
textSize = 15f; setTextColor(Color.WHITE)
|
||||
setLineSpacing(0f, 1.15f)
|
||||
setBackgroundColor(Color.parseColor("#1C2028"))
|
||||
setPadding(24, 20, 24, 20)
|
||||
}, LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { bottomMargin = 40 })
|
||||
|
||||
// ===== 开机自启动 =====
|
||||
root.addView(TextView(this).apply {
|
||||
text = "通用设置"
|
||||
textSize = 15f; setTextColor(Color.parseColor("#7AA7D9"))
|
||||
setPadding(0, 0, 0, 8)
|
||||
})
|
||||
val bootSwitch = Switch(this).apply {
|
||||
text = "开机自动启动"; textSize = 17f
|
||||
setTextColor(Color.WHITE)
|
||||
isChecked = isBootReceiverEnabled()
|
||||
val pad = (24 * resources.displayMetrics.density).toInt()
|
||||
setPadding(pad, pad / 2, pad, pad / 2)
|
||||
setBackgroundColor(Color.parseColor("#1C2028"))
|
||||
}
|
||||
bootSwitch.setOnCheckedChangeListener { _, checked ->
|
||||
val pm = packageManager
|
||||
val comp = ComponentName(this, BootReceiver::class.java)
|
||||
pm.setComponentEnabledSetting(
|
||||
comp,
|
||||
if (checked) PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
}
|
||||
root.addView(bootSwitch, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { bottomMargin = 40 })
|
||||
|
||||
// ===== 会员状态 =====
|
||||
root.addView(TextView(this).apply {
|
||||
text = "会员状态"
|
||||
textSize = 15f; setTextColor(Color.parseColor("#7AA7D9"))
|
||||
setPadding(0, 0, 0, 8)
|
||||
})
|
||||
statusText = TextView(this).apply {
|
||||
textSize = 15f; setLineSpacing(0f, 1.15f)
|
||||
setTextColor(Color.WHITE)
|
||||
setBackgroundColor(Color.parseColor("#1C2028"))
|
||||
setPadding(24, 20, 24, 20)
|
||||
text = if (isVip) {
|
||||
"✓ 已是会员\n${if (permanent) "永久有效" else "有效期至:$expire"}${if (cardKey?.isNotEmpty() == true) "\n卡号:$cardKey" else ""}"
|
||||
} else {
|
||||
"当前为免费用户(每天 1 小时)\n尚未成为会员"
|
||||
}
|
||||
}
|
||||
root.addView(statusText, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { bottomMargin = 24 })
|
||||
|
||||
|
||||
if (isVip) {
|
||||
root.addView(TextView(this).apply {
|
||||
text = "✓ 已经是会员"
|
||||
textSize = 18f; setTextColor(Color.parseColor("#4CAF50"))
|
||||
gravity = Gravity.CENTER; setPadding(0, 24, 0, 24)
|
||||
})
|
||||
} else {
|
||||
val btn = Button(this).apply {
|
||||
text = "支付成为会员(月/季/年)"
|
||||
textSize = 17f
|
||||
setBackgroundColor(Color.parseColor("#1B6EF3"))
|
||||
setOnClickListener { startActivity(Intent(this@SettingsActivity, ShopActivity::class.java)) }
|
||||
}
|
||||
root.addView(btn, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBootReceiverEnabled(): Boolean =
|
||||
packageManager.getComponentEnabledSetting(ComponentName(this, BootReceiver::class.java)) ==
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) { finish(); return true }
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import org.json.JSONArray
|
||||
|
||||
/**
|
||||
* 套餐商店 —— App 内直接购买
|
||||
* 流程: GET card-types → 用户选定套餐+支付方式 → POST /api/public/orders
|
||||
* (contact_info=MACHINE:TV-xxx) → 拉起浏览器 pay_url 扫码
|
||||
* → 轮询 /api/public/orders/query?order_no= → paid → 查询卡号 → 自动verify → 进主界面
|
||||
*/
|
||||
class ShopActivity : Activity() {
|
||||
|
||||
private val plans = mutableListOf<Plan>()
|
||||
|
||||
data class Plan(val id: Int, val name: String, val price: String, val days: Int)
|
||||
|
||||
private lateinit var list: ListView
|
||||
private lateinit var statusText: TextView
|
||||
private var payType = "alipay"
|
||||
private var orderNo = ""
|
||||
private var machineId = ""
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var pollTask: Runnable? = null
|
||||
private var pollCount = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
try {
|
||||
// crashGuard: 若上次有崩溃标记,先清before检查
|
||||
getSharedPreferences("iptv_crash", MODE_PRIVATE).let {
|
||||
if (it.getBoolean("crashed", false)) {
|
||||
it.edit().putBoolean("crashed", false).apply()
|
||||
// 清理license cache以防被污染
|
||||
getSharedPreferences("iptv_license", MODE_PRIVATE).edit().remove("token").remove("card_key").apply()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { }
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setBackgroundColor(Color.parseColor("#12141A"))
|
||||
setPadding(64, 48, 64, 48)
|
||||
}
|
||||
setContentView(root)
|
||||
root.gravity = Gravity.CENTER
|
||||
|
||||
machineId = LicenseClient.machineCode(this)
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "IPTV · 选择套餐"
|
||||
textSize = 26f; setTextColor(Color.WHITE)
|
||||
gravity = Gravity.CENTER; setPadding(0, 0, 0, 12)
|
||||
})
|
||||
root.addView(TextView(this).apply {
|
||||
text = "支付成功自动激活本机(无需卡密)"
|
||||
textSize = 14f; setTextColor(Color.LTGRAY)
|
||||
gravity = Gravity.CENTER; setPadding(0, 0, 0, 32)
|
||||
})
|
||||
|
||||
list = ListView(this).apply {
|
||||
setBackgroundColor(Color.parseColor("#1C1F26"))
|
||||
}
|
||||
root.addView(list, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f
|
||||
).apply { bottomMargin = 24 })
|
||||
|
||||
statusText = TextView(this).apply {
|
||||
textSize = 14f; setTextColor(Color.LTGRAY)
|
||||
gravity = Gravity.CENTER; setPadding(0, 0, 0, 24)
|
||||
}
|
||||
root.addView(statusText)
|
||||
|
||||
val row = LinearLayout(this).apply { orientation = LinearLayout.HORIZONTAL }
|
||||
val alipay = Button(this).apply { text = "支付宝"; textSize = 15f }
|
||||
val wxpay = Button(this).apply { text = "微信"; textSize = 15f }
|
||||
fun sel(b: Button, t: String) {
|
||||
val active = payType == t
|
||||
b.setBackgroundColor(if (active) Color.parseColor("#1B6EF3") else Color.parseColor("#2A2F3A"))
|
||||
}
|
||||
alipay.setOnClickListener { payType = "alipay"; sel(alipay, "alipay"); sel(wxpay, "wxpay") }
|
||||
wxpay.setOnClickListener { payType = "wxpay"; sel(alipay, "alipay"); sel(wxpay, "wxpay") }
|
||||
sel(alipay, "alipay"); sel(wxpay, "wxpay")
|
||||
row.addView(alipay, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f).apply { rightMargin = 16 })
|
||||
row.addView(wxpay, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
|
||||
root.addView(row)
|
||||
|
||||
list.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ ->
|
||||
if (pos < plans.size) createOrder(plans[pos])
|
||||
}
|
||||
|
||||
loadPlans()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun loadPlans() {
|
||||
statusText.text = "加载套餐…"
|
||||
Thread {
|
||||
try {
|
||||
val r = CardAuthApi.get("/api/public/projects/2/card-types")
|
||||
val arr = r.optJSONArray("data") ?: JSONArray()
|
||||
plans.clear()
|
||||
for (i in 0 until arr.length()) {
|
||||
val p = arr.getJSONObject(i)
|
||||
// 接口不返回 status 字段时视为启用(默认 1),只有显式 status=0 才跳过
|
||||
val st = if (p.has("status")) p.optInt("status", 1) else 1
|
||||
if (st == 1) {
|
||||
val days = p.optInt("duration_days", 0)
|
||||
plans.add(Plan(p.optInt("id"), p.optString("name"), p.optString("price"), days))
|
||||
}
|
||||
}
|
||||
if (plans.isEmpty()) {
|
||||
runOnUiThread { statusText.text = "暂无可用套餐(请稍后重试)" }
|
||||
}
|
||||
runOnUiThread {
|
||||
plans.sortBy { it.days }
|
||||
list.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, plans.map {
|
||||
val d = it.days
|
||||
"📱 ${it.name} ¥${it.price} ${if (d >= 999) "永久" else "${d}天"}"
|
||||
})
|
||||
statusText.text = "选择套餐后按 OK 购买"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread { statusText.text = "加载失败: ${e.javaClass.simpleName}: ${e.message?.take(150)}" }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun createOrder(plan: Plan) {
|
||||
statusText.text = "创建订单…"
|
||||
Thread {
|
||||
try {
|
||||
val body = org.json.JSONObject().apply {
|
||||
put("project_id", 2)
|
||||
put("card_type_id", plan.id)
|
||||
put("pay_type", payType)
|
||||
put("contact_info", "MACHINE:$machineId")
|
||||
}
|
||||
val r = CardAuthApi.post("/api/public/orders", body)
|
||||
val d = r.optJSONObject("data")
|
||||
val url = d?.optString("pay_url") ?: ""
|
||||
runOnUiThread {
|
||||
if (url.isNotEmpty()) {
|
||||
orderNo = d!!.optString("order_no", "")
|
||||
statusText.text = "订单已创建,正在打开支付页…支付成功后自动激活"
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
startPolling()
|
||||
} else {
|
||||
statusText.text = "下单失败: ${r.optString("message")}"
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
runOnUiThread { statusText.text = "下单错误: ${e.message}" }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun startPolling() {
|
||||
pollCount = 0
|
||||
pollTask = object : Runnable {
|
||||
override fun run() {
|
||||
pollCount++
|
||||
if (pollCount > 60) { // 最多3分钟
|
||||
handler.post { statusText.text = "等待支付超时,可重新下单" }
|
||||
return
|
||||
}
|
||||
Thread {
|
||||
var paid = false
|
||||
var cardKey = ""
|
||||
try {
|
||||
val r = CardAuthApi.get("/api/public/orders/query?order_no=$orderNo")
|
||||
val d = r.optJSONObject("data") ?: org.json.JSONObject()
|
||||
paid = d.optString("status") == "paid"
|
||||
cardKey = d.optString("card_key", "")
|
||||
} catch (e: Exception) { }
|
||||
runOnUiThread {
|
||||
if (paid) {
|
||||
statusText.text = "✓ 支付成功!正在激活本机…"
|
||||
if (cardKey.isNotEmpty()) activateAndEnter(cardKey)
|
||||
else { // 兜底: 直接用verify免卡路径
|
||||
handler.postDelayed({
|
||||
getSharedPreferences("iptv_license", MODE_PRIVATE)
|
||||
.edit().putBoolean("is_vip", true).apply()
|
||||
enterMain()
|
||||
}, 500)
|
||||
}
|
||||
} else {
|
||||
statusText.text = "等待支付…(${pollCount * 3}秒)"
|
||||
handler.postDelayed(this, 3000)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
handler.postDelayed(pollTask!!, 3000)
|
||||
}
|
||||
|
||||
private fun activateAndEnter(cardKey: String) {
|
||||
Thread {
|
||||
val r = LicenseClient.verify(cardKey, machineId)
|
||||
runOnUiThread {
|
||||
getSharedPreferences("iptv_license", MODE_PRIVATE).edit()
|
||||
.putBoolean("is_vip", r.ok)
|
||||
.putString("card_key", if (r.ok) cardKey else "")
|
||||
.putString("vip_expire", r.expireTime)
|
||||
.putBoolean("is_permanent", r.isPermanent)
|
||||
.apply()
|
||||
if (r.ok) {
|
||||
statusText.text = "✓ 激活成功,开始播放"
|
||||
enterMain()
|
||||
} else {
|
||||
statusText.text = "激活校验失败: ${r.message}(可直接输入卡密重试)"
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun enterMain() {
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && pollTask != null) {
|
||||
// 支付轮询中返回 → 取消轮询
|
||||
handler.removeCallbacks(pollTask!!); pollTask = null
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.util.Base64
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* iptv807 播放源本地解密 —— App 内直接解密,完全不经过 VPS
|
||||
*
|
||||
* 解密步骤(Python mirror 端到端 verified):
|
||||
* 1. GET https://m.iptv807.com/?act=play&tid=X&id=Y (UA=Android mobile, Referer=m.iptv807.com)
|
||||
* 2. 找 <script>.split("") 混淆块, 逐语句 (quote-aware ; 分割)
|
||||
* 3. evalConcat: 双引号对 - 其后紧跟 .split("").reverse().join("") → 取反向值,并跳过整个 suffix(含 "" 内嵌引号不再被当作独立literal)
|
||||
* 4. 从 assignments 找:
|
||||
* - 首个 32hex = xKey (加密 key)
|
||||
* - 另一个 32hex = oldToken
|
||||
* - 含 %XX 的长串 = newToken
|
||||
* 5. xorConst = html 里最早的 "16hex" 字面量
|
||||
* 6. decrypt option:
|
||||
* reversed → base64 → XOR(key=xKey+xorConst) → base64 → eat token 替换 → xKey 剥离 → URLDecode UTF-8
|
||||
*/
|
||||
object SourceResolver {
|
||||
|
||||
@Volatile private var cache: MutableMap<String, Cached>? = null
|
||||
private data class Cached(val lines: List<String>, val ts: Long)
|
||||
private const val TTL = 8 * 60 * 1000L
|
||||
|
||||
private const val BASE = "https://m.iptv807.com/"
|
||||
private const val UA = "Mozilla/5.0 (Linux; Android 12; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36"
|
||||
|
||||
@Volatile var lastError: String = ""
|
||||
|
||||
/** 磁盘兜底缓存(App 重启后仍可用上次成功的线路) */
|
||||
private var appCtx: android.content.Context? = null
|
||||
fun init(ctx: android.content.Context) { appCtx = ctx.applicationContext }
|
||||
|
||||
private fun saveDisk(key: String, lines: List<String>) {
|
||||
runCatching {
|
||||
appCtx?.getSharedPreferences("iptv_lines", android.content.Context.MODE_PRIVATE)?.edit()
|
||||
?.putString(key, org.json.JSONArray(lines).toString())
|
||||
?.putLong("ts_$key", System.currentTimeMillis())
|
||||
?.apply()
|
||||
}
|
||||
}
|
||||
private fun loadDisk(key: String): List<String>? = runCatching {
|
||||
val j = appCtx?.getSharedPreferences("iptv_lines", android.content.Context.MODE_PRIVATE)?.getString(key, null)
|
||||
?: return null
|
||||
val arr = org.json.JSONArray(j)
|
||||
(0 until arr.length()).map { arr.getString(it) }.takeIf { it.isNotEmpty() }
|
||||
}.getOrNull()
|
||||
|
||||
/** 兼容之前的 method signature */
|
||||
fun resolve(tid: String, id: String): List<String>? = resolveLines(tid, id)
|
||||
|
||||
/** 强制丢弃缓存重新抓源(用于"缓存线路全失效"时) */
|
||||
fun resolveFresh(tid: String, id: String): List<String>? {
|
||||
cache?.remove("$tid-$id")
|
||||
return resolveLines(tid, id)
|
||||
}
|
||||
|
||||
fun resolveLines(tid: String, id: String): List<String>? {
|
||||
val key = "$tid-$id"
|
||||
cache?.get(key)?.let { if (System.currentTimeMillis() - it.ts < TTL) return it.lines }
|
||||
|
||||
synchronized(this) {
|
||||
// double-check
|
||||
cache?.get(key)?.let { if (System.currentTimeMillis() - it.ts < TTL) return it.lines }
|
||||
try {
|
||||
val url = "$BASE?act=play&tid=$tid&id=$id"
|
||||
val html = httpGet(url, BASE)
|
||||
|
||||
// 1) script blocks - find mix
|
||||
val blocks = Regex("<script>([\\s\\S]*?)</script>").findAll(html).map { it.groupValues[1] }.toList()
|
||||
val mixBlock = blocks.firstOrNull { it.contains(".split(\"\")") } ?: run {
|
||||
lastError = "no-mix-block"; return null
|
||||
}
|
||||
|
||||
// 2) quote-aware split by ;
|
||||
val stmts = StringBuilder()
|
||||
var inStr = false
|
||||
for (c in mixBlock) {
|
||||
when {
|
||||
c == '"' -> { inStr = !inStr; stmts.append(c) }
|
||||
c == ';' && !inStr -> { stmts.append('\n'); }
|
||||
else -> stmts.append(c)
|
||||
}
|
||||
}
|
||||
val statements = stmts.toString().split('\n').map { it.trim() }.filter { it.isNotEmpty() }
|
||||
|
||||
// 3) extensions
|
||||
val REV = ".split(\"\").reverse().join(\"\")"
|
||||
fun evalConcat(s: String): String = buildString {
|
||||
var i = 0
|
||||
while (i < s.length) {
|
||||
if (s[i] == '"') {
|
||||
var j = i + 1; var lit = ""
|
||||
while (j < s.length && s[j] != '"') { lit += s[j]; j++ }
|
||||
if (j >= s.length) { append(lit); i = s.length; break }
|
||||
val tail = s.substring((j + 1).coerceAtMost(s.length))
|
||||
if (tail.startsWith(REV)) {
|
||||
append(lit.reversed())
|
||||
i = j + 1 + REV.length
|
||||
} else {
|
||||
append(lit)
|
||||
i = j + 1
|
||||
}
|
||||
while (i < s.length && (s[i] == '+' || s[i] == ' ')) i++
|
||||
} else i++
|
||||
}
|
||||
}
|
||||
|
||||
// 关键:rhs 原样交给 evalConcat(不要剥前导引号/尾部括号,那会破坏字面量配对)
|
||||
val assign = LinkedHashMap<String, String>()
|
||||
for (st in statements) {
|
||||
if (st.isEmpty()) continue
|
||||
val mVar = Regex("^var\\s+(\\w+)\\s*=\\s*(.*)$").find(st)
|
||||
if (mVar != null) {
|
||||
val name = mVar.groupValues[1]
|
||||
val rhs = mVar.groupValues[2].trim()
|
||||
assign[name] = when {
|
||||
rhs.isEmpty() || rhs == "\"\"" -> ""
|
||||
Regex("^\"[a-f0-9]{32}\"$").matches(rhs) -> rhs.substring(1, rhs.length - 1)
|
||||
else -> evalConcat(rhs)
|
||||
}
|
||||
continue
|
||||
}
|
||||
val mCopy = Regex("^(\\w+)\\s*=\\s*(\\w+)$").find(st)
|
||||
if (mCopy != null) {
|
||||
assign[mCopy.groupValues[1]] = assign[mCopy.groupValues[2]] ?: ""
|
||||
continue
|
||||
}
|
||||
val mHex = Regex("^(\\w+)\\s*=\\s*\"([a-f0-9]{32})\"$").find(st)
|
||||
if (mHex != null) { assign[mHex.groupValues[1]] = mHex.groupValues[2]; continue }
|
||||
}
|
||||
|
||||
val vals = assign.values.filter { it.isNotEmpty() }
|
||||
val hex32 = vals.filter { Regex("^[a-f0-9]{32}$").matches(it) }
|
||||
if (hex32.isEmpty()) { lastError = "no-hex32"; return null }
|
||||
val xKey = hex32[0]
|
||||
// ★★ 核心修复2:混淆块里常有 "xx = yy" 复制语句 → hex32[1] 可能 == xKey,
|
||||
// 那样 replace("token=$oldToken") 就匹配不到 → URL 保留过期旧 token → 全部 404。
|
||||
// 必须取"第一个不等于 xKey"的 32hex 才是真正的 oldToken。
|
||||
val oldToken = hex32.firstOrNull { it != xKey } ?: xKey
|
||||
val newToken = vals.firstOrNull {
|
||||
it.contains(Regex("%[0-9A-Fa-f]{2}")) &&
|
||||
!Regex("^[a-f0-9]{32}$").matches(it) &&
|
||||
!Regex("^[a-f0-9]{16}$").matches(it) &&
|
||||
it != xKey
|
||||
} ?: run { lastError = "no-newToken"; return null }
|
||||
|
||||
val xorConst = Regex("\"([a-f0-9]{16})\"").findAll(html).map { it.groupValues[1] }.firstOrNull() ?: run {
|
||||
lastError = "no-xorConst"; return null
|
||||
}
|
||||
|
||||
// 4) decrypt options
|
||||
fun decrypt(encrypted: String): String? = try {
|
||||
val rev = encrypted.reversed()
|
||||
val b1 = String(Base64.decode(rev, Base64.DEFAULT), charset("ISO-8859-1"))
|
||||
val key = xKey + xorConst
|
||||
val sb = StringBuilder()
|
||||
for (i in b1.indices) {
|
||||
sb.append(((b1[i].toInt() and 0xFF) xor (key[i % key.length].toInt() and 0xFF)).toChar())
|
||||
}
|
||||
val b2raw = sb.toString()
|
||||
val b2 = String(Base64.decode(b2raw, Base64.DEFAULT), charset("ISO-8859-1"))
|
||||
var u = b2.replace("token=$oldToken", "token=$newToken")
|
||||
u = u.replace(xKey, "")
|
||||
// 兜底:若 token= 后仍是纯32位hex(旧token没替换成功),强制换成新 token
|
||||
u = Regex("token=[a-f0-9]{32}").replace(u, "token=$newToken")
|
||||
// ★★ 核心修复:b2 里 token 已是 %2B 编码形态,绝对不能再 URLDecoder(会把 %2B 解成 '+' 被服务端当空格 → 404)
|
||||
u
|
||||
} catch (e: Exception) { null }
|
||||
|
||||
val lines = Regex("<option value=\"([^\"]+)\"[^>]*>", RegexOption.IGNORE_CASE)
|
||||
.findAll(html)
|
||||
.mapNotNull { decrypt(it.groupValues[1]) }
|
||||
.toList()
|
||||
if (lines.isEmpty()) { lastError = "decode-empty"; return null }
|
||||
|
||||
if (cache == null) cache = ConcurrentHashMap()
|
||||
cache!![key] = Cached(lines, System.currentTimeMillis())
|
||||
saveDisk(key, lines)
|
||||
lastError = "ok ${lines.size} lines"
|
||||
return lines
|
||||
} catch (e: Exception) {
|
||||
// 网络波动时:用该频道最近一次成功的线路兜底(即使已过期)
|
||||
cache?.get(key)?.let { c ->
|
||||
lastError = "网络波动,用上次缓存"
|
||||
return c.lines
|
||||
}
|
||||
loadDisk(key)?.let { d ->
|
||||
lastError = "网络波动,用磁盘缓存"
|
||||
return d
|
||||
}
|
||||
lastError = "exception: ${e.javaClass.simpleName}: ${e.message?.take(120)}"
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 验完整链:先 resolver,再对 line[0] HEAD拉流确认 */
|
||||
fun verifyChain(tid: String, id: String): String {
|
||||
val lines = resolveLines(tid, id) ?: return "resolve 错: $lastError"
|
||||
val u = lines.firstOrNull() ?: return "no lines"
|
||||
// HEAD / small byte read to verify FLV
|
||||
return try {
|
||||
val conn = (URL(u).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 8000
|
||||
readTimeout = 10000
|
||||
setRequestProperty("User-Agent", UA)
|
||||
setRequestProperty("Referer", BASE)
|
||||
setRequestProperty("Range", "bytes=0-63")
|
||||
setRequestProperty("Origin", "https://m.iptv807.com")
|
||||
setInstanceFollowRedirects(true)
|
||||
}
|
||||
val code = conn.responseCode
|
||||
val hdr = conn.getHeaderField("Location") ?: ""
|
||||
val magic = runCatching {
|
||||
val inn = conn.inputStream
|
||||
val bs = ByteArray(3); var n = 0
|
||||
while (n < 3) { val v = inn.read(); if (v < 0) break; bs[n++] = v.toByte() }
|
||||
String(bs)
|
||||
}.getOrElse { "" }
|
||||
conn.disconnect()
|
||||
when {
|
||||
code == 302 && hdr.startsWith("https://t") -> "ok 302"
|
||||
magic.startsWith("FLV") -> "ok direct FLV"
|
||||
else -> "direct code=$code magic=$magic"
|
||||
}
|
||||
} catch (e: Exception) { "stream err: ${e.message}" }
|
||||
}
|
||||
|
||||
private fun httpGet(urlStr: String, referer: String?): String {
|
||||
var lastEx: Exception? = null
|
||||
for (attempt in 0 until 3) {
|
||||
try {
|
||||
val conn = (URL(urlStr).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
connectTimeout = 15000
|
||||
readTimeout = 20000
|
||||
setRequestProperty("User-Agent", UA)
|
||||
setRequestProperty("Accept", "text/html,*/*")
|
||||
setRequestProperty("Connection", "close")
|
||||
if (referer != null) setRequestProperty("Referer", referer)
|
||||
}
|
||||
val code = conn.responseCode
|
||||
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
|
||||
val text = try {
|
||||
stream?.let { BufferedReader(InputStreamReader(it, StandardCharsets.UTF_8)).readText() } ?: ""
|
||||
} finally { runCatching { stream?.close(); conn.disconnect() } }
|
||||
if (code !in 200..299) throw RuntimeException("HTTP $code")
|
||||
if (text.isNotEmpty()) return text
|
||||
lastEx = RuntimeException("empty body")
|
||||
} catch (e: Exception) {
|
||||
lastEx = e
|
||||
}
|
||||
try { Thread.sleep(700L * (attempt + 1)) } catch (ie: InterruptedException) {}
|
||||
}
|
||||
throw (lastEx ?: RuntimeException("unknown"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.iptv807.tv
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
|
||||
/**
|
||||
* 免费时长用完 → 引导购买页
|
||||
* 显示卡密商城地址(CardAuth /#/shop),用户点遥控OK打开购买页
|
||||
*/
|
||||
class TrialExpiredActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
try {
|
||||
// crashGuard: 若上次有崩溃标记,先清before检查
|
||||
getSharedPreferences("iptv_crash", MODE_PRIVATE).let {
|
||||
if (it.getBoolean("crashed", false)) {
|
||||
it.edit().putBoolean("crashed", false).apply()
|
||||
// 清理license cache以防被污染
|
||||
getSharedPreferences("iptv_license", MODE_PRIVATE).edit().remove("token").remove("card_key").apply()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) { }
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = android.view.Gravity.CENTER
|
||||
setBackgroundColor(0xFF12141Au.toInt())
|
||||
setPadding(96, 64, 96, 64)
|
||||
}
|
||||
setContentView(root)
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "⏰ 今日免费时长已用完"
|
||||
textSize = 30f; setTextColor(0xFFFFFFFFu.toInt())
|
||||
gravity = android.view.Gravity.CENTER; setPadding(0, 0, 0, 24)
|
||||
})
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "免费用户每天可观看 1 小时\n购买卡密后立即解锁不限时长观看"
|
||||
textSize = 17f; setTextColor(0xFFBBBBBBu.toInt())
|
||||
gravity = android.view.Gravity.CENTER; setPadding(0, 0, 0, 56)
|
||||
})
|
||||
|
||||
root.addView(Button(this).apply {
|
||||
text = "购买套餐(OK键)"
|
||||
textSize = 19f
|
||||
setOnClickListener { startActivity(android.content.Intent(this@TrialExpiredActivity, ShopActivity::class.java)); finish() }
|
||||
})
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "或直接访问:http://app.88531.cn:8881/#/shop"
|
||||
textSize = 13f; setTextColor(0xFF888888u.toInt())
|
||||
gravity = android.view.Gravity.CENTER; setPadding(0, 32, 0, 0)
|
||||
})
|
||||
|
||||
root.addView(TextView(this).apply {
|
||||
text = "\n明天再来可继续免费观看 1 小时"
|
||||
textSize = 14f; setTextColor(0xFF7799AAu.toInt())
|
||||
gravity = android.view.Gravity.CENTER; setPadding(0, 24, 0, 0)
|
||||
})
|
||||
}
|
||||
|
||||
private fun openShop() {
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("http://app.88531.cn:8881/#/shop")))
|
||||
} catch (e: Exception) { }
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 80 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
|
||||
<path android:fillColor="#1B6EF3" android:pathData="M0,0h108v108h-108z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp" android:height="108dp" android:viewportWidth="108" android:viewportHeight="108">
|
||||
<group android:scaleX="0.55" android:scaleY="0.55" android:translateX="24.3" android:translateY="24.3">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M32,28h44l10,12v40a4,4 0 0,1 -4,4H26a4,4 0 0,1 -4,-4V40z"/>
|
||||
<path android:fillColor="#1B6EF3" android:pathData="M38,50h32v22h-32z"/>
|
||||
<path android:fillColor="#FFD600" android:pathData="M54,56l8,5l-8,5z"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_bg"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_fg"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">港澳台直播</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<style name="Theme.IPTVTV" parent="android:Theme.Material.NoActionBar.Fullscreen">
|
||||
<item name="android:windowBackground">@android:color/black</item>
|
||||
<item name="android:colorPrimary">#1B6EF3</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true">
|
||||
<trust-anchors>
|
||||
<certificates src="system" />
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
||||
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.2.2" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
android.suppressUnsupportedCompileSdk=35
|
||||
kotlin.code.style=official
|
||||
@@ -0,0 +1 @@
|
||||
v5.2.1 根因: obfuscated var字符串含内嵌引号("caf074ce…"里的".join(\"\")"是 HTML escape \" 不是纯") — my evalConcat treats them as separate literals and mangles to "++.split..." garbage. Fix: quote-aware ++ skip via char-set approach with REV-suffix as one unit.
|
||||
@@ -0,0 +1,16 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
rootProject.name = "IPTV807TV"
|
||||
include(":app")
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""给 LoginActivity/TrialExpired 加ShopActivity入口 + Manifest声明"""
|
||||
import io
|
||||
|
||||
# 1) LoginActivity
|
||||
p = '/vol1/1000/HD1/APP/iptv-tv-app/app/src/main/java/com/iptv807/tv/LoginActivity.kt'
|
||||
s = open(p).read()
|
||||
if 'ShopActivity' not in s:
|
||||
s = s.replace(''' val freeBtn = Button(this).apply {''', ''' val shopBtn = Button(this).apply {
|
||||
text = "购买套餐(支付宝/微信)"; textSize = 15f
|
||||
setBackgroundColor(Color.parseColor("#1B6EF3"))
|
||||
setOnClickListener { startActivity(android.content.Intent(this@LoginActivity, ShopActivity::class.java)) }
|
||||
}
|
||||
root.addView(shopBtn, LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { topMargin = 24 })
|
||||
|
||||
val freeBtn = Button(this).apply {''')
|
||||
s = s.replace(''' text = "没有卡密?点「先看一会」进免费试看"''', ''' text = "已有卡密可直接激活;或购买套餐自动绑定本机"''')
|
||||
open(p, 'w').write(s)
|
||||
print('LoginActivity patched')
|
||||
else:
|
||||
print('LoginActivity already patched')
|
||||
|
||||
# 2) TrialExpiredActivity
|
||||
p = '/vol1/1000/HD1/APP/iptv-tv-app/app/src/main/java/com/iptv807/tv/TrialExpiredActivity.kt'
|
||||
s = open(p).read()
|
||||
if 'ShopActivity' not in s:
|
||||
s = s.replace(''' root.addView(Button(this).apply {
|
||||
text = "打开购买页(遥控OK)"
|
||||
textSize = 19f
|
||||
setOnClickListener { openShop() }
|
||||
})''', ''' root.addView(Button(this).apply {
|
||||
text = "购买套餐(OK键)"
|
||||
textSize = 19f
|
||||
setOnClickListener { startActivity(android.content.Intent(this@TrialExpiredActivity, ShopActivity::class.java)); finish() }
|
||||
})''')
|
||||
open(p, 'w').write(s)
|
||||
print('TrialExpired patched')
|
||||
|
||||
# 3) Manifest
|
||||
p = '/vol1/1000/HD1/APP/iptv-tv-app/app/src/main/AndroidManifest.xml'
|
||||
s = open(p).read()
|
||||
if 'ShopActivity' not in s:
|
||||
s = s.replace('</application>', ''' <activity android:name=".ShopActivity" android:exported="false" />
|
||||
</application>''')
|
||||
open(p, 'w').write(s)
|
||||
print('Manifest patched')
|
||||
print('done')
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>港澳台頻道列表 - 電視直播</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="application-name" content="網絡電視直播移動版,IPTV電視直播手机版" />
|
||||
<meta name="title" content="港澳台頻道列表 - 電視直播"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="keywords" content="網絡電視,網路電視,電視直播,手机版,電視,免費電視">
|
||||
<meta name="description" content="網絡電視,網路電視,全球電視直播,高清電視,免費電視,IPTV電視直播手机版">
|
||||
<meta name="referrer" content="no-referrer-when-downgrade" />
|
||||
<link rel="shortcut icon" href="https://d.2026016.xyz/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="https://apps.bdimg.com/libs/jquerymobile/1.3.0/jquery.mobile-1.3.0.min.css" />
|
||||
<script src="https://apps.bdimg.com/libs/jquery/2.0.0/jquery.min.js"></script>
|
||||
<script src="https://apps.bdimg.com/libs/jquerymobile/1.3.0/jquery.mobile-1.3.0.min.js"></script>
|
||||
<style type="text/css">
|
||||
span {font-size:14px;}
|
||||
.headerNfooter {background-color: #C00;color: #fff;text-shadow: none;}
|
||||
.headerNfooter a:link,.headerNfooter a:hover,.headerNfooter a:visited,.headerNfooter a:active { color: #fff; text-decoration: none;}
|
||||
.headerNfooter h1 {text-indent: -999em; }
|
||||
#errorTip {text-align:center; color:red; font-size:15px; display:none; margin:5px 0;}
|
||||
</style></head><body>
|
||||
<div data-role="page">
|
||||
<div class="headerNfooter" data-role="header" data-theme="f" data-position="fixed">
|
||||
<h1>港澳台頻道列表 - 電視直播</h1><a href="#" data-iconpos="left" data-icon="back" data-rel="back">返回</a>
|
||||
<a href="?act=home" data-icon="home" data-role="button" data-inline="true">首頁</a>
|
||||
</div>
|
||||
<!--top--><div id="ad-container" style="width: 100%;"><div id="container-c95365b7eb64cacc6988f80de291dd94"></div>
|
||||
<script>
|
||||
$(function(){
|
||||
let adScript = document.createElement('script');
|
||||
adScript.src = "https://bauval.org/21/c95365b7eb64cacc6988f80de291dd94";
|
||||
adScript.setAttribute('data-cfasync','false');
|
||||
document.body.appendChild(adScript);
|
||||
});
|
||||
</script></div><div data-role="content"><div id="errorTip">所有直播线路已失效,请稍后重试或手动切换线路!</div><ul data-role="listview" data-inset="true" data-divider-theme="a" data-filter="true" data-filter-placeholder="搜索"><li><a href="?act=play&tid=gt&id=1" data-ajax="false">凤凰卫视中文台</a></li><li><a href="?act=play&tid=gt&id=2" data-ajax="false">凤凰卫视资讯台</a></li><li><a href="?act=play&tid=gt&id=3" data-ajax="false">凤凰卫视香港台</a></li><li><a href="?act=play&tid=gt&id=5" data-ajax="false">TVB翡翠台</a></li><li><a href="?act=play&tid=gt&id=6" data-ajax="false">TVB无线新闻台</a></li><li><a href="?act=play&tid=gt&id=8" data-ajax="false">TVB Plus</a></li><li><a href="?act=play&tid=gt&id=9" data-ajax="false">TVB明珠台</a></li><li><a href="?act=play&tid=gt&id=11" data-ajax="false">香港有线新闻台</a></li><li><a href="?act=play&tid=gt&id=12" data-ajax="false">香港有线18台</a></li><li><a href="?act=play&tid=gt&id=13" data-ajax="false">香港有线18台(备)</a></li><li><a href="?act=play&tid=gt&id=17" data-ajax="false">NOW新闻台</a></li><li><a href="?act=play&tid=gt&id=18" data-ajax="false">NOW财经台</a></li><li><a href="?act=play&tid=gt&id=19" data-ajax="false">ViuTV</a></li><li><a href="?act=play&tid=gt&id=20" data-ajax="false">ViuTV6</a></li><li><a href="?act=play&tid=gt&id=22" data-ajax="false">HOY国际财经台</a></li><li><a href="?act=play&tid=gt&id=23" data-ajax="false">HOY资讯台</a></li><li><a href="?act=play&tid=gt&id=24" data-ajax="false">HOY TV</a></li><li><a href="?act=play&tid=gt&id=26" data-ajax="false">港台电视31</a></li><li><a href="?act=play&tid=gt&id=27" data-ajax="false">港台电视32</a></li><li><a href="?act=play&tid=gt&id=30" data-ajax="false">香港卫视HKS</a></li><li><a href="?act=play&tid=gt&id=35" data-ajax="false">澳门莲花</a></li><li><a href="?act=play&tid=gt&id=36" data-ajax="false">澳門卫视</a></li><li><a href="?act=play&tid=gt&id=37" data-ajax="false">澳视资讯</a></li><li><a href="?act=play&tid=gt&id=38" data-ajax="false">澳視澳門</a></li><li><a href="?act=play&tid=gt&id=39" data-ajax="false">澳門Macau</a></li><li><a href="?act=play&tid=gt&id=40" data-ajax="false">澳视综艺</a></li><li><a href="?act=play&tid=gt&id=41" data-ajax="false">澳亚卫视</a></li><li><a href="?act=play&tid=gt&id=43" data-ajax="false">中天新闻台</a></li><li><a href="?act=play&tid=gt&id=44" data-ajax="false">东森新闻台</a></li><li><a href="?act=play&tid=gt&id=45" data-ajax="false">东森财经新闻台</a></li><li><a href="?act=play&tid=gt&id=46" data-ajax="false">中视新闻台</a></li><li><a href="?act=play&tid=gt&id=47" data-ajax="false">台视新闻台</a></li><li><a href="?act=play&tid=gt&id=48" data-ajax="false">三立新闻台</a></li><li><a href="?act=play&tid=gt&id=49" data-ajax="false">非凡新闻</a></li><li><a href="?act=play&tid=gt&id=50" data-ajax="false">民视新闻台</a></li><li><a href="?act=play&tid=gt&id=51" data-ajax="false">华视新闻台</a></li><li><a href="?act=play&tid=gt&id=52" data-ajax="false">镜电视新闻台</a></li><li><a href="?act=play&tid=gt&id=53" data-ajax="false">寰宇新闻台</a></li><li><a href="?act=play&tid=gt&id=54" data-ajax="false">寰宇新闻台湾台</a></li><li><a href="?act=play&tid=gt&id=55" data-ajax="false">TVBS新闻台</a></li><li><a href="?act=play&tid=gt&id=56" data-ajax="false">TVBS HD</a></li><li><a href="?act=play&tid=gt&id=60" data-ajax="false">中视HD</a></li><li><a href="?act=play&tid=gt&id=61" data-ajax="false">台视HD</a></li><li><a href="?act=play&tid=gt&id=62" data-ajax="false">民视HD</a></li><li><a href="?act=play&tid=gt&id=63" data-ajax="false">华视HD</a></li><li><a href="?act=play&tid=gt&id=73" data-ajax="false">緯來體育台</a></li><li><a href="?act=play&tid=gt&id=74" data-ajax="false">纬来日本台</a></li><li><a href="?act=play&tid=gt&id=75" data-ajax="false">纬来育乐台</a></li><li><a href="?act=play&tid=gt&id=76" data-ajax="false">ELEVEN 1(DAZN 1)</a></li><li><a href="?act=play&tid=gt&id=77" data-ajax="false">ELEVEN 2(DAZN 2)</a></li><li><a href="?act=play&tid=gt&id=79" data-ajax="false">爱尔达体育1台</a></li><li><a href="?act=play&tid=gt&id=80" data-ajax="false">爱尔达体育2台</a></li></ul></div>
|
||||
<!--end-->
|
||||
<div data-role="footer">
|
||||
<h4>版權聲明 © 所有源來自互聯網搜索</h4>
|
||||
</div>
|
||||
</div>
|
||||
<script>(function(s){s.dataset.zone='11422200',s.src='https://n6wxm.com/vignette.min.js'})([document.documentElement, document.body].filter(Boolean).pop().appendChild(document.createElement('script')))</script><script data-cfasync="false" src="https://czernik.org/14/92553edbe36d3be2167ff02e7dfdc14b"></script>
|
||||
</body>
|
||||
<span style="display:none">
|
||||
<script>
|
||||
!function(p){"use strict";!function(t){var s=window,e=document,i=p,c="".concat("https:"===e.location.protocol?"https://":"http://","sdk.51.la/js-sdk-pro.min.js"),n=e.createElement("script"),r=e.getElementsByTagName("script")[0];n.type="text/javascript",n.setAttribute("charset","UTF-8"),n.async=!0,n.src=c,n.id="LA_COLLECT",i.d=n;var o=function(){s.LA.ids.push(i)};s.LA?s.LA.ids&&o():(s.LA=p,s.LA.ids=[],o()),r.parentNode.insertBefore(n,r)}()}({id:"KnfaacjCp88BlrkE",ck:"KnfaacjCp88BlrkE"});
|
||||
</script><!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=G-PY41HWX4PP'></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-PY41HWX4PP');
|
||||
</script>从缓存读取内容
|
||||
</span>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>体育頻道列表 - 電視直播</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="application-name" content="網絡電視直播移動版,IPTV電視直播手机版" />
|
||||
<meta name="title" content="体育頻道列表 - 電視直播"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="keywords" content="網絡電視,網路電視,電視直播,手机版,電視,免費電視">
|
||||
<meta name="description" content="網絡電視,網路電視,全球電視直播,高清電視,免費電視,IPTV電視直播手机版">
|
||||
<meta name="referrer" content="no-referrer-when-downgrade" />
|
||||
<link rel="shortcut icon" href="https://d.2026016.xyz/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="https://apps.bdimg.com/libs/jquerymobile/1.3.0/jquery.mobile-1.3.0.min.css" />
|
||||
<script src="https://apps.bdimg.com/libs/jquery/2.0.0/jquery.min.js"></script>
|
||||
<script src="https://apps.bdimg.com/libs/jquerymobile/1.3.0/jquery.mobile-1.3.0.min.js"></script>
|
||||
<style type="text/css">
|
||||
span {font-size:14px;}
|
||||
.headerNfooter {background-color: #C00;color: #fff;text-shadow: none;}
|
||||
.headerNfooter a:link,.headerNfooter a:hover,.headerNfooter a:visited,.headerNfooter a:active { color: #fff; text-decoration: none;}
|
||||
.headerNfooter h1 {text-indent: -999em; }
|
||||
#errorTip {text-align:center; color:red; font-size:15px; display:none; margin:5px 0;}
|
||||
</style></head><body>
|
||||
<div data-role="page">
|
||||
<div class="headerNfooter" data-role="header" data-theme="f" data-position="fixed">
|
||||
<h1>体育頻道列表 - 電視直播</h1><a href="#" data-iconpos="left" data-icon="back" data-rel="back">返回</a>
|
||||
<a href="?act=home" data-icon="home" data-role="button" data-inline="true">首頁</a>
|
||||
</div>
|
||||
<!--top--><div id="ad-container" style="width: 100%;"><div id="container-c95365b7eb64cacc6988f80de291dd94"></div>
|
||||
<script>
|
||||
$(function(){
|
||||
let adScript = document.createElement('script');
|
||||
adScript.src = "https://bauval.org/21/c95365b7eb64cacc6988f80de291dd94";
|
||||
adScript.setAttribute('data-cfasync','false');
|
||||
document.body.appendChild(adScript);
|
||||
});
|
||||
</script></div><div data-role="content"><div id="errorTip">所有直播线路已失效,请稍后重试或手动切换线路!</div><ul data-role="listview" data-inset="true" data-divider-theme="a" data-filter="true" data-filter-placeholder="搜索"><li><a href="?act=play&tid=ty&id=1" data-ajax="false">CCTV5体育</a></li><li><a href="?act=play&tid=ty&id=2" data-ajax="false">CCTV5+体育赛事</a></li><li><a href="?act=play&tid=ty&id=3" data-ajax="false">广东体育</a></li><li><a href="?act=play&tid=ty&id=4" data-ajax="false">劲爆体育</a></li><li><a href="?act=play&tid=ty&id=5" data-ajax="false">五星体育</a></li><li><a href="?act=play&tid=ty&id=6" data-ajax="false">新视觉HD</a></li><li><a href="?act=play&tid=ty&id=7" data-ajax="false">CCTV16奥林匹克</a></li><li><a href="?act=play&tid=ty&id=12" data-ajax="false">极速汽车</a></li><li><a href="?act=play&tid=ty&id=13" data-ajax="false">游戏风云</a></li><li><a href="?act=play&tid=ty&id=14" data-ajax="false">四海钓鱼</a></li><li><a href="?act=play&tid=ty&id=15" data-ajax="false">快乐垂钓</a></li><li><a href="?act=play&tid=ty&id=16" data-ajax="false">先锋乒羽</a></li><li><a href="?act=play&tid=ty&id=19" data-ajax="false">南国都市</a></li><li><a href="?act=play&tid=ty&id=20" data-ajax="false">广州竞赛</a></li><li><a href="?act=play&tid=ty&id=21" data-ajax="false">CCTV风云足球</a></li><li><a href="?act=play&tid=ty&id=22" data-ajax="false">魅力足球</a></li><li><a href="?act=play&tid=ty&id=23" data-ajax="false">緯來體育台</a></li><li><a href="?act=play&tid=ty&id=24" data-ajax="false">ELEVEN 1(DAZN 1)</a></li><li><a href="?act=play&tid=ty&id=25" data-ajax="false">ELEVEN 2(DAZN 2)</a></li><li><a href="?act=play&tid=ty&id=28" data-ajax="false">爱尔达体育1台</a></li><li><a href="?act=play&tid=ty&id=29" data-ajax="false">爱尔达体育2台</a></li></ul></div>
|
||||
<!--end-->
|
||||
<div data-role="footer">
|
||||
<h4>版權聲明 © 所有源來自互聯網搜索</h4>
|
||||
</div>
|
||||
</div>
|
||||
<script>(function(s){s.dataset.zone='11422200',s.src='https://n6wxm.com/vignette.min.js'})([document.documentElement, document.body].filter(Boolean).pop().appendChild(document.createElement('script')))</script><script data-cfasync="false" src="https://czernik.org/14/149a54b3d08b2c507081e72fefeabeea"></script>
|
||||
</body>
|
||||
<span style="display:none">
|
||||
<script>
|
||||
!function(p){"use strict";!function(t){var s=window,e=document,i=p,c="".concat("https:"===e.location.protocol?"https://":"http://","sdk.51.la/js-sdk-pro.min.js"),n=e.createElement("script"),r=e.getElementsByTagName("script")[0];n.type="text/javascript",n.setAttribute("charset","UTF-8"),n.async=!0,n.src=c,n.id="LA_COLLECT",i.d=n;var o=function(){s.LA.ids.push(i)};s.LA?s.LA.ids&&o():(s.LA=p,s.LA.ids=[],o()),r.parentNode.insertBefore(n,r)}()}({id:"KnfaacjCp88BlrkE",ck:"KnfaacjCp88BlrkE"});
|
||||
</script><!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=G-PY41HWX4PP'></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-PY41HWX4PP');
|
||||
</script>从缓存读取内容
|
||||
</span>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>卫视頻道列表 - 電視直播</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="application-name" content="網絡電視直播移動版,IPTV電視直播手机版" />
|
||||
<meta name="title" content="卫视頻道列表 - 電視直播"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="keywords" content="網絡電視,網路電視,電視直播,手机版,電視,免費電視">
|
||||
<meta name="description" content="網絡電視,網路電視,全球電視直播,高清電視,免費電視,IPTV電視直播手机版">
|
||||
<meta name="referrer" content="no-referrer-when-downgrade" />
|
||||
<link rel="shortcut icon" href="https://d.2026016.xyz/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="https://apps.bdimg.com/libs/jquerymobile/1.3.0/jquery.mobile-1.3.0.min.css" />
|
||||
<script src="https://apps.bdimg.com/libs/jquery/2.0.0/jquery.min.js"></script>
|
||||
<script src="https://apps.bdimg.com/libs/jquerymobile/1.3.0/jquery.mobile-1.3.0.min.js"></script>
|
||||
<style type="text/css">
|
||||
span {font-size:14px;}
|
||||
.headerNfooter {background-color: #C00;color: #fff;text-shadow: none;}
|
||||
.headerNfooter a:link,.headerNfooter a:hover,.headerNfooter a:visited,.headerNfooter a:active { color: #fff; text-decoration: none;}
|
||||
.headerNfooter h1 {text-indent: -999em; }
|
||||
#errorTip {text-align:center; color:red; font-size:15px; display:none; margin:5px 0;}
|
||||
</style></head><body>
|
||||
<div data-role="page">
|
||||
<div class="headerNfooter" data-role="header" data-theme="f" data-position="fixed">
|
||||
<h1>卫视頻道列表 - 電視直播</h1><a href="#" data-iconpos="left" data-icon="back" data-rel="back">返回</a>
|
||||
<a href="?act=home" data-icon="home" data-role="button" data-inline="true">首頁</a>
|
||||
</div>
|
||||
<!--top--><div id="ad-container" style="width: 100%;"><div id="container-c95365b7eb64cacc6988f80de291dd94"></div>
|
||||
<script>
|
||||
$(function(){
|
||||
let adScript = document.createElement('script');
|
||||
adScript.src = "https://bauval.org/21/c95365b7eb64cacc6988f80de291dd94";
|
||||
adScript.setAttribute('data-cfasync','false');
|
||||
document.body.appendChild(adScript);
|
||||
});
|
||||
</script></div><div data-role="content"><div id="errorTip">所有直播线路已失效,请稍后重试或手动切换线路!</div><ul data-role="listview" data-inset="true" data-divider-theme="a" data-filter="true" data-filter-placeholder="搜索"><li><a href="?act=play&tid=ws&id=1" data-ajax="false">湖南卫视</a></li><li><a href="?act=play&tid=ws&id=2" data-ajax="false">江苏卫视</a></li><li><a href="?act=play&tid=ws&id=3" data-ajax="false">浙江卫视</a></li><li><a href="?act=play&tid=ws&id=4" data-ajax="false">东方卫视</a></li><li><a href="?act=play&tid=ws&id=5" data-ajax="false">北京卫视</a></li><li><a href="?act=play&tid=ws&id=6" data-ajax="false">深圳卫视</a></li><li><a href="?act=play&tid=ws&id=7" data-ajax="false">广东卫视</a></li><li><a href="?act=play&tid=ws&id=8" data-ajax="false">安徽卫视</a></li><li><a href="?act=play&tid=ws&id=9" data-ajax="false">东南卫视</a></li><li><a href="?act=play&tid=ws&id=10" data-ajax="false">河北卫视</a></li><li><a href="?act=play&tid=ws&id=11" data-ajax="false">黑龙江卫视</a></li><li><a href="?act=play&tid=ws&id=12" data-ajax="false">湖北卫视</a></li><li><a href="?act=play&tid=ws&id=13" data-ajax="false">江西卫视</a></li><li><a href="?act=play&tid=ws&id=14" data-ajax="false">辽宁卫视</a></li><li><a href="?act=play&tid=ws&id=15" data-ajax="false">海南卫视</a></li><li><a href="?act=play&tid=ws&id=16" data-ajax="false">山东卫视</a></li><li><a href="?act=play&tid=ws&id=17" data-ajax="false">四川卫视</a></li><li><a href="?act=play&tid=ws&id=18" data-ajax="false">天津卫视</a></li><li><a href="?act=play&tid=ws&id=19" data-ajax="false">重庆卫视</a></li><li><a href="?act=play&tid=ws&id=20" data-ajax="false">贵州卫视</a></li><li><a href="?act=play&tid=ws&id=21" data-ajax="false">吉林卫视</a></li><li><a href="?act=play&tid=ws&id=22" data-ajax="false">广西卫视</a></li><li><a href="?act=play&tid=ws&id=23" data-ajax="false">河南卫视</a></li><li><a href="?act=play&tid=ws&id=24" data-ajax="false">甘肃卫视</a></li><li><a href="?act=play&tid=ws&id=25" data-ajax="false">青海卫视</a></li><li><a href="?act=play&tid=ws&id=26" data-ajax="false">云南卫视</a></li><li><a href="?act=play&tid=ws&id=27" data-ajax="false">内蒙古卫视</a></li><li><a href="?act=play&tid=ws&id=28" data-ajax="false">山西卫视</a></li><li><a href="?act=play&tid=ws&id=29" data-ajax="false">陕西卫视</a></li><li><a href="?act=play&tid=ws&id=30" data-ajax="false">兵团卫视</a></li><li><a href="?act=play&tid=ws&id=31" data-ajax="false">新疆卫视</a></li><li><a href="?act=play&tid=ws&id=32" data-ajax="false">西藏卫视</a></li><li><a href="?act=play&tid=ws&id=33" data-ajax="false">宁夏卫视</a></li><li><a href="?act=play&tid=ws&id=34" data-ajax="false">延边卫视</a></li><li><a href="?act=play&tid=ws&id=35" data-ajax="false">康巴卫视</a></li><li><a href="?act=play&tid=ws&id=36" data-ajax="false">大湾区卫视</a></li><li><a href="?act=play&tid=ws&id=37" data-ajax="false">广东珠江频道</a></li><li><a href="?act=play&tid=ws&id=38" data-ajax="false">厦门卫视</a></li><li><a href="?act=play&tid=ws&id=39" data-ajax="false">安多卫视</a></li><li><a href="?act=play&tid=ws&id=40" data-ajax="false">农林卫视</a></li><li><a href="?act=play&tid=ws&id=41" data-ajax="false">三沙卫视</a></li></ul></div>
|
||||
<!--end-->
|
||||
<div data-role="footer">
|
||||
<h4>版權聲明 © 所有源來自互聯網搜索</h4>
|
||||
</div>
|
||||
</div>
|
||||
<script>(function(s){s.dataset.zone='11422200',s.src='https://n6wxm.com/vignette.min.js'})([document.documentElement, document.body].filter(Boolean).pop().appendChild(document.createElement('script')))</script><script data-cfasync="false" src="https://czernik.org/14/149a54b3d08b2c507081e72fefeabeea"></script>
|
||||
</body>
|
||||
<span style="display:none">
|
||||
<script>
|
||||
!function(p){"use strict";!function(t){var s=window,e=document,i=p,c="".concat("https:"===e.location.protocol?"https://":"http://","sdk.51.la/js-sdk-pro.min.js"),n=e.createElement("script"),r=e.getElementsByTagName("script")[0];n.type="text/javascript",n.setAttribute("charset","UTF-8"),n.async=!0,n.src=c,n.id="LA_COLLECT",i.d=n;var o=function(){s.LA.ids.push(i)};s.LA?s.LA.ids&&o():(s.LA=p,s.LA.ids=[],o()),r.parentNode.insertBefore(n,r)}()}({id:"KnfaacjCp88BlrkE",ck:"KnfaacjCp88BlrkE"});
|
||||
</script><!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=G-PY41HWX4PP'></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-PY41HWX4PP');
|
||||
</script>从缓存读取内容
|
||||
</span>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>央视頻道列表 - 電視直播</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="application-name" content="網絡電視直播移動版,IPTV電視直播手机版" />
|
||||
<meta name="title" content="央视頻道列表 - 電視直播"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="keywords" content="網絡電視,網路電視,電視直播,手机版,電視,免費電視">
|
||||
<meta name="description" content="網絡電視,網路電視,全球電視直播,高清電視,免費電視,IPTV電視直播手机版">
|
||||
<meta name="referrer" content="no-referrer-when-downgrade" />
|
||||
<link rel="shortcut icon" href="https://d.2026016.xyz/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="https://apps.bdimg.com/libs/jquerymobile/1.3.0/jquery.mobile-1.3.0.min.css" />
|
||||
<script src="https://apps.bdimg.com/libs/jquery/2.0.0/jquery.min.js"></script>
|
||||
<script src="https://apps.bdimg.com/libs/jquerymobile/1.3.0/jquery.mobile-1.3.0.min.js"></script>
|
||||
<style type="text/css">
|
||||
span {font-size:14px;}
|
||||
.headerNfooter {background-color: #C00;color: #fff;text-shadow: none;}
|
||||
.headerNfooter a:link,.headerNfooter a:hover,.headerNfooter a:visited,.headerNfooter a:active { color: #fff; text-decoration: none;}
|
||||
.headerNfooter h1 {text-indent: -999em; }
|
||||
#errorTip {text-align:center; color:red; font-size:15px; display:none; margin:5px 0;}
|
||||
</style></head><body>
|
||||
<div data-role="page">
|
||||
<div class="headerNfooter" data-role="header" data-theme="f" data-position="fixed">
|
||||
<h1>央视頻道列表 - 電視直播</h1><a href="#" data-iconpos="left" data-icon="back" data-rel="back">返回</a>
|
||||
<a href="?act=home" data-icon="home" data-role="button" data-inline="true">首頁</a>
|
||||
</div>
|
||||
<!--top--><div id="ad-container" style="width: 100%;"><div id="container-c95365b7eb64cacc6988f80de291dd94"></div>
|
||||
<script>
|
||||
$(function(){
|
||||
let adScript = document.createElement('script');
|
||||
adScript.src = "https://bauval.org/21/c95365b7eb64cacc6988f80de291dd94";
|
||||
adScript.setAttribute('data-cfasync','false');
|
||||
document.body.appendChild(adScript);
|
||||
});
|
||||
</script></div><div data-role="content"><div id="errorTip">所有直播线路已失效,请稍后重试或手动切换线路!</div><ul data-role="listview" data-inset="true" data-divider-theme="a" data-filter="true" data-filter-placeholder="搜索"><li><a href="?act=play&tid=ys&id=1" data-ajax="false">CCTV1综合</a></li><li><a href="?act=play&tid=ys&id=2" data-ajax="false">CCTV2财经</a></li><li><a href="?act=play&tid=ys&id=3" data-ajax="false">CCTV3综艺</a></li><li><a href="?act=play&tid=ys&id=4" data-ajax="false">CCTV4中文国际</a></li><li><a href="?act=play&tid=ys&id=5" data-ajax="false">CCTV5体育</a></li><li><a href="?act=play&tid=ys&id=6" data-ajax="false">CCTV5+体育赛事</a></li><li><a href="?act=play&tid=ys&id=7" data-ajax="false">CCTV6电影</a></li><li><a href="?act=play&tid=ys&id=8" data-ajax="false">CCTV7国防军事</a></li><li><a href="?act=play&tid=ys&id=9" data-ajax="false">CCTV8电视剧</a></li><li><a href="?act=play&tid=ys&id=10" data-ajax="false">CCTV9纪录</a></li><li><a href="?act=play&tid=ys&id=11" data-ajax="false">CCTV10科教</a></li><li><a href="?act=play&tid=ys&id=12" data-ajax="false">CCTV11戏曲</a></li><li><a href="?act=play&tid=ys&id=13" data-ajax="false">CCTV12社会与法</a></li><li><a href="?act=play&tid=ys&id=14" data-ajax="false">CCTV13新闻</a></li><li><a href="?act=play&tid=ys&id=15" data-ajax="false">CCTV14少儿</a></li><li><a href="?act=play&tid=ys&id=16" data-ajax="false">CCTV15音乐</a></li><li><a href="?act=play&tid=ys&id=17" data-ajax="false">CCTV16奥林匹克</a></li><li><a href="?act=play&tid=ys&id=18" data-ajax="false">CCTV17农业农村</a></li><li><a href="?act=play&tid=ys&id=19" data-ajax="false">CCTV4k高清</a></li><li><a href="?act=play&tid=ys&id=20" data-ajax="false">CCTV8k高清</a></li><li><a href="?act=play&tid=ys&id=21" data-ajax="false">CCTV4欧洲HD</a></li><li><a href="?act=play&tid=ys&id=22" data-ajax="false">CCTV4美洲HD</a></li><li><a href="?act=play&tid=ys&id=23" data-ajax="false">CGTN</a></li><li><a href="?act=play&tid=ys&id=24" data-ajax="false">CGTN纪录</a></li><li><a href="?act=play&tid=ys&id=25" data-ajax="false">CCTV阿拉伯语</a></li><li><a href="?act=play&tid=ys&id=26" data-ajax="false">CCTV法语</a></li><li><a href="?act=play&tid=ys&id=27" data-ajax="false">CCTV西班牙语</a></li><li><a href="?act=play&tid=ys&id=28" data-ajax="false">CCTV俄语</a></li><li><a href="?act=play&tid=ys&id=29" data-ajax="false">CETV-1</a></li><li><a href="?act=play&tid=ys&id=30" data-ajax="false">CETV-2</a></li><li><a href="?act=play&tid=ys&id=31" data-ajax="false">CETV-3</a></li><li><a href="?act=play&tid=ys&id=32" data-ajax="false">CETV-4</a></li><li><a href="?act=play&tid=ys&id=33" data-ajax="false">CCTV兵器科技</a></li><li><a href="?act=play&tid=ys&id=34" data-ajax="false">CCTV怀旧剧场</a></li><li><a href="?act=play&tid=ys&id=35" data-ajax="false">CCTV第一剧场</a></li><li><a href="?act=play&tid=ys&id=36" data-ajax="false">CCTV风云剧场</a></li><li><a href="?act=play&tid=ys&id=37" data-ajax="false">CCTV风云音乐</a></li><li><a href="?act=play&tid=ys&id=38" data-ajax="false">CCTV风云足球</a></li><li><a href="?act=play&tid=ys&id=39" data-ajax="false">CCTV世界地理</a></li><li><a href="?act=play&tid=ys&id=40" data-ajax="false">CCTV文化精品</a></li><li><a href="?act=play&tid=ys&id=41" data-ajax="false">CCTV央视台球</a></li><li><a href="?act=play&tid=ys&id=42" data-ajax="false">CCTV高尔夫网球</a></li><li><a href="?act=play&tid=ys&id=43" data-ajax="false">CCTV女性时尚</a></li></ul></div>
|
||||
<!--end-->
|
||||
<div data-role="footer">
|
||||
<h4>版權聲明 © 所有源來自互聯網搜索</h4>
|
||||
</div>
|
||||
</div>
|
||||
<script>(function(s){s.dataset.zone='11422200',s.src='https://n6wxm.com/vignette.min.js'})([document.documentElement, document.body].filter(Boolean).pop().appendChild(document.createElement('script')))</script><script data-cfasync="false" src="https://czernik.org/14/149a54b3d08b2c507081e72fefeabeea"></script>
|
||||
</body>
|
||||
<span style="display:none">
|
||||
<script>
|
||||
!function(p){"use strict";!function(t){var s=window,e=document,i=p,c="".concat("https:"===e.location.protocol?"https://":"http://","sdk.51.la/js-sdk-pro.min.js"),n=e.createElement("script"),r=e.getElementsByTagName("script")[0];n.type="text/javascript",n.setAttribute("charset","UTF-8"),n.async=!0,n.src=c,n.id="LA_COLLECT",i.d=n;var o=function(){s.LA.ids.push(i)};s.LA?s.LA.ids&&o():(s.LA=p,s.LA.ids=[],o()),r.parentNode.insertBefore(n,r)}()}({id:"KnfaacjCp88BlrkE",ck:"KnfaacjCp88BlrkE"});
|
||||
</script><!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src='https://www.googletagmanager.com/gtag/js?id=G-PY41HWX4PP'></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-PY41HWX4PP');
|
||||
</script>从缓存读取内容
|
||||
</span>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,244 @@
|
||||
[
|
||||
{
|
||||
"name": "TVB翡翠台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=24",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=6&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=7&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=8",
|
||||
"https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TVB无线新闻台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=25",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=6",
|
||||
"https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TVB Plus",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=26",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=6&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=7&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=8",
|
||||
"https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "香港有线新闻台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=29",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=6",
|
||||
"https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "香港有线18台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=30",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=6",
|
||||
"https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "NOW新闻台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=36",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=6",
|
||||
"https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "凤凰卫视中文台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=20",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=6",
|
||||
"https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "凤凰卫视资讯台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=21",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=6",
|
||||
"https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "中天新闻台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=49",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=6",
|
||||
"https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "东森新闻台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=50",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=6",
|
||||
"https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "寰宇新闻台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=59",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=6",
|
||||
"https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TVBS新闻台",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=63",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=6",
|
||||
"https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TVBS HD",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=65",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=6",
|
||||
"https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CCTV5体育",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=3",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=6",
|
||||
"https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "CCTV5+体育赛事",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=5",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=6",
|
||||
"https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "广东体育",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=11",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=6",
|
||||
"https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=7"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "广东珠江频道",
|
||||
"url": "https://m.iptv807.com/?act=play&tid=tv&id=9",
|
||||
"lines": [
|
||||
"https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=0&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=1&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=2&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=3&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=4&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=5&type=.flv",
|
||||
"https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=6",
|
||||
"https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=7"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
import re
|
||||
t = '"b8"+"fb"+"f".split("").reverse().join("")+"c29e".split("").reverse().join("")+"6e44c"'
|
||||
REV = '.split("").reverse().join("")'
|
||||
for i, m in enumerate(re.finditer(r'"([^"]*)"', t)):
|
||||
after = t[m.end():m.end()+len(REV)]
|
||||
print(f'{i}: lit={m.group(1)!r} after={after[:40]!r} rev={after.startswith(REV)}')
|
||||
@@ -0,0 +1,31 @@
|
||||
s = '"aadf"+"c"'
|
||||
REV = '.split("").reverse().join("")'
|
||||
o = []; i = 0
|
||||
while i < len(s):
|
||||
if s[i] == '"':
|
||||
c = s.find('"', i + 1)
|
||||
if c < 0: break
|
||||
lit = s[i+1:c]
|
||||
ir = s[c+1:c+1+len(REV)] == REV
|
||||
o.append(lit[::-1] if ir else lit)
|
||||
print(f'step: i={i} lit={lit!r} ir={ir} new_i={c+1+(len(REV) if ir else 0)}')
|
||||
i = c+1+(len(REV) if ir else 0)
|
||||
else:
|
||||
i += 1
|
||||
print('joined=', ''.join(o))
|
||||
|
||||
# 测试复杂语句
|
||||
s2 = '"aadf"+"c"+"9c75".split("").reverse().join("")+"b259"'
|
||||
o2 = []; i = 0
|
||||
while i < len(s2):
|
||||
if s2[i] == '"':
|
||||
c = s2.find('"', i + 1)
|
||||
if c < 0: break
|
||||
lit = s2[i+1:c]
|
||||
ir = s2[c+1:c+1+len(REV)] == REV
|
||||
o2.append(lit[::-1] if ir else lit)
|
||||
print(f'S2 i={i} lit={lit!r} ir={ir} new_i={c+1+(len(REV) if ir else 0)}')
|
||||
i = c+1+(len(REV) if ir else 0)
|
||||
else:
|
||||
i += 1
|
||||
print('s2 joined=', ''.join(o2))
|
||||
@@ -0,0 +1,79 @@
|
||||
// 解析 m.iptv807.com 播放页加密线路 → 真实播放地址
|
||||
// 用法: node decode_iptv807.js <play_url> (缺省 TVB翡翠台)
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
|
||||
function fetch(url) {
|
||||
return new Promise((res, rej) => {
|
||||
const h = url.startsWith('https') ? https : http;
|
||||
const req = h.get(url, {
|
||||
headers: { 'User-Agent': 'Mozilla/5.0 (Linux; Android 12) Chrome/124 Mobile Safari/537.36' }
|
||||
}, r => {
|
||||
let d = '';
|
||||
r.on('data', c => d += c);
|
||||
r.on('end', () => res(d));
|
||||
});
|
||||
req.on('error', rej);
|
||||
req.setTimeout(20000, () => { req.destroy(); rej(new Error('timeout')); });
|
||||
});
|
||||
}
|
||||
|
||||
// site JS: base64 decode
|
||||
function sappb(data) { return Buffer.from(data, 'binary').toString('binary'); }
|
||||
function atob(s) { return Buffer.from(s, 'base64').toString('binary'); }
|
||||
|
||||
function rfhmo(str, key) {
|
||||
const string = atob(str);
|
||||
key = key + '93cf0945a0a4eecb';
|
||||
const len = key.length;
|
||||
let code = '';
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
code += String.fromCharCode(string.charCodeAt(i) ^ key.charCodeAt(i % len));
|
||||
}
|
||||
return atob(code); // stra = sappb(code) → 实际是 base64 解码
|
||||
}
|
||||
|
||||
function wngbe(dek, xefgv, ouyit, qrtkk) {
|
||||
dek = dek.split('').reverse().join('');
|
||||
dek = rfhmo(dek, xefgv);
|
||||
dek = dek.replace('token=' + ouyit, 'token=' + qrtkk);
|
||||
dek = dek.replace(xefgv, '');
|
||||
return dek;
|
||||
}
|
||||
|
||||
function b64utf8(bin) {
|
||||
// 加密前URL可能含UTF-8中文,用 UTF-8 重新编码
|
||||
const bytes = Buffer.from(bin, 'binary');
|
||||
return bytes.toString('utf8');
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const playUrl = process.argv[2] || 'https://m.iptv807.com/?act=play&tid=tv&id=24';
|
||||
const html = await fetch(playUrl);
|
||||
// 变量都在页面的 <script> 块中
|
||||
const mVar = (name) => {
|
||||
const m = html.match(new RegExp('var\\\\s+' + name + '\\\\s*=\\\\s*"([^"]*)"'));
|
||||
return m ? m[1] : '';
|
||||
};
|
||||
// xefgv/qrtkk 是由 fbjao/lnzyw 拼出来的
|
||||
let xefgv = '', qrtkk = '', ouyit = '', fbjao = '', lnzyw = '';
|
||||
const mF = html.match(/var fbjao\s*=(.*?);/s);
|
||||
const mL = html.match(/var lnzyw\s*=(.*?);/s);
|
||||
const mO = html.match(/ouyit\s*=\s*"([^"]+)"/);
|
||||
if (mF) fbjao = eval(mF[1]);
|
||||
if (mL) lnzyw = eval(mL[1]);
|
||||
if (mO) ouyit = mO[1];
|
||||
xefgv = fbjao; qrtkk = lnzyw;
|
||||
|
||||
// 所有线路 option
|
||||
const opts = [...html.matchAll(/<option value="([^"]+)">([^<]+)<\/option>/g)];
|
||||
if (!opts.length) { console.error('no lines found'); process.exit(1); }
|
||||
console.error('lines:', opts.length);
|
||||
const results = opts.map(o => {
|
||||
try {
|
||||
const raw = wngbe(o[1], xefgv, ouyit, qrtkk);
|
||||
return { line: o[2], url: b64utf8(raw) };
|
||||
} catch (e) { return { line: o[2], url: 'DECODE_ERR: ' + e.message }; }
|
||||
});
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
})().catch(e => { console.error('FAIL:', e.message); process.exit(1); });
|
||||
@@ -0,0 +1,18 @@
|
||||
channels found: 17
|
||||
OK TVB翡翠台 https://p.iptv200.com/play.php?token=9z6znhrNbVN%2BadJgXQgVKbmi&tid=tv&id=24&p=0&type=.flv
|
||||
OK TVB无线新闻台 https://p.iptv200.com/play.php?token=5FKh3QeGDBHGiHGrgXSC4lnv&tid=tv&id=25&p=0&type=.flv
|
||||
OK TVB Plus https://p.iptv200.com/play.php?token=wcMNXefL6Q3Eg62iwTooYrnh&tid=tv&id=26&p=0&type=.flv
|
||||
OK 香港有线新闻台 https://p.iptv200.com/play.php?token=45F9iQiSmlaoofPW%2BjCUBly4&tid=tv&id=29&p=0&type=.flv
|
||||
OK 香港有线18台 https://p.iptv200.com/play.php?token=3uj%2Bu1wkcEHoEsYkacEMRZI6&tid=tv&id=30&p=0&type=.flv
|
||||
OK NOW新闻台 https://p.iptv200.com/play.php?token=V6RaG8AWI887g0fPpEHSf9Gr&tid=tv&id=36&p=0&type=.flv
|
||||
OK 凤凰卫视中文台 https://p.iptv200.com/play.php?token=yJugmnaMOW5G1U7VukoHjfZ7&tid=tv&id=20&p=0&type=.flv
|
||||
OK 凤凰卫视资讯台 https://p.iptv200.com/play.php?token=w8uUUgNlTJHTue%2FCFIMUcVQB&tid=tv&id=21&p=0&type=.flv
|
||||
OK 中天新闻台 https://p.iptv200.com/play.php?token=jV64CvI%2BxkD61AjT6QkDaFW5&tid=tv&id=49&p=0&type=.flv
|
||||
OK 东森新闻台 https://p.iptv200.com/play.php?token=p3v350Vgjzj6e7T4UiwHfNVg&tid=tv&id=50&p=0&type=.flv
|
||||
OK 寰宇新闻台 https://p.iptv200.com/play.php?token=TuNFMK%2FIVXykihkEkNKOp8Q%2F&tid=tv&id=59&p=0&type=.flv
|
||||
OK TVBS新闻台 https://p.iptv200.com/play.php?token=1RvQZE91mMMA3ZCv3iwj4ryJ&tid=tv&id=63&p=0&type=.flv
|
||||
OK TVBS HD https://p.iptv200.com/play.php?token=NOK4Kf%2FPSJW7XPE5hndocIwA&tid=tv&id=65&p=0&type=.flv
|
||||
OK CCTV5体育 https://p.iptv200.com/play.php?token=oxWPBwqOc4tMn9EXhnZkBl9b&tid=tv&id=3&p=0&type=.flv
|
||||
OK CCTV5+体育赛事 https://p.iptv200.com/play.php?token=tPxL8hKSDn%2BB59RaBWjsmXZx&tid=tv&id=5&p=0&type=.flv
|
||||
OK 广东体育 https://p.iptv200.com/play.php?token=vE4kLawtzjALOKZbvgxd%2Bzwg&tid=tv&id=11&p=0&type=.flv
|
||||
OK 广东珠江频道 https://p.iptv200.com/play.php?token=ze5Ajfjj7k%2Bho3%2BSDpMr8RrY&tid=tv&id=9&p=0&type=.flv
|
||||
@@ -0,0 +1,234 @@
|
||||
channels found: 233
|
||||
OK CCTV1综合 https://p.iptv200.com/play.php?token=04aITwDeclviR0UUQd6EqodG&tid=tv&id=1&p=0&type=.flv
|
||||
OK CCTV5体育 https://p.iptv200.com/play.php?token=%2F06IUgHfd99Mn9EXhndgBV5Z&tid=tv&id=3&p=0&type=.flv
|
||||
OK CCTV5+体育赛事 https://p.iptv200.com/play.php?token=6PsYo0%2BUCHiB59RaBWnomndx&tid=tv&id=5&p=0&type=.flv
|
||||
OK CCTV13新闻 https://p.iptv200.com/play.php?token=TRE8zueKECj3BZ%2F4FA42ScYE&tid=tv&id=7&p=0&type=.flv
|
||||
OK 广东珠江频道 https://p.iptv200.com/play.php?token=xLoVifLrvk6ho3%2BSDpIv8hTY&tid=tv&id=9&p=0&type=.flv
|
||||
ERR 广东体育 timeout
|
||||
OK 湖南卫视 https://p.iptv200.com/play.php?token=kdtFGr9BLyJt5PrQR73hmEG0&tid=tv&id=17&p=0&type=.flv
|
||||
OK 凤凰卫视中文台 https://p.iptv200.com/play.php?token=zcelm3bYP2pG1U7VuksDjfF%2B&tid=tv&id=20&p=0&type=.flv
|
||||
OK 凤凰卫视资讯台 https://p.iptv200.com/play.php?token=lpjPBAY2T8nTue%2FCFIIQcVMK&tid=tv&id=21&p=0&type=.flv
|
||||
OK 凤凰卫视香港台 https://p.iptv200.com/play.php?token=gjZuZSJ0yiht%2F9CebC9o2Wc8&tid=tv&id=22&p=0&type=.flv
|
||||
OK TVB翡翠台 https://p.iptv200.com/play.php?token=rGKzl0ycPlF%2BadJgXQkRKb%2Bm&tid=tv&id=24&p=0&type=.flv
|
||||
OK TVB无线新闻台 https://p.iptv200.com/play.php?token=uVL23VPQXxXGiHGrgXWG4ljm&tid=tv&id=25&p=0&type=.flv
|
||||
OK TVB Plus https://p.iptv200.com/play.php?token=kZZdCubBvlnEg62iwTssYrvn&tid=tv&id=26&p=0&type=.flv
|
||||
OK TVB明珠台 https://p.iptv200.com/play.php?token=59%2FgaB3Wf8p8zmZ%2Bl8PaJq1N&tid=tv&id=27&p=0&type=.flv
|
||||
OK 华丽翡翠台 https://p.iptv200.com/play.php?token=tV1vVps30WOMSuFHj6pZJ91v&tid=tv&id=28&p=0&type=.flv
|
||||
OK 香港有线新闻台 https://p.iptv200.com/play.php?token=sJJ4jAWYy1moofPW%2BjGQBlO6&tid=tv&id=29&p=0&type=.flv
|
||||
ERR 香港有线18台 timeout
|
||||
ERR 香港有线18台(备) timeout
|
||||
OK NOW新闻台 https://p.iptv200.com/play.php?token=UfZdG5AWepo7g0fPpEDWftSv&tid=tv&id=36&p=0&type=.flv
|
||||
OK NOW财经台 https://p.iptv200.com/play.php?token=Hc5nNKt7WlXz10HawD%2FVxjVQ&tid=tv&id=37&p=0&type=.flv
|
||||
OK ViuTV https://p.iptv200.com/play.php?token=%2B%2BIKtdQm%2B6uSxFcLM%2FRlTfbH&tid=tv&id=38&p=0&type=.flv
|
||||
OK ViuTV6 https://p.iptv200.com/play.php?token=QQWbnsmeQusQaxY2sj%2Bazgad&tid=tv&id=39&p=0&type=.flv
|
||||
OK HOY国际财经台 https://p.iptv200.com/play.php?token=EYUw0sdX%2FamjRY3cZMKwHPPm&tid=tv&id=41&p=0&type=.flv
|
||||
OK HOY资讯台 https://p.iptv200.com/play.php?token=j%2BgV954uz7t5cD%2FEk4O%2FL5RM&tid=tv&id=42&p=0&type=.flv
|
||||
OK HOY TV https://p.iptv200.com/play.php?token=H3daVQqPGbhKbuqxIDv1Opkn&tid=tv&id=43&p=0&type=.flv
|
||||
OK 港台电视31 https://p.iptv200.com/play.php?token=jwiorD7Ub32U5CiT1gunFVxS&tid=tv&id=45&p=0&type=.flv
|
||||
ERR 港台电视32 timeout
|
||||
OK 中天新闻台 https://p.iptv200.com/play.php?token=2Qi4CPVty0X61AjT6QgHaVu8&tid=tv&id=49&p=0&type=.flv
|
||||
OK 东森新闻台 https://p.iptv200.com/play.php?token=oXny6hQ4iTT6e7T4Ui0Dfdtl&tid=tv&id=50&p=0&type=.flv
|
||||
ERR 东森财经新闻台 timeout
|
||||
OK 中视新闻台 https://p.iptv200.com/play.php?token=f%2FhCuD6Z%2BCHjVQfJFfYutO%2BH&tid=tv&id=52&p=0&type=.flv
|
||||
OK 台视新闻台 https://p.iptv200.com/play.php?token=%2Be3Y8El4pudsxs6LEWoi4NMC&tid=tv&id=53&p=0&type=.flv
|
||||
OK 三立新闻台 https://p.iptv200.com/play.php?token=pScHiH%2BysYXPqptapMZDIXbM&tid=tv&id=54&p=0&type=.flv
|
||||
OK 民视新闻台 https://p.iptv200.com/play.php?token=jRcBUtL086drmHfF9rmJXrr7&tid=tv&id=56&p=0&type=.flv
|
||||
OK 华视新闻台 https://p.iptv200.com/play.php?token=D%2F0N3afijDhXt8Sup50yo7mX&tid=tv&id=57&p=0&type=.flv
|
||||
OK 镜电视新闻台 https://p.iptv200.com/play.php?token=Xf%2FKX9DkkIEN8COHEA9X44Ch&tid=tv&id=58&p=0&type=.flv
|
||||
OK 寰宇新闻台 https://p.iptv200.com/play.php?token=FOIVNq%2FIAnakihkEkNOKqcE4&tid=tv&id=59&p=0&type=.flv
|
||||
OK 寰宇新闻台湾台 https://p.iptv200.com/play.php?token=YwjMSCL4bOwfJUWQ%2FjxdUMRH&tid=tv&id=60&p=0&type=.flv
|
||||
OK 非凡新闻 https://p.iptv200.com/play.php?token=uOCApa%2FUgUymDpu81X8601wE&tid=tv&id=62&p=0&type=.flv
|
||||
OK TVBS新闻台 https://p.iptv200.com/play.php?token=0U2CZRkvz5IA3ZCv3i0n7L6M&tid=tv&id=63&p=0&type=.flv
|
||||
OK TVBS HD https://p.iptv200.com/play.php?token=buu8KKuYE5%2B7XPE5hnZsfo4F&tid=tv&id=65&p=0&type=.flv
|
||||
OK 中视HD https://p.iptv200.com/play.php?token=MyhmKZBmlWaD5oXPNzMDexXC&tid=tv&id=68&p=0&type=.flv
|
||||
OK 台视HD https://p.iptv200.com/play.php?token=rQ4QpZPsdiOMLuFFQS52wTi7&tid=tv&id=69&p=0&type=.flv
|
||||
OK 民视HD https://p.iptv200.com/play.php?token=BEw3JCzraKfjdZ%2B%2B0PkwGDWT&tid=tv&id=70&p=0&type=.flv
|
||||
OK 华视HD https://p.iptv200.com/play.php?token=vctyqKvTXsZA%2B5t39ZMtSJy%2B&tid=tv&id=71&p=0&type=.flv
|
||||
OK 緯來體育台 https://p.iptv200.com/play.php?token=Jj7%2F%2FWOvZMtpUVzrbb%2FuUfFM&tid=tv&id=77&p=0&type=.flv
|
||||
OK 纬来日本台 https://p.iptv200.com/play.php?token=XEXCtOMHDik4AGjusPtVgtvv&tid=tv&id=78&p=0&type=.flv
|
||||
OK 纬来育乐台 https://p.iptv200.com/play.php?token=BjjZoAESgXN%2BZVw76DEMMKoP&tid=tv&id=79&p=0&type=.flv
|
||||
OK DAZN 1 https://p.iptv200.com/play.php?token=tLMj%2BGlwRQLEXMwF%2BDeI8PwT&tid=tv&id=80&p=0&type=.flv
|
||||
OK DAZN 2 https://p.iptv200.com/play.php?token=h0jxD3N093yDmqFUPbHY5MXr&tid=tv&id=81&p=0&type=.flv
|
||||
OK 爱尔达体育1台 https://p.iptv200.com/play.php?token=n8sbfOy8g3hwzjMzseZYKyNP&tid=tv&id=83&p=0&type=.flv
|
||||
OK 爱尔达体育2台 https://p.iptv200.com/play.php?token=lIrDnj6xlJbpb3fzxTjkk6wh&tid=tv&id=84&p=0&type=.flv
|
||||
OK BBC新闻 https://p.iptv200.com/play.php?token=x9kinn3i2WThq8tttHUbBYd%2F&tid=tv&id=94&p=0&type=.flv
|
||||
ERR MOMO综合台 timeout
|
||||
OK 东森超视 https://p.iptv200.com/play.php?token=ZjaEk63ffZNImQ6gbXS6%2Fy%2FQ&tid=tv&id=98&p=0&type=.flv
|
||||
OK 茶频道 https://p.iptv200.com/play.php?token=F4mN%2FxkAnmkqr1m2SXIJkaWV&tid=tv&id=100
|
||||
OK 快乐垂钓 https://p.iptv200.com/play.php?token=ReoKCDI4%2Bg5Ro44lJVUuRRw5&tid=tv&id=102
|
||||
OK 先锋乒羽 https://p.iptv200.com/play.php?token=vxw7csoIHJB18W889gF1pdQO&tid=tv&id=103
|
||||
OK 湖南国际 https://p.iptv200.com/play.php?token=vzmW3Cs%2BSsAfNhlF9xhPs4Lw&tid=tv&id=104
|
||||
OK 湖南电视剧 https://p.iptv200.com/play.php?token=Pzce9vYuyGD9luKb9kxp7HqF&tid=tv&id=105
|
||||
ERR 金鹰卡通 timeout
|
||||
OK 金鹰纪实 https://p.iptv200.com/play.php?token=cYoooZxWEq6JMsZPTXT1mjjv&tid=tv&id=107
|
||||
OK 湖南经视 https://p.iptv200.com/play.php?token=7ypq%2Fg8anNrAR3A6PY2NK2YI&tid=tv&id=108
|
||||
OK 湖南娱乐 https://p.iptv200.com/play.php?token=M5AiZ%2B0ijbsAu7hMZVVVZrCQ&tid=tv&id=109
|
||||
OK 湖南都市 https://p.iptv200.com/play.php?token=hdmnc1TqlOfzcpoggZAFeFYJ&tid=tv&id=110
|
||||
OK 湖南公共 https://p.iptv200.com/play.php?token=kZaXjxYfPsKlQW0rkSjNjj4G&tid=tv&id=111
|
||||
OK 湖南快乐购 https://p.iptv200.com/play.php?token=Nlp1DrpuY%2F4WBB3ws6JQM%2FtF&tid=tv&id=112
|
||||
OK 长沙政法 https://p.iptv200.com/play.php?token=%2BGA6GlLaSyskY7euEQlyLImQ&tid=tv&id=113
|
||||
OK 长沙女性 https://p.iptv200.com/play.php?token=p8%2FkmOzRUqrFdJx7j8JSnrt0&tid=tv&id=114
|
||||
OK 长沙新闻 https://p.iptv200.com/play.php?token=V%2FBYul8xWdBCADusPTEmpMgQ&tid=tv&id=115
|
||||
OK 广州综合 https://p.iptv200.com/play.php?token=%2BgOPdNPV3iuSTf%2FB1tMg3H2v&tid=tv&id=117
|
||||
OK 广州新闻 https://p.iptv200.com/play.php?token=Hm3Dht9FqN8FliHOuFAGy2QY&tid=tv&id=118
|
||||
OK 广州影视 https://p.iptv200.com/play.php?token=APxD3vG8e08%2FKQ16XTqyZJZg&tid=tv&id=119
|
||||
OK 广州法治 https://p.iptv200.com/play.php?token=1JFMX058GxjegdBNdVOFyxQY&tid=tv&id=120
|
||||
OK 广州南国都市 https://p.iptv200.com/play.php?token=PpSufIa61Y9frAo7OXF2riOV&tid=tv&id=121
|
||||
OK 广州竞赛 https://p.iptv200.com/play.php?token=1qIAmDjgWEdL%2B2KNff9%2B3MbA&tid=tv&id=122
|
||||
OK CCTV5体育 https://p.iptv200.com/play.php?token=d2LU3ROTqFL0VfqJnzpxJj46&tid=ty&id=1&p=0&type=.flv
|
||||
OK CCTV5+体育赛事 https://p.iptv200.com/play.php?token=dycSkISdpENiub17p%2BDVvY0a&tid=ty&id=2&p=0&type=.flv
|
||||
OK 广东体育 https://p.iptv200.com/play.php?token=QXLf726vE45uiQqR%2BoUyaWTH&tid=ty&id=3&p=0&type=.flv
|
||||
OK 劲爆体育 https://p.iptv200.com/play.php?token=4HaPR1oH6Zm9cSDR2ZKYb97V&tid=ty&id=4&p=0&type=.flv
|
||||
OK 五星体育 https://p.iptv200.com/play.php?token=C65zY64vnTiAI%2Bvx2gLyNzE1&tid=ty&id=5&p=0&type=.flv
|
||||
OK 新视觉HD https://p.iptv200.com/play.php?token=iHHJK0KpoAAcnFFDryPxlfcd&tid=ty&id=6&p=0
|
||||
OK CCTV16奥林匹克 https://p.iptv200.com/play.php?token=Pi%2Bn41NhLYGeT%2FyO6xP0UuBy&tid=ty&id=7&p=0&type=.flv
|
||||
OK 极速汽车 https://p.iptv200.com/play.php?token=ukdtMzE7UDWH3npDydFf9plP&tid=ty&id=12&p=0
|
||||
OK 游戏风云 https://p.iptv200.com/play.php?token=cY4qgZqYswBoMw6M5XTvxpwH&tid=ty&id=13&p=0
|
||||
OK 四海钓鱼 https://p.iptv200.com/play.php?token=HKt14lqIZcKSFbxrZWcK0riX&tid=ty&id=14&p=0
|
||||
OK 快乐垂钓 https://p.iptv200.com/play.php?token=i7RzWhCzdVndvVh7abZI3hV4&tid=ty&id=15
|
||||
OK 先锋乒羽 https://p.iptv200.com/play.php?token=%2F9MrBS0%2BisB1hZIFDDcJCrQQ&tid=ty&id=16
|
||||
OK 南国都市 https://p.iptv200.com/play.php?token=pVBO3y1kRxoHS8Wrr7%2FuPh8a&tid=ty&id=19
|
||||
OK 广州竞��� https://p.iptv200.com/play.php?token=kesawnF1V743OOR9S6Ei6z09&tid=ty&id=20
|
||||
OK CCTV风云足球 https://p.iptv200.com/play.php?token=Ju%2Flw0dy5hIQC4Z4X2DecAqs&tid=ty&id=21&p=0
|
||||
OK 魅力足球 https://p.iptv200.com/play.php?token=2CaMJIyb6AsQVIt5dM8sh3bh&tid=ty&id=22&p=0
|
||||
OK 緯來體育台 https://p.iptv200.com/play.php?token=pE4HZSXaLP8Cy9k9cLY4mwIz&tid=ty&id=23&p=0&type=.flv
|
||||
OK ELEVEN 1(DAZN 1) https://p.iptv200.com/play.php?token=OzqIPyd%2BL7W986pYbGRDIImh&tid=ty&id=24&p=0&type=.flv
|
||||
OK ELEVEN 2(DAZN 2) https://p.iptv200.com/play.php?token=11p%2FIPtewrCkCpRQcbmIDHvk&tid=ty&id=25&p=0&type=.flv
|
||||
OK 爱尔达体育1台 https://p.iptv200.com/play.php?token=Ujr65TjdPRHIP5djvZ2zT%2B94&tid=ty&id=28&p=0&type=.flv
|
||||
OK 爱尔达体育2台 https://p.iptv200.com/play.php?token=elPzUzi1HC1bTDqK%2FDcJlYxi&tid=ty&id=29&p=0&type=.flv
|
||||
OK CCTV1综合 https://p.iptv200.com/play.php?token=OQemSJcbpY%2FyaUIgZF3s9ogi&tid=ys&id=1&p=0&type=.flv
|
||||
OK CCTV2财经 https://p.iptv200.com/play.php?token=7gk7X8Ayw%2By%2B2OGv7kSyGUng&tid=ys&id=2&p=0&type=.flv
|
||||
OK CCTV3综艺 https://p.iptv200.com/play.php?token=L2EHzlRFzS%2BYd850HTfoX3oM&tid=ys&id=3&p=0&type=.flv
|
||||
OK CCTV4中文国际 https://p.iptv200.com/play.php?token=KHjS%2BR4tgk7nEFEyfIfyQDpl&tid=ys&id=4&p=0&type=.flv
|
||||
OK CCTV5体育 https://p.iptv200.com/play.php?token=wD7rhSr5%2BS2DbtUoUn%2FTiwtm&tid=ys&id=5&p=0&type=.flv
|
||||
OK CCTV5+体育赛事 https://p.iptv200.com/play.php?token=65qROx3f9KPH8hrcpoY4%2Ftg%2F&tid=ys&id=6&p=0&type=.flv
|
||||
OK CCTV6电影 https://p.iptv200.com/play.php?token=9BZTWpDS6Hhvv%2BugiONSj9Tu&tid=ys&id=7&p=0&type=.flv
|
||||
OK CCTV7国防军事 https://p.iptv200.com/play.php?token=nMQFs5SJw2nsBTEX%2FrqGym%2FX&tid=ys&id=8&p=0&type=.flv
|
||||
OK CCTV8电视剧 https://p.iptv200.com/play.php?token=xdk01Uuxjs6tdzaD9N1f8onI&tid=ys&id=9&p=0&type=.flv
|
||||
OK CCTV9纪录 https://p.iptv200.com/play.php?token=fAyqRqDMvuSi3iESgDMv9Jub&tid=ys&id=10&p=0&type=.flv
|
||||
ERR CCTV10科教 timeout
|
||||
OK CCTV11戏曲 https://p.iptv200.com/play.php?token=JnmOgg0yWhaxfOdIyYpnvD8J&tid=ys&id=12&p=0&type=.flv
|
||||
OK CCTV12社会与法 https://p.iptv200.com/play.php?token=0w%2F6bM3TiyL0fmgGX8l8Ij%2B6&tid=ys&id=13&p=0&type=.flv
|
||||
OK CCTV13新闻 https://p.iptv200.com/play.php?token=EQYahN1YJI9S6is2Zdkav%2Bzw&tid=ys&id=14&p=0&type=.flv
|
||||
OK CCTV14少儿 https://p.iptv200.com/play.php?token=JcbkXaKHv5im0q8%2Fjrqpdj7a&tid=ys&id=15&p=0&type=.flv
|
||||
OK CCTV15音乐 https://p.iptv200.com/play.php?token=bfMCNoMJBywoKDPIpsDj1M9L&tid=ys&id=16&p=0&type=.flv
|
||||
OK CCTV16奥林匹克 https://p.iptv200.com/play.php?token=Jv2DDFpwtah3w9X%2FdF5DuipM&tid=ys&id=17&p=0&type=.flv
|
||||
OK CCTV17农业农村 https://p.iptv200.com/play.php?token=Ji0NdwPtaVPwLCl5o%2BiZUGlG&tid=ys&id=18&p=0&type=.flv
|
||||
OK CCTV4k高清 https://p.iptv200.com/play.php?token=Xea%2F%2FrEKUojM1%2BV3683IZeQv&tid=ys&id=19&p=0
|
||||
OK CCTV8k高清 https://p.iptv200.com/play.php?token=Xb2RminN5gihcqyVQAnJ%2BNGp&tid=ys&id=20&p=0
|
||||
OK CCTV4欧洲HD https://p.iptv200.com/play.php?token=OLFNHMBheDsRQZgHkK02qvc6&tid=ys&id=21&p=0
|
||||
ERR CCTV4美洲HD timeout
|
||||
OK CGTN https://p.iptv200.com/play.php?token=BheqPbI8MNZybJo%2FHGc24NIa&tid=ys&id=23&p=0
|
||||
OK CGTN纪录 https://p.iptv200.com/play.php?token=W%2FoRZRyoyI6VaxsxoqVAK4Pe&tid=ys&id=24&p=0
|
||||
OK CCTV阿拉伯语 https://p.iptv200.com/play.php?token=2BZrUPTHmGrH8Ar%2Bc7jgXTMv&tid=ys&id=25&p=0
|
||||
OK CCTV法语 https://p.iptv200.com/play.php?token=VpONv0o9refyRmGKluLbhaBT&tid=ys&id=26&p=0
|
||||
OK CCTV西班牙语 https://p.iptv200.com/play.php?token=pPF%2BV8g%2BgeY25hjOXR6qWt%2F%2B&tid=ys&id=27&p=0
|
||||
OK CCTV俄语 https://p.iptv200.com/play.php?token=5gSY1nRMLxgyYsvByZaFePXr&tid=ys&id=28&p=0
|
||||
OK CETV-1 https://p.iptv200.com/play.php?token=bL2111RRoqi%2F5P7lQYgKBe9X&tid=ys&id=29&p=0
|
||||
OK CETV-2 https://p.iptv200.com/play.php?token=5xS45O3uWL56jnSxwiY%2BneB5&tid=ys&id=30&p=0
|
||||
OK CETV-3 https://p.iptv200.com/play.php?token=vDOHFWxrHTf5%2FJInbeO84DG4&tid=ys&id=31&p=0
|
||||
OK CETV-4 https://p.iptv200.com/play.php?token=My34uHMpzXnMmoc33SXDixRD&tid=ys&id=32&p=0
|
||||
OK CCTV兵器科技 https://p.iptv200.com/play.php?token=KX3OtZts1UQ%2FNGKgRi27fGVx&tid=ys&id=33&p=0
|
||||
OK CCTV怀旧剧场 https://p.iptv200.com/play.php?token=0bxE2kqpJLextV8qc8ju8ya%2B&tid=ys&id=34&p=0
|
||||
OK CCTV第一剧场 https://p.iptv200.com/play.php?token=uJDjzrV5uS3GGrRWwzzZ7gjX&tid=ys&id=35&p=0
|
||||
OK CCTV风云剧场 https://p.iptv200.com/play.php?token=2mt69Pu8tsMrR49%2FoavFzW0l&tid=ys&id=36&p=0
|
||||
OK CCTV风云音乐 https://p.iptv200.com/play.php?token=JAY6vH78qf9ie7m3YkUaO6Wn&tid=ys&id=37&p=0
|
||||
OK CCTV风云足球 https://p.iptv200.com/play.php?token=SpiJ8rLgqwS%2BPMZzTBHEc2V1&tid=ys&id=38&p=0
|
||||
OK CCTV世界地理 https://p.iptv200.com/play.php?token=eKLSOV%2B02bsvG7%2FhZ8Gth3pr&tid=ys&id=39&p=0
|
||||
OK CCTV文化精品 https://p.iptv200.com/play.php?token=gXlLR3LnV0Hla57NQ7hCGHZ0&tid=ys&id=40&p=0
|
||||
OK CCTV央视台球 https://p.iptv200.com/play.php?token=B3NGGt7%2B0w7YnWrw96Iz3aaL&tid=ys&id=41&p=0
|
||||
OK CCTV高尔夫网球 https://p.iptv200.com/play.php?token=iMajD8SHylSsgDDhS0njPhO9&tid=ys&id=42&p=0
|
||||
OK CCTV女性时尚 https://p.iptv200.com/play.php?token=M%2BUZcxguzr%2FEsnK9cZTV1gT%2F&tid=ys&id=43&p=0
|
||||
OK 湖南卫视 https://p.iptv200.com/play.php?token=AfhXLHDBQKjvet8nDo%2FxGg9c&tid=ws&id=1&p=0&type=.flv
|
||||
OK 江苏卫视 https://p.iptv200.com/play.php?token=fsV5wF1Le3bclFruiHN6iNjd&tid=ws&id=2&p=0&type=.flv
|
||||
OK 浙江卫视 https://p.iptv200.com/play.php?token=BDMADR%2BQ7Hm4rYjoqnz0Ndvn&tid=ws&id=3&p=0&type=.flv
|
||||
OK 东方卫视 https://p.iptv200.com/play.php?token=0Mbn02DZxzMU6zSCTLcgJR%2Bx&tid=ws&id=4&p=0&type=.flv
|
||||
OK 北京卫视 https://p.iptv200.com/play.php?token=8pYbcW%2FaBEX3o6CXofe84DR7&tid=ws&id=5&p=0&type=.flv
|
||||
OK 深圳卫视 https://p.iptv200.com/play.php?token=2y5tY28ju2Ui2It3H40DYVmc&tid=ws&id=6&p=0&type=.flv
|
||||
OK 广东卫视 https://p.iptv200.com/play.php?token=lndegIbEvroxMzoCEf1pW9YQ&tid=ws&id=7&p=0&type=.flv
|
||||
OK 安徽卫视 https://p.iptv200.com/play.php?token=dYiHa%2FWq7upAeJqWQhf7cnaK&tid=ws&id=8&p=0&type=.flv
|
||||
OK 东南卫视 https://p.iptv200.com/play.php?token=tJRQ64OuSo3lkR4wKOFl62zM&tid=ws&id=9&p=0&type=.flv
|
||||
OK 河北卫视 https://p.iptv200.com/play.php?token=hnVVKoPwvevIJKBPl9PpX3Au&tid=ws&id=10&p=0
|
||||
OK 黑龙江卫视 https://p.iptv200.com/play.php?token=b%2FCmX5if27ckgtfCuWeTf7Rn&tid=ws&id=11&p=0&type=.flv
|
||||
OK 湖北卫视 https://p.iptv200.com/play.php?token=3JOFvU34SLUhFGcSwVmdgx%2FS&tid=ws&id=12&p=0&type=.flv
|
||||
OK 江西卫视 https://p.iptv200.com/play.php?token=RBfW%2FK7Qw4qgZifLFba940F1&tid=ws&id=13&p=0&type=.flv
|
||||
OK 辽宁卫视 https://p.iptv200.com/play.php?token=rmZhMOb3I4mKI9ESTd8%2BUqU7&tid=ws&id=14&p=0&type=.flv
|
||||
OK 海南卫视 https://p.iptv200.com/play.php?token=EmzMR%2BPtA%2FI2OMid6CbF5EGI&tid=ws&id=15&p=0
|
||||
OK 山东卫视 https://p.iptv200.com/play.php?token=gj8m5DEgL5cDpbizKDoSDH46&tid=ws&id=16&p=0&type=.flv
|
||||
OK 四川卫视 https://p.iptv200.com/play.php?token=MXiVLHS8Hpqp3dMP6eSOIXmF&tid=ws&id=17&p=0
|
||||
ERR 天津卫视 timeout
|
||||
OK 重庆卫视 https://p.iptv200.com/play.php?token=HMdAii2u4Q%2Fdp11PWYWmRVsK&tid=ws&id=19&p=0&type=.flv
|
||||
OK 贵州卫视 https://p.iptv200.com/play.php?token=l7BNe48fC31%2BgvKaC5pVZPGt&tid=ws&id=20&p=0
|
||||
OK 吉林卫视 https://p.iptv200.com/play.php?token=ke%2F%2FSik%2B%2FT31Mt%2FBeJ4Dq3Xq&tid=ws&id=21&p=0
|
||||
OK 广西卫视 https://p.iptv200.com/play.php?token=Sle4OoFPVWqN3n6QMfoxaoSv&tid=ws&id=22&p=0
|
||||
OK 河南卫视 https://p.iptv200.com/play.php?token=NWvj2AdEAPb7r6GeBUpiW5l5&tid=ws&id=23&p=0
|
||||
ERR 甘肃卫视 timeout
|
||||
OK 青海卫视 https://p.iptv200.com/play.php?token=vdDSebq0sKhbTEfc9rgs4KU5&tid=ws&id=25&p=0
|
||||
OK 云南卫视 https://p.iptv200.com/play.php?token=TFOjRkd2kDtrH1kA4KBGzgZV&tid=ws&id=26&p=0
|
||||
OK 内蒙古卫视 https://p.iptv200.com/play.php?token=NX%2BDJcAWE3kDcuJ%2FpQankKGx&tid=ws&id=27&p=0
|
||||
OK 山西卫视 https://p.iptv200.com/play.php?token=jNc0Pd7dM4W3d5cUMNUlfOLM&tid=ws&id=28&p=0
|
||||
OK 陕西卫视 https://p.iptv200.com/play.php?token=IJafQ1bx1%2B8PgHKpSkVxp4WJ&tid=ws&id=29&p=0
|
||||
ERR 兵团卫视 timeout
|
||||
OK 新疆卫视 https://p.iptv200.com/play.php?token=tjTBEXsEcjpZWz77aYmBIdwo&tid=ws&id=31&p=0
|
||||
OK 西藏卫视 https://p.iptv200.com/play.php?token=NuE19Kq6rYusP7niRnqo6rt5&tid=ws&id=32&p=0
|
||||
OK 宁夏卫视 https://p.iptv200.com/play.php?token=mGoxkP9M1%2FG0iuyzVVlxBylg&tid=ws&id=33&p=0
|
||||
OK 延边卫视 https://p.iptv200.com/play.php?token=Q6J2zz5DsYfVEFuMakECdoqp&tid=ws&id=34&p=0
|
||||
OK 康巴卫视 https://p.iptv200.com/play.php?token=DCtNRsZGstT2f8nYIi5lfbhN&tid=ws&id=35&p=0
|
||||
OK 大湾区卫视 https://p.iptv200.com/play.php?token=fJyD79X%2FBEDuZgJBvNg%2BH8lF&tid=ws&id=36&p=0
|
||||
OK 广东珠江频道 https://p.iptv200.com/play.php?token=TYVpK810j1jC%2Bv0D8m%2B49XlN&tid=ws&id=37&p=0&type=.flv
|
||||
OK 厦门卫视 https://p.iptv200.com/play.php?token=ck4tVMxZoOOFlMtFU6dobYXT&tid=ws&id=38
|
||||
OK 安多卫视 https://p.iptv200.com/play.php?token=mmLH%2FDT2XaZJDaQ9y0uMTM7v&tid=ws&id=39
|
||||
OK 农林卫视 https://p.iptv200.com/play.php?token=Y9ZOh%2FldUzJw9fPhTJn%2FlISB&tid=ws&id=40
|
||||
OK 三沙卫视 https://p.iptv200.com/play.php?token=pg3cwHrhY75tmIwWu3n7ajQk&tid=ws&id=41
|
||||
OK 凤凰卫视中文台 https://p.iptv200.com/play.php?token=Nq55Qf%2BdiaBWMGNPKBS1HEFf&tid=gt&id=1&p=0&type=.flv
|
||||
OK 凤凰卫视资讯台 https://p.iptv200.com/play.php?token=SvYml3egKKYSQQYcmsFBg4Tg&tid=gt&id=2&p=0&type=.flv
|
||||
OK 凤凰卫视香港台 https://p.iptv200.com/play.php?token=n4IlUpC75P1NL57Sm0P1b4ZV&tid=gt&id=3&p=0&type=.flv
|
||||
OK TVB翡翠台 https://p.iptv200.com/play.php?token=cOpKwymv342yI1ybwOBXRY3y&tid=gt&id=5&p=0&type=.flv
|
||||
OK TVB无线新闻台 https://p.iptv200.com/play.php?token=snxXIrovLDySXKYrrmka%2FXXQ&tid=gt&id=6&p=0&type=.flv
|
||||
OK TVB Plus https://p.iptv200.com/play.php?token=kdTJAZiuZU%2BpVpRL4iNO1%2FDQ&tid=gt&id=8&p=0&type=.flv
|
||||
OK TVB明珠台 https://p.iptv200.com/play.php?token=7kbbyQFR0hlq6JyV0z%2B9b1Kv&tid=gt&id=9&p=0&type=.flv
|
||||
OK 香港有线新闻台 https://p.iptv200.com/play.php?token=tfQVuDs7qKpcSTAKedbTtkFv&tid=gt&id=11&p=0&type=.flv
|
||||
OK 香港有线18台 https://p.iptv200.com/play.php?token=0wK9NtNq10L4qSYU7ij1g2R%2B&tid=gt&id=12&p=0&type=.flv
|
||||
OK 香港有线18台(备) https://p.iptv200.com/play.php?token=QUfW5A5NLIxy1JJwtLrLZHQ%2B&tid=gt&id=13&p=0&type=.flv
|
||||
OK NOW新闻台 https://p.iptv200.com/play.php?token=khPINXLkHBgSFlYEBL0oDOxq&tid=gt&id=17&p=0&type=.flv
|
||||
OK NOW财经台 https://p.iptv200.com/play.php?token=RQLqonuyq%2FduONhocbHXH83h&tid=gt&id=18&p=0&type=.flv
|
||||
OK ViuTV https://p.iptv200.com/play.php?token=3qy19ESWD5%2BzJlfqfYpjcAyP&tid=gt&id=19&p=0&type=.flv
|
||||
OK ViuTV6 https://p.iptv200.com/play.php?token=JN4vOUotpJPsL1Roe2efSL0C&tid=gt&id=20&p=0&type=.flv
|
||||
OK HOY国际财经台 https://p.iptv200.com/play.php?token=APA69Hs9z2MPEmAUrl%2F6EseE&tid=gt&id=22&p=0&type=.flv
|
||||
OK HOY资讯台 https://p.iptv200.com/play.php?token=oQ%2FRCTEuiP2Zo%2BVhAErXsJFQ&tid=gt&id=23&p=0&type=.flv
|
||||
OK HOY TV https://p.iptv200.com/play.php?token=DJG5u%2BG9QcsypHQAMo4R4Ei3&tid=gt&id=24&p=0&type=.flv
|
||||
OK 港台电视31 https://p.iptv200.com/play.php?token=zANH4EOTsPP2KNLcJq%2B7%2Fqnt&tid=gt&id=26&p=0&type=.flv
|
||||
OK 港台电视32 https://p.iptv200.com/play.php?token=y5aTk5csotbtAO5DMmdESkcG&tid=gt&id=27&p=0&type=.flv
|
||||
OK 香港卫视HKS https://p.iptv200.com/play.php?token=cxmm0oIqhsAMCDRhQ%2F1bWytj&tid=gt&id=30&p=0
|
||||
OK 澳门莲花 https://p.iptv200.com/play.php?token=lSerZNkL0ofTtnn2kZZKRO1A&tid=gt&id=35&p=0
|
||||
OK 澳門卫视 https://p.iptv200.com/play.php?token=pfgNZwyBAiwvaYWl0jpjUbNt&tid=gt&id=36&p=0
|
||||
OK 澳视资讯 https://p.iptv200.com/play.php?token=kYOwRF6FQ54yJDy4F%2FBcfqYh&tid=gt&id=37&p=0
|
||||
OK 澳視澳門 https://p.iptv200.com/play.php?token=qwOxvBw7YTVbs%2BSpf0RO0GiX&tid=gt&id=38&p=0
|
||||
OK 澳門Macau https://p.iptv200.com/play.php?token=0tNDZ3N8cmDtUg8Bsyn%2FJYmS&tid=gt&id=39&p=0
|
||||
OK 澳视综艺 https://p.iptv200.com/play.php?token=0VmUkEBcHBPpF1TgwLjN7qK%2B&tid=gt&id=40&p=0
|
||||
OK 澳亚卫视 https://p.iptv200.com/play.php?token=zaKG7HRzLz4t67h1PjKdL2Td&tid=gt&id=41&p=0
|
||||
OK 中天新闻台 https://p.iptv200.com/play.php?token=2AtZ26BJoRQ499MOpRsi8Nj%2B&tid=gt&id=43&p=0&type=.flv
|
||||
OK 东森新闻台 https://p.iptv200.com/play.php?token=WRMZX6mWmK88Pt%2BmFDZsVoFj&tid=gt&id=44&p=0&type=.flv
|
||||
OK 东森财经新闻台 https://p.iptv200.com/play.php?token=r4T3e99i2%2F2ftKd0xxgtp29n&tid=gt&id=45&p=0&type=.flv
|
||||
OK 中视新闻台 https://p.iptv200.com/play.php?token=s2MgidwMzYhStqmyls%2F1ckrv&tid=gt&id=46&p=0&type=.flv
|
||||
OK 台视新闻台 https://p.iptv200.com/play.php?token=7BUXEkOu8l%2BzwYPI2HHAhe1n&tid=gt&id=47&p=0&type=.flv
|
||||
OK 三立新闻台 https://p.iptv200.com/play.php?token=nTdKaJNel15QoW7jMku4ykn4&tid=gt&id=48&p=0&type=.flv
|
||||
OK 非凡新闻 https://p.iptv200.com/play.php?token=PexSrjJY3yAzQqfUv1czsOE8&tid=gt&id=49&p=0&type=.flv
|
||||
OK 民视新闻台 https://p.iptv200.com/play.php?token=ybYMk0sjxtnEjXzK8UjwLxMo&tid=gt&id=50&p=0&type=.flv
|
||||
OK 华视新闻台 https://p.iptv200.com/play.php?token=6016ovhDGhuoecLjVUw%2FDZyY&tid=gt&id=51&p=0&type=.flv
|
||||
OK 镜电视新闻台 https://p.iptv200.com/play.php?token=s60yR0aYecSOVIdbBKbmObFE&tid=gt&id=52&p=0&type=.flv
|
||||
OK 寰宇新闻台 https://p.iptv200.com/play.php?token=0x9OeJxj7ari1NTcxZJMXD7N&tid=gt&id=53&p=0&type=.flv
|
||||
OK 寰宇新闻台湾台 https://p.iptv200.com/play.php?token=v9JFcX7RJCjoh2jvBHGuT7jv&tid=gt&id=54&p=0&type=.flv
|
||||
OK TVBS新闻台 https://p.iptv200.com/play.php?token=Ya0WqN3Tv7hjIBukDQnZP5FP&tid=gt&id=55&p=0&type=.flv
|
||||
OK TVBS HD https://p.iptv200.com/play.php?token=6smcgLK9hFHlBGwb852u1aPz&tid=gt&id=56&p=0&type=.flv
|
||||
OK 中视HD https://p.iptv200.com/play.php?token=haJ6qBZD4sHWBewY1aFF8cHa&tid=gt&id=60&p=0&type=.flv
|
||||
OK 台视HD https://p.iptv200.com/play.php?token=sZnzKYLfmxhBGch9pYD9is9r&tid=gt&id=61&p=0&type=.flv
|
||||
OK 民视HD https://p.iptv200.com/play.php?token=MHgdMa6OaVZhMEW0IYulmtfj&tid=gt&id=62&p=0&type=.flv
|
||||
OK 华视HD https://p.iptv200.com/play.php?token=j%2BkQRz%2Fr2eXIaE1yKYxdm%2Bg8&tid=gt&id=63&p=0&type=.flv
|
||||
OK 緯來體育台 https://p.iptv200.com/play.php?token=JELqYDHzVse3W%2BB6doN%2FsQo6&tid=gt&id=73&p=0&type=.flv
|
||||
OK 纬来日本台 https://p.iptv200.com/play.php?token=2byEB4qtSRb%2B8jK2jVx7MB6h&tid=gt&id=74&p=0&type=.flv
|
||||
OK 纬来育乐台 https://p.iptv200.com/play.php?token=cjoB6d8vHLLDPWf%2Fnbl%2FFRwN&tid=gt&id=75&p=0&type=.flv
|
||||
OK ELEVEN 1(DAZN 1) https://p.iptv200.com/play.php?token=GX5c4HF2f3mv%2BsFr69ekRUTX&tid=gt&id=76&p=0&type=.flv
|
||||
OK ELEVEN 2(DAZN 2) https://p.iptv200.com/play.php?token=acnZabzWhg43ksaLL9T4XJH0&tid=gt&id=77&p=0&type=.flv
|
||||
OK 爱尔达体育1台 https://p.iptv200.com/play.php?token=W%2FRjPt5Tb57FY0S85ayv0rJT&tid=gt&id=79&p=0&type=.flv
|
||||
OK 爱尔达体育2台 https://p.iptv200.com/play.php?token=V6eIy5KGtDe%2B5LsV64jQIFlC&tid=gt&id=80&p=0&type=.flv
|
||||
@@ -0,0 +1,93 @@
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
const UA = 'Mozilla/5.0 (Linux; Android 12; SM-S901B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36';
|
||||
function fetch(url) {
|
||||
return new Promise((res, rej) => {
|
||||
const h = url.startsWith('https') ? https : http;
|
||||
const req = h.get(url, { headers: { 'User-Agent': UA } }, r => {
|
||||
let d = '';
|
||||
r.on('data', c => d += c);
|
||||
r.on('end', () => res(d));
|
||||
});
|
||||
req.on('error', rej);
|
||||
req.setTimeout(15000, () => { req.destroy(); rej(new Error('timeout')); });
|
||||
});
|
||||
}
|
||||
// 动态eval加密变量,再按解密函数链跑(bbot: page uses randomly-named vars each request)
|
||||
function decodePage(html) {
|
||||
// 提取 var xx=""; var yy="...拼接..."; var qq=""; var ww="..."; ouyit/token块
|
||||
const blocks = [];
|
||||
const re = /<script>([\s\S]*?)<\/script>/g;
|
||||
let m;
|
||||
while ((m = re.exec(html))) {
|
||||
const body = m[1];
|
||||
if (body.includes('.split("")') && body.includes('var')) blocks.push(body);
|
||||
if (body.includes('function ') && body.includes('keyStr') && body.includes('charCodeAt')) blocks.push(body);
|
||||
if (body.includes('wngbe') || /function \w+\(dek\)/.test(body)) blocks.push(body);
|
||||
}
|
||||
const context = {};
|
||||
const sandbox = new Function(blocks.join('\n') + `
|
||||
// 返回解密函数名 + 猜出变量名
|
||||
const fnNames = [];
|
||||
// 找形如 function XXXX(dek){...} 就把名字报出来
|
||||
const decls = [...arguments.callee ? '' : ''][0];
|
||||
return { xefgv: typeof xefgv !== 'undefined' ? xefgv : null, ouyit: typeof ouyit !== 'undefined' ? ouyit : null, qrtkk: typeof qrtkk !== 'undefined' ? qrtkk : null };
|
||||
`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 更简单:直接在Node vm里跑整段,把全部函数和变量都留在沙箱里
|
||||
import vm from 'vm';
|
||||
async function processPage(url) {
|
||||
const html = await fetch(url);
|
||||
const ctx = { window: {}, atob: null };
|
||||
const sandbox = vm.createContext({ window: {}, document: { getElementById: () => null, querySelectorAll: () => [], querySelector: () => null, addEventListener(){}, createElement() { return { setAttribute(){}, style:{} }; }, body: { appendChild(){} } }, location: { href: url }, navigator: { userAgent: UA }, setInterval: () => 0, clearInterval(){}, setTimeout: () => 0, clearTimeout(){}, fetch: () => {}, console, addEventListener(){}, $: () => ({ hide(){}, show(){}, selectmenu(){}, length:0 }), Hls: { isSupported: () => false, Events: {} }, mpegts: { isSupported: () => false, Events: {} }, devicePixelRatio: 2, innerWidth: 1280, innerHeight: 720, atob: (s) => Buffer.from(s, 'base64').toString('binary') });
|
||||
// 只跑不依赖DOM交互的脚本:解密脚本+变量, crib main script(避免动画循环定时器问题——用vm.runInContext就够了,主脚本里$()等没被调用)
|
||||
const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map(m => m[1]);
|
||||
for (const s of scripts) {
|
||||
try { vm.runInContext(s, sandbox, { timeout: 3000 }); } catch (e) { /* DOM依赖的忽略 */ }
|
||||
}
|
||||
// 抓options
|
||||
const opts = [...html.matchAll(/<option value="([^"]+)">([^<]+)<\/option>/g)];
|
||||
// 找到call_in_main
|
||||
const main = scripts.find(s => s.includes('startPlayer') && s.includes('(uri)'));
|
||||
const decName = (main && (main.match(/const \w+ = (\w+)\(uri\)/) || [])[1]) || (main && (main.match(/const puri ?= ?(\w+)\(/) || [])[1]);
|
||||
if (!decName) return { url, error: 'no dec fn' };
|
||||
const results = opts.map(o => {
|
||||
try { return { line: o[2], url: sandbox[decName](o[1]) }; }
|
||||
catch (e) { return { line: o[2], error: String(e).slice(0,100) }; }
|
||||
});
|
||||
return { url, title: (html.match(/<title>([^<]*)</) || [])[1], results };
|
||||
}
|
||||
|
||||
// 频道列表:从各分类页抓全部链接(data-ajax="false")
|
||||
const tids = ['tv', 'ty', 'ys', 'ws', 'gt'];
|
||||
const seen = new Map(); // link -> name(按tid顺序去重)
|
||||
for (const tid of tids) {
|
||||
try {
|
||||
const page = await fetch('https://m.iptv807.com/?tid=' + tid);
|
||||
for (const m of page.matchAll(/<a href="(\?act=play[^"]*)"[^>]*>([^<]+)<\/a>/g)) {
|
||||
const link = 'https://m.iptv807.com/' + m[1];
|
||||
if (!seen.has(link)) seen.set(link, m[2]);
|
||||
}
|
||||
} catch (e) { console.error('cat fail', tid, e.message); }
|
||||
}
|
||||
const channels = [...seen.entries()].map(([link, name]) => ({ link, name }));
|
||||
// 去掉首页广告位那条假频道
|
||||
const real = channels.filter(c => c.link.includes('&id='));
|
||||
console.error('channels found:', channels.length);
|
||||
|
||||
const out = [];
|
||||
for (const ch of real) {
|
||||
try {
|
||||
const r = await processPage(ch.link);
|
||||
out.push({ name: ch.name, url: r.url, lines: r.results?.map(l => l.url) });
|
||||
console.error('OK', ch.name, r.results?.[0]?.url || r.error);
|
||||
} catch (e) {
|
||||
out.push({ name: ch.name, error: e.message });
|
||||
console.error('ERR', ch.name, e.message);
|
||||
}
|
||||
}
|
||||
// 输出 JSON 到stdout
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
@@ -0,0 +1,41 @@
|
||||
import re
|
||||
|
||||
# Check Kotlin source resolver for same bug + apply v4-style fix:
|
||||
src_file = '/vol1/1000/HD1/APP/iptv-tv-app/app/src/main/java/com/iptv807/tv/SourceResolver.kt'
|
||||
s = open(src_file).read()
|
||||
|
||||
# 拿出当前 evalConcat 部分
|
||||
anchor = ''' if (s[i] == '"') { // 处理字符串字面量
|
||||
val close = s.indexOf('"', i + 1)
|
||||
if (close < 0) break
|
||||
val lit = s.substring(i + 1, close)
|
||||
val tail = s.substring((close + 1).coerceAtMost(s.length))
|
||||
val isRev = tail.startsWith(".split(\\"\\").reverse().join(\\"\\")")
|
||||
sb.append(if (isRev) lit.reversed() else lit)
|
||||
i = if (isRev) close + 1 + ".split(\\"\\").reverse().join(\\"\\")".length else close + 1
|
||||
} else i++
|
||||
}'''
|
||||
|
||||
new = ''' if (s[i] == '"') {
|
||||
var j = i + 1; var lit = ""
|
||||
while (j < s.length && s[j] != '"') { lit += s[j]; j++ }
|
||||
if (j >= s.length) {
|
||||
sb.append(lit)
|
||||
i = s.length
|
||||
} else {
|
||||
val REV = ".split(\\"\\").reverse().join(\\"\\")"
|
||||
val tail = s.substring((j+1).coerceAtMost(s.length))
|
||||
if (tail.startsWith(REV)) {
|
||||
sb.append(lit.reversed())
|
||||
i = j + 1 + REV.length
|
||||
} else {
|
||||
sb.append(lit)
|
||||
i = j + 1
|
||||
}
|
||||
while (i < s.length && (s[i] == '+' || s[i] == ' ')) i++
|
||||
}
|
||||
} else i++'''
|
||||
assert anchor in s, 'anchor missing'
|
||||
s = s.replace(anchor, new, 1)
|
||||
open(src_file, 'w').write(s)
|
||||
print('kotlin eval fixed')
|
||||
@@ -0,0 +1,59 @@
|
||||
# 修 Kotlin evalConcat — 用 Python 里的 v4 版本的精确 Kotlin 翻译替换
|
||||
src_file = '/vol1/1000/HD1/APP/iptv-tv-app/app/src/main/java/com/iptv807/tv/SourceResolver.kt'
|
||||
s = open(src_file).read()
|
||||
|
||||
old_fn = ''' fun evalConcat(s: String): String = StringBuilder().also { sb ->
|
||||
var i = 0
|
||||
while (i < s.length) {
|
||||
if (s[i] == '"') { // 处理字符串字面量
|
||||
val close = s.indexOf('"', i + 1)
|
||||
if (close < 0) break
|
||||
val lit = s.substring(i + 1, close)
|
||||
val tail = s.substring((close + 1).coerceAtMost(s.length))
|
||||
val isRev = tail.startsWith(".split(\\\\"\\\\").reverse().join(\\\\"\\\\")")
|
||||
sb.append(if (isRev) lit.reversed() else lit)
|
||||
i = if (isRev) close + 1 + ".split(\\\\"\\\\").reverse().join(\\\\"\\\\")".length else close + 1
|
||||
} else i++
|
||||
}
|
||||
}.toString()'''
|
||||
|
||||
new_fn = ''' fun evalConcat(s: String): String = StringBuilder().also { sb ->
|
||||
val REV = ".split(\\\\"\\\\").reverse().join(\\\\"\\\\")"
|
||||
var i = 0
|
||||
while (i < s.length) {
|
||||
if (s[i] == '"') {
|
||||
var j = i + 1
|
||||
var lit = ""
|
||||
while (j < s.length && s[j] != '"') { lit += s[j]; j++ }
|
||||
if (j >= s.length) {
|
||||
sb.append(lit)
|
||||
i = s.length
|
||||
} else {
|
||||
val tail = s.substring((j + 1).coerceAtMost(s.length))
|
||||
if (tail.startsWith(REV)) {
|
||||
sb.append(lit.reversed())
|
||||
i = j + 1 + REV.length
|
||||
} else {
|
||||
sb.append(lit)
|
||||
i = j + 1
|
||||
}
|
||||
while (i < s.length && (s[i] == '+' || s[i] == ' ')) i++
|
||||
}
|
||||
} else i++
|
||||
}
|
||||
}.toString()'''
|
||||
|
||||
if old_fn in s:
|
||||
s = s.replace(old_fn, new_fn, 1)
|
||||
open(src_file, 'w').write(s)
|
||||
print('REPLACED OK')
|
||||
else:
|
||||
print('old_fn NOT FOUND, doing alternative approach')
|
||||
# Fallback: locate the evalConcat definition and replace all lines
|
||||
import re
|
||||
m = re.search(r'fun evalConcat\(s: String\): String = StringBuilder\(\)\.also \{ sb ->[\s\S]*?\}\.toString\(\)', s)
|
||||
if m:
|
||||
print('found block len:', len(m.group(0)))
|
||||
s = s.replace(m.group(0), new_fn.strip(' '))
|
||||
open(src_file, 'w').write(s)
|
||||
print('REPLACED via regex')
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""补丁v2: my previous patch added a stray '}\n\n} else {' —— 移除多余的 '}'"""
|
||||
import sys
|
||||
P = sys.argv[1]
|
||||
src = open(P).read()
|
||||
bad = """ }
|
||||
}
|
||||
|
||||
} else {"""
|
||||
good = """ }
|
||||
} else {"""
|
||||
assert bad in src, 'stray not found'
|
||||
src = src.replace(bad, good, 1)
|
||||
open(P, 'w').write(src)
|
||||
print('FIXED')
|
||||
@@ -0,0 +1,9 @@
|
||||
# 修正 Kotlin 字符串引号转义 (REV 应该是 ".split(\"\").reverse().join(\"\")")
|
||||
src_file = '/vol1/1000/HD1/APP/iptv-tv-app/app/src/main/java/com/iptv807/tv/SourceResolver.kt'
|
||||
s = open(src_file).read()
|
||||
|
||||
wrong = 'val REV = ".split(\\\\"\\\\").reverse().join(\\\\"\\\\")"'
|
||||
right = 'val REV = ".split(\\"\\").reverse().join(\\"\\")"'
|
||||
s = s.replace(wrong, right)
|
||||
open(src_file, 'w').write(s)
|
||||
print('fixed REV string literal')
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CardAuth processOrderPaid 补丁:无bot_qq订单 → 自动发卡并绑定机器码"""
|
||||
import sys
|
||||
|
||||
P = sys.argv[1] if len(sys.argv) > 1 else 'OrderController.php'
|
||||
src = open(P).read()
|
||||
|
||||
anchor = """ // 更新订单状态
|
||||
$db->execute(
|
||||
"UPDATE {$db->table('orders')} SET status = 'paid', paid_at = NOW() WHERE id = ?",
|
||||
[$orderId]
|
||||
);"""
|
||||
|
||||
assert anchor in src, "anchor NOT FOUND"
|
||||
|
||||
patch = """ } else {
|
||||
// ===== 无 bot_qq 订单:自动发卡并绑定机器码(IPTV App 直购闭环)=====
|
||||
// contact_info 约定格式: "MACHINE:TV-XXXX" (App下单时带上电视机器码)
|
||||
$machineId = '';
|
||||
if (preg_match('/MACHINE:([A-Za-z0-9\\-_]+)/', $order['contact_info'] ?? '', $mm)) {
|
||||
$machineId = $mm[1];
|
||||
}
|
||||
// 从同项目同卡类型的未用卡池取一张
|
||||
$card = $db->fetch(
|
||||
"SELECT * FROM {$db->table('cards')} WHERE project_id = ? AND card_type_id = ? AND status = 'unused' ORDER BY id ASC LIMIT 1",
|
||||
[$order['project_id'], $order['card_type_id']]
|
||||
);
|
||||
if ($card) {
|
||||
$expireTime = null;
|
||||
if ($durationDays > 0) {
|
||||
$expireTime = date('Y-m-d H:i:s', strtotime("+{$durationDays} days"));
|
||||
}
|
||||
$bindInfo = json_encode([
|
||||
'machine_id' => $machineId ?: 'AUTO-' . $order['order_no'],
|
||||
'ip' => '',
|
||||
'device_info' => 'auto-delivery',
|
||||
'first_bind_at' => date('Y-m-d H:i:s'),
|
||||
'order_no' => $order['order_no'],
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
$db->execute(
|
||||
"UPDATE {$db->table('cards')} SET status = 'used', bind_info = ?, bound_at = NOW(), expire_time = ? WHERE id = ?",
|
||||
[$bindInfo, $expireTime, $card['id']]
|
||||
);
|
||||
// 订单关联该卡,查询接口可显示
|
||||
$db->execute(
|
||||
"UPDATE {$db->table('orders')} SET card_id = ? WHERE id = ?",
|
||||
[$card['id'], $orderId]
|
||||
);
|
||||
}
|
||||
// 无未用卡池时订单仍标 paid(余卡告警交给管理端)
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
$db->execute(
|
||||
"UPDATE {$db->table('orders')} SET status = 'paid', paid_at = NOW() WHERE id = ?",
|
||||
[$orderId]
|
||||
);"""
|
||||
|
||||
src = src.replace(anchor, patch, 1)
|
||||
open(P, 'w').write(src)
|
||||
print("PATCHED OK")
|
||||
@@ -0,0 +1,44 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/fresh2.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split("")' in b)
|
||||
|
||||
def eval_concat(expr):
|
||||
out = []; i = 0; REV = '.split("").reverse().join("")'
|
||||
while i < len(expr):
|
||||
if expr[i] == '"':
|
||||
close = expr.find('"', i + 1)
|
||||
if close < 0: break
|
||||
lit = expr[i+1:close]
|
||||
is_rev = expr[close+1:close+1+len(REV)+3] .startswith(REV)
|
||||
out.append(lit[::-1] if is_rev else lit)
|
||||
i = close + 1 + (len(REV) if is_rev else 0)
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
# 保护引号、按分号拆
|
||||
stmts = re.split(r';(?=(?:[^"]*"[^"]*")*[^"]*$)', mix)
|
||||
stmts = [s.strip() for s in stmts if s.strip()]
|
||||
|
||||
assign = {}
|
||||
for stmt in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"([^"]*)"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2))
|
||||
continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = ''
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), '')
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2)
|
||||
continue
|
||||
print("UNMATCHED:", stmt[:80])
|
||||
|
||||
print('assign:', assign)
|
||||
@@ -0,0 +1,68 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/fresh2.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split("")' in b)
|
||||
|
||||
def eval_concat(expr):
|
||||
out = []; i = 0; REV = '.split("").reverse().join("")'
|
||||
while i < len(expr):
|
||||
if expr[i] == '"':
|
||||
close = expr.find('"', i + 1)
|
||||
if close < 0: break
|
||||
lit = expr[i+1:close]
|
||||
is_rev = expr[close+1:close+1+len(REV)+3].startswith(REV)
|
||||
out.append(lit[::-1] if is_rev else lit)
|
||||
i = close + 1 + (len(REV) if is_rev else 0)
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
stmts = [s.strip() for s in re.split(r';(?=(?:[^"]*"[^"]*")*[^"]*$)', mix) if s.strip()]
|
||||
assign = {}
|
||||
for stmt in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2))
|
||||
continue
|
||||
if stmt.startswith('var ') and '"' in stmt:
|
||||
# greedy: parse literally from first to last quoted segment
|
||||
m2 = re.match(r'^var\s+(\w+)\s*=\s*(.*)$', stmt)
|
||||
if m2:
|
||||
name = m2.group(1)
|
||||
expr = m2.group(2).strip()
|
||||
# 找末尾最后一个引号前的内容
|
||||
if expr.count('"') >= 2:
|
||||
# last "的位置
|
||||
last = expr.rfind('"')
|
||||
expr2 = expr[:last + 1]
|
||||
assign[name] = eval_concat(expr2)
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), '')
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2)
|
||||
continue
|
||||
print('UNMATCHED:', stmt[:60])
|
||||
|
||||
print('assign:', {k: (v[:35]+'..' if len(v) > 35 else v) for k, v in assign.items()})
|
||||
|
||||
vals = list(assign.values())
|
||||
xKey = next(v for v in vals if v and not re.match(r'^[a-f0-9]{32}$', v) and v != '')
|
||||
newTok = next(v for v in vals[::-1] if v and not re.match(r'^[a-f0-9]{32}$', v) and v != xKey)
|
||||
oldToken = next(v for v in vals if v and re.match(r'^[a-f0-9]{32}$', v))
|
||||
xorConst = re.search(r'"([a-f0-9]{16})"', mix).group(1)
|
||||
print(f'xKey: {xKey!r}\nnewTok: {newTok!r}\nold: {oldToken[:8]}..\nconst: {xorConst}')
|
||||
|
||||
def b64str(s): return base64.b64decode(s.encode()).decode('latin1')
|
||||
def decrypt(enc):
|
||||
b1 = b64str(enc[::-1])
|
||||
key = xKey + xorConst
|
||||
xored = ''.join(chr((ord(b1[i]) ^ ord(key[i % len(key)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64str(xored)
|
||||
return urllib.parse.unquote(b2.replace('token=' + oldToken, 'token=' + newTok).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"', html)
|
||||
print('line1:', decrypt(opts[0])[:120])
|
||||
@@ -0,0 +1,77 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/fresh2.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split("")' in b)
|
||||
|
||||
def eval_concat(expr):
|
||||
out = []; i = 0; REV = '.split("").reverse().join("")'
|
||||
while i < len(expr):
|
||||
if expr[i] == '"':
|
||||
close = expr.find('"', i + 1)
|
||||
if close < 0: break
|
||||
lit = expr[i+1:close]
|
||||
is_rev = expr[close+1:close+1+len(REV)+3].startswith(REV)
|
||||
out.append(lit[::-1] if is_rev else lit)
|
||||
i = close + 1 + (len(REV) if is_rev else 0)
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
# 手写分号分割(考虑引号)
|
||||
stmts = []
|
||||
buf = ''
|
||||
in_str = False
|
||||
for ch in mix:
|
||||
if ch == '"':
|
||||
in_str = not in_str
|
||||
if ch == ';' and not in_str:
|
||||
stmts.append(buf.strip()); buf = ''
|
||||
else:
|
||||
buf += ch
|
||||
if buf.strip():
|
||||
stmts.append(buf.strip())
|
||||
|
||||
assign = {}
|
||||
for stmt in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2))
|
||||
continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)$', stmt) # 末尾可能无close quote
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2))
|
||||
continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = ''
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), '')
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2)
|
||||
continue
|
||||
print('UNMATCHED:', stmt[:60])
|
||||
|
||||
print('assign:', {k: (v[:40] + '..' if len(v) > 40 else v) for k, v in assign.items()})
|
||||
|
||||
xKey = assign.get('fkeco') or ''
|
||||
newTok = assign.get('uncmd') or ''
|
||||
oldToken = next((v for v in assign.values() if re.match(r'^[a-f0-9]{32}$', v)), '')
|
||||
xorConst = re.search(r'"([a-f0-9]{16})"', mix).group(1)
|
||||
|
||||
def b64str(s):
|
||||
return base64.b64decode(s.encode()).decode('latin1')
|
||||
|
||||
def decrypt(enc):
|
||||
b1 = b64str(enc[::-1])
|
||||
key = xKey + xorConst
|
||||
xored = ''.join(chr((ord(b1[i]) ^ ord(key[i % len(key)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64str(xored)
|
||||
# token替换 + 去掉key字符串
|
||||
return urllib.parse.unquote(b2.replace('token=' + oldToken, 'token=' + newTok).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"', html)
|
||||
print('line1:', decrypt(opts[0])[:130])
|
||||
@@ -0,0 +1,49 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/fresh2.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split' in b)
|
||||
|
||||
# 1) 直接 per-语句 regex
|
||||
stmts = [s.strip() for s in re.split(r';(?=(?:"[^"]*"\s*)*[^"]*$)', mix)]
|
||||
|
||||
def eval_concat(expr):
|
||||
REV = '.split("").reverse().join("")'
|
||||
out = []; i = 0
|
||||
while i < len(expr):
|
||||
if expr[i] == '"':
|
||||
close = expr.find('"', i + 1)
|
||||
if close < 0: break
|
||||
lit = expr[i+1:close]
|
||||
is_rev = expr[close+1:close+1+len(REV)].startswith(REV)
|
||||
out.append(lit[::-1] if is_rev else lit)
|
||||
i = close + 1 + (len(REV) if is_rev else 0)
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
assign = {}
|
||||
for stmt in stmts:
|
||||
if not stmt: continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2))
|
||||
continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"', stmt) # no trailing $
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2))
|
||||
continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = ''
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), '')
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2)
|
||||
continue
|
||||
print('UNMATCHED:', stmt[:50])
|
||||
|
||||
print('assign:', {k: (v[:35]+'..' if len(v) > 35 else v) for k, v in assign.items()})
|
||||
@@ -0,0 +1,82 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/fresh2.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split' in b)
|
||||
|
||||
stmts = []
|
||||
buf = ''; inside = False
|
||||
for ch in mix:
|
||||
if ch == '"':
|
||||
inside = not inside
|
||||
buf += ch
|
||||
continue
|
||||
if ch == ';' and not inside:
|
||||
stmts.append(buf.strip())
|
||||
buf = ''
|
||||
continue
|
||||
buf += ch
|
||||
if buf.strip():
|
||||
stmts.append(buf.strip())
|
||||
|
||||
def eval_concat(expr):
|
||||
REV = '.split("").reverse().join("")'
|
||||
out = []; i = 0
|
||||
while i < len(expr):
|
||||
if expr[i] == '"':
|
||||
close = expr.find('"', i + 1)
|
||||
if close < 0: break
|
||||
lit = expr[i+1:close]
|
||||
is_rev = expr[close+1:close+1+len(REV)].startswith(REV)
|
||||
out.append(lit[::-1] if is_rev else lit)
|
||||
i = close + 1 + (len(REV) if is_rev else 0)
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
assign = {}
|
||||
for stmt in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2))
|
||||
continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"', stmt) # no $ : allow trailing join("") suffix
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2))
|
||||
continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = ''
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), '')
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2)
|
||||
continue
|
||||
print('UNMATCHED:', stmt[:60])
|
||||
|
||||
print('assign:', {k: (v[:35] + '..' if len(v) > 35 else v) for k, v in assign.items()})
|
||||
|
||||
vals = [v for v in assign.values() if v]
|
||||
xKey = next(v for v in vals if not re.match(r'^[a-f0-9]{32}$', v) and not v.startswith('%'))
|
||||
# newTok: also starts with % (encoded) — find it
|
||||
newTok = next(v for v in vals if v.startswith('%'))
|
||||
oldToken = next(v for v in vals if re.match(r'^[a-f0-9]{32}$', v))
|
||||
xorConst = re.search(r'"([a-f0-9]{16})"', mix).group(1)
|
||||
print(f'xKey={xKey}\nnewTok={newTok}\nold={oldToken[:10]}..\nconst={xorConst}')
|
||||
|
||||
def b64str(s):
|
||||
return base64.b64decode(s.encode()).decode('latin1')
|
||||
|
||||
def decrypt(enc):
|
||||
b1 = b64str(enc[::-1])
|
||||
key = xKey + xorConst
|
||||
x = ''.join(chr((ord(b1[i]) ^ ord(key[i % len(key)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64str(x)
|
||||
# NOTE: 服务端只做 token=old→token=new 的字符串替换和 xKey "???"拼接剥离
|
||||
return urllib.parse.unquote(b2.replace('token=' + oldToken, 'token=' + newTok).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"', html)
|
||||
print('line1:', decrypt(opts[0])[:130])
|
||||
@@ -0,0 +1,88 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/fresh2.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split' in b)
|
||||
|
||||
stmts = []
|
||||
buf = ''; inside = False
|
||||
for ch in mix:
|
||||
if ch == '"':
|
||||
inside = not inside
|
||||
buf += ch
|
||||
continue
|
||||
if ch == ';' and not inside:
|
||||
stmts.append(buf.strip())
|
||||
buf = ''
|
||||
continue
|
||||
buf += ch
|
||||
if buf.strip():
|
||||
stmts.append(buf.strip())
|
||||
|
||||
def eval_concat(expr):
|
||||
# 关键:" 拆开 — expr begins WITHOUT leading quote (它已在var assignment里被消费)
|
||||
# expr: b8"+"fb"+"f".split("").reverse().join("")+"c29e"...
|
||||
# 用 " 切:多段:['b8', '+', 'fb', '+', 'f', '.split(。'...]
|
||||
# 期望JS语义: "b8" + "fb" + "f".split("").reverse().join("") + "c29e"...
|
||||
# b8 (normal), fb (normal), f (rev单字不变), c29e... (逐段 'rev'取决于 q[i+1] 之后是否跟着 REV)
|
||||
# 重新实现:每次 分割 " 按 char,两个引号一组 literal; quote 组之间字符 `+` 连接;
|
||||
REV = '.split("").reverse().join("")'
|
||||
parts = expr.split('"')
|
||||
# parts结构: [pre, lit1, sep1, lit2, sep2, ...]
|
||||
# sep里如果以 ' + .split' 开头等 (REV后跟随的+), 判断 REV 属于前面那次
|
||||
result = []
|
||||
i = 1 # parts[0]是空的或非literal常数
|
||||
while i < len(parts):
|
||||
lit = parts[i]
|
||||
sep = parts[i+1] if i + 1 < len(parts) else ''
|
||||
if sep.startswith('+ .split') or sep.startswith('+.split'):
|
||||
result.append(lit)
|
||||
else: result.append(lit) # simplify: all normal
|
||||
i += 2
|
||||
# 但rev:
|
||||
return ''.join(result)
|
||||
|
||||
# simpler and correct: tokenize quotes pairs with ownership of next chars
|
||||
def eval_v2(expr):
|
||||
REV = '.split("").reverse().join("")'
|
||||
# find quotes pairing; check if literal is followed by REV
|
||||
result = []
|
||||
idx = 0
|
||||
while True:
|
||||
q1 = expr.find('"', idx)
|
||||
if q1 < 0: break
|
||||
q2 = expr.find('"', q1 + 1)
|
||||
if q2 < 0: break
|
||||
lit = expr[q1+1:q2]
|
||||
is_rev = expr[q2+1:q2+1+len(REV)] == REV
|
||||
result.append(lit[::-1] if is_rev else lit)
|
||||
idx = q2 + 1 + (len(REV) if is_rev else 0)
|
||||
# skip + 连接符
|
||||
while idx < len(expr) and expr[idx] in ' +.snp':
|
||||
idx += 1
|
||||
return ''.join(result)
|
||||
|
||||
assign = {}
|
||||
for stmt in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*(.*)$', stmt)
|
||||
if m:
|
||||
name = m.group(1); expr = m.group(2).strip()
|
||||
if not expr or not expr.startswith('"'):
|
||||
if expr == '':
|
||||
assign[name] = ''
|
||||
continue
|
||||
m2 = re.match(r'^"([a-f0-9]{32})"$', expr)
|
||||
if m2:
|
||||
assign[name] = m2.group(1)
|
||||
continue
|
||||
continue
|
||||
# 从expr中来(uns greedy)
|
||||
if expr.startswith('"'):
|
||||
# 内部从头到尾 pair
|
||||
assign[name] = eval_v2(expr[1:]) # strip leading "
|
||||
continue
|
||||
# 无开头引号? fallback to 正常
|
||||
assign[name] = expr
|
||||
continue
|
||||
print('un:', stmt[:40])
|
||||
|
||||
print('assign:', {k: (v[:25]+'..' if len(v) > 25 else v) for k, v in assign.items()})
|
||||
@@ -0,0 +1,84 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/fresh2.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split' in b)
|
||||
|
||||
stmts = []
|
||||
buf = ''; inside = False
|
||||
for ch in mix:
|
||||
if ch == '"':
|
||||
inside = not inside
|
||||
buf += ch
|
||||
continue
|
||||
if ch == ';' and not inside:
|
||||
stmts.append(buf.strip())
|
||||
buf = ''
|
||||
continue
|
||||
buf += ch
|
||||
if buf.strip():
|
||||
stmts.append(buf.strip())
|
||||
|
||||
def eval_v2(expr):
|
||||
REV = '.split("").reverse().join("")'
|
||||
result = []; idx = 0
|
||||
while True:
|
||||
q1 = expr.find('"', idx)
|
||||
if q1 < 0: break
|
||||
q2 = expr.find('"', q1 + 1)
|
||||
if q2 < 0: break
|
||||
lit = expr[q1+1:q2]
|
||||
is_rev = expr[q2+1:q2+1+len(REV)] == REV
|
||||
result.append(lit[::-1] if is_rev else lit)
|
||||
idx = q2 + 1 + (len(REV) if is_rev else 0)
|
||||
while idx < len(expr) and expr[idx] in ' +.srne(--,)':
|
||||
idx += 1
|
||||
return ''.join(result)
|
||||
|
||||
assign = {}
|
||||
for stmt in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*("(?:[^"]*)".*)?$', stmt)
|
||||
if m:
|
||||
name = m.group(1); expr = (m.group(2) or '').strip()
|
||||
if not expr:
|
||||
assign[name] = ''; continue
|
||||
if expr.startswith('"'):
|
||||
assign[name] = eval_v2(expr)
|
||||
continue
|
||||
# 字面量 var xx = "32hex"
|
||||
m2 = re.match(r'^"([a-f0-9]{32})"$', expr)
|
||||
if m2:
|
||||
assign[name] = m2.group(1); continue
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), '')
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2)
|
||||
continue
|
||||
print('UNMATCHED:', stmt[:60])
|
||||
|
||||
print('assign:', {k: (v[:30] + '..' if len(v) > 30 else v) for k, v in assign.items()})
|
||||
|
||||
vals = [v for v in assign.values() if v]
|
||||
xKey = next((v for v in vals if not re.match(r'^[a-f0-9]{32}$', v) and not v.startswith('%')), '')
|
||||
newTok = next((v for v in vals if v.startswith('%')), '')
|
||||
oldToken = next((v for v in vals if re.match(r'^[a-f0-9]{32}$', v)), '')
|
||||
xorConst = re.search(r'"([a-f0-9]{16})"', mix).group(1)
|
||||
print(f'xKey={xKey}\nnewTok={newTok}\nold={oldToken[:10]}.. const={xorConst}')
|
||||
|
||||
def b64str(s):
|
||||
return base64.b64decode(s.encode()).decode('latin1')
|
||||
|
||||
def decrypt(enc):
|
||||
b1 = b64str(enc[::-1])
|
||||
key = xKey + xorConst
|
||||
x = ''.join(chr((ord(b1[i]) ^ ord(key[i % len(key)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64str(x)
|
||||
return urllib.parse.unquote(b2.replace('token=' + oldToken, 'token=' + newTok).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"', html)
|
||||
if opts:
|
||||
print('line1:', decrypt(opts[0])[:130])
|
||||
print('line2:', decrypt(opts[1])[:130])
|
||||
@@ -0,0 +1,96 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/fresh2.html', encoding='utf8', errors='ignore').read()
|
||||
blocks = re.findall(r'<script>([\s\S]*?)</script>', html)
|
||||
mix = next(b for b in blocks if '.split' in b)
|
||||
|
||||
stmts = []
|
||||
buf = ''; inside = False
|
||||
for ch in mix:
|
||||
if ch == '"':
|
||||
inside = not inside
|
||||
buf += ch
|
||||
continue
|
||||
if ch == ';' and not inside:
|
||||
stmts.append(buf.strip())
|
||||
buf = ''
|
||||
continue
|
||||
buf += ch
|
||||
if buf.strip():
|
||||
stmts.append(buf.strip())
|
||||
|
||||
def eval_v2(expr):
|
||||
REV = '.split("").reverse().join("")'
|
||||
result = []; idx = 0
|
||||
while True:
|
||||
q1 = expr.find('"', idx)
|
||||
if q1 < 0: break
|
||||
q2 = expr.find('"', q1 + 1)
|
||||
if q2 < 0: break
|
||||
lit = expr[q1+1:q2]
|
||||
is_rev = expr[q2+1:q2+1+len(REV)] == REV
|
||||
result.append(lit[::-1] if is_rev else lit)
|
||||
idx = q2 + 1 + (len(REV) if is_rev else 0)
|
||||
while idx < len(expr) and expr[idx] in ' +.srne(-,)':
|
||||
idx += 1
|
||||
return ''.join(result)
|
||||
|
||||
assign = {}
|
||||
for stmt in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*("(?:[^"]*)".*)?$', stmt)
|
||||
if m:
|
||||
name = m.group(1); expr = (m.group(2) or '').strip()
|
||||
if not expr: assign[name] = ''; continue
|
||||
if expr.startswith('"'):
|
||||
assign[name] = eval_v2(expr)
|
||||
continue
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), '')
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', stmt)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2)
|
||||
continue
|
||||
|
||||
# 变量含义:
|
||||
# xKey = urdlw后的 assign值 (fkeco) — 32 hex-lookin b8fbfe...? 不,b8fbfe...是 xKey? 长度34?看长度
|
||||
candidate_vals = {k: v for k, v in assign.items() if v}
|
||||
for k, v in candidate_vals.items():
|
||||
print(f'{k} len={len(v)}: {v[:40]}')
|
||||
|
||||
# xKey 从源代码里看是 KEY; 它有 literal "b8fbfe92cc44e64533fcf75e8010097e" — hmm长度=36? page的 fbjao21拼接变32位hex
|
||||
# but svg based on原始 code:
|
||||
# var xefgv = ""; var fbjao = "..拼接.."; xefgv = fbjao; (xKey)
|
||||
# var qrtkk = ""; var lnzyw = "..拼接.."; qrtkk = lnzyw; (newToken)
|
||||
# ouyit = "32hex" (oldToken)
|
||||
# 所以 xKey / newTok 都是 mixed literals (not hex32)
|
||||
# BUT实际assign里 fkeco='b8fbfe92cc44e64533fcf75e801009..'是一个32hex串! 那 xKey 也是hex32? — 注意newTok(last)='%' URL-encode形式。
|
||||
# 因此爷就取:
|
||||
xKey = next(v for v in assign.values() if v and re.match(r'^[a-f0-9]{32}$', v) and v != next(u for u in assign.values() if re.match(r'^[a-f0-9]{32}$', u)))
|
||||
|
||||
# Hmm confusing; more precisely:
|
||||
vals = list(assign.values())
|
||||
hex32s = [v for v in vals if re.match(r'^[a-f0-9]{32}$', v)]
|
||||
print('hex32s:', hex32s)
|
||||
# 常识:b8fbfe.. 是 xKey (from urdlw eval), old token '1c2e5965...' another hex32
|
||||
# New token = uncmd starting '%'
|
||||
newTok = next((v for v in vals if v.startswith('%')), '')
|
||||
oldToken = next((v for v in hex32s if v != 'b8fbfe92cc44e64533fcf75e8010097e'), '')
|
||||
xKey = 'b8fbfe92cc44e64533fcf75e8010097e' # hardcoded from first
|
||||
xorConst = re.findall(r'"([a-f0-9]{16})"', html)[0]
|
||||
print(f'xKey={xKey}\nnewTok={newTok}\nold={oldToken}\nconst={xorConst}')
|
||||
|
||||
def b64str(s): return base64.b64decode(s.encode()).decode('latin1')
|
||||
def decrypt(enc):
|
||||
b1 = b64str(enc[::-1])
|
||||
key = xKey + xorConst
|
||||
x = ''.join(chr((ord(b1[i]) ^ ord(key[i % len(key)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64str(x)
|
||||
return urllib.parse.unquote(b2.replace('token=' + oldToken, 'token=' + newTok).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"', html)
|
||||
if opts:
|
||||
print('L1:', decrypt(opts[0])[:130])
|
||||
print('L2:', decrypt(opts[1])[:130])
|
||||
@@ -0,0 +1,72 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/a1.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split("")' in b)
|
||||
stmts = [s.strip() for s in mix.split(';') if s.strip()]
|
||||
|
||||
REV = '.split("").reverse().join("")'
|
||||
def ev(expr):
|
||||
out = []; i = 0
|
||||
while i < len(expr):
|
||||
if expr[i] == '"':
|
||||
j = i + 1; lit = ''
|
||||
while j < len(expr) and expr[j] != '"':
|
||||
lit += expr[j]; j += 1
|
||||
if j >= len(expr):
|
||||
out.append(lit); break
|
||||
rest = expr[j+1:j+1+50]
|
||||
if rest.startswith(REV):
|
||||
out.append(lit[::-1]); i = j + 1 + len(REV)
|
||||
else:
|
||||
out.append(lit); i = j + 1
|
||||
while i < len(expr) and expr[i] in '+ ':
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
assign = {}
|
||||
for st in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*$', st)
|
||||
if m: assign[m.group(1)] = ''; continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = ev(m.group(2)); continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"', st) # no closing
|
||||
if m:
|
||||
assign[m.group(1)] = ev(m.group(2)); continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), ''); continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2); continue
|
||||
print('unmet:', st[:50])
|
||||
|
||||
print('assign:', {k: (v[:20]+'..' if len(v) > 20 else v) for k, v in assign.items()})
|
||||
|
||||
# 按 JS 语言语义变量:
|
||||
# xKey = cdgsc (=jvfon_eval)
|
||||
# oldToken= jbdjv (裸 32hex)
|
||||
# newToken= xptcr (=xcyvc_eval)
|
||||
vals = [v for v in assign.values() if v]
|
||||
hex32 = [v for v in vals if re.match(r'^[a-f0-9]{32}$', v)]
|
||||
xKey = next(v for v in vals if re.match(r'^[a-f0-9]{32}$', v) and not v.startswith('%'))
|
||||
# 修复: xKey应该是 jvfon_eval (32hex) — 从var(以 "(.*)" 成)来的
|
||||
# oldToken = jbdjv bare(32hex) — 第二个 hex32
|
||||
oldToken = next((h for h in hex32 if h != xKey), hex32[0])
|
||||
newToken = next((v for v in vals if re.search(r'%[0-9A-Fa-f]{2}', v) and not re.match(r'^[a-f0-9]{32}$', v) and not re.match(r'^[a-f0-9]{16}$', v)), '')
|
||||
print(f'xKey={xKey[:24]}...\noldTok={oldToken[:24]}...\nnewTok={newToken[:24]}...\nconst={re.findall(chr(34)+"([a-f0-9]{16})"+chr(34), html)[0]}')
|
||||
xorConst = re.findall(r'"([a-f0-9]{16})"', html)[0]
|
||||
|
||||
def b64(s): return base64.b64decode(s.encode()).decode('latin1')
|
||||
def dec(e):
|
||||
b1 = b64(e[::-1])
|
||||
k = xKey + xorConst
|
||||
x = ''.join(chr((ord(b1[i]) ^ ord(k[i % len(k)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64(x)
|
||||
return urllib.parse.unquote(b2.replace('token=' + oldToken, 'token=' + newToken).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"[^>]*>', html)
|
||||
print('L1:', dec(opts[0])[:135])
|
||||
print('L2:', dec(opts[1])[:135])
|
||||
@@ -0,0 +1,61 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/android_proxy.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split("")' in b)
|
||||
|
||||
def eval_frags(expr):
|
||||
REVr = '.split("").reverse().join("")'
|
||||
out = []; i = 0
|
||||
while i < len(expr):
|
||||
m = re.match(r'"([^"]*)"(\.split\(""\)\.reverse\(\)\.join\(""\))?', expr[i:])
|
||||
if m:
|
||||
lit = m.group(1)
|
||||
is_rev = m.group(2) is not None
|
||||
out.append(lit[::-1] if is_rev else lit)
|
||||
i += m.end()
|
||||
while i < len(expr) and expr[i] in ' +':
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
# 真实数据验证:
|
||||
assign = {}
|
||||
for st in [s.strip() for s in mix.split(';') if s.strip()]:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*(.*)$', st)
|
||||
if not m:
|
||||
m = re.match(r'^(\w+)\s*=\s*(.*)$', st)
|
||||
if m:
|
||||
name = m.group(1); rhs = m.group(2).strip().rstrip(')').strip()
|
||||
if rhs in assign: assign[name] = assign[rhs]
|
||||
continue
|
||||
else:
|
||||
name = m.group(1); rhs = m.group(2).strip().rstrip(')').strip()
|
||||
if not rhs: assign[name] = ''; continue
|
||||
eval_body = rhs
|
||||
if eval_body.startswith('"'): eval_body = eval_body[1:] # strip leading quote (assignment's opening)
|
||||
# 还有末尾可能结尾在 .join("") 之后的 )
|
||||
|
||||
assign[name] = eval_frags(eval_body)
|
||||
continue
|
||||
|
||||
print('assign:', {k: (v[:25]+'..' if len(v) > 25 else v) for k, v in assign.items()})
|
||||
|
||||
vals = [v for v in assign.values() if v]
|
||||
hex32 = [v for v in vals if re.match(r'^[a-f0-9]{32}$', v)]
|
||||
xKey = hex32[0]
|
||||
oldToken = next(h for h in hex32 if h != xKey)
|
||||
newToken = next(v for v in vals if re.search(r'%[0-9A-Fa-f]{2}', v) and not re.match(r'^[a-f0-9]{32}$', v) and not re.match(r'^[a-f0-9]{16}$', v) and v != xKey)
|
||||
xorConst = re.findall(r'"([a-f0-9]{16})"', html)[0]
|
||||
print(f'xKey={xKey} new={newToken[:26]} old={oldToken[:10]}.. c={xorConst}')
|
||||
|
||||
def b64(s): return base64.b64decode(s.encode()).decode('latin1')
|
||||
def dec(e):
|
||||
b1 = b64(e[::-1])
|
||||
k = xKey + xorConst
|
||||
x = ''.join(chr((ord(b1[i]) ^ ord(k[i % len(k)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64(x)
|
||||
return urllib.parse.unquote(b2.replace('token=' + oldToken, 'token=' + newToken).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"[^>]*>', html)
|
||||
print('L1:', dec(opts[0])[:130])
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""License-SaaS 客户端 API 端到端测试(HMAC-SHA256签名协议)"""
|
||||
import hmac, hashlib, json, time, uuid, urllib.request, urllib.error
|
||||
|
||||
APP_KEY = 'iptv807-tv'
|
||||
CLIENT_SECRET = '91a62f41ff09c2eed51fa571a78c69014c5f9eacbaeca56c'
|
||||
BASE = 'http://192.168.68.19:8090'
|
||||
|
||||
def signed(method, path, body=None):
|
||||
body_str = json.dumps(body or {}, ensure_ascii=False, separators=(',', ':'))
|
||||
ts = str(int(time.time()))
|
||||
nonce = uuid.uuid4().hex[:16]
|
||||
canonical = f"{method}\n{path}\n{ts}\n{nonce}\n{body_str}"
|
||||
sig = hmac.new(CLIENT_SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
|
||||
req = urllib.request.Request(BASE + path, data=(body_str.encode() if method == 'POST' else None), method=method)
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
req.add_header('X-Timestamp', ts)
|
||||
req.add_header('X-Nonce', nonce)
|
||||
req.add_header('X-Signature', sig)
|
||||
req.add_header('X-App-Key', APP_KEY)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
return json.loads(r.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
return {'http': e.code, 'body': e.read().decode()[:200]}
|
||||
|
||||
print('app-info:', signed('GET', '/api/client/app-info'))
|
||||
r = signed('POST', '/api/client/register', {'username': 'test01', 'password': 'test123456', 'cardKey': 'LS-F10E53D1-8C91BFD6-7E098CFF', 'machineCode': 'ANDROID-TV-001'})
|
||||
print('register:', r)
|
||||
r = signed('POST', '/api/client/login', {'username': 'test01', 'password': 'test123456', 'machineCode': 'ANDROID-TV-001'})
|
||||
print('login:', r)
|
||||
r = signed('POST', '/api/client/heartbeat', {'token': r.get('client_token', '')})
|
||||
print('heartbeat:', r)
|
||||
@@ -0,0 +1,64 @@
|
||||
import re, base64, urllib.parse
|
||||
html = open('/tmp/now24.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split' in b)
|
||||
|
||||
stmts = []; buf=''; ins=False
|
||||
for ch in mix:
|
||||
if ch=='"':
|
||||
ins = not ins
|
||||
buf += ch
|
||||
continue
|
||||
if ch==';' and not ins:
|
||||
stmts.append(buf.strip()); buf=''
|
||||
else: buf += ch
|
||||
if buf.strip(): stmts.append(buf.strip())
|
||||
|
||||
def eval_v2(expr):
|
||||
REV='.split("").reverse().join("")'
|
||||
result=[]; idx=0
|
||||
while True:
|
||||
q1=expr.find('"', idx)
|
||||
if q1<0: break
|
||||
q2=expr.find('"', q1+1)
|
||||
if q2<0: break
|
||||
lit=expr[q1+1:q2]
|
||||
is_rev=expr[q2+1:q2+1+len(REV)]==REV
|
||||
result.append(lit[::-1] if is_rev else lit)
|
||||
idx=q2+1+(len(REV) if is_rev else 0)
|
||||
while idx<len(expr) and expr[idx] in ' +.srne(-,)':
|
||||
idx+=1
|
||||
return ''.join(result)
|
||||
|
||||
assign={}
|
||||
for stmt in stmts:
|
||||
m=re.match(r'^var\s+(\w+)\s*=\s*("(?:[^"]*)".*)?$', stmt)
|
||||
if m:
|
||||
name=m.group(1); expr=(m.group(2) or '').strip()
|
||||
if not expr: assign[name]=''; continue
|
||||
if expr.startswith('"'): assign[name]=eval_v2(expr); continue
|
||||
continue
|
||||
# 32-hex literal assignment (var or bare)
|
||||
m=re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', stmt)
|
||||
if m: assign[m.group(1)]=m.group(2); continue
|
||||
|
||||
print('assign:', {k:(v[:30] if len(v)<31 else v[:30]+'..') for k,v in assign.items()})
|
||||
|
||||
vals=[v for v in assign.values() if v]
|
||||
hex32=[v for v in vals if re.match(r'^[a-f0-9]{32}$', v)]
|
||||
xKey = hex32[0]
|
||||
oldToken = next((h for h in hex32 if h != xKey), hex32[0]) # 有些页 old==xkey?
|
||||
newToken = next((v for v in vals if v.startswith('%')), '')
|
||||
xorConst = re.findall(r'"([a-f0-9]{16})"', html)[0]
|
||||
print(f'xKey={xKey} old={oldToken} newTok={newToken} c={xorConst}')
|
||||
|
||||
def b64(s): return base64.b64decode(s.encode()).decode('latin1')
|
||||
def decrypt(enc):
|
||||
b1 = b64(enc[::-1])
|
||||
key = xKey + xorConst
|
||||
x=''.join(chr((ord(b1[i]) ^ ord(key[i%len(key)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64(x)
|
||||
return urllib.parse.unquote(b2.replace('token='+oldToken, 'token='+newToken).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"[^>]*>線路\d*', html)
|
||||
for i, o in enumerate(opts[:2]):
|
||||
print(f'L{i+1}:', decrypt(o)[:130])
|
||||
@@ -0,0 +1,48 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
# 测试有 trailing join 的真实语句(android_proxy.html 实际数据):
|
||||
html = open('/tmp/android_proxy.html', encoding='utf8', errors='ignore').read()
|
||||
blocks = re.findall(r'<script>([\s\S]*?)</script>', html)
|
||||
mixBlock = next(b for b in blocks if '.split("")' in b)
|
||||
stmts = [s.strip() for s in mixBlock.split(';') if s.strip()]
|
||||
print('stmt[1] (dfuts):')
|
||||
print(repr(stmts[1]))
|
||||
print()
|
||||
|
||||
def eval_concat(expr):
|
||||
REV = '.split("").reverse().join("")'
|
||||
out = []; i = 0
|
||||
while i < len(expr):
|
||||
if expr[i] == '"':
|
||||
close = expr.find('"', i+1)
|
||||
if close < 0: break
|
||||
lit = expr[i+1:close]
|
||||
is_rev = expr[close+1:close+1+len(REV)] == REV
|
||||
out.append(lit[::-1] if is_rev else lit)
|
||||
i = close+1+(len(REV) if is_rev else 0)
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
# 正确assignment解析(Python顺利):
|
||||
assign = {}
|
||||
for st in stmts:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*"(.*)"$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2)); continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*(.*)"\)$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = eval_concat(m.group(2)); continue
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*(.*)"$'.rstrip('"$')+'("\\)$)?$', st)
|
||||
if m and not m.group(0).endswith('\\') : pass
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = ''; continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), ''); continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2); continue
|
||||
|
||||
print('assign:', {k: (v[:20]+'..' if len(v) > 20 else v) for k, v in assign.items()})
|
||||
@@ -0,0 +1,75 @@
|
||||
import re, base64, urllib.parse
|
||||
|
||||
html = open('/tmp/android_proxy.html', encoding='utf8', errors='ignore').read()
|
||||
mix = next(b for b in re.findall(r'<script>([\s\S]*?)</script>', html) if '.split("")' in b)
|
||||
|
||||
# 修正版 eval:游标从 expr 起点扫描,遇 quote 开literal,遇REV tag 反转
|
||||
def eval_v3(expr):
|
||||
REV = '.split("").reverse().join("")'
|
||||
frags = []
|
||||
i = 0
|
||||
while i < len(expr):
|
||||
if expr[i] == '"':
|
||||
j = i + 1; lit = ''
|
||||
while j < len(expr) and expr[j] != '"':
|
||||
lit += expr[j]; j += 1
|
||||
if j >= len(expr):
|
||||
frags.append(lit); break # unterminated - take rest
|
||||
rest = expr[j+1:j+1+50]
|
||||
is_rev = rest.startswith(REV)
|
||||
frags.append(lit[::-1] if is_rev else lit)
|
||||
i = j + 1 + (len(REV) if is_rev else 0)
|
||||
while i < len(expr) and expr[i] == '+':
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(frags)
|
||||
|
||||
assert eval_v3('"b8"+"fb"+"f".split("").reverse().join("")+"c29e"') == 'b8fbfe92c', "test failed"
|
||||
print('v3 test OK')
|
||||
|
||||
assign = {}
|
||||
for st in [s.strip() for s in mix.split(';') if s.strip()]:
|
||||
m = re.match(r'^var\s+(\w+)\s*=\s*(.*)$', st)
|
||||
if m:
|
||||
name = m.group(1); rhs = m.group(2).strip()
|
||||
if not rhs or rhs == '"':
|
||||
assign[name] = ''; continue
|
||||
# rhs 开头是 ",保留整段从quote开始
|
||||
if rhs.startswith('"'):
|
||||
assign[name] = eval_v3(rhs)
|
||||
continue
|
||||
m2 = re.match(r'^"([a-f0-9]{32})"$', rhs)
|
||||
if m2:
|
||||
assign[name] = m2.group(1); continue
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*(\w+)$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = assign.get(m.group(2), '')
|
||||
continue
|
||||
m = re.match(r'^(\w+)\s*=\s*"([a-f0-9]{32})"$', st)
|
||||
if m:
|
||||
assign[m.group(1)] = m.group(2)
|
||||
continue
|
||||
print('un:', st[:40])
|
||||
|
||||
print('assign:', {k: (v[:25]+'. .' if len(v) > 25 else v) for k, v in assign.items()})
|
||||
|
||||
vals = [v for v in assign.values() if v]
|
||||
hex32 = [v for v in vals if re.match(r'^[a-f0-9]{32}$', v)]
|
||||
xKey = hex32[0]
|
||||
oldToken = next(h for h in hex32 if h != xKey)
|
||||
newToken = next(v for v in vals if re.search(r'%[0-9A-Fa-f]{2}', v) and not re.match(r'^[a-f0-9]{32}$', v) and not re.match(r'^[a-f0-9]{16}$', v) and v != xKey)
|
||||
xorConst = re.findall(r'"([a-f0-9]{16})"', html)[0]
|
||||
print(f'xKey={xKey}\nnewTok={newToken[:30]}..\nold={oldToken[:10]}..\nc={xorConst}')
|
||||
|
||||
def b64(s): return base64.b64decode(s.encode()).decode('latin1')
|
||||
def dec(e):
|
||||
b1 = b64(e[::-1])
|
||||
k = xKey + xorConst
|
||||
x = ''.join(chr((ord(b1[i]) ^ ord(k[i % len(k)])) & 0xff) for i in range(len(b1)))
|
||||
b2 = b64(x)
|
||||
return urllib.parse.unquote(b2.replace('token=' + oldToken, 'token=' + newToken).replace(xKey, ''))
|
||||
|
||||
opts = re.findall(r'<option value="([^"]+)"[^>]*>', html)
|
||||
print('L1:', dec(opts[0])[:130])
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""验证 channels.json 里各频道线路1可拉流(302 → 流头 FLV/m3u8)"""
|
||||
import json, urllib.request, socket
|
||||
socket.setdefaulttimeout(12)
|
||||
|
||||
UA = 'Mozilla/5.0 (Linux; Android 12) Chrome/124 Mobile Safari/537.36'
|
||||
data = json.load(open('/vol1/1000/HD1/APP/iptv-tv-app/tools/channels.json'))
|
||||
ok = [c for c in data if c.get('lines')]
|
||||
print('channels:', len(ok))
|
||||
for c in ok:
|
||||
url = c['lines'][0]
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={'User-Agent': UA, 'Referer': 'https://m.iptv807.com/'})
|
||||
# 不跟随重定向,先拿302目标
|
||||
class NoRedir(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *a, **k): return None
|
||||
op = urllib.request.build_opener(NoRedir)
|
||||
try:
|
||||
op.open(req)
|
||||
red = None
|
||||
except urllib.error.HTTPError as e:
|
||||
red = e.headers.get('Location')
|
||||
if not red:
|
||||
print(f"❌ {c['name']}: no redirect")
|
||||
continue
|
||||
# 拉最终流前32字节
|
||||
req2 = urllib.request.Request(red, headers={'User-Agent': UA})
|
||||
with urllib.request.urlopen(req2) as r:
|
||||
head = r.read(32)
|
||||
magic = 'FLV' if head.startswith(b'FLV') else ('m3u8' if b'#EXTM3U' in head else head[:8])
|
||||
print(f"✅ {c['name']}: {magic}")
|
||||
except Exception as e:
|
||||
print(f"❌ {c['name']}: {str(e)[:80]}")
|
||||