├── .github └── workflows │ └── epg.yml ├── .gitignore ├── README.md ├── md5.txt ├── requirements.txt ├── worker.js └── xml2json.py /.github/workflows/epg.yml: -------------------------------------------------------------------------------- 1 | name: EPG 2 | # 每天00:40 10:15 15:00 18:30更新 3 | on: 4 | schedule: 5 | - cron: 40 16 * * * 6 | - cron: 15 02 * * * 7 | - cron: 00 07 * * * 8 | - cron: 30 10 * * * 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - url: https://epg.112114.xyz/pp.xml.gz 19 | branch: 112114 20 | sleep: 0s 21 | - url: https://e.erw.cc/e.xml.gz 22 | branch: erw 23 | sleep: 30s 24 | #- url: http://epg.51zmt.top:8000/e.xml 25 | # branch: 51zmt 26 | # sleep: 60s 27 | 28 | steps: 29 | - name: download files 30 | run: | 31 | wget https://raw.githubusercontent.com/${{ github.repository }}/${{ matrix.branch }}/md5.txt -O md5.txt 32 | url="${{ matrix.url }}" 33 | if [[ "$url" == *".gz" ]]; then 34 | echo "url is end with gz" 35 | wget $url -O e.xml.gz 36 | gzip -df e.xml.gz 37 | else 38 | echo "url is not end with gz" 39 | wget $url -O e.xml 40 | fi 41 | 42 | - name: check md5 43 | run: | 44 | md5=$(md5sum ./e.xml | awk '{print $1}') 45 | if ! grep -q "$md5" ./md5.txt; then 46 | echo "isDiff=$md5" >> $GITHUB_ENV 47 | now_time=$(date -u -d '+8 hour' '+%Y-%m-%d %H:%M:%S') 48 | echo "now_time=$now_time" >> $GITHUB_ENV 49 | tag=$(date -u -d '+8 hour' '+%Y.%m.%d') 50 | echo "tag=$tag" >> $GITHUB_ENV 51 | fi 52 | 53 | - name: checkout code 54 | uses: actions/checkout@v4 55 | if: ${{ env.isDiff }} 56 | with: 57 | ref: ${{ matrix.branch }} 58 | path: epg 59 | 60 | - uses: actions/setup-python@v5 61 | if: ${{ env.isDiff }} 62 | with: 63 | python-version: '3.12' 64 | 65 | - name: xml to json 66 | if: ${{ env.isDiff }} 67 | working-directory: epg 68 | run: | 69 | mkdir ./xml ./json 70 | mv -f ../e.xml ./xml/e.xml 71 | pip install -r requirements.txt 72 | python xml2json.py 73 | 74 | - name: move file 75 | if: ${{ env.isDiff }} 76 | working-directory: epg 77 | run: | 78 | now_date=$(date -u -d '+8 hour' '+%Y-%m-%d') 79 | mv -f ./xml/e.xml ./xml/${{ matrix.branch }}.xml 80 | mv -f ./json/e.json ./json/${{ matrix.branch }}.json 81 | echo "${{ env.isDiff }}" > ./md5.txt 82 | sleep ${{ matrix.sleep }} 83 | 84 | - uses: ncipollo/release-action@v1 85 | if: ${{ env.isDiff }} 86 | with: 87 | tag: ${{ env.tag }} 88 | body: "更新时间: ${{ env.now_time }}" 89 | allowUpdates: true 90 | artifacts: | 91 | epg/xml/*.xml 92 | epg/json/*.json 93 | 94 | - name: Commit 95 | if: ${{ env.isDiff }} 96 | working-directory: epg 97 | run: | 98 | git config --local user.name "github-actions[bot]" 99 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 100 | sed "2c * 更新时间: ${{ env.now_time }}" README.md -i 101 | rm -rf ./xml 102 | rm -rf ./json 103 | git add --all 104 | git commit -m "${{ env.now_time }}" 105 | git push -f origin ${{ matrix.branch }} 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### 节目表 2 | * 更新时间: 2025-06-06 00:51:36 3 | * 数据来自[sparks/epg](https://github.com/sparkssssssssss/epg),将2024/02/14(含)以后的数据备份至本项目 4 | * 将`worker.js`的内容复制到[CF Workers](https://workers.cloudflare.com/)中保存,即可从本项目获取节目表数据 5 | - 不论是`XML`格式还是`Diyp & 百川`的格式,都直接填`对应URL`即可 6 | - `Diyp & 百川`接口测试:`对应URL?ch=CCTV1&date=2024-02-14` 7 | * **因CF访问到达上限,原URL已停用,请大家自行部署。** 8 | * 本项目不保证节目表的准确性,仅供学习交流使用,如有侵权,请联系删除 9 | -------------------------------------------------------------------------------- /md5.txt: -------------------------------------------------------------------------------- 1 | 4702921d221f80df3109a076299a8f27 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | xmltodict -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | const Config = { 2 | repository: 'celetor/epg', 3 | branch: '112114' 4 | } 5 | 6 | const init = { 7 | status: 200, 8 | headers: { 9 | 'content-type': 'application/json' 10 | }, 11 | }; 12 | 13 | 14 | async function jq_fetch(request) { 15 | let times = 5; 16 | let real_url = ''; 17 | let isRedirect = false; 18 | let response = await fetch(request); 19 | 20 | while (times > 0) { 21 | console.log('status', response.status); 22 | if (response.status === 301 || response.status === 302) { 23 | isRedirect = true; 24 | real_url = response.headers.get('location'); 25 | } else if (response.redirected === true) { 26 | isRedirect = true; 27 | real_url = response.url; 28 | } else { 29 | break; 30 | } 31 | if (isRedirect) { 32 | console.log('real_url', real_url); 33 | let init = { 34 | 'headers': {} 35 | }; 36 | for (var p of response.headers) { 37 | if (p[0].toLowerCase() !== 'location') { 38 | if (p[0].toLowerCase() === 'set-cookie') { 39 | init.headers['cookie'] = p[1]; 40 | } else { 41 | init.headers[p[0]] = p[1]; 42 | } 43 | } 44 | } 45 | response = await fetch(new Request(real_url, init)); 46 | console.log('response', response); 47 | times -= 1; 48 | } 49 | } 50 | return response; 51 | } 52 | 53 | function makeRes(body, status = 200, headers = {}) { 54 | headers['access-control-allow-origin'] = '*'; 55 | return new Response(body, {status, headers}); 56 | } 57 | 58 | function getNowDate() { 59 | const utc_timestamp = (new Date()).getTime(); 60 | const china_time = new Date(utc_timestamp + 8 * 60 * 60 * 1000); 61 | const month = china_time.getMonth() + 1; 62 | const day = china_time.getDate(); 63 | return `${china_time.getFullYear()}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day}`; 64 | } 65 | 66 | function getFormatTime(time) { 67 | // 20231129002400 +0800 68 | let result = { 69 | date: '', 70 | time: '' 71 | }; 72 | 73 | if (time.length < 8) { 74 | result['date'] = getNowDate(); 75 | return result; 76 | } 77 | 78 | let year = time.substring(0, 4); 79 | let month = time.substring(4, 6); 80 | let day = time.substring(6, 8); 81 | result['date'] = year + '-' + month + '-' + day; 82 | 83 | if (time.length >= 12) { 84 | let hour = time.substring(8, 10); 85 | let minute = time.substring(10, 12); 86 | result['time'] = hour + ':' + minute; 87 | } 88 | return result; 89 | } 90 | 91 | async function diypHandle(channel, date, request) { 92 | const tag = date.replaceAll('-', '.'); 93 | // https://github.com/celetor/epg/releases/download/2024.02.14/112114.json 94 | const res = await jq_fetch(new Request(`https://github.com/${Config.repository}/releases/download/${tag}/${Config.branch}.json`, request)); 95 | const response = await res.json(); 96 | 97 | console.log(channel, date); 98 | const program_info = { 99 | "date": date, 100 | "channel_name": channel, 101 | "url": `https://github.com/${Config.repository}`, 102 | "epg_data": [] 103 | } 104 | response.forEach(function (element) { 105 | if (element['@channel'] === channel && element['@start'].startsWith(date.replaceAll('-', ''))) { 106 | program_info['epg_data'].push({ 107 | "start": getFormatTime(element['@start'])['time'], 108 | "end": getFormatTime(element['@stop'])['time'], 109 | "title": element['title']['#text'], 110 | "desc": (element['desc'] && element['desc']['#text']) ? element['desc']['#text'] : '' 111 | }); 112 | } 113 | 114 | }); 115 | console.log(program_info); 116 | if (program_info['epg_data'].length === 0) { 117 | program_info['epg_data'].push({ 118 | "start": "00:00", 119 | "end": "23:59", 120 | "title": "未知节目", 121 | "desc": "" 122 | }); 123 | } 124 | return new Response(JSON.stringify(program_info), init); 125 | } 126 | 127 | async function fetchHandler(event) { 128 | let request = event.request; 129 | let uri = new URL(request.url); 130 | 131 | let channel = uri.searchParams.get("ch"); 132 | if (!channel || channel.length === 0) { 133 | const xml_res = await jq_fetch(new Request( 134 | `https://github.com/${Config.repository}/releases/latest/download/${Config.branch}.xml`, request 135 | )); 136 | const xml_blob = await xml_res.blob(); 137 | init['headers']['content-type'] = 'text/xml'; 138 | return new Response(xml_blob, init); 139 | } 140 | 141 | let date = uri.searchParams.get("date"); 142 | if (date) { 143 | date = getFormatTime(date.replace(/\D+/g, ''))['date']; 144 | } else { 145 | date = getNowDate(); 146 | } 147 | 148 | channel = channel.replaceAll('-', '').toUpperCase(); 149 | if (parseInt(date.replaceAll('-', '')) >= 20240214) { 150 | return diypHandle(channel, date, request); 151 | } else { 152 | return new Response(JSON.stringify({ 153 | "date": date, 154 | "channel_name": channel, 155 | "url": `https://github.com/${Config.repository}`, 156 | "epg_data": [{ 157 | "start": "00:00", 158 | "end": "23:59", 159 | "title": "未知节目", 160 | "desc": "" 161 | }] 162 | }), init); 163 | } 164 | } 165 | 166 | addEventListener('fetch', event => { 167 | const ret = fetchHandler(event).catch(err => makeRes('cfworker error:\n' + err.stack, 502)); 168 | event.respondWith(ret) 169 | }) 170 | -------------------------------------------------------------------------------- /xml2json.py: -------------------------------------------------------------------------------- 1 | import xmltodict 2 | import json 3 | 4 | with open('./xml/e.xml', 'r', encoding='utf-8') as f: 5 | xml_data = f.read() 6 | 7 | xml_dict = xmltodict.parse(xml_data) 8 | 9 | channel = xml_dict['tv']['channel'] 10 | programme = xml_dict['tv']['programme'] 11 | 12 | for k in programme: 13 | for v in channel: 14 | if v['@id'] == k['@channel']: 15 | k['@channel'] = v['display-name']['#text'] 16 | break 17 | 18 | with open('./json/e.json', 'w', encoding='utf-8') as file: 19 | json.dump(programme, file, indent=4, ensure_ascii=False) 20 | --------------------------------------------------------------------------------