├── data
├── info.json
├── region.json
└── cdn.json
├── server
├── cron.go
├── main.go
├── store.go
└── service.go
├── LICENSE
├── readme.md
├── .github
└── workflows
│ ├── deploy-pages.yml
│ └── update-cdn-data.yml
└── script
└── ccb.js
/data/info.json:
--------------------------------------------------------------------------------
1 | {
2 | "lastSuccessTime": "2025-12-20T20:20:58Z"
3 | }
--------------------------------------------------------------------------------
/data/region.json:
--------------------------------------------------------------------------------
1 | [
2 | "北京",
3 | "上海",
4 | "广州",
5 | "深圳",
6 | "杭州",
7 | "成都",
8 | "南京",
9 | "天津",
10 | "武汉",
11 | "福建",
12 | "郑州",
13 | "西安",
14 | "沈阳",
15 | "哈市",
16 | "呼市",
17 | "新疆",
18 | "外建",
19 | "香港",
20 | "海外"
21 | ]
--------------------------------------------------------------------------------
/server/cron.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/robfig/cron/v3"
5 | )
6 |
7 | // 定时任务, 每七天的凌晨四点更新一次子域名列表
8 | func cronTask() {
9 | c := cron.New()
10 | c.AddFunc("0 4 */7 * *", func() {
11 | updateSubDomainData()
12 | })
13 | c.Start()
14 | }
15 |
--------------------------------------------------------------------------------
/server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/gin-gonic/gin"
4 |
5 | func main() {
6 |
7 | fillRegionList()
8 | cronTask()
9 |
10 | r := gin.Default()
11 |
12 | // region api
13 | r.GET("/region", func(c *gin.Context) {
14 | c.JSON(200, gin.H{
15 | "data": regionList,
16 | })
17 | requestCount++
18 | })
19 |
20 | // cdn api
21 | r.GET("/cdn", func(c *gin.Context) {
22 | if cdnMap == nil || len(cdnMap) == 0 {
23 | updateSubDomainData()
24 | }
25 | c.JSON(200, gin.H{
26 | "data": cdnMap[c.Query("region")],
27 | })
28 | requestCount++
29 | })
30 |
31 | // info api
32 | r.GET("/info", func(c *gin.Context) {
33 | c.JSON(200, gin.H{
34 | "info": "查看服务的相关信息",
35 | "lastSuccessTime": lastSuccessTime,
36 | "requestCount": requestCount,
37 | "regionList": regionList,
38 | "cdnMap": cdnMap,
39 | })
40 | })
41 |
42 | r.Run()
43 | }
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 鼠鼠今天吃嘉然
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/server/store.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "time"
4 |
5 | type Region struct {
6 | Abbr string
7 | Name string
8 | }
9 |
10 | var (
11 | // 如果要自行添加地区, 就在这里加
12 | regionPatternMap = []Region{
13 | {Abbr: "-bj", Name: "北京"},
14 | {Abbr: "-sh-", Name: "上海"},
15 | {Abbr: "-gdgz-", Name: "广州"},
16 | {Abbr: "-sz-", Name: "深圳"},
17 | {Abbr: "-zjhz-", Name: "杭州"},
18 | {Abbr: "-sccd-", Name: "成都"},
19 | {Abbr: "-jsnj-", Name: "南京"},
20 | {Abbr: "-tj-", Name: "天津"},
21 | {Abbr: "-hbwh-", Name: "武汉"},
22 | {Abbr: "-fj", Name: "福建"},
23 | {Abbr: "-hnzz-", Name: "郑州"},
24 | {Abbr: "-sxxa-", Name: "西安"},
25 | {Abbr: "-lnsy-", Name: "沈阳"},
26 | {Abbr: "-hljheb-", Name: "哈市"},
27 | {Abbr: "-nmghhht-", Name: "呼市"},
28 | {Abbr: "-xj-", Name: "新疆"},
29 | {Abbr: "-gotcha", Name: "外建"},
30 | {Abbr: "-hk-", Name: "香港"},
31 | {Abbr: "-kaigai-", Name: "海外"},
32 | }
33 |
34 | kaigaiCdnList = []string{
35 | "upos-hz-mirrorakam.akamaized.net",
36 | "upos-sz-mirroraliov.bilivideo.com",
37 | "upos-sz-mirrorcosov.bilivideo.com",
38 | }
39 |
40 | fuzhouCdnList = []string{
41 | "cn-fjfz-fx-01-01.bilivideo.com",
42 | "cn-fjfz-fx-01-02.bilivideo.com",
43 | "cn-fjfz-fx-01-03.bilivideo.com",
44 | "cn-fjfz-fx-01-04.bilivideo.com",
45 | "cn-fjfz-fx-01-05.bilivideo.com",
46 | "cn-fjfz-fx-01-06.bilivideo.com",
47 | }
48 |
49 | regionList = make([]string, 0, len(regionPatternMap))
50 |
51 | // region : [url]
52 | cdnMap = make(map[string][]string)
53 |
54 | lastSuccessTime = time.Now()
55 |
56 | requestCount = 0
57 | )
58 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Custom CDN of Bilibili (CCB) - 修改B站视频的CDN
2 |
3 |
4 |
5 | ## 项目介绍
6 |
7 | 通过解析 bilivideo.com 的子域名列表获取 CDN 域名列表,以及修改网页的 DOM 元素,实现切换批站的播放源。
8 |
9 | 
10 |
11 |
12 |
13 | ## 更新介绍
14 |
15 | 版本 1.0.1 更新:
16 |
17 | 1. 提供强力模式——开启该模式后,脚本会通过强制修改 backupUrl 数组,实现强制切换节点的功能;
18 |
19 | 2. 支持强力模式 开启/关闭。该模式默认关闭,点击油猴脚本的设置处即可进行开关(具体位置可以见图);
20 |
21 | 3. 在地区框选择 "编辑",即可在右边出现的编辑框输入自定义的视频源地址,回车键触发修改(请确保手动输入的视频源有该视频资源);
22 |
23 |
24 |
25 |
26 | ## 使用注意
27 |
28 | 1. 对于锁区视频无效,且无法强制切换大区(比如大陆用户即使选择了杭州的Akamai节点,也会被强制送回大陆);
29 |
30 | 2. 对于该源不存在的视频资源,批站也会强制分配新的节点,会导致选了也是白选;
31 |
32 | 3. 如遇到切换失败,并且控制台报错显示 403,那大概率是 DNS 等网络相关的问题,挂上代理就行(不需要全局,规则模式就行,重要的是让 DNS 走代理);
33 |
34 | 4. 通过定时服务更新部署在github page上的文档,如果刷新不出来地区和节点列表,那估计你是在白名单地区(胡建、荷兰、苏联等),请挂梯子;
35 |
36 | 5. 考虑到运营商当前的各种 QoS,如果感觉视频很卡,那大概率是你脸黑,多试试同运营商同省的节点;
37 |
38 |
39 | ## 项目结构
40 |
41 | 1. script - 前端脚本;
42 |
43 | 2. server - 后端服务,可以直接部署在服务器上;
44 |
45 | 3. data 和 .github - 定时执行 workflow,然后把 json 数据保存下来提供静态访问,该模块的主要目的是节约服务器成本;
46 |
47 |
48 | ## 二开注意
49 |
50 | 1. 如果直接部署在服务器上,并且想要实现定时更新功能,那么记得修改 cron.go 文件;
51 |
52 | 2. 如果想增加地区,一共有 3 个地方的代码要同时改,分别是 store.go、region.json、update-cdn-data.yml
53 |
54 | 3. 如果想增加适配的页面,那么在修改 ccb.js 的时候,记得同时修改 @match 和 location.href.startsWith
55 |
56 |
57 | ## 项目地址
58 | https://github.com/Kanda-Akihito-Kun/ccb
59 |
60 |
61 | ## 插件下载地址
62 | https://greasyfork.org/zh-CN/scripts/527498-custom-cdn-of-bilibili-ccb-%E4%BF%AE%E6%94%B9%E5%93%94%E5%93%A9%E5%93%94%E5%93%A9%E7%9A%84%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E6%BA%90?locale_override=1
63 |
64 |
65 | ## 联系方式
66 |
67 | B站用户:鼠鼠今天吃嘉然 / GitHub 提 issue
68 |
--------------------------------------------------------------------------------
/server/service.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net/http"
9 | "sort"
10 | "strings"
11 | "time"
12 | )
13 |
14 | func fillRegionList() {
15 | for _, v := range regionPatternMap {
16 | regionList = append(regionList, v.Name)
17 | }
18 | }
19 |
20 | func matchSubDomainToRegion(response Response) {
21 | for _, subDomain := range response.Data.Result {
22 | for _, v := range regionPatternMap {
23 | if strings.Contains(subDomain, v.Abbr) {
24 | cdnMap[v.Name] = append(cdnMap[v.Name], subDomain)
25 | }
26 | }
27 | }
28 | }
29 |
30 | type Response struct {
31 | Status bool `json:"status"`
32 | Code int `json:"code"`
33 | Msg string `json:"msg"`
34 | Data struct {
35 | Domain string `json:"domain"`
36 | Page int `json:"page"`
37 | PageSize int `json:"pageSize"`
38 | Result []string `json:"result"`
39 | } `json:"data"`
40 | }
41 |
42 | /*
43 | 更新 bilivideo.com 的子域名记录,
44 | 但是由于接口的限流策略导致请求多了可能会炸, 所以别搞太猛
45 | */
46 | func updateSubDomainData() {
47 | cdnMap = make(map[string][]string)
48 |
49 | // 这个接口一次请求不全, 要分页获取
50 | for i := range 20 {
51 | url := fmt.Sprintf("https://chaziyu.com/ipchaxun.do?domain=bilivideo.com&page=%d", i)
52 |
53 | client := &http.Client{
54 | Timeout: 10 * time.Second,
55 | }
56 |
57 | req, err := http.NewRequest("GET", url, nil)
58 | if err != nil {
59 | log.Printf("创建请求失败 [%d]: %v", i, err)
60 | return
61 | }
62 |
63 | // 添加请求头
64 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
65 | req.Header.Set("Accept", "application/json")
66 | req.Header.Set("Referer", "https://chaziyu.com")
67 |
68 | resp, err := client.Do(req)
69 | defer resp.Body.Close()
70 |
71 | body, err := io.ReadAll(resp.Body)
72 | if err != nil {
73 | log.Printf("读取响应失败 [%d]: %v", i, err)
74 | return
75 | }
76 |
77 | var response Response
78 | if err := json.Unmarshal(body, &response); err != nil {
79 | log.Printf("解析JSON失败 [%d]: %v", i, err)
80 | return
81 | }
82 |
83 | matchSubDomainToRegion(response)
84 | }
85 |
86 | // 手动添加
87 | cdnMap["海外"] = kaigaiCdnList
88 | cdnMap["福建"] = append(fuzhouCdnList, cdnMap["福建"]...)
89 |
90 | // 节点内部排序
91 | for region := range cdnMap {
92 | sort.Strings(cdnMap[region])
93 | }
94 |
95 | lastSuccessTime = time.Now()
96 | requestCount = 0
97 | }
98 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | schedule:
7 | - cron: '0 21 * * 6' # 每周日东八区凌晨 5 点 (UTC 的前一天 21 点) 运行
8 | workflow_dispatch:
9 |
10 | permissions:
11 | contents: read
12 | pages: write
13 | id-token: write
14 | actions: read
15 |
16 | concurrency:
17 | group: "pages"
18 | cancel-in-progress: false
19 |
20 | jobs:
21 | deploy:
22 | environment:
23 | name: github-pages
24 | url: ${{ steps.deployment.outputs.page_url }}
25 | runs-on: ubuntu-latest
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v4
29 |
30 | - name: Setup Pages
31 | uses: actions/configure-pages@v4
32 |
33 | - name: Create API Directory
34 | run: |
35 | mkdir -p _site/api
36 | cp data/*.json _site/api/
37 | # 创建API文档页面
38 | cat > _site/index.html << 'EOL'
39 |
40 |
41 |
42 | CCB API Documentation
43 |
44 |
45 |
50 |
51 |
52 | CCB API Documentation
53 |
54 |
Get Region List
55 |
Returns the list of available regions.
56 |
GET /api/region.json
57 |
58 |
59 |
Get CDN Data
60 |
Returns the CDN data for all regions.
61 |
GET /api/cdn.json
62 |
63 |
64 |
Get Service Info
65 |
Returns service information including last update time.
66 |
GET /api/info.json
67 |
68 |
69 |
70 | EOL
71 |
72 | - name: Upload artifact
73 | uses: actions/upload-pages-artifact@v3
74 |
75 | - name: Deploy to GitHub Pages
76 | id: deployment
77 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/.github/workflows/update-cdn-data.yml:
--------------------------------------------------------------------------------
1 | name: Update CDN Data
2 |
3 | on:
4 | push:
5 | branches: [ main ] # 当推送到 main 分支时触发
6 | schedule:
7 | - cron: '0 20 * * 6' # 每周日东八区凌晨 4 点 (UTC 的前一天 20 点) 运行
8 | workflow_dispatch: # 允许手动触发
9 |
10 | jobs:
11 | update-cdn:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v4
18 | with:
19 | go-version: '1.20'
20 |
21 | - name: Create Data Directory
22 | run: mkdir -p data
23 |
24 | - name: Update CDN Data
25 | run: |
26 | cat > update.go << 'EOL'
27 | package main
28 |
29 | import (
30 | "encoding/json"
31 | "fmt"
32 | "io"
33 | "log"
34 | "net/http"
35 | "sort"
36 | "os"
37 | "strings"
38 | "time"
39 | )
40 |
41 | type Region struct {
42 | Abbr string
43 | Name string
44 | }
45 |
46 | type Response struct {
47 | Status bool `json:"status"`
48 | Code int `json:"code"`
49 | Msg string `json:"msg"`
50 | Data struct {
51 | Domain string `json:"domain"`
52 | Page int `json:"page"`
53 | PageSize int `json:"pageSize"`
54 | Result []string `json:"result"`
55 | } `json:"data"`
56 | }
57 |
58 | var (
59 | regionPatternMap = []Region{
60 | {Abbr: "-bj", Name: "北京"},
61 | {Abbr: "-sh-", Name: "上海"},
62 | {Abbr: "-gdgz-", Name: "广州"},
63 | {Abbr: "-sz-", Name: "深圳"},
64 | {Abbr: "-zjhz-", Name: "杭州"},
65 | {Abbr: "-sccd-", Name: "成都"},
66 | {Abbr: "-jsnj-", Name: "南京"},
67 | {Abbr: "-tj-", Name: "天津"},
68 | {Abbr: "-hbwh-", Name: "武汉"},
69 | {Abbr: "-fj", Name: "福建"},
70 | {Abbr: "-hnzz-", Name: "郑州"},
71 | {Abbr: "-sxxa-", Name: "西安"},
72 | {Abbr: "-lnsy-", Name: "沈阳"},
73 | {Abbr: "-hljheb-", Name: "哈市"},
74 | {Abbr: "-nmghhht-", Name: "呼市"},
75 | {Abbr: "-xj-", Name: "新疆"},
76 | {Abbr: "-gotcha", Name: "外建"},
77 | {Abbr: "-hk-", Name: "香港"},
78 | {Abbr: "-kaigai-", Name: "海外"},
79 | }
80 |
81 | kaigaiCdnList = []string{
82 | "upos-hz-mirrorakam.akamaized.net",
83 | "upos-sz-mirroraliov.bilivideo.com",
84 | "upos-sz-mirrorcosov.bilivideo.com",
85 | }
86 |
87 | fuzhouCdnList = []string{
88 | "cn-fjfz-fx-01-01.bilivideo.com",
89 | "cn-fjfz-fx-01-02.bilivideo.com",
90 | "cn-fjfz-fx-01-03.bilivideo.com",
91 | "cn-fjfz-fx-01-04.bilivideo.com",
92 | "cn-fjfz-fx-01-05.bilivideo.com",
93 | "cn-fjfz-fx-01-06.bilivideo.com",
94 | }
95 |
96 | cdnMap = make(map[string][]string)
97 | )
98 |
99 | func matchSubDomainToRegion(response Response) {
100 | for _, subDomain := range response.Data.Result {
101 | for _, v := range regionPatternMap {
102 | if strings.Contains(subDomain, v.Abbr) {
103 | cdnMap[v.Name] = append(cdnMap[v.Name], subDomain)
104 | }
105 | }
106 | }
107 | }
108 |
109 | func main() {
110 | for i := 0; i < 20; i++ {
111 | url := fmt.Sprintf("https://chaziyu.com/ipchaxun.do?domain=bilivideo.com&page=%d", i)
112 | client := &http.Client{Timeout: 10 * time.Second}
113 | req, err := http.NewRequest("GET", url, nil)
114 |
115 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
116 | req.Header.Set("Accept", "application/json")
117 | req.Header.Set("Referer", "https://chaziyu.com")
118 |
119 | resp, err := client.Do(req)
120 | if err != nil {
121 | log.Printf("请求失败 [%d]: %v", i, err)
122 | continue
123 | }
124 |
125 | body, err := io.ReadAll(resp.Body)
126 | resp.Body.Close()
127 | if err != nil {
128 | log.Printf("读取响应失败 [%d]: %v", i, err)
129 | continue
130 | }
131 |
132 | var response Response
133 | if err := json.Unmarshal(body, &response); err != nil {
134 | log.Printf("解析JSON失败 [%d]: %v", i, err)
135 | continue
136 | }
137 |
138 | matchSubDomainToRegion(response)
139 | }
140 |
141 | // 手动添加
142 | cdnMap["海外"] = kaigaiCdnList
143 | cdnMap["福建"] = append(fuzhouCdnList, cdnMap["福建"]...)
144 |
145 | // 节点内部排序
146 | for region := range cdnMap {
147 | sort.Strings(cdnMap[region])
148 | }
149 |
150 | // 生成region列表
151 | var regionList []string
152 | for _, v := range regionPatternMap {
153 | regionList = append(regionList, v.Name)
154 | }
155 |
156 | // 保存CDN数据
157 | cdnData, err := json.MarshalIndent(cdnMap, "", " ")
158 | if err != nil {
159 | log.Fatal("序列化CDN数据失败:", err)
160 | }
161 | if err := os.WriteFile("data/cdn.json", cdnData, 0644); err != nil {
162 | log.Fatal("保存CDN数据失败:", err)
163 | }
164 |
165 | // 保存地区列表
166 | regionData, err := json.MarshalIndent(regionList, "", " ")
167 | if err != nil {
168 | log.Fatal("序列化地区列表失败:", err)
169 | }
170 | if err := os.WriteFile("data/region.json", regionData, 0644); err != nil {
171 | log.Fatal("保存地区列表失败:", err)
172 | }
173 |
174 | // 保存更新时间
175 | info := map[string]interface{}{
176 | "lastSuccessTime": time.Now().Format(time.RFC3339),
177 | }
178 | infoData, err := json.MarshalIndent(info, "", " ")
179 | if err != nil {
180 | log.Fatal("序列化信息失败:", err)
181 | }
182 | if err := os.WriteFile("data/info.json", infoData, 0644); err != nil {
183 | log.Fatal("保存信息失败:", err)
184 | }
185 | }
186 | EOL
187 |
188 | go run update.go
189 |
190 | - name: Commit and Push Changes
191 | run: |
192 | git config --global user.name 'GitHub Action'
193 | git config --global user.email 'action@github.com'
194 | git add data/
195 | git commit -m "Update CDN data $(date -u)"
196 | git push
--------------------------------------------------------------------------------
/data/cdn.json:
--------------------------------------------------------------------------------
1 | {
2 | "上海": [
3 | "cn-sh-ct-01-06.bilivideo.com",
4 | "cn-sh-ct-01-13.bilivideo.com",
5 | "cn-sh-ct-01-15.bilivideo.com",
6 | "cn-sh-ct-01-23.bilivideo.com",
7 | "cn-sh-ct-01-24.bilivideo.com",
8 | "cn-sh-ct-01-35.bilivideo.com",
9 | "cn-sh-ct-01-36.bilivideo.com",
10 | "cn-sh-office-bcache-01.bilivideo.com"
11 | ],
12 | "北京": [
13 | "cn-bj-cc-03-14.bilivideo.com",
14 | "cn-bj-cc-03-17.bilivideo.com",
15 | "cn-bj-fx-01-01.bilivideo.com",
16 | "cn-bj-fx-01-04.bilivideo.com",
17 | "cn-bj-fx-01-05.bilivideo.com",
18 | "cn-bj-se-01-05.bilivideo.com"
19 | ],
20 | "南京": [
21 | "cn-jsnj-fx-02-05.bilivideo.com",
22 | "cn-jsnj-fx-02-07.bilivideo.com",
23 | "cn-jsnj-fx-02-10.bilivideo.com",
24 | "cn-jsnj-gd-01-02.bilivideo.com"
25 | ],
26 | "呼市": [
27 | "cn-nmghhht-cm-01-11.bilivideo.com",
28 | "cn-nmghhht-cu-01-01.bilivideo.com",
29 | "cn-nmghhht-cu-01-08.bilivideo.com",
30 | "cn-nmghhht-cu-01-09.bilivideo.com",
31 | "cn-nmghhht-cu-01-10.bilivideo.com",
32 | "cn-nmghhht-cu-01-12.bilivideo.com",
33 | "cn-nmghhht-cu-01-15.bilivideo.com"
34 | ],
35 | "哈市": [
36 | "cn-hljheb-cm-01-01.bilivideo.com",
37 | "cn-hljheb-cm-01-03.bilivideo.com",
38 | "cn-hljheb-ct-01-02.bilivideo.com",
39 | "cn-hljheb-ct-01-03.bilivideo.com",
40 | "cn-hljheb-ct-01-04.bilivideo.com",
41 | "cn-hljheb-ct-01-07.bilivideo.com"
42 | ],
43 | "外建": [
44 | "c0--cn-gotcha01.bilivideo.com",
45 | "d0--cn-gotcha09.bilivideo.com",
46 | "d1--cn-gotcha101.bilivideo.com",
47 | "d1--cn-gotcha102.bilivideo.com",
48 | "d1--cn-gotcha204-1.bilivideo.com",
49 | "d1--cn-gotcha204-2.bilivideo.com",
50 | "d1--cn-gotcha204-3.bilivideo.com",
51 | "d1--cn-gotcha204-4.bilivideo.com",
52 | "d1--cn-gotcha207.bilivideo.com",
53 | "d1--cn-gotcha211.bilivideo.com",
54 | "d1--cn-gotcha308.bilivideo.com",
55 | "d1--ov-gotcha01.bilivideo.com",
56 | "d1--ov-gotcha03.bilivideo.com",
57 | "d1--ov-gotcha207.bilivideo.com",
58 | "d1--ov-gotcha207b.bilivideo.com",
59 | "d1--ov-gotcha208.bilivideo.com",
60 | "d1--ov-gotcha209.bilivideo.com",
61 | "d1--ov-gotcha210.bilivideo.com",
62 | "d1--p1--cn-gotcha04.bilivideo.com",
63 | "d1--tf-gotcha04.bilivideo.com"
64 | ],
65 | "天津": [
66 | "cn-tj-cm-02-01.bilivideo.com",
67 | "cn-tj-cm-02-02.bilivideo.com",
68 | "cn-tj-cm-02-04.bilivideo.com",
69 | "cn-tj-cm-02-05.bilivideo.com",
70 | "cn-tj-cm-02-06.bilivideo.com",
71 | "cn-tj-cm-02-07.bilivideo.com",
72 | "cn-tj-cu-01-02.bilivideo.com",
73 | "cn-tj-cu-01-03.bilivideo.com",
74 | "cn-tj-cu-01-04.bilivideo.com",
75 | "cn-tj-cu-01-05.bilivideo.com",
76 | "cn-tj-cu-01-06.bilivideo.com",
77 | "cn-tj-cu-01-07.bilivideo.com",
78 | "cn-tj-cu-01-09.bilivideo.com",
79 | "cn-tj-cu-01-10.bilivideo.com",
80 | "cn-tj-cu-01-11.bilivideo.com",
81 | "cn-tj-cu-01-12.bilivideo.com",
82 | "cn-tj-cu-01-13.bilivideo.com"
83 | ],
84 | "广州": [
85 | "cn-gdgz-cm-01-02.bilivideo.com",
86 | "cn-gdgz-cm-01-10.bilivideo.com",
87 | "cn-gdgz-fx-01-01.bilivideo.com",
88 | "cn-gdgz-fx-01-02.bilivideo.com",
89 | "cn-gdgz-fx-01-03.bilivideo.com",
90 | "cn-gdgz-fx-01-04.bilivideo.com",
91 | "cn-gdgz-fx-01-06.bilivideo.com",
92 | "cn-gdgz-fx-01-08.bilivideo.com",
93 | "cn-gdgz-fx-01-09.bilivideo.com",
94 | "cn-gdgz-fx-01-10.bilivideo.com",
95 | "cn-gdgz-gd-01-01.bilivideo.com"
96 | ],
97 | "成都": [
98 | "cn-sccd-cm-03-01.bilivideo.com",
99 | "cn-sccd-cm-03-02.bilivideo.com",
100 | "cn-sccd-cm-03-05.bilivideo.com",
101 | "cn-sccd-ct-01-02.bilivideo.com",
102 | "cn-sccd-ct-01-08.bilivideo.com",
103 | "cn-sccd-ct-01-10.bilivideo.com",
104 | "cn-sccd-ct-01-17.bilivideo.com",
105 | "cn-sccd-ct-01-18.bilivideo.com",
106 | "cn-sccd-ct-01-19.bilivideo.com",
107 | "cn-sccd-ct-01-20.bilivideo.com",
108 | "cn-sccd-ct-01-21.bilivideo.com",
109 | "cn-sccd-ct-01-22.bilivideo.com",
110 | "cn-sccd-ct-01-23.bilivideo.com",
111 | "cn-sccd-ct-01-24.bilivideo.com",
112 | "cn-sccd-ct-01-25.bilivideo.com",
113 | "cn-sccd-ct-01-26.bilivideo.com",
114 | "cn-sccd-ct-01-27.bilivideo.com",
115 | "cn-sccd-ct-01-29.bilivideo.com",
116 | "cn-sccd-cu-01-02.bilivideo.com",
117 | "cn-sccd-cu-01-03.bilivideo.com",
118 | "cn-sccd-cu-01-04.bilivideo.com",
119 | "cn-sccd-cu-01-05.bilivideo.com",
120 | "cn-sccd-cu-01-06.bilivideo.com",
121 | "cn-sccd-cu-01-07.bilivideo.com",
122 | "cn-sccd-cu-01-09.bilivideo.com",
123 | "cn-sccd-fx-01-01.bilivideo.com",
124 | "cn-sccd-fx-01-06.bilivideo.com"
125 | ],
126 | "新疆": [
127 | "cn-xj-cm-02-01.bilivideo.com",
128 | "cn-xj-cm-02-03.bilivideo.com",
129 | "cn-xj-cm-02-04.bilivideo.com",
130 | "cn-xj-cm-02-06.bilivideo.com",
131 | "cn-xj-ct-01-01.bilivideo.com",
132 | "cn-xj-ct-01-02.bilivideo.com",
133 | "cn-xj-ct-01-03.bilivideo.com",
134 | "cn-xj-ct-01-04.bilivideo.com",
135 | "cn-xj-ct-01-05.bilivideo.com",
136 | "cn-xj-ct-02-02.bilivideo.com"
137 | ],
138 | "杭州": [
139 | "cn-zjhz-cm-01-01.bilivideo.com",
140 | "cn-zjhz-cm-01-04.bilivideo.com",
141 | "cn-zjhz-cm-01-07.bilivideo.com",
142 | "cn-zjhz-cm-01-12.bilivideo.com",
143 | "cn-zjhz-cm-01-17.bilivideo.com",
144 | "cn-zjhz-cu-01-01.bilivideo.com",
145 | "cn-zjhz-cu-01-02.bilivideo.com",
146 | "cn-zjhz-cu-01-05.bilivideo.com",
147 | "cn-zjhz-cu-v-02.bilivideo.com"
148 | ],
149 | "武汉": [
150 | "cn-hbwh-cm-01-01.bilivideo.com",
151 | "cn-hbwh-cm-01-02.bilivideo.com",
152 | "cn-hbwh-cm-01-04.bilivideo.com",
153 | "cn-hbwh-cm-01-05.bilivideo.com",
154 | "cn-hbwh-cm-01-06.bilivideo.com",
155 | "cn-hbwh-cm-01-08.bilivideo.com",
156 | "cn-hbwh-cm-01-09.bilivideo.com",
157 | "cn-hbwh-cm-01-10.bilivideo.com",
158 | "cn-hbwh-cm-01-12.bilivideo.com",
159 | "cn-hbwh-cm-01-13.bilivideo.com",
160 | "cn-hbwh-cm-01-17.bilivideo.com",
161 | "cn-hbwh-cm-01-19.bilivideo.com",
162 | "cn-hbwh-fx-01-01.bilivideo.com",
163 | "cn-hbwh-fx-01-02.bilivideo.com",
164 | "cn-hbwh-fx-01-12.bilivideo.com",
165 | "cn-hbwh-fx-01-13.bilivideo.com"
166 | ],
167 | "沈阳": [
168 | "cn-lnsy-cm-01-01.bilivideo.com",
169 | "cn-lnsy-cm-01-03.bilivideo.com",
170 | "cn-lnsy-cm-01-04.bilivideo.com",
171 | "cn-lnsy-cm-01-05.bilivideo.com",
172 | "cn-lnsy-cm-01-06.bilivideo.com",
173 | "cn-lnsy-cu-01-03.bilivideo.com",
174 | "cn-lnsy-cu-01-06.bilivideo.com"
175 | ],
176 | "海外": [
177 | "upos-hz-mirrorakam.akamaized.net",
178 | "upos-sz-mirroraliov.bilivideo.com",
179 | "upos-sz-mirrorcosov.bilivideo.com"
180 | ],
181 | "深圳": [
182 | "upos-sz-dynqn.bilivideo.com",
183 | "upos-sz-estgcos.bilivideo.com",
184 | "upos-sz-estghw.bilivideo.com",
185 | "upos-sz-mirror08c.bilivideo.com",
186 | "upos-sz-mirror08h.bilivideo.com",
187 | "upos-sz-mirroralibstar1.bilivideo.com",
188 | "upos-sz-mirroraliov.bilivideo.com",
189 | "upos-sz-mirrorbd.bilivideo.com",
190 | "upos-sz-mirrorcf1ov.bilivideo.com",
191 | "upos-sz-mirrorcosdisp.bilivideo.com",
192 | "upos-sz-mirrorctos.bilivideo.com",
193 | "upos-sz-mirrorhwdisp.bilivideo.com",
194 | "upos-sz-originbstar.bilivideo.com",
195 | "upos-sz-origincosgzhw.bilivideo.com",
196 | "upos-sz-origincosv.bilivideo.com"
197 | ],
198 | "福建": [
199 | "cn-fjfz-fx-01-01.bilivideo.com",
200 | "cn-fjfz-fx-01-02.bilivideo.com",
201 | "cn-fjfz-fx-01-03.bilivideo.com",
202 | "cn-fjfz-fx-01-04.bilivideo.com",
203 | "cn-fjfz-fx-01-05.bilivideo.com",
204 | "cn-fjfz-fx-01-06.bilivideo.com",
205 | "cn-fjqz-cm-01-01.bilivideo.com",
206 | "cn-fjqz-cm-01-02.bilivideo.com",
207 | "cn-fjqz-cm-01-03.bilivideo.com",
208 | "cn-fjqz-cm-01-04.bilivideo.com",
209 | "cn-fjqz-cm-01-05.bilivideo.com",
210 | "cn-fjqz-cm-01-06.bilivideo.com",
211 | "cn-fjqz-cm-01-07.bilivideo.com",
212 | "cn-fjqz-cm-01-08.bilivideo.com"
213 | ],
214 | "西安": [
215 | "cn-sxxa-cm-01-01.bilivideo.com",
216 | "cn-sxxa-cm-01-02.bilivideo.com",
217 | "cn-sxxa-cm-01-04.bilivideo.com",
218 | "cn-sxxa-cm-01-09.bilivideo.com",
219 | "cn-sxxa-cm-01-12.bilivideo.com",
220 | "cn-sxxa-ct-03-02.bilivideo.com",
221 | "cn-sxxa-ct-03-03.bilivideo.com",
222 | "cn-sxxa-ct-03-04.bilivideo.com",
223 | "cn-sxxa-cu-02-01.bilivideo.com",
224 | "cn-sxxa-cu-02-02.bilivideo.com"
225 | ],
226 | "郑州": [
227 | "cn-hnzz-cm-01-01.bilivideo.com",
228 | "cn-hnzz-cm-01-02.bilivideo.com",
229 | "cn-hnzz-cm-01-03.bilivideo.com",
230 | "cn-hnzz-cm-01-04.bilivideo.com",
231 | "cn-hnzz-cm-01-05.bilivideo.com",
232 | "cn-hnzz-cm-01-06.bilivideo.com",
233 | "cn-hnzz-cm-01-09.bilivideo.com",
234 | "cn-hnzz-cm-01-11.bilivideo.com",
235 | "cn-hnzz-fx-01-01.bilivideo.com",
236 | "cn-hnzz-fx-01-08.bilivideo.com"
237 | ],
238 | "香港": [
239 | "cn-hk-eq-01-03.bilivideo.com",
240 | "cn-hk-eq-01-09.bilivideo.com",
241 | "cn-hk-eq-01-10.bilivideo.com",
242 | "cn-hk-eq-01-12.bilivideo.com",
243 | "cn-hk-eq-01-13.bilivideo.com",
244 | "cn-hk-eq-01-14.bilivideo.com",
245 | "cn-hk-eq-bcache-13.bilivideo.com"
246 | ]
247 | }
--------------------------------------------------------------------------------
/script/ccb.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Custom CDN of Bilibili (CCB) - 修改哔哩哔哩的视频播放源
3 | // @namespace CCB
4 | // @license MIT
5 | // @version 1.0.0
6 | // @description 修改哔哩哔哩的视频播放源 - 部署于 GitHub Action 版本
7 | // @author 鼠鼠今天吃嘉然
8 | // @run-at document-start
9 | // @match https://www.bilibili.com/video/*
10 | // @match https://www.bilibili.com/bangumi/play/*
11 | // @match https://www.bilibili.com/festival/*
12 | // @match https://www.bilibili.com/list/*
13 | // @connect https://kanda-akihito-kun.github.io/ccb/api/
14 | // @grant GM_xmlhttpRequest
15 | // @grant GM_getValue
16 | // @grant GM_setValue
17 | // @grant GM_registerMenuCommand
18 | // @grant unsafeWindow
19 | // ==/UserScript==
20 |
21 | const api = 'https://kanda-akihito-kun.github.io/ccb/api'
22 |
23 | // 日志输出函数
24 | const PluginName = 'CCB'
25 | const log = console.log.bind(console, `[${PluginName}]:`)
26 |
27 | const defaultCdnNode = '使用默认源'
28 | var cdnNodeStored = 'CCB'
29 | var regionStored = 'region'
30 | var powerModeStored = 'powerMode'
31 |
32 | // 获取当前节点名称
33 | const getCurCdnNode = () => {
34 | return GM_getValue(cdnNodeStored, cdnList[0])
35 | }
36 |
37 | // 获取强力模式状态
38 | const getPowerMode = () => {
39 | return GM_getValue(powerModeStored, false)
40 | }
41 |
42 | // CDN 列表
43 | const initCdnList = [
44 | 'upos-sz-mirroraliov.bilivideo.com',
45 | 'upos-sz-mirroralib.bilivideo.com',
46 | 'upos-sz-estgcos.bilivideo.com',
47 | ]
48 | var cdnList = [
49 | defaultCdnNode,
50 | ...initCdnList
51 | ]
52 |
53 | // 要是选择了 defaultCdnNode 就不要改节点
54 | const isCcbEnabled = () => {
55 | return getCurCdnNode() !== defaultCdnNode
56 | }
57 |
58 | // 替换播放源
59 | const Replacement = (() => {
60 | const toURL = ((url) => {
61 | if (url.indexOf('://') === -1) {
62 | url = 'https://' + url
63 | return url.endsWith('/') ? url : `${url}/`
64 | }
65 | })
66 |
67 | let domain = getCurCdnNode()
68 |
69 | log(`播放源已修改为: ${domain}`)
70 |
71 | return toURL(domain)
72 | })()
73 |
74 | // 地区列表
75 | var regionList = ['编辑']
76 |
77 | const getRegionList = async () => {
78 | try {
79 | const response = await fetch(`${api}/region.json`);
80 | const data = await response.json();
81 | // 直接使用 JSON 数据
82 | regionList = ["编辑", ...data];
83 | log(`已更新地区列表: ${data}`);
84 | } catch (error) {
85 | log('获取地区列表失败:', error);
86 | }
87 | }
88 |
89 | const getCdnListByRegion = async (region) => {
90 | try {
91 | if (region === '编辑') {
92 | cdnList = [defaultCdnNode, ...initCdnList];
93 | return;
94 | }
95 |
96 | const response = await fetch(`${api}/cdn.json`);
97 | const data = await response.json();
98 |
99 | // 从完整的 CDN 数据中获取指定地区的数据
100 | const regionData = data[region] || [];
101 | cdnList = [defaultCdnNode, ...regionData];
102 |
103 | // 更新 CDN 选择器
104 | const cdnSelect = document.querySelector('.bpx-player-ctrl-setting-checkbox select:last-child');
105 | if (cdnSelect) {
106 | cdnSelect.innerHTML = cdnList.map(cdn =>
107 | ``
108 | ).join('');
109 | }
110 | log(`已更新 ${region} 地区的 CDN 列表`);
111 | } catch (error) {
112 | log('获取 CDN 列表失败:', error);
113 | }
114 | }
115 |
116 | const playInfoTransformer = playInfo => {
117 | const urlTransformer = i => {
118 | const newUrl = i.base_url.replace(
119 | /https:\/\/.*?\//,
120 | Replacement
121 | )
122 | i.baseUrl = newUrl;
123 | i.base_url = newUrl
124 |
125 | // 只有在强力模式开启时才处理 backupUrl
126 | if (getPowerMode()) {
127 | if (i.backupUrl && Array.isArray(i.backupUrl)) {
128 | i.backupUrl = i.backupUrl.map(url =>
129 | url.replace(/https:\/\/.*?\//, Replacement)
130 | );
131 | }
132 | if (i.backup_url && Array.isArray(i.backup_url)) {
133 | i.backup_url = i.backup_url.map(url =>
134 | url.replace(/https:\/\/.*?\//, Replacement)
135 | );
136 | }
137 | }
138 | };
139 |
140 | const durlTransformer = i => {
141 | i.url = i.url.replace(
142 | /https:\/\/.*?\//,
143 | Replacement
144 | )
145 | };
146 |
147 | if (playInfo.code !== (void 0) && playInfo.code !== 0) {
148 | log('Failed to get playInfo, message:', playInfo.message)
149 | return
150 | }
151 |
152 | let video_info
153 | if (playInfo.result) { // bangumi pages'
154 | video_info = playInfo.result.dash === (void 0) ? playInfo.result.video_info : playInfo.result
155 | if (!video_info?.dash) {
156 | if (playInfo.result.durl && playInfo.result.durls) {
157 | video_info = playInfo.result // documentary trail viewing, m.bilibili.com/bangumi/play/* trail or non-trail viewing
158 | } else {
159 | log('Failed to get video_info, limit_play_reason:', playInfo.result.play_check?.limit_play_reason)
160 | }
161 |
162 | // durl & durls are for trial viewing, and they usually exist when limit_play_reason=PAY
163 | video_info?.durl?.forEach(durlTransformer)
164 | video_info?.durls?.forEach(durl => { durl.durl?.forEach(durlTransformer) })
165 | return
166 | }
167 | } else { // video pages'
168 | video_info = playInfo.data
169 | }
170 | try {
171 | // 可能是充电专属视频的接口
172 | if (video_info.dash) {
173 | // 绝大部分视频的 video_info 接口返回的数据格式长这样
174 | video_info.dash.video.forEach(urlTransformer)
175 | video_info.dash.audio.forEach(urlTransformer)
176 | } else if (video_info.durl) {
177 | video_info.durl.forEach(durlTransformer)
178 | } else if (video_info.video_info) {
179 | // 可能是限免视频的接口
180 | video_info.video_info.dash.video.forEach(urlTransformer)
181 | video_info.video_info.dash.audio.forEach(urlTransformer)
182 | }
183 | } catch (err) {
184 | // 我也不知道这是啥格式了
185 | log('ERR:', err)
186 | }
187 | }
188 |
189 | // Network Request Interceptor
190 | const interceptNetResponse = (theWindow => {
191 | const interceptors = []
192 | const interceptNetResponse = (handler) => interceptors.push(handler)
193 |
194 | // when response === null && url is String, it's checking if the url is handleable
195 | const handleInterceptedResponse = (response, url) => interceptors.reduce((modified, handler) => {
196 | const ret = handler(modified, url)
197 | return ret ? ret : modified
198 | }, response)
199 | const OriginalXMLHttpRequest = theWindow.XMLHttpRequest
200 |
201 | class XMLHttpRequest extends OriginalXMLHttpRequest {
202 | get responseText() {
203 | if (this.readyState !== this.DONE) return super.responseText
204 | return handleInterceptedResponse(super.responseText, this.responseURL)
205 | }
206 | get response() {
207 | if (this.readyState !== this.DONE) return super.response
208 | return handleInterceptedResponse(super.response, this.responseURL)
209 | }
210 | }
211 |
212 | theWindow.XMLHttpRequest = XMLHttpRequest
213 |
214 | const OriginalFetch = fetch
215 | theWindow.fetch = (input, init) => (!handleInterceptedResponse(null, input) ? OriginalFetch(input, init) :
216 | OriginalFetch(input, init).then(response =>
217 | new Promise((resolve) => response.text()
218 | .then(text => resolve(new Response(handleInterceptedResponse(text, input), {
219 | status: response.status,
220 | statusText: response.statusText,
221 | headers: response.headers
222 | })))
223 | )
224 | )
225 | );
226 |
227 | return interceptNetResponse
228 | })(unsafeWindow)
229 |
230 | const waitForElm = (selectors) => new Promise(resolve => {
231 | const findElement = () => {
232 | const selArray = Array.isArray(selectors) ? selectors : [selectors];
233 | for (const s of selArray) {
234 | const ele = document.querySelector(s);
235 | if (ele) return ele;
236 | }
237 | return null;
238 | };
239 |
240 | let ele = findElement();
241 | if (ele) return resolve(ele);
242 |
243 | const observer = new MutationObserver(mutations => {
244 | let ele = findElement();
245 | if (ele) {
246 | observer.disconnect();
247 | resolve(ele);
248 | }
249 | });
250 |
251 | observer.observe(document.documentElement, {
252 | childList: true,
253 | subtree: true
254 | });
255 |
256 | log('waitForElm, MutationObserver started for selectors:', selectors);
257 | })
258 |
259 | // Parse HTML string to DOM Element
260 | function fromHTML(html) {
261 | if (!html) throw Error('html cannot be null or undefined', html)
262 | const template = document.createElement('template')
263 | template.innerHTML = html
264 | const result = template.content.children
265 | return result.length === 1 ? result[0] : result
266 | }
267 |
268 | (function () {
269 | 'use strict';
270 |
271 | // 注册油猴脚本菜单命令
272 | const updateMenuCommand = () => {
273 | const currentMode = getPowerMode()
274 | const statusIcon = currentMode ? '⚡' : '❎'
275 | const statusText = currentMode ? '开启' : '关闭'
276 | const menuText = `${statusIcon} 强力模式 (当前${statusText},点击此处进行切换)`
277 |
278 | GM_registerMenuCommand(menuText, () => {
279 | const newMode = !getPowerMode()
280 | GM_setValue(powerModeStored, newMode)
281 |
282 | const newStatusText = newMode ? '开启' : '关闭'
283 | const newStatusIcon = newMode ? '⚡' : '❎'
284 |
285 | // 添加日志输出
286 | log(`强力模式已${newStatusText} ${newStatusIcon}`)
287 |
288 | const description = newMode
289 | ? '强力模式已开启。\n当前会强行指定节点,即使遇到视频加载失败也不自动切换。\n如遇视频加载失败或严重卡顿,请关闭该模式。'
290 | : '强力模式已关闭。\n当前只会修改主要CDN节点,保持备用节点不变。\n如需强制指定节点,请确保节点有效后再进行开启。'
291 |
292 | alert(`ℹ ${newStatusText}强力模式\n\n${description}\n\n页面将自动刷新以使设置生效...`)
293 |
294 | location.reload()
295 | })
296 | }
297 |
298 | // 初始化菜单命令
299 | updateMenuCommand()
300 |
301 | // Hook Bilibili PlayUrl Api
302 | interceptNetResponse((response, url) => {
303 | if (!isCcbEnabled()) return
304 | if (url.startsWith('https://api.bilibili.com/x/player/wbi/playurl') ||
305 | url.startsWith('https://api.bilibili.com/pgc/player/web/v2/playurl') ||
306 | url.startsWith('https://api.bilibili.com/x/player/playurl') ||
307 | url.startsWith('https://api.bilibili.com/x/player/online') ||
308 | url.startsWith('https://api.bilibili.com/x/player/wbi') ||
309 | url.startsWith('https://api.bilibili.com/pgc/player/web/playurl') ||
310 | url.startsWith('https://api.bilibili.com/pugv/player/web/playurl') // at /cheese/
311 | ) {
312 | if (response === null) return true
313 |
314 | log('(Intercepted) playurl api response.')
315 | const responseText = response
316 | const playInfo = JSON.parse(responseText)
317 | playInfoTransformer(playInfo)
318 | return JSON.stringify(playInfo)
319 | }
320 | });
321 |
322 | // 响应式 window.__playinfo__
323 | if (unsafeWindow.__playinfo__) {
324 | playInfoTransformer(unsafeWindow.__playinfo__)
325 | } else {
326 | let internalPlayInfo = unsafeWindow.__playinfo__
327 | Object.defineProperty(unsafeWindow, '__playinfo__', {
328 | get: () => internalPlayInfo,
329 | set: v => {
330 | if (isCcbEnabled()) playInfoTransformer(v);
331 | internalPlayInfo = v
332 | }
333 | })
334 | }
335 |
336 | // 添加组件
337 | if (location.href.startsWith('https://www.bilibili.com/video/')
338 | || location.href.startsWith('https://www.bilibili.com/bangumi/play/')
339 | || location.href.startsWith('https://www.bilibili.com/festival/')
340 | || location.href.startsWith('https://www.bilibili.com/list/')
341 | ) {
342 | // 不知道为什么, 批站会在部分限免视频的播放器前面套娃一层
343 | waitForElm([
344 | '#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-left',
345 | '#bilibili-player > div > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-left'
346 | ])
347 | .then(async settingsBar => {
348 | // 先获取地区列表
349 | await getRegionList();
350 | // 根据之前保存的地区信息加载 CDN 列表
351 | await getCdnListByRegion(GM_getValue(regionStored, regionList[0]))
352 |
353 | // 地区
354 | const regionSelector = fromHTML(`
355 |
356 |
359 |
360 | `)
361 |
362 | // 监听地区选择框, 一旦改变就保存最新信息并获取该地区的 CDN 列表
363 | const regionNode = regionSelector.querySelector('select')
364 |
365 | // CDN 选择下拉列表
366 | const cdnSelector = fromHTML(`
367 |
368 |
371 |
372 | `)
373 |
374 | // 监听 CDN 选择框, 一旦改变就保存最新信息并刷新页面
375 | const selectNode = cdnSelector.querySelector('select')
376 | selectNode.addEventListener('change', (e) => {
377 | const selectedCDN = e.target.value
378 | GM_setValue(cdnNodeStored, selectedCDN)
379 | // 刷新网页
380 | location.reload()
381 | })
382 |
383 | // 创建自定义CDN输入框
384 | const currentCdn = GM_getValue(cdnNodeStored, '')
385 | const customCdnInput = fromHTML(`
386 |
387 |
388 |
389 | `)
390 |
391 | const customInput = customCdnInput.querySelector('input')
392 |
393 | // 检查当前地区是否为编辑模式,决定显示CDN选择器还是输入框
394 | const toggleCdnDisplay = (region) => {
395 | if (region === '编辑') {
396 | // 更新输入框的placeholder为当前选择的CDN
397 | customInput.placeholder = GM_getValue(cdnNodeStored, '')
398 | cdnSelector.style.display = 'none'
399 | customCdnInput.style.display = 'flex'
400 | } else {
401 | cdnSelector.style.display = 'flex'
402 | customCdnInput.style.display = 'none'
403 | }
404 | }
405 |
406 | // 监听自定义CDN输入框的回车事件
407 | customInput.addEventListener('keypress', (e) => {
408 | if (e.key === 'Enter') {
409 | const customCDN = e.target.value.trim()
410 | if (customCDN) {
411 | GM_setValue(cdnNodeStored, customCDN)
412 | // 刷新网页
413 | location.reload()
414 | }
415 | }
416 | })
417 |
418 | // 更新地区选择器的事件处理
419 | regionNode.addEventListener('change', async (e) => {
420 | const selectedRegion = e.target.value
421 | GM_setValue(regionStored, selectedRegion)
422 |
423 | // 切换显示模式
424 | toggleCdnDisplay(selectedRegion)
425 |
426 | if (selectedRegion !== '编辑') {
427 | // 请求该地区的 CDN 列表
428 | await getCdnListByRegion(selectedRegion)
429 | }
430 | })
431 |
432 | // 初始化显示状态
433 | const currentRegion = GM_getValue(regionStored, regionList[0])
434 | toggleCdnDisplay(currentRegion)
435 |
436 | settingsBar.appendChild(regionNode)
437 | settingsBar.appendChild(cdnSelector)
438 | settingsBar.appendChild(customCdnInput)
439 | log('CDN selector added')
440 | });
441 | }
442 | })();
--------------------------------------------------------------------------------