├── 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 | ![image](https://github.com/user-attachments/assets/ae6534e3-1c78-484f-81e7-69d8608242f1) 10 | 11 | 12 | 13 | ## 更新介绍 14 | 15 | 版本 1.0.1 更新: 16 | 17 | 1. 提供强力模式——开启该模式后,脚本会通过强制修改 backupUrl 数组,实现强制切换节点的功能; 18 | 19 | 2. 支持强力模式 开启/关闭。该模式默认关闭,点击油猴脚本的设置处即可进行开关(具体位置可以见图); 20 | 21 | 3. 在地区框选择 "编辑",即可在右边出现的编辑框输入自定义的视频源地址,回车键触发修改(请确保手动输入的视频源有该视频资源); 22 | 23 | 3e793589-f961-45b1-8fc9-e60948b72d7a 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 | 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 | })(); --------------------------------------------------------------------------------