chore: prepare repository for public release

This commit is contained in:
xixu-me committed 2026-05-14 12:40:08 +08:00
1 parent 1070c96c30
commit fbeb51b315
5 files changed
+190 -11

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "08:00"
timezone: "Asia/Shanghai"
open-pull-requests-limit: 5
commit-message:
prefix: "deps"
include: "scope"
groups:
github-actions:
patterns:
- "*"
+30 -1
View File
@@ -35,14 +35,43 @@ jobs:
fi
- name: Extract and decode TVBox JSON
run: python3 scripts/extract_tvbox.py
id: extract
run: |
set +e
python3 scripts/extract_tvbox.py
status="$?"
set -e
if [ "${status}" -eq 0 ]; then
echo "upstream_error=false" >> "${GITHUB_OUTPUT}"
exit 0
fi
if [ "${status}" -eq 75 ]; then
echo "upstream_error=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
exit "${status}"
- name: Report skipped upstream update
if: ${{ steps.extract.outputs.upstream_error == 'true' }}
run: |
{
echo "### TVBox update skipped"
echo
echo "The upstream endpoint could not be fetched or decoded during this run."
echo "Existing generated files were left unchanged."
} >> "${GITHUB_STEP_SUMMARY}"
- name: Validate generated files
if: ${{ steps.extract.outputs.upstream_error == 'false' }}
run: |
python3 -m json.tool "${OUT_JSON}" > /dev/null
python3 -m json.tool "${OUT_META}" > /dev/null
- name: Commit and push changes
if: ${{ steps.extract.outputs.upstream_error == 'false' }}
run: |
if git diff --quiet -- "${OUT_JSON}" "${OUT_META}"; then
echo "No TVBox JSON update detected."
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 xixu-me
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+81
View File
@@ -0,0 +1,81 @@
# TVBox JSON Monitor
[![Monitor TVBox JSON](https://github.com/xixu-me/tvbox-json-monitor/actions/workflows/tvbox-json-monitor.yml/badge.svg)](https://github.com/xixu-me/tvbox-json-monitor/actions/workflows/tvbox-json-monitor.yml)
TVBox JSON Monitor 定时抓取一个 TVBox 兼容端点,从 JPEG-like 响应里提取附加的配置载荷,解码为严格 JSON,并在内容变化时自动提交更新。
> [!IMPORTANT]
> 本仓库只保存从配置端点解码得到的 JSON 文件,不托管视频内容,不代理媒体流,也不提供播放服务。
## 输出文件
| 文件 | 说明 |
| --- | --- |
| [`data/tvbox.json`](data/tvbox.json) | 解码并格式化后的 TVBox 配置,保持为标准 JSON。 |
| [`data/tvbox.meta.json`](data/tvbox.meta.json) | 最近一次抓取与解码的元数据,包括哈希、大小、响应头和源地址。 |
可直接使用 raw 地址读取最新 JSON:
```text
https://raw.githubusercontent.com/xixu-me/tvbox-json-monitor/main/data/tvbox.json
```
## 工作方式
上游端点返回给 TVBox 客户端的是一个 JPEG-like payload,真实配置追加在 JPEG EOI 标记 `FF D9` 之后。脚本会按以下步骤处理:
1. 使用 TVBox 风格的 `User-Agent` 请求配置端点。
2. 定位 JPEG EOI 标记 `FF D9`。
3. 读取 EOI 之后的附加载荷。
4. 在附加载荷中查找 `**` 分隔符。
5. 对分隔符之后的内容进行 Base64 解码。
6. 移除上游配置中的 JSONC 风格整行注释。
7. 解析并重新序列化为 2 空格缩进的严格 JSON。
## 自动更新
GitHub Actions 每 5 分钟检查一次上游配置,也支持在 Actions 页面手动触发。
当 `data/tvbox.json` 或 `data/tvbox.meta.json` 有变化时,工作流会提交类似下面的 commit:
```text
chore(tvbox): update decoded JSON 2026-05-13T16:00:00Z
```
如果上游端点临时不可用、返回格式异常或解码失败,本次更新会被跳过,已有数据保持不变,工作流不会因为这类上游问题失败。脚本使用退出码 `75` 标记这类上游问题;仓库变量缺失、脚本错误或文件写入失败等非上游问题仍会使工作流失败。
## 仓库配置
工作流依赖一个 GitHub Actions repository variable:
```text
Name: TVBOX_URL
Value: https://www.xn--sss604efuw.com/tv/
```
配置路径:
```text
Repository -> Settings -> Secrets and variables -> Actions -> Variables
```
由于工作流需要把生成文件提交回仓库,请确保 Actions 具备写入权限:
```text
Repository -> Settings -> Actions -> General -> Workflow permissions -> Read and write permissions
```
## 本地运行
项目只依赖 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
```
运行成功后,脚本会更新 `data/tvbox.json` 和 `data/tvbox.meta.json`,并在终端输出本次解码元数据。
+40 -10
View File
@@ -4,12 +4,21 @@
from __future__ import annotations
import base64
import binascii
import hashlib
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
UPSTREAM_ERROR_EXIT_CODE = 75
class UpstreamPayloadError(Exception):
"""Raised when the upstream endpoint cannot provide a usable payload."""
def main() -> None:
source_url = os.environ["TVBOX_URL"]
@@ -25,27 +34,41 @@ def main() -> None:
},
)
with urllib.request.urlopen(request, timeout=30) as response:
raw_payload = response.read()
final_url = response.geturl()
response_headers = dict(response.headers.items())
try:
with urllib.request.urlopen(request, timeout=30) as response:
raw_payload = response.read()
final_url = response.geturl()
response_headers = dict(response.headers.items())
except urllib.error.HTTPError as error:
raise UpstreamPayloadError(
f"Upstream endpoint returned HTTP {error.code}."
) from error
except urllib.error.URLError as error:
raise UpstreamPayloadError(
f"Could not fetch upstream endpoint: {error.reason}."
) from error
# 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.")
raise UpstreamPayloadError("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.')
raise UpstreamPayloadError(
'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")
try:
decoded_bytes = base64.b64decode(base64_payload, validate=True)
decoded_text = decoded_bytes.decode("utf-8-sig")
except (binascii.Error, UnicodeDecodeError) as error:
raise UpstreamPayloadError("Payload could not be decoded.") from error
# The upstream configuration may contain JSONC-style full-line comments.
# This repository stores strict JSON for compatibility with standard tools.
@@ -55,7 +78,10 @@ def main() -> None:
if not line.lstrip().startswith("//")
)
parsed_json = json.loads(strict_json_text)
try:
parsed_json = json.loads(strict_json_text)
except json.JSONDecodeError as error:
raise UpstreamPayloadError("Decoded payload is not valid JSON.") from error
output_json.parent.mkdir(parents=True, exist_ok=True)
output_meta.parent.mkdir(parents=True, exist_ok=True)
@@ -92,4 +118,8 @@ def main() -> None:
if __name__ == "__main__":
main()
try:
main()
except UpstreamPayloadError as error:
print(f"Upstream payload error: {error}", file=sys.stderr)
raise SystemExit(UPSTREAM_ERROR_EXIT_CODE) from error