chore: prepare repository for public release
This commit is contained in:
1 parent
1070c96c30
commit
fbeb51b315
5 files changed
+182
-3
No files matched your search
@@ -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:
|
||||
- "*"
|
||||
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,81 @@
|
||||
# TVBox JSON Monitor
|
||||
|
||||
[](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`,并在终端输出本次解码元数据。
|
||||
@@ -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:
|
||||
},
|
||||
)
|
||||
|
||||
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 :]
|
||||
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("//")
|
||||
)
|
||||
|
||||
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__":
|
||||
try:
|
||||
main()
|
||||
except UpstreamPayloadError as error:
|
||||
print(f"Upstream payload error: {error}", file=sys.stderr)
|
||||
raise SystemExit(UPSTREAM_ERROR_EXIT_CODE) from error
|
||||
Reference in new issue
Block a user