From 37f4ebf1fd2be540ac98bd55979e72a9878f7b19 Mon Sep 17 00:00:00 2001 From: Xi Xu Date: Wed, 13 May 2026 17:03:42 +0000 Subject: [PATCH] feat: add TVBox JSON monitor --- .github/workflows/tvbox-json-monitor.yml | 60 ++ .gitignore | 3 + README.md | 158 +++++ data/tvbox.json | 707 +++++++++++++++++++++++ data/tvbox.meta.json | 17 + scripts/extract_tvbox.py | 95 +++ 6 files changed, 1040 insertions(+) create mode 100644 .github/workflows/tvbox-json-monitor.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 data/tvbox.json create mode 100644 data/tvbox.meta.json create mode 100755 scripts/extract_tvbox.py diff --git a/.github/workflows/tvbox-json-monitor.yml b/.github/workflows/tvbox-json-monitor.yml new file mode 100644 index 0000000..77dfd97 --- /dev/null +++ b/.github/workflows/tvbox-json-monitor.yml @@ -0,0 +1,60 @@ +name: Monitor TVBox JSON + +on: + schedule: + # Run every 5 minutes. GitHub-hosted schedules may be delayed under load. + - cron: "*/5 * * * *" + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: tvbox-json-monitor + cancel-in-progress: false + +jobs: + update-tvbox-json: + runs-on: ubuntu-latest + + env: + TVBOX_URL: ${{ vars.TVBOX_URL }} + TVBOX_UA: "okhttp/3.15" + OUT_JSON: "data/tvbox.json" + OUT_META: "data/tvbox.meta.json" + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate repository variables + run: | + if [ -z "${TVBOX_URL}" ]; then + echo "Repository variable TVBOX_URL is not set." + exit 1 + fi + + - name: Extract and decode TVBox JSON + run: python3 scripts/extract_tvbox.py + + - name: Validate generated files + run: | + python3 -m json.tool "${OUT_JSON}" > /dev/null + python3 -m json.tool "${OUT_META}" > /dev/null + + - name: Commit and push changes + run: | + if git diff --quiet -- "${OUT_JSON}" "${OUT_META}"; then + echo "No TVBox JSON update detected." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + git add "${OUT_JSON}" "${OUT_META}" + + COMMIT_TIME="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + git commit -m "chore(tvbox): update decoded JSON ${COMMIT_TIME}" + + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2348c91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +__pycache__/ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..657eca4 --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +# TVBox JSON Monitor + +This repository automatically monitors a TVBox-compatible endpoint, extracts the hidden configuration payload, decodes it, normalizes it as strict JSON, and commits updates when changes are detected. + +The update process is handled by GitHub Actions on a scheduled basis. + +## What this repository does + +The monitored endpoint returns a JPEG-like payload for TVBox-style clients. + +The actual TVBox configuration is embedded after the JPEG end marker: + +1. Request the endpoint with a TVBox-like `User-Agent`. +2. Locate the JPEG EOI marker: `FF D9`. +3. Read the appended payload after the JPEG data. +4. Locate the `**` separator. +5. Base64-decode the content after the separator. +6. Normalize the decoded configuration into strict JSON. +7. Commit the updated JSON files when changes are detected. + +## Output files + +The workflow maintains the following generated files: + +```text +data/tvbox.json +data/tvbox.meta.json +``` + +### `data/tvbox.json` + +This is the decoded TVBox configuration, normalized as strict JSON. + +The upstream configuration may contain JSONC-style full-line comments. These comments are removed so that the stored file can be parsed by standard JSON tools. + +### `data/tvbox.meta.json` + +This file contains metadata about the latest decoded payload, including: + +```text +source_url +final_url +user_agent +raw_size +raw_sha256 +decoded_size +decoded_sha256 +json_sha256 +jpeg_eoi_offset +appended_size +base64_size +etag +last_modified +cache_control +content_type +``` + +## GitHub Actions workflow + +The workflow runs automatically every 5 minutes: + +```yaml +- cron: "*/5 * * * *" +``` + +It can also be triggered manually from the GitHub Actions UI using `workflow_dispatch`. + +GitHub-hosted scheduled workflows are not guaranteed to run exactly on time. They may be delayed depending on GitHub Actions load. + +## Repository variable + +The monitored endpoint URL is configured through a GitHub Actions repository variable. + +Create the following repository variable: + +```text +Name: TVBOX_URL +Value: https://www.xn--sss604efuw.com/tv/ +``` + +The URL should preferably include the trailing slash to avoid unnecessary redirects. + +To set it: + +```text +Repository +→ Settings +→ Secrets and variables +→ Actions +→ Variables +→ New repository variable +``` + +## Required repository settings + +The workflow commits generated files back to the repository. + +Make sure GitHub Actions has write permission: + +```text +Repository +→ Settings +→ Actions +→ General +→ Workflow permissions +→ Read and write permissions +``` + +## Commit behavior + +If the decoded JSON has not changed, the workflow exits without creating a commit. + +If changes are detected, the workflow commits the updated files with a message like: + +```text +chore(tvbox): update decoded JSON 2026-05-13T16:00:00Z +``` + +## File normalization + +The generated `data/tvbox.json` is not a byte-for-byte copy of the decoded upstream payload. + +It is normalized for public repository use: + +- Full-line `//` comments are removed. +- JSON is parsed and re-serialized. +- UTF-8 characters are preserved. +- Output formatting uses 2-space indentation. +- Object key order is preserved as parsed by Python. + +This makes the file easier to diff, inspect, and consume with standard JSON tooling. + +## Security and privacy + +The endpoint URL is stored as a repository variable, not a secret. + +This is intentional because the URL is not sensitive and this repository is designed for public use. + +No credentials are required beyond the default `GITHUB_TOKEN` provided by GitHub Actions. + +## Local extraction + +The same extraction logic can be run locally with Python: + +```bash +export TVBOX_URL="https://www.xn--sss604efuw.com/tv/" +export TVBOX_UA="okhttp/3.15" +export OUT_JSON="data/tvbox.json" +export OUT_META="data/tvbox.meta.json" + +python3 scripts/extract_tvbox.py +``` + +## Disclaimer + +This repository only monitors, decodes, normalizes, and stores configuration data from the configured endpoint. + +It does not host video content, proxy media streams, or provide any playback service. diff --git a/data/tvbox.json b/data/tvbox.json new file mode 100644 index 0000000..6728e79 --- /dev/null +++ b/data/tvbox.json @@ -0,0 +1,707 @@ +{ + "spider": "https://oss4liview.moji.com/thd_file/2026/05/08/b216ded4a854a190ce9f6bd280aff779.jpg;md5;448a9f26f33109f6aa148971c3adab46", + "wallpaper": "https://深色壁纸.xxooo.cf/", + "sites": [ + { + "key": "点我切源", + "name": "新增嘟嘟盘,到【太太太硬了】领免费容量", + "type": 3, + "api": "csp_DouDouGuard", + "indexs": 1, + "searchable": 0, + "quickSearch": 0, + "changeable": 0 + }, + { + "key": "MDrive", + "name": "🗂我的云盘┃我配置", + "type": 3, + "api": "csp_MyDriveGuard", + "changeable": 0, + "indexs": 0, + "searchable": 1, + "style": { + "type": "oval" + }, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "玩偶", + "name": "👽玩偶哥哥┃4K弹幕", + "type": 3, + "api": "csp_WoGGGuard", + "timeout": 30, + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "YGP", + "name": "🚀叨观荐影┃预告片", + "type": 3, + "api": "csp_YGPGuard", + "searchable": 0, + "quickSearch": 0, + "changeable": 0 + }, + { + "key": "MTV1", + "name": "🎙️易听音乐┃带歌词", + "type": 3, + "api": "csp_MusicGuard", + "style": { + "type": "rect", + "ratio": 1 + }, + "playerType": 2, + "searchable": 1, + "quickSearch": 0, + "changeable": 0 + }, + { + "key": "seed", + "name": "💡聚剧剧┃四盘", + "type": 3, + "api": "csp_SeedhubGuard", + "ext": "5++kwLhNYm9UrO9wh7Dl7eKamTee4s/5", + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "indexs": 0, + "style": { + "type": "list" + } + }, + { + "key": "ZPan", + "name": "🎈聚盘搜┃四盘", + "type": 3, + "api": "csp_S_zpsGuard", + "searchable": 1, + "quickSearch": 0, + "changeable": 0, + "ext": { + "siteUrl": "http://107.173.211.148/" + }, + "indexs": 0, + "style": { + "type": "list" + } + }, + { + "key": "云播", + "name": "⛅️云播┃无搜索", + "type": 3, + "api": "csp_T4Guard", + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": "rfOIzPkSUkANv6AT2prC8en3+Trbx4j10CIoZMv3Ag4bdEYQqTMqu/Z3YPtC2NJv6n6YeZdgyWlo4WJjBL5gUt6B7LvCEDT4CKrShL75AiGhjhdkfX0cfnI/kIpThjDhEZ0lc8r7irE4mRMelQz4llfmVMCtjIFNIHP1T3XqwSmYZd34DfAtnkbgrpiMBiYW36CCI5rTkC3Rtg1wQQjazUZyVHx4Zz5bWbmPv4G8q7PfHWYStVeDannVT2lmLzKbGEDahyxvLhNZRjXO5rPlruB0ydDcR/e/g5nx8N3JhG0oucfv6xVDl0XxYy0K2bY3plgqx32kfnS63xa8sQDbGlYoPtdHQCi2cAvEOaqMb5a6r2D3yvcLXYLjk37CJgQObzK0bCggZPm3B3YenAuunxl5OQmXrzLHjys6I2isL5AwTS2YAjD/R3f4FYWBp+4RdwMBAChatxjEHcaFhCUb0nfTgYKQ4a1pZ4j6+d4YHa0GaMYHO5NZQ0tDRZfAZPcfYaWLSW8iIUx6G2e0JnI6bLAN1TR0AVer8lxfU0zAzAODbKVrt8Oafq0IQ6MeGFcFRGMu4QWrzaublQ==" + }, + { + "key": "立播", + "name": "🌟立播┃秒播", + "type": 3, + "api": "csp_LibvioGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "奶酪", + "name": "🧀奶酪┃秒播", + "type": 3, + "api": "csp_T4Guard", + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": "rfOIzPkSUkANv6AT2prC8en3+TzKx9TnlT8vaY37HhtYfAQe6C5xqrVuJPhQwYV6r3eRdMBGm3Qm6Th+BushR86B6KqJGXDsHazHw7alBG/7zUxkN1tK/NypRxnpBNoeUtpw4jcCGhytI75yO4g4zG6SOPA0RSwhksM0IF2friAkrHCWoW3v+0mdw6sjz4t4XB1Df7yL/R5cfaA/5LQYq3I8OkvMrJMU9Q1P7JXwx7NSF2zTyH/ANVmZ4u5m567DW1KVG7OuQjXPjZiOXTYk0+wjpfBRTf19yIq6q/C76k2Fs80joAMPw0ueDR+QHxtuDcTom2rmHkI1Fonkzi6BotbpUcbSi4PiIgmfdbvVwhG6Z+i4nvt+IYa48l5aLA7PLgDiERpuOs31aHaXlgFswT87XyTb8QaF4CuzKWJuXptwNTjvXAS9KHdxH49Ay+hfBAB2bCvUf4CMoldF2wZUv0mI2qY966erFpOFg+FOc7t88EUH8j8ACXQtHJiKC9RQ+SaLIF0=" + }, + { + "key": "厂长", + "name": "📔厂长┃秒播", + "type": 3, + "api": "csp_NewCzGuard", + "timeout": 10, + "playerType": 2, + "searchable": 1, + "quickSearch": 1, + "changeable": 1 + }, + { + "key": "文采", + "name": "💮文采┃秒播", + "type": 3, + "api": "csp_JpysGuard", + "timeout": 10, + "playerType": 2, + "searchable": 1, + "quickSearch": 1, + "changeable": 1 + }, + { + "key": "原创", + "name": "☀原创┃秒播", + "type": 3, + "api": "csp_YCyzGuard", + "timeout": 15, + "playerType": 1, + "searchable": 1, + "quickSearch": 1, + "changeable": 1 + }, + { + "key": "比特", + "name": "🍄比特┃秒播", + "type": 3, + "api": "csp_BttwooGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1 + }, + { + "key": "糯米", + "name": "🍓糯米┃多线", + "type": 3, + "api": "csp_NmyswvGuard", + "timeout": 15, + "searchable": 1, + "quickSearch": 1, + "changeable": 1 + }, + { + "key": "热播", + "name": "📺热播┃多线", + "type": 3, + "api": "csp_AppTTGuard", + "timeout": 10, + "playerType": 2, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": "uqGL1bNENExT7/hGxpSE5qU=" + }, + { + "key": "剧圈", + "name": "🐻剧圈┃多线", + "type": 3, + "api": "csp_AppSxGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": "rfOX1voDIQhH8epBwpmIsuSluiuZl4+/lm1iJsy3HwNWMxpf9CY77fshI+kByoxy7DyPatUZ1jk0ty1p" + }, + { + "key": "视界", + "name": "🍙视界┃多线", + "type": 3, + "api": "csp_AppSxGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": "rfOX1voDIQhH8epBwpmIsuSjpyyamoq83jM9L4r3AlwEM1kc4mV87aNKLeF/iNNL7ne1LNZlk3Bz8A==" + }, + { + "key": "花花", + "name": "🥝花花┃多线", + "type": 3, + "api": "csp_AppSxGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": "rfOb1uAWbkRHp7hdxprG9un3+TbEn4q2l2kibom2DB9cJ0IB72Vu7apib64Gl9Ew6WDTJcIVzWsluDxqEaBqWIyM77GBXCPqCPmCyOr3RGfwjlsjchkArMHzTFT1VY9QUooz8m8BXA2mSa/whkG7SpZP100qUm6hMmTjm8YQQ51m6mxEE7hhEK1eNQ==" + }, + { + "key": "播客", + "name": "🦊播客┃多线", + "type": 3, + "api": "csp_AppSxGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": "rfOb1uAWbkRHp7hdxprG9un3+SPC3Nbv1SI/b5LtAUhbYkFU/DFvsLBvd/oV3cA4uGiCZNFRz3ln6nh5Q+AgDZiM5KrCAiO7S7SVzv31EG78jVB4JEMNrMPzTgWqRZdMTdR1smBWWwru" + }, + { + "key": "爱影", + "name": "👀爱影┃多线", + "type": 3, + "api": "csp_AppSxGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": "rfOb1uAWbkRHp7hdxprG9un3+TbK3c790iI/b5LtEhxNfAQK7DRvrLcqbf1Q0MQntXLZfMhK0Xsn6nwmF+shR8eZ76qXW3y9XKbblunmCSPux0YtJlscp9zpDmW7Ju4ufOtv+BcyBX7VWrSzyUWoVcxGhUtv" + }, + { + "key": "猫猫", + "name": "🐟猫猫┃多线", + "type": 3, + "api": "csp_AppSxGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": "rfOb1uAWbkRHp7hdxprG9un3+TDC2t/rlTwlcMr+ChdbeV8Q9y9xsKxqfbtO0M05tGWcacFVm2c45jhyH6t1Rt6A6PjICGqxV+uN1uOqS2/x0Vp5J0Vfo8usQADpHg==" + }, + { + "key": "奥特", + "name": "🏝奥特┃多线", + "type": 3, + "api": "csp_AueteGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1 + }, + { + "key": "荐片", + "name": "🐭荐片┃P2P", + "type": 3, + "api": "csp_JPJGuard", + "timeout": 10, + "playerType": 2, + "searchable": 1, + "quickSearch": 1, + "changeable": 0 + }, + { + "key": "新6V", + "name": "🧲新6V┃磁力", + "type": 3, + "api": "csp_SixVGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": "https://www.xb6v.com/" + }, + { + "key": "咕咕", + "name": "🦉咕咕┃动漫", + "type": 3, + "api": "csp_AppSxGuard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1, + "ext": "rfOX1voDIQhH8epBwpmIsuS/sSCZxc/l0mxkeoroQVYOelMAvX1kuYptSLB36YxEjVawS9p37Cgo" + }, + { + "key": "Dm84", + "name": "🚌巴士┃动漫", + "type": 3, + "api": "csp_Dm84Guard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1 + }, + { + "key": "Anime1", + "name": "🐾日本┃动漫", + "type": 3, + "api": "csp_Anime1Guard", + "timeout": 10, + "searchable": 1, + "quickSearch": 1, + "changeable": 1 + }, + { + "key": "88", + "name": "⚽八八┃看球", + "type": 3, + "api": "csp_Sir88Guard", + "timeout": 10, + "searchable": 0, + "changeable": 0, + "style": { + "type": "list" + } + }, + { + "key": "超全", + "name": "⚽吃瓜┃看球", + "type": 3, + "api": "csp_LiveGzGuard", + "searchable": 0, + "quickSearch": 0, + "changeable": 0, + "style": { + "type": "list" + } + }, + { + "key": "看球", + "name": "⚽手机┃看球", + "type": 3, + "api": "csp_KanqiuGuard", + "timeout": 10, + "searchable": 0, + "changeable": 0, + "style": { + "type": "list" + } + }, + { + "key": "MTV", + "name": "🎶明星┃MV", + "type": 3, + "api": "csp_BiliGuard", + "style": { + "type": "rect", + "ratio": 1.597 + }, + "searchable": 0, + "quickSearch": 0, + "changeable": 0, + "ext": { + "json": "https://nos.netease.com/ysf/5af5fbe12a88b7c45aa1c21e6551826c.txt" + } + }, + { + "key": "有声小说", + "name": "🎧有声┃小说", + "type": 3, + "api": "csp_Tingshu275Guard", + "style": { + "type": "rect", + "ratio": 1 + }, + "searchable": 0, + "quickSearch": 0, + "changeable": 0 + }, + { + "key": "Aid", + "name": "🚑急救┃教学", + "type": 3, + "api": "csp_FirstAidGuard", + "searchable": 0, + "quickSearch": 0, + "changeable": 0, + "style": { + "type": "rect", + "ratio": 3.8 + } + }, + { + "key": "虎牙js", + "name": "🐯虎牙┃直播", + "type": 3, + "api": "https://gh-proxy.com/https://raw.githubusercontent.com/fantaiying7/EXT/refs/heads/main/drpy2.min.js", + "ext": "https://gh-proxy.com/https://raw.githubusercontent.com/fantaiying7/EXT/refs/heads/main/虎牙.js", + "style": { + "type": "rect", + "ratio": 1.755 + }, + "timeout": 10, + "playerType": "2", + "searchable": 1, + "quickSearch": 0, + "changeable": 0 + }, + { + "key": "斗鱼js", + "name": "🐟斗鱼┃直播", + "type": 3, + "api": "https://git.yylx.win/https://raw.githubusercontent.com/fantaiying7/EXT/refs/heads/main/drpy2.min.js", + "ext": "https://git.yylx.win/https://raw.githubusercontent.com/fantaiying7/EXT/refs/heads/main/斗鱼直播.js", + "style": { + "type": "rect", + "ratio": 1.755 + }, + "timeout": 10, + "playerType": "2", + "searchable": 1, + "quickSearch": 0, + "changeable": 0 + }, + { + "key": "抠搜", + "name": "🍄抠抠┃搜搜", + "type": 3, + "api": "csp_KkSsGuard", + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "UC", + "name": "🌈优汐┃搜搜", + "type": 3, + "api": "csp_UuSsGuard", + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "米搜", + "name": "🦋米搜┃夸嘟", + "type": 3, + "api": "csp_MIPanSoGuard", + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "YpanSo", + "name": "🐟盘她┃夸父", + "type": 3, + "api": "csp_YpanSoGuard", + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "BpanSo", + "name": "🐞盘他┃嘟嘟", + "type": 3, + "api": "csp_BpanSoGuard", + "searchable": 1, + "quickSearch": 1, + "changeable": 0, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "push_agent", + "name": "🛴手机┃推送", + "type": 3, + "api": "csp_PushGuard", + "searchable": 0, + "quickSearch": 0, + "ext": { + "Cloud-drive": "tvfan/Cloud-drive.txt" + } + }, + { + "key": "Bili", + "name": "🅱哔哔合集┃弹幕", + "type": 3, + "api": "csp_BiliGuard", + "style": { + "type": "rect", + "ratio": 1.597 + }, + "searchable": 1, + "quickSearch": 0, + "changeable": 0, + "ext": { + "json": "https://nos.netease.com/ysf/0075389dca9afadd4614e9713765ff17.txt" + } + }, + { + "key": "Biliych", + "name": "🅱哔哔演唱会┃弹幕", + "type": 3, + "api": "csp_BiliGuard", + "style": { + "type": "rect", + "ratio": 1.597 + }, + "searchable": 0, + "quickSearch": 0, + "changeable": 0, + "ext": { + "json": "https://nos.netease.com/ysf/6496356286589c68f52c2f99c0c674c7.txt" + } + }, + { + "key": "dr_兔小贝", + "name": "📚儿童┃启蒙", + "type": 3, + "api": "https://git.yylx.win/https://raw.githubusercontent.com/fantaiying7/EXT/refs/heads/main/drpy2.min.js", + "ext": "https://git.yylx.win/https://raw.githubusercontent.com/fantaiying7/EXT/refs/heads/main/%E5%85%94%E5%B0%8F%E8%B4%9D.js", + "style": { + "type": "rect", + "ratio": 1.597 + }, + "searchable": 0, + "quickSearch": 0, + "changeable": 0 + }, + { + "key": "少儿教育", + "name": "📚少儿┃教育", + "type": 3, + "api": "csp_BiliGuard", + "style": { + "type": "rect", + "ratio": 1.597 + }, + "searchable": 0, + "quickSearch": 0, + "changeable": 0, + "ext": { + "json": "https://nos.netease.com/ysf/89370c8ddf36b5e1beb4d71adb921bda.txt" + } + }, + { + "key": "小学课堂", + "name": "📚小学┃课堂", + "type": 3, + "api": "csp_BiliGuard", + "style": { + "type": "rect", + "ratio": 1.597 + }, + "searchable": 0, + "quickSearch": 0, + "changeable": 0, + "ext": { + "json": "https://nos.netease.com/ysf/d7a21cf34ede56f5c686ecfba5fc7e3f.txt" + } + }, + { + "key": "初中课堂", + "name": "📚初中┃课堂", + "type": 3, + "api": "csp_BiliGuard", + "style": { + "type": "rect", + "ratio": 1.597 + }, + "searchable": 0, + "quickSearch": 0, + "changeable": 0, + "ext": { + "json": "https://nos.netease.com/ysf/8f55d520f8d70056695740ef151744a7.txt" + } + }, + { + "key": "高中教育", + "name": "📚高中┃课堂", + "type": 3, + "api": "csp_BiliGuard", + "style": { + "type": "rect", + "ratio": 1.597 + }, + "searchable": 0, + "quickSearch": 0, + "changeable": 0, + "ext": { + "json": "https://nos.netease.com/ysf/c66a4b5356141c49fd45ec51568017b4.txt" + } + }, + { + "key": "fan", + "name": "导航 www.饭太硬.com", + "type": 3, + "api": "csp_XPathGuard", + "searchable": 1, + "quickSearch": 0, + "changeable": 0 + }, + { + "key": "cc", + "name": "请勿相信视频中广告", + "type": 3, + "api": "csp_XPathGuard", + "searchable": 1, + "quickSearch": 0, + "changeable": 0 + } + ], + "rules": [ + { + "name": "cdn.ryplay", + "hosts": [ + "cdn.ryplay" + ], + "regex": [ + "5.480000", + "#EXT-X-DISCONTINUITY\\r*\\n*#EXTINF:5.480000,[\\s\\S]*?#EXT-X-DISCONTINUITY", + "#EXT-X-DISCONTINUITY\\s*\\r?\\n#EXTINF:5.320000,\\s*\\r?\\n.+\\.ts\\s*\\r?\\n(?:#EXTINF:[\\d.]+,\\s*\\r?\\n.+\\.ts\\s*\\r?\\n)*?#EXTINF:3.360000,\\s*\\r?\\n.+\\.ts\\s*\\r?\\n#EXT-X-DISCONTINUITY", + "#EXT-X-DISCONTINUITY\\s*\\r?\\n#EXTINF:4.000000,\\s*\\r?\\n.+\\.ts\\s*\\r?\\n(?:#EXTINF:[\\d.]+,\\s*\\r?\\n.+\\.ts\\s*\\r?\\n)*?#EXTINF:0.560000,\\s*\\r?\\n.+\\.ts\\s*\\r?\\n#EXT-X-DISCONTINUITY", + "#EXTINF.*?\\s+.*?1o.*?\\.ts\\s+" + ] + } + ], + "logo": "https://bdcache1-f1.v3mh.com/image/25-12-16/6941612717007.gif", + "hosts": [ + "img1.wsyzy.org=danmu.888484.xyz", + "fiizvfck.top=danmu.888484.xyz" + ], + "lives": [ + { + "name": "develop202", + "type": 0, + "url": "https://gh.927223.xyz/https://raw.githubusercontent.com/develop202/migu_video/refs/heads/main/interface.txt", + "playerType": 2, + "epg": "http://diyp5.112114.xyz/?ch={name}&date={date}", + "logo": "https://epg.112114.xyz/logo/{name}.png" + }, + { + "name": "Kimentanm", + "type": 0, + "url": "https://gh.927223.xyz/https://raw.githubusercontent.com/Kimentanm/aptv/master/m3u/iptv.m3u", + "playerType": 2 + }, + { + "name": "范明明(需开启V6网络)", + "type": 0, + "url": "https://nos.netease.com/ysf/3d75a78a0fc7ede372c03598d6d10367.m3u", + "playerType": 2 + }, + { + "name": "虎牙一起看", + "type": 0, + "url": "https://sub.ottiptv.cc/huyayqk.m3u", + "playerType": 2, + "timeout": 10, + "ua": "okHttp/Mod-1.5.0.0" + }, + { + "name": "斗鱼一起看", + "type": 0, + "url": "https://sub.ottiptv.cc/douyuyqk.m3u", + "playerType": 2, + "timeout": 10, + "ua": "okHttp/Mod-1.5.0.0" + }, + { + "name": "B站直播", + "type": 0, + "url": "https://sub.ottiptv.cc/bililive.m3u", + "playerType": 2, + "timeout": 10, + "ua": "okHttp/Mod-1.5.0.0" + }, + { + "name": "YY轮播", + "type": 0, + "url": "https://sub.ottiptv.cc/yylunbo.m3u", + "playerType": 2, + "timeout": 10, + "ua": "okHttp/Mod-1.5.0.0" + } + ] +} diff --git a/data/tvbox.meta.json b/data/tvbox.meta.json new file mode 100644 index 0000000..0cc1464 --- /dev/null +++ b/data/tvbox.meta.json @@ -0,0 +1,17 @@ +{ + "source_url": "https://www.xn--sss604efuw.com/tv/", + "final_url": "https://www.xn--sss604efuw.com/tv/", + "user_agent": "okhttp/3.15", + "raw_size": 20711, + "raw_sha256": "b993e136f723dd1fa87b413a21e0ef24b505550325d93d87d9e9d6f4da54a0e7", + "decoded_size": 13724, + "decoded_sha256": "05b5451b0935ba2d6a89e5f681e94b0b4541103a8badf115cc90577cba52339c", + "json_sha256": "5e1e3aabfdc9f138c86883749a255b1367cc50b94244d70a5066a081214f302b", + "jpeg_eoi_offset": 2399, + "appended_size": 18310, + "base64_size": 18300, + "etag": "\"69fe04c3-50e7\"", + "last_modified": "Fri, 08 May 2026 15:44:03 GMT", + "cache_control": "max-age=300", + "content_type": "text/html" +} diff --git a/scripts/extract_tvbox.py b/scripts/extract_tvbox.py new file mode 100755 index 0000000..00c8bdc --- /dev/null +++ b/scripts/extract_tvbox.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Extract, decode, and normalize a TVBox JSON configuration payload.""" + +from __future__ import annotations + +import base64 +import hashlib +import json +import os +import urllib.request +from pathlib import Path + + +def main() -> None: + source_url = os.environ["TVBOX_URL"] + user_agent = os.environ.get("TVBOX_UA", "okhttp/3.15") + output_json = Path(os.environ.get("OUT_JSON", "data/tvbox.json")) + output_meta = Path(os.environ.get("OUT_META", "data/tvbox.meta.json")) + + request = urllib.request.Request( + source_url, + headers={ + "User-Agent": user_agent, + "Accept": "*/*", + }, + ) + + with urllib.request.urlopen(request, timeout=30) as response: + raw_payload = response.read() + final_url = response.geturl() + response_headers = dict(response.headers.items()) + + # The endpoint returns a JPEG-like payload to TVBox clients. The actual + # configuration is appended after the JPEG EOI marker. + jpeg_eoi_offset = raw_payload.find(b"\xff\xd9") + if jpeg_eoi_offset < 0: + raise SystemExit("JPEG EOI marker FF D9 was not found.") + + appended_payload = raw_payload[jpeg_eoi_offset + 2 :] + + # The appended section uses a short prefix followed by the "**" separator. + separator_offset = appended_payload.find(b"**") + if separator_offset < 0: + raise SystemExit('Payload separator "**" was not found after JPEG EOI.') + + base64_payload = appended_payload[separator_offset + 2 :] + decoded_bytes = base64.b64decode(base64_payload, validate=True) + decoded_text = decoded_bytes.decode("utf-8-sig") + + # The upstream configuration may contain JSONC-style full-line comments. + # This repository stores strict JSON for compatibility with standard tools. + strict_json_text = "\n".join( + line + for line in decoded_text.splitlines() + if not line.lstrip().startswith("//") + ) + + parsed_json = json.loads(strict_json_text) + + output_json.parent.mkdir(parents=True, exist_ok=True) + output_meta.parent.mkdir(parents=True, exist_ok=True) + + output_json.write_text( + json.dumps(parsed_json, ensure_ascii=False, indent=2, sort_keys=False) + "\n", + encoding="utf-8", + ) + + metadata = { + "source_url": source_url, + "final_url": final_url, + "user_agent": user_agent, + "raw_size": len(raw_payload), + "raw_sha256": hashlib.sha256(raw_payload).hexdigest(), + "decoded_size": len(decoded_bytes), + "decoded_sha256": hashlib.sha256(decoded_bytes).hexdigest(), + "json_sha256": hashlib.sha256(output_json.read_bytes()).hexdigest(), + "jpeg_eoi_offset": jpeg_eoi_offset, + "appended_size": len(appended_payload), + "base64_size": len(base64_payload), + "etag": response_headers.get("ETag"), + "last_modified": response_headers.get("Last-Modified"), + "cache_control": response_headers.get("Cache-Control"), + "content_type": response_headers.get("Content-Type"), + } + + output_meta.write_text( + json.dumps(metadata, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + print(json.dumps(metadata, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main()