├── README.md ├── assets ├── image-20240811070032-qh29h3n.png ├── image-20240811070706-tvv2rxv.png └── image.png ├── background.js ├── common.js ├── content.js ├── icon.png ├── manifest.json ├── options.html ├── options.js ├── scripts ├── baidu_disk.js ├── bilibili_iframe.js ├── bilibili_web.js ├── siyuan.js ├── youtube_embed.js ├── youtube_web.js └── zhihu.js ├── zepto.min.js └── 视频笔记模版.sy.zip /README.md: -------------------------------------------------------------------------------- 1 | # 思源笔记:视频笔记插件 2 | 3 | ##### 1、Chrome商店搜索[思源笔记:视频笔记插件](https://chromewebstore.google.com/detail/%E6%80%9D%E6%BA%90%E7%AC%94%E8%AE%B0%EF%BC%9A%E8%A7%86%E9%A2%91%E7%AC%94%E8%AE%B0%E6%8F%92%E4%BB%B6/ggggnakoippfjjggdgadahifiankomni)安装,无法打开Chrome商店,请在[Release](https://github.com/coriger/siyuan-video-extension/releases)页下载最新版本手动安装 4 | 5 | ##### 2、初次使用,先进行模版安装和参数配置 6 | 7 | - 下载项目中的`视频笔记模版.sy.zip`​文件,导入思源 8 | 9 | - 把导入的文件`视频笔记模版`导出为模版 10 | 11 | - 点击插件图标按钮,进入插件配置页 12 | ![image](assets/image.png) 13 | 14 | - Token:在思源笔记`设置-关于`菜单里,找到`API token`,填入 15 | ​![image](assets/image-20240811070706-tvv2rxv.png)​ 16 | 17 | - 笔记本:选择一个数据同步的笔记本 18 | 19 | - 模版文件路径:在思源的数据目录中找到`视频笔记模版.md`文件,拿到完整路径,填入 20 | ​![image](assets/image-20240811070032-qh29h3n.png)​ 21 | 22 | - 点击保存配置,完成初始化配置,后续插件更新无须再次配置 23 | 24 | ##### 3、支持平台 25 | 26 | - [B站](https://www.bilibili.com/video/BV1rdYfeLE87/) 27 | - [百度网盘](https://www.bilibili.com/video/BV19QYqeBEgi) 28 | - Youtube:单视频、视频列表 29 | - 知乎:话题精华问题、问题高赞答案 30 | - supr-blog 31 | 32 | ##### 4、备注 33 | 34 | - 本插件只适用于思源web端 35 | - 目前支持B站、百度网盘、Youtube视频数据的同步,需要支持其他站点的可以开issue提需求,或者自行二次开发 36 | - 本插件开发主要是为自用,做不到充分的测试覆盖,如果使用出现任何异常请直接CTRL+F5强刷页面,基本可以解决大部分异常情况,如果还是不行请开issue反馈 37 | - 下载按钮不正常显示,是因为网站前端缓存机制不触发请求的原因,本插件是通过劫持请求获取数据,如果出现不显示的情况,只需要强刷页面或者切换分P即可 -------------------------------------------------------------------------------- /assets/image-20240811070032-qh29h3n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coriger/siyuan-video-extension/6868bf8be86a0638f243248f47d42c7b66082c39/assets/image-20240811070032-qh29h3n.png -------------------------------------------------------------------------------- /assets/image-20240811070706-tvv2rxv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coriger/siyuan-video-extension/6868bf8be86a0638f243248f47d42c7b66082c39/assets/image-20240811070706-tvv2rxv.png -------------------------------------------------------------------------------- /assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coriger/siyuan-video-extension/6868bf8be86a0638f243248f47d42c7b66082c39/assets/image.png -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // 外部视频播放tab页 始终锁定这个页面 2 | var tabId = ""; 3 | 4 | chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { 5 | // 查询当前iframe视频进度的指令 6 | if (request.action === "queryInnerIframe") { 7 | console.log("queryInnerIframe:"+request.frameUrl); 8 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 9 | // 发送消息到iframe中的content脚本 10 | chrome.tabs.sendMessage(tabs[0].id, {action: "queryIframeVideo",frameUrl:request.frameUrl},function(response){ 11 | console.log("queryInnerIframe:"+response); 12 | // 返回当前时间戳 13 | sendResponse({currentTime:parseStrFromTime(response.time)}); // 发送 14 | }); 15 | }); 16 | return true; // 保持消息通道打开以响应异步请求 17 | } 18 | 19 | // 查询外部视频进度的指令 20 | if (request.action === "queryOuterVideo") { 21 | var tabExist = false; 22 | chrome.tabs.query({}, function(tabs) { 23 | // 遍历tabs 24 | for (var i = 0; i < tabs.length; i++) { 25 | // 判断url是否存在 26 | if (tabs[i].url == request.videoUrl){ 27 | console.log("找到tab"); 28 | tabExist = true; 29 | // 假设你已经有了tabs数组,并且知道要切换到的标签页的索引i 30 | // 首先,使用chrome.windows.update将目标窗口置于最前面(如果它不在最前面的话) 31 | chrome.windows.update(tabs[i].windowId, {focused: true}, function() { 32 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 33 | chrome.tabs.update(tabs[i].id, {active: true}, function() { 34 | // 发送消息到content中 进行查询时间戳操作 35 | chrome.tabs.sendMessage(tabs[i].id, {action: "queryOuterVideo","videoUrl":request.videoUrl},function(response){ 36 | // 返回当前时间戳 37 | sendResponse({currentTime:parseStrFromTime(response.time)}); // 发送 38 | }); 39 | }); 40 | }); 41 | return; 42 | } 43 | } 44 | 45 | if(!tabExist){ 46 | console.log("没有找到tab"); 47 | // 发送提示通知 48 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 49 | // 发送消息到iframe中的content脚本 50 | chrome.tabs.sendMessage(tabs[0].id, {action: "noticMsg",msg:"请在浏览器中打开当前视频"},function(response){}); 51 | }); 52 | } 53 | }); 54 | return true; // 保持消息通道打开以响应异步请求 55 | } 56 | 57 | // 跳转内嵌iframe视频指令 58 | if (request.action === "dumpInnerVideo") { 59 | var timeInSeconds = parseTimeFromStr(request.time); 60 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 61 | // 遍历tabs 62 | // 发送消息到iframe中的content脚本 63 | chrome.tabs.sendMessage(tabs[0].id, {action: "dumpFrameVideo",time:timeInSeconds,frameUrl:request.frameUrl}); 64 | }); 65 | return true; // 保持消息通道打开以响应异步请求 66 | } 67 | 68 | // 跳转外部视频指令 69 | if (request.action === "dumpOuterVideo") { 70 | var timeInSeconds = parseTimeFromStr(request.time); 71 | // 先判断一下有没有tabId 72 | if(tabId && tabId != ""){ 73 | // 有的话再判断下tabId对应的tab是否还存在 74 | chrome.tabs.get(tabId,function(tab){ 75 | if(!tab){ 76 | // 不存在 则创建 77 | createAndDump(request.videoUrl,timeInSeconds); 78 | }else{ 79 | // tab还存在,则直接在该tab上打开新链接,并且切换为活动页 80 | if (tab.url == request.videoUrl){ 81 | dump(tabId,request.videoUrl,timeInSeconds) 82 | }else{ 83 | switchAndDump(tabId,request.videoUrl,timeInSeconds); 84 | } 85 | } 86 | }) 87 | }else{ 88 | // 不存在 则创建并跳转 89 | createAndDump(request.videoUrl,timeInSeconds); 90 | } 91 | 92 | return true; // 保持消息通道打开以响应异步请求 93 | } 94 | 95 | // 定位视频详情页窗口 96 | if (request.action === "openOuterVideo") { 97 | console.log("openOuterVideo : " + request.videoUrl); 98 | // 先判断一下有没有tabId 99 | if(tabId && tabId != ""){ 100 | // 有的话再判断下tabId对应的tab是否还存在 101 | chrome.tabs.get(tabId,function(tab){ 102 | if(!tab){ 103 | createTab(request.videoUrl); 104 | }else{ 105 | // tab还存在 判断url是否一致 一致的话 直接切到该tab上 106 | if (tab.url == request.videoUrl){ 107 | active(tabId) 108 | }else{ 109 | switchAndActive(tabId,request.videoUrl); 110 | } 111 | } 112 | }) 113 | }else{ 114 | // 不存在 则创建 115 | createTab(request.videoUrl); 116 | } 117 | 118 | return true; // 保持消息通道打开以响应异步请求 119 | } 120 | 121 | // iframe截图指令 122 | if (request.action === "screenshot") { 123 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 124 | // 遍历tabs 125 | // 发送消息到iframe中的content脚本 126 | chrome.tabs.sendMessage(tabs[0].id, {action: "screenIframe",frameUrl:request.frameUrl}); 127 | }); 128 | return true; // 保持消息通道打开以响应异步请求 129 | } 130 | 131 | // 外部视频截图指令 132 | if (request.action === "screenshotOuterVideo") { 133 | var tabExist = false; 134 | console.log("screenshotOuterVideo : " + request.videoUrl); 135 | chrome.tabs.query({}, function(tabs) { 136 | // 遍历tabs 137 | for (var i = 0; i < tabs.length; i++) { 138 | // 判断url是否存在 139 | if (tabs[i].url == request.videoUrl){ 140 | tabExist = true; 141 | console.log("已找到tab:"+request.action+" "+request.videoUrl); 142 | // 假设你已经有了tabs数组,并且知道要切换到的标签页的索引i 143 | // 首先,使用chrome.windows.update将目标窗口置于最前面(如果它不在最前面的话) 144 | chrome.windows.update(tabs[i].windowId, {focused: true}, function() { 145 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 146 | chrome.tabs.update(tabs[i].id, {active: true}, function() { 147 | chrome.tabs.sendMessage(tabs[i].id, {action: "screenshotOuterVideo",videoUrl:request.videoUrl}); 148 | }); 149 | }); 150 | return; 151 | } 152 | } 153 | 154 | if(!tabExist){ 155 | console.log("没有找到tab"); 156 | // 发送提示通知 157 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 158 | // 发送消息到iframe中的content脚本 159 | chrome.tabs.sendMessage(tabs[0].id, {action: "noticMsg",msg:"请在浏览器中打开当前视频"},function(response){}); 160 | }); 161 | } 162 | }); 163 | 164 | return true; // 保持消息通道打开以响应异步请求 165 | } 166 | 167 | // iframe内嵌写入截图指令 168 | if (request.action === "screenInsert") { 169 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 170 | // 遍历tabs 171 | // 发送消息到iframe中的content脚本 172 | chrome.tabs.sendMessage(tabs[0].id, {action: "screenInsert",imgUrl: request.imgUrl,currentTime:request.currentTime,frameUrl:request.frameUrl}); 173 | }); 174 | return true; // 保持消息通道打开以响应异步请求 175 | } 176 | 177 | // 外部视频写入截图指令 178 | if (request.action === "screenOuterInsert") { 179 | var tabExist = false; 180 | console.log("screenOuterInsert : " + request.imgUrl + " " + request.currentTime); 181 | chrome.tabs.query({}, function(tabs) { 182 | // 遍历tabs 183 | for (var i = 0; i < tabs.length; i++) { 184 | // 判断url是否存在 185 | if (tabs[i].url == request.videoUrl){ 186 | console.log("已找到tab"); 187 | tabExist = true; 188 | // 假设你已经有了tabs数组,并且知道要切换到的标签页的索引i 189 | // 首先,使用chrome.windows.update将目标窗口置于最前面(如果它不在最前面的话) 190 | chrome.windows.update(tabs[i].windowId, {focused: true}, function() { 191 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 192 | chrome.tabs.update(tabs[i].id, {active: true}, function() { 193 | // 发送给所有content脚本的页面 194 | for (var j = 0; j < tabs.length; j++) { 195 | chrome.tabs.sendMessage(tabs[j].id, {action: "screenOuterInsert",imgUrl: request.imgUrl,currentTime:request.currentTime,videoUrl: request.videoUrl}); 196 | } 197 | }); 198 | }); 199 | return; 200 | } 201 | } 202 | 203 | if(!tabExist){ 204 | console.log("没有找到tab"); 205 | // 发送提示通知 206 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 207 | // 发送消息到iframe中的content脚本 208 | chrome.tabs.sendMessage(tabs[0].id, {action: "noticMsg",msg:"请在浏览器中打开当前视频"},function(response){}); 209 | }); 210 | } 211 | }); 212 | return true; // 保持消息通道打开以响应异步请求 213 | } 214 | 215 | }); 216 | 217 | 218 | /** 219 | * 创建一个Tab视频播放页 并跳转到指定为止 220 | * @param {*} request 221 | * @param {*} videoUrl 222 | * @param {*} timeInSeconds 223 | */ 224 | function createAndDump(videoUrl,timeInSeconds){ 225 | chrome.tabs.create({url: videoUrl}, function(tab) { 226 | tabId = tab.id; 227 | chrome.windows.update(tab.windowId, {focused: true}, function() { 228 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 229 | chrome.tabs.update(tab.id, {active: true}, function() { 230 | // 发送消息到content中 进行跳转操作 231 | chrome.tabs.sendMessage(tab.id, {action: "dumpOuterVideo",time:timeInSeconds,"videoUrl":videoUrl}); 232 | }); 233 | }); 234 | }); 235 | } 236 | 237 | /** 238 | * 视频页面跳转到指定位置 239 | * @param {*} tabId 240 | * @param {*} timeInSeconds 241 | */ 242 | function dump(tabId,videoUrl,timeInSeconds){ 243 | chrome.windows.update(tabId, {focused: true}, function() { 244 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 245 | chrome.tabs.update(tabId, {active: true}, function() { 246 | // 发送消息到content中 进行跳转操作 247 | chrome.tabs.sendMessage(tabId, {action: "dumpOuterVideo",time:timeInSeconds,"videoUrl":videoUrl}); 248 | }); 249 | }); 250 | } 251 | 252 | /** 253 | * 切换url 并跳转到指定位置 254 | * @param {*} tabId 255 | * @param {*} request 256 | * @param {*} videoUrl 257 | * @param {*} timeInSeconds 258 | */ 259 | function switchAndDump(tabId,videoUrl,timeInSeconds){ 260 | chrome.tabs.update(tabId, {url: videoUrl}, function() { 261 | chrome.windows.update(tabId, {focused: true}, function() { 262 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 263 | chrome.tabs.update(tabId, {active: true}, function() { 264 | // 发送消息到content中 进行跳转操作 265 | chrome.tabs.sendMessage(tabId, {action: "dumpOuterVideo",time:timeInSeconds,"videoUrl":videoUrl}); 266 | }); 267 | }); 268 | }); 269 | } 270 | 271 | /** 272 | * 激活tab页 273 | * @param {*} tabId 274 | */ 275 | function active(tabId){ 276 | chrome.windows.update(tabId, {focused: true}, function() { 277 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 278 | chrome.tabs.update(tabId, {active: true}, function() { 279 | }); 280 | }); 281 | } 282 | 283 | 284 | /** 285 | * 切换url并激活 286 | * @param {*} tabId 287 | * @param {*} request 288 | * @param {*} videoUrl 289 | */ 290 | function switchAndActive(tabId, videoUrl) { 291 | // 不一致 则先跳转url,再切到该tab上 292 | chrome.tabs.update(tabId, { url: videoUrl }, function () { 293 | chrome.windows.update(tabId, { focused: true }, function () { 294 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 295 | chrome.tabs.update(tabId, { active: true }, function () {}); 296 | }); 297 | }); 298 | } 299 | 300 | /** 301 | * 创建一个Tab视频播放页 302 | * @param {*} videoUrl 303 | */ 304 | function createTab(videoUrl) { 305 | chrome.tabs.create({ url: videoUrl }, function (tab) { 306 | tabId = tab.id; 307 | chrome.windows.update(tab.windowId, { focused: true }, function () { 308 | // 然后,使用chrome.tabs.update将目标标签页设置为活动标签页 309 | chrome.tabs.update(tab.id, { active: true }, function () {}); 310 | }); 311 | }); 312 | } 313 | 314 | /** 315 | * 针对百度网盘接口的监听 316 | * 无法拦截 只是二次请求 317 | */ 318 | chrome.webRequest.onCompleted.addListener(function (details) { 319 | // 针对https://pan.baidu.com/api/list的接口监听 320 | // 注入下载按钮 321 | if (details.url.indexOf("pan.baidu.com/api/list") > -1 && details.url.indexOf("sysy") == -1){ 322 | // 这里直接发起请求 323 | fetch(details.url+"&sysy=1") 324 | .then(response => { 325 | // 确保响应是成功的(状态码在200-299之间) 326 | if (!response.ok) { 327 | console.log("response is not ok "+response.ok); 328 | throw new Error('Network response was not ok: ' + response.status); 329 | } 330 | // 解析响应体为文本 331 | return response.text(); 332 | }) 333 | .then(async result => { 334 | // 打印返回的文本内容 335 | var json = JSON.parse(result); 336 | console.log(json) 337 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 338 | // 遍历tabs 339 | // 发送消息到iframe中的content脚本 340 | chrome.tabs.sendMessage(tabs[0].id, {action: "injectDownloadButton",data:json}); 341 | }); 342 | }) 343 | .catch(error => { 344 | // 捕获并打印任何错误 345 | console.error('There has been a problem with your fetch operation:', error); 346 | }); 347 | } 348 | 349 | }, {urls: ["*://pan.baidu.com/*"]}); 350 | 351 | 352 | 353 | /** 354 | * 针对bilibili.com接口的监听 355 | * 无法拦截 只是二次请求 356 | */ 357 | chrome.webRequest.onCompleted.addListener(function (details) { 358 | // 针对api.bilibili.com/pgc/view/web/ep/list的接口监听 359 | // 监听到了之后发送消息给content.js 只有当前页是bangumi/play才处理这个消息 360 | // 注入下载正片按钮 361 | if (details.url.indexOf("api.bilibili.com/pgc/view/web/ep/list") > -1 && details.url.indexOf("sysy") == -1){ 362 | // 这里直接发起请求 363 | fetch(details.url+"&sysy=1") 364 | .then(response => { 365 | // 确保响应是成功的(状态码在200-299之间) 366 | if (!response.ok) { 367 | console.log("response is not ok "+response.ok); 368 | throw new Error('Network response was not ok: ' + response.status); 369 | } 370 | // 解析响应体为文本 371 | return response.text(); 372 | }) 373 | .then(async result => { 374 | // 打印返回的文本内容 375 | var json = JSON.parse(result); 376 | console.log(json) 377 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 378 | // 遍历tabs 379 | // 发送消息到iframe中的content脚本 380 | chrome.tabs.sendMessage(tabs[0].id, {action: "injectBilibiliZhengPianButton",data:json}); 381 | }); 382 | }) 383 | .catch(error => { 384 | // 捕获并打印任何错误 385 | console.error('There has been a problem with your fetch operation:', error); 386 | }); 387 | } 388 | 389 | 390 | // 针对合集接口的监听 391 | if (details.url.indexOf("api.bilibili.com/x/web-interface/wbi/view/detail") > -1 && details.url.indexOf("sysy") == -1){ 392 | // 这里直接发起请求 393 | fetch(details.url+"&sysy=1") 394 | .then(response => { 395 | // 确保响应是成功的(状态码在200-299之间) 396 | if (!response.ok) { 397 | console.log("response is not ok "+response.ok); 398 | throw new Error('Network response was not ok: ' + response.status); 399 | } 400 | // 解析响应体为文本 401 | return response.text(); 402 | }) 403 | .then(async result => { 404 | // 打印返回的文本内容 405 | var json = JSON.parse(result); 406 | console.log(json) 407 | chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { 408 | // 遍历tabs 409 | // 发送消息到iframe中的content脚本 410 | console.log("injectBilibiliHeJiButton") 411 | chrome.tabs.sendMessage(tabs[0].id, {action: "injectBilibiliHeJiButton",data:json}); 412 | }); 413 | }) 414 | .catch(error => { 415 | // 捕获并打印任何错误 416 | console.error('There has been a problem with your fetch operation:', error); 417 | }); 418 | } 419 | 420 | }, {urls: ["*://*.bilibili.com/*"]}); 421 | 422 | /** 423 | * 时间戳字符串转换成秒数 424 | * @param {*} timeStr 425 | * @returns 426 | */ 427 | function parseTimeFromStr(timeStr){ 428 | var timeInSeconds = ""; 429 | if (timeStr && timeStr != '') { 430 | // 这里判断下timeInSeconds的格式 如果是包含:的字符串,则转换为秒数 431 | if (timeStr.indexOf(':') > -1) { 432 | // 格式为xx:yy:zz,则转换为秒数 433 | var time = timeStr.split(':'); 434 | // 如果是xx:yy则表示分钟:秒 435 | // 如果是xx:yy:zz则表示小时:分钟:秒 436 | if (time.length == 2) { 437 | timeInSeconds = parseInt(time[0]) * 60 + parseInt(time[1]); 438 | } else if (time.length == 3) { 439 | timeInSeconds = parseInt(time[0]) * 60 * 60 + parseInt(time[1]) * 60 + parseInt(time[2]); 440 | } 441 | }else{ 442 | timeInSeconds = parseInt(timeInSeconds); 443 | } 444 | } 445 | 446 | return timeInSeconds; 447 | } 448 | 449 | /** 450 | * 把秒数转换成时间戳字符串 451 | * @param {*} currentTime 452 | * @returns 453 | */ 454 | function parseStrFromTime(currentTime){ 455 | var time = "00:00"; 456 | if (currentTime && currentTime !== "") { 457 | // 这里currentTime单位是秒,把它转换一下,小于60秒则直接显示秒,大于60秒则显示分钟:秒,大于60分钟,则显示小时:分钟:秒,且每个单位如果是个位数则前面补0 458 | // 时间戳1分钟以内 直接用秒表示 459 | if(currentTime < 10) { 460 | // 小于10s 按00:0x 461 | // 所有的秒只保留整数部分,即小数点后不显示 462 | time = "00:0"+ parseInt(currentTime).toString(); 463 | }else if(currentTime < 60){ 464 | // 小于1分钟 按00:xx 465 | time = "00:"+ parseInt(currentTime).toString(); 466 | }else if(currentTime < 60 * 60) { 467 | // 小于1小时 按xx:yy 468 | var min = parseInt(currentTime/60); 469 | if(min < 10) { 470 | time = "0"+min.toString()+":"; 471 | } else { 472 | time = min.toString()+":"; 473 | } 474 | var sec = currentTime%60; 475 | if(sec < 10) { 476 | time += "0"+parseInt(sec).toString(); 477 | } else { 478 | time += parseInt(sec).toString(); 479 | } 480 | }else { 481 | var hour = parseInt(currentTime/3600); 482 | if(hour < 10) { 483 | time = "0"+hour.toString()+":"; 484 | } else { 485 | time = hour.toString()+":"; 486 | } 487 | var min = parseInt((currentTime%3600)/60); 488 | if(min < 10) { 489 | time += "0"+min.toString()+":"; 490 | } else { 491 | time += min.toString()+":"; 492 | } 493 | var sec = currentTime%60; 494 | if(sec < 10) { 495 | time += "0"+parseInt(sec).toString(); 496 | } else { 497 | time += parseInt(sec).toString(); 498 | } 499 | } 500 | } 501 | 502 | return time; 503 | } -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | var currentPageUrl; 2 | var Authorization; 3 | var notebook; 4 | var pageTemplateUrl; 5 | 6 | // 定义要从存储中检索的键 7 | const keys = ['token', 'notebook', 'pageTemplateUrl']; 8 | 9 | // 初始化函数,从存储中加载值 10 | function initializeParams() { 11 | getValuesFromStorage(keys, function(result) { 12 | updateParams(result); 13 | }); 14 | } 15 | 16 | // 更新参数值 17 | function updateParams(values) { 18 | Authorization = values.token; 19 | notebook = values.notebook; 20 | pageTemplateUrl = values.pageTemplateUrl; 21 | 22 | console.log('Authorization:', Authorization); 23 | console.log('notebook:', notebook); 24 | console.log('pageTemplateUrl:', pageTemplateUrl); 25 | } 26 | 27 | // 从存储中获取值的函数 28 | function getValuesFromStorage(keys, callback) { 29 | chrome.storage.local.get(keys, function(result) { 30 | callback(result); 31 | }); 32 | } 33 | 34 | // 监听存储变化,更新参数值 35 | chrome.storage.onChanged.addListener(function(changes, namespace) { 36 | var changedValues = {}; 37 | keys.forEach(function(key) { 38 | if (changes[key]) { 39 | changedValues[key] = changes[key].newValue; 40 | } 41 | }); 42 | if (Object.keys(changedValues).length > 0) { 43 | updateParams(changedValues); 44 | } 45 | }); 46 | 47 | // 调用初始化函数,加载初始值 48 | initializeParams(); 49 | 50 | /** 51 | * 把视频时长转换成字符串格式 52 | * 参数单位是毫秒 53 | * @param {*} milliseconds 54 | * @returns 55 | */ 56 | function parseVideoTimeFromDuration(milliseconds){ 57 | // 计算小时数 58 | var hours = Math.floor(milliseconds / (60 * 60 * 1000)); 59 | // 计算剩余的分钟数 60 | var minutes = Math.floor((milliseconds % (60 * 60 * 1000)) / (60 * 1000)); 61 | // 计算剩余的秒数 62 | var seconds = Math.floor((milliseconds % (60 * 1000)) / 1000); 63 | 64 | // 格式化小时、分钟和秒,确保它们是两位数 65 | hours = hours.toString().padStart(2, '0'); 66 | minutes = minutes.toString().padStart(2, '0'); 67 | seconds = seconds.toString().padStart(2, '0'); 68 | 69 | // 根据时长判断并拼接字符串 70 | if (hours > 0) { 71 | return `${hours}:${minutes}:${seconds}`; // xx:yy:zz 72 | } else if (minutes > 0) { 73 | return `${minutes}:${seconds}`; // xx:yy 74 | } else { 75 | // 如果分钟和小时都为0,但秒数可能不为0(尽管在这个特定情况下它会是0,因为至少要有1秒) 76 | // 但为了完整性,我们还是返回秒数(尽管前导0可能看起来不必要) 77 | return `00:${seconds}`; // 00:xx,但注意这个分支实际上不太可能被触发,除非有特别的逻辑需要它 78 | // 或者,如果确实只需要在秒数大于0时才显示,可以改为: 79 | // return seconds > 0 ? `00:${seconds}` : '00:00'; 80 | } 81 | } 82 | 83 | 84 | /** 85 | * 调用思源api 86 | * @param {} url 87 | * @param {*} json 88 | * @returns 89 | */ 90 | async function invokeSiyuanApi(url,json){ 91 | console.log("invoke siyuan api:"+url) 92 | console.log("invoke siyuan json:"+JSON.stringify(json)) 93 | 94 | try { 95 | const response = await fetch(url, { 96 | method: "POST", 97 | headers: { 98 | "Authorization": "token "+Authorization, 99 | "Content-Type": "application/json", 100 | }, 101 | body: JSON.stringify(json) 102 | }); 103 | // 确保响应状态码是2xx 104 | if (!response.ok) { 105 | throw new Error('Network response was not ok'); 106 | } 107 | // 你可以继续处理响应,例如获取JSON数据 108 | const data = await response.json(); 109 | console.log("invoke siyuan api success,result is "+JSON.stringify(data)) 110 | return data; 111 | } catch (error) { 112 | console.error('There has been a problem with your invokeSiyuanApi operation:', error); 113 | } 114 | } 115 | 116 | 117 | /** 118 | * 上传文件 119 | * @param {*} url 120 | * @param {*} json 121 | * @returns 122 | */ 123 | async function invokeSiyuanUploadApi(formData){ 124 | 125 | try { 126 | const response = await fetch("http://127.0.0.1:6806/api/asset/upload", { 127 | method: "POST", 128 | headers: { 129 | "Authorization": "token "+Authorization, 130 | }, 131 | body: formData 132 | }); 133 | // 确保响应状态码是2xx 134 | if (!response.ok) { 135 | throw new Error('Network response was not ok'); 136 | } 137 | // 你可以继续处理响应,例如获取JSON数据 138 | const data = await response.json(); 139 | console.log("invoke siyuan upload api success,result is "+JSON.stringify(data)) 140 | return data; 141 | } catch (error) { 142 | console.error('There has been a problem with your invokeSiyuanApi operation:', error); 143 | } 144 | } 145 | 146 | function parseStrFromTime(currentTime){ 147 | var time = "00:00"; 148 | if (currentTime && currentTime !== "" && currentTime > 0) { 149 | // 这里currentTime单位是秒,把它转换一下,小于60秒则直接显示秒,大于60秒则显示分钟:秒,大于60分钟,则显示小时:分钟:秒,且每个单位如果是个位数则前面补0 150 | // 时间戳1分钟以内 直接用秒表示 151 | if(currentTime < 10) { 152 | // 小于10s 按00:0x 153 | // 所有的秒只保留整数部分,即小数点后不显示 154 | time = "00:0"+ parseInt(currentTime).toString(); 155 | }else if(currentTime < 60){ 156 | // 小于1分钟 按00:xx 157 | time = "00:"+ parseInt(currentTime).toString(); 158 | }else if(currentTime < 60 * 60) { 159 | // 小于1小时 按xx:yy 160 | var min = parseInt(currentTime/60); 161 | if(min < 10) { 162 | time = "0"+min.toString()+":"; 163 | } else { 164 | time = min.toString()+":"; 165 | } 166 | var sec = currentTime%60; 167 | if(sec < 10) { 168 | time += "0"+parseInt(sec).toString(); 169 | } else { 170 | time += parseInt(sec).toString(); 171 | } 172 | }else { 173 | var hour = parseInt(currentTime/3600); 174 | if(hour < 10) { 175 | time = "0"+hour.toString()+":"; 176 | } else { 177 | time = hour.toString()+":"; 178 | } 179 | var min = parseInt((currentTime%3600)/60); 180 | if(min < 10) { 181 | time += "0"+min.toString()+":"; 182 | } else { 183 | time += min.toString()+":"; 184 | } 185 | var sec = currentTime%60; 186 | if(sec < 10) { 187 | time += "0"+parseInt(sec).toString(); 188 | } else { 189 | time += parseInt(sec).toString(); 190 | } 191 | } 192 | } 193 | 194 | return time; 195 | } 196 | 197 | /** 198 | * 时间戳字符串转换成秒数 199 | * @param {*} timeStr 200 | * @returns 201 | */ 202 | function parseTimeFromStr(timeStr){ 203 | var timeInSeconds = ""; 204 | if (timeStr && timeStr != '') { 205 | // 这里判断下timeInSeconds的格式 如果是包含:的字符串,则转换为秒数 206 | if (timeStr.indexOf(':') > -1) { 207 | // 格式为xx:yy:zz,则转换为秒数 208 | var time = timeStr.split(':'); 209 | // 如果是xx:yy则表示分钟:秒 210 | // 如果是xx:yy:zz则表示小时:分钟:秒 211 | if (time.length == 2) { 212 | timeInSeconds = parseInt(time[0]) * 60 + parseInt(time[1]); 213 | } else if (time.length == 3) { 214 | timeInSeconds = parseInt(time[0]) * 60 * 60 + parseInt(time[1]) * 60 + parseInt(time[2]); 215 | } 216 | }else{ 217 | timeInSeconds = parseInt(timeInSeconds); 218 | } 219 | } 220 | 221 | return timeInSeconds; 222 | } 223 | 224 | async function fetchData(feedUrl) { 225 | try { 226 | const response = await fetch(feedUrl); 227 | // 确保响应状态码是2xx 228 | if (!response.ok) { 229 | throw new Error('Network response was not ok'); 230 | } 231 | 232 | const responseText = await response.text(); 233 | // console.log(responseText) 234 | 235 | // 解析响应文本为JSON 236 | const jsonData = JSON.parse(responseText); 237 | // console.log(jsonData); 238 | 239 | return jsonData; 240 | } catch (error) { 241 | console.error('There has been a problem with your fetch operation:', error); 242 | } 243 | } -------------------------------------------------------------------------------- /content.js: -------------------------------------------------------------------------------- 1 | let lastTarget; 2 | let lastRange; 3 | let insertDefault = true; 4 | let screenDefault = true; 5 | 6 | $(function () { 7 | // 获取当前tab页面的url 根据不同域名进行不同的注入处理 8 | currentPageUrl = document.location.href; 9 | console.log("currentPageUrl is " + currentPageUrl); 10 | 11 | if (currentPageUrl.indexOf('/stage/build/desktop') != -1) { 12 | // 思源页面 注入时间戳按钮 13 | injectVideoJumpButton() 14 | // 绑定快捷键 15 | document.addEventListener('keydown', (event) => { 16 | if (event.ctrlKey && event.key === '1') { 17 | event.preventDefault(); // 阻止默认行为 18 | var insert1Btn = document.getElementById('extension-video-insert1'); 19 | insert1Btn.click(); 20 | } else if (event.ctrlKey && event.key === '2') { 21 | event.preventDefault(); // 阻止默认行为 22 | var screen1Btn = document.getElementById('extension-video-screen1'); 23 | screen1Btn.click(); 24 | } else if (event.ctrlKey && event.key === '3') { 25 | event.preventDefault(); // 阻止默认行为 26 | var insert2Btn = document.getElementById('extension-video-insert2'); 27 | insert2Btn.click(); 28 | } else if (event.ctrlKey && event.key === '4') { 29 | event.preventDefault(); // 阻止默认行为 30 | var screen2Btn = document.getElementById('extension-video-screen2'); 31 | screen2Btn.click(); 32 | } else if (event.ctrlKey && event.key === '5') { 33 | event.preventDefault(); // 阻止默认行为 34 | var resetBtn = document.getElementById('extension-video-reset'); 35 | resetBtn.click(); 36 | } 37 | }); 38 | // 监听鼠标事件 39 | document.body.addEventListener('mouseup', function (event) { 40 | var target = event.target; 41 | if (target.tagName.toLowerCase() === 'div' && target.getAttribute('contenteditable') === 'true') { 42 | lastTarget = target; 43 | // 获取当前节点父节点的data-node-id 44 | console.log("mouseup : current node id is ", target.tagName.toLowerCase(), target.parentElement.getAttribute('data-node-id')); 45 | if (!target.parentElement.getAttribute('data-node-id')) { 46 | console.log("mouseup : parent node id is ", target.innerText); 47 | } 48 | let sel = window.getSelection(); 49 | if (sel.rangeCount > 0) { 50 | lastRange = sel.getRangeAt(0); 51 | } 52 | } 53 | if (lastTarget) { 54 | // console.log("mouseup lastTarget is " + lastTarget.innerHTML); 55 | } 56 | }) 57 | 58 | // 监听点击事件 这里主要是处理思源页面中的时间戳标签点击事件 59 | // 这里把时间戳形态统一处理方便未来扩展 60 | // 格式:链接:空 锚文本:时间戳 标题:视频页链接,这个必须要有,这样的话时间戳才好被其他文档引用,对于被引用的时间戳打开形式可以用悬浮窗或者固定窗口来实现,这种情况一般也是辅助文本来使用,可能适用于学生考试党,或者一些视频教程 61 | // 在思源任何位置出现被点击先判断当前页是否存在iframe,存在则替换iframe链接播放 62 | document.body.addEventListener('click', function (event) { 63 | requestAnimationFrame(async function () { 64 | // 判断当前节点是否是div,且具有contenteditable属性 65 | var target = event.target; 66 | 67 | if (target.tagName.toLowerCase() === 'span') { 68 | var href = target.getAttribute('data-href'); 69 | var dataType = target.getAttribute('data-type'); 70 | // 这里判断是不是时间戳链接 href:## 或者 ### 71 | if ((href == '##' || href == '###') && dataType == 'a') { 72 | // 重置焦点 73 | if (lastTarget && lastRange) { 74 | let sel = window.getSelection(); 75 | sel.removeAllRanges(); 76 | sel.addRange(lastRange); 77 | } 78 | 79 | if (href == '##') { 80 | // iframe内嵌模式 81 | // 内部跳转 82 | var time = target.innerText; 83 | // 去除[] 84 | time = time.replace(/\[|\]/g, ''); 85 | // 这里可以同时固定住当前页面的视频 86 | document.querySelectorAll(".fn__flex-1.protyle").forEach(function (node) { 87 | // 获取class属性值 88 | var className = node.getAttribute("class") 89 | if (className == 'fn__flex-1 protyle') { 90 | // 判断当前文档树是否展开 如果展开 点击关闭 91 | // dock__item ariaLabel dock__item--active 92 | var menuNode = document.querySelector(".dock__item.ariaLabel.dock__item--active"); 93 | if (menuNode) { 94 | // 如果是大纲 就不执行关闭 95 | var dataTitle = menuNode.getAttribute("data-title"); 96 | if (dataTitle && dataTitle == "大纲") { 97 | console.log("大纲模式,不处理"); 98 | } else { 99 | menuNode.click(); 100 | } 101 | } 102 | 103 | var position = node.querySelectorAll(".iframe-content")[0].style.position; 104 | if (position != "fixed") { 105 | node.querySelectorAll(".iframe-content")[0].style.position = "fixed"; 106 | // iframe-content的width要和.protyle-wysiwyg.iframe中的width保持一致 107 | // node.querySelectorAll("iframe")[0].style.removeProperty("width"); 108 | } 109 | 110 | var frameUrl = node.querySelectorAll("iframe")[0].getAttribute("src") 111 | // 跳转当前内嵌页面视频进度 112 | dumpInnerVideo(time, frameUrl); 113 | } 114 | }) 115 | } else if (href == '###') { 116 | // 左右分屏模式 这种更通用 117 | var hrefText = target.innerText; 118 | // 判断文本类型 http链接 还是 时间戳 119 | if (hrefText && hrefText.indexOf('http') != -1) { 120 | // 跳转页面 先定位tab 没有则创建 这种一般是首次打开 没有时间戳笔记的时候快速定位视频页面 121 | openOuterVideo(hrefText); 122 | } else if (hrefText && hrefText.indexOf(':') != -1) { 123 | // 时间戳 124 | var time = hrefText.replace(/\[|\]/g, ''); 125 | var videoUrl = target.getAttribute('data-title'); 126 | if (videoUrl && videoUrl != "") { 127 | // 跳转外部页面视频进度 128 | dumpOuterVideo(time, videoUrl); 129 | } 130 | } 131 | } 132 | } else if (dataType == 'a' && target.innerText == '>>') { 133 | var blockId = target.parentElement.parentElement.getAttribute("data-node-id") 134 | // 快进 5s一加 135 | // 获取父节点的第二个子节点 136 | var time = target.parentElement.firstChild.nextElementSibling.innerText; 137 | // 去除[] 138 | time = time.replace(/\[|\]/g, ''); 139 | // 把时间戳转换成秒 140 | var seconds = parseTimeFromStr(time); 141 | // 增加2s 142 | var newTime = parseStrFromTime(seconds + 5); 143 | 144 | var blockMd = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/getBlockKramdown", { 145 | "id": blockId 146 | }); 147 | 148 | var newMd = cleanKramdown(blockMd.data.kramdown) 149 | console.log("blockMd is =>>>>>>>> " + newMd); 150 | // 找到newMd首次出现的[],把里面的内容替换成newTime字段 151 | newMd = newMd.replace(`[[${time}]]`, `[[${newTime}]]`); 152 | // 更新当前block数据 153 | await invokeSiyuanApi("http://127.0.0.1:6806/api/block/updateBlock", { 154 | "data": newMd, 155 | "dataType": "markdown", 156 | "id": blockId 157 | }); 158 | } else if (dataType == 'a' && target.innerText == '<<') { 159 | var blockId = target.parentElement.parentElement.getAttribute("data-node-id") 160 | // 快退 5s一减 161 | // 获取时间戳时间 162 | var time = target.parentElement.firstChild.nextElementSibling.innerText; 163 | // 去除[] 164 | time = time.replace(/\[|\]/g, ''); 165 | // 把时间戳转换成秒 166 | var seconds = parseTimeFromStr(time); 167 | // 增加2s 168 | var newTime = parseStrFromTime(seconds - 5); 169 | 170 | var blockMd = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/getBlockKramdown", { 171 | "id": blockId 172 | }); 173 | 174 | var newMd = cleanKramdown(blockMd.data.kramdown) 175 | console.log("blockMd is =>>>>>>>> " + newMd); 176 | // 找到newMd首次出现的[],把里面的内容替换成newTime字段 177 | newMd = newMd.replace(`[[${time}]]`, `[[${newTime}]]`); 178 | // 更新当前block数据 179 | await invokeSiyuanApi("http://127.0.0.1:6806/api/block/updateBlock", { 180 | "data": newMd, 181 | "dataType": "markdown", 182 | "id": blockId 183 | }); 184 | } 185 | } 186 | }) 187 | }, true); // 我需要在所有监听之后执行,所以这里需要设置useCapture为true 188 | } else if (currentPageUrl.indexOf('bilibili.com/video') != -1) { 189 | // bilibili 列表 &&单视频 合集需要单独劫持 190 | injectBilibiliVideoDownButton() 191 | } else if (currentPageUrl.indexOf('youtube.com/watch') != -1) { 192 | // 单页面下载按钮 193 | injectYoutubeVideoDownButton() 194 | } else if (currentPageUrl.indexOf('youtube.com/playlist') != -1) { 195 | // 列表页面下载按钮 196 | injectYoutubePlaylistDownButton() 197 | } else if (currentPageUrl.indexOf('plugins/siyuan-blog') != -1) { // 思源分享插件页面 198 | // 等待页面出现iframe节点 199 | var observer = new MutationObserver(function (mutations) { 200 | if (document.querySelectorAll("iframe").length > 0) { 201 | // 移除监听 202 | observer.disconnect(); 203 | 204 | // 找到所有data-type=NodeBlockquote的节点,从里面找data-type为NodeParagraph的节点,再从里面找contenteditable=false的节点的innerText的值是否为空,如果为空,则移除当前NodeBlockquote节点 205 | document.querySelectorAll("[data-type=NodeBlockquote]").forEach(function (node) { 206 | // 查询所有的NodeParagraph节点 207 | var isEmpty = true; 208 | node.querySelectorAll("[data-type=NodeParagraph]").forEach(function (pnode) { 209 | var contentEditableFalse = pnode.querySelector('[contenteditable="false"]'); 210 | if (contentEditableFalse && contentEditableFalse.innerText) { 211 | isEmpty = false; 212 | } 213 | 214 | if (contentEditableFalse && !contentEditableFalse.innerText) { 215 | pnode.remove(); 216 | } 217 | }) 218 | 219 | if (isEmpty) { 220 | node.remove(); 221 | } 222 | }) 223 | 224 | // 处理一下iframe样式 225 | // 移除.iframe-content的display属性 把position改为fixed 226 | document.querySelectorAll(".iframe-content").forEach(function (node) { 227 | node.style.display = "block"; 228 | node.style.position = "fixed"; 229 | }) 230 | 231 | // 找到页面的iframe,移除style中的pointer-events属性 232 | document.querySelectorAll("iframe").forEach(function (node) { 233 | node.style.pointerEvents = "auto"; 234 | }) 235 | 236 | // 找到data-type=NodeHeading里的孙子节点a标签,如果innerText为<< >> 则移除a标签 237 | document.querySelectorAll("[data-type=NodeHeading]").forEach(function (node) { 238 | // 查询所有的span节点 239 | node.querySelectorAll("span").forEach(function (span) { 240 | console.log(span); 241 | if (span.innerText == ">>" || span.innerText == "<<") { 242 | span.remove(); 243 | } 244 | }) 245 | }) 246 | 247 | // 点击事件监听 248 | document.body.addEventListener('click', function (event) { 249 | var target = event.target; 250 | // 判断是不是a标签 251 | if (target.tagName.toLowerCase() === 'a') { 252 | var href = target.getAttribute('href'); 253 | if (href && href == "##") { 254 | event.preventDefault(); 255 | var time = target.innerText; 256 | // 去除[] 257 | time = time.replace(/\[|\]/g, ''); 258 | // 找到当前iframe 259 | var frameUrl = document.querySelectorAll("iframe")[0].getAttribute("src") 260 | // 跳转当前内嵌页面视频进度 261 | dumpInnerVideo(time, frameUrl); 262 | } else if (target.innerText == '<<') { 263 | // 阻止默认跳转 264 | event.preventDefault(); 265 | } else if (target.innerText == '>>') { 266 | // 阻止默认跳转 267 | event.preventDefault(); 268 | } 269 | } 270 | }, true); 271 | } 272 | }) 273 | 274 | observer.observe(document, { childList: true, subtree: true }); 275 | } else if (currentPageUrl.indexOf('/supr-blog/') != -1) { 276 | // DOM 变化时的逻辑 277 | function applyLogic() { 278 | document.querySelectorAll(".theme-reco-md-content iframe").forEach(function (node) { 279 | const rect = node.getBoundingClientRect(); 280 | // node.style.left = "900px"; 281 | if (node.style.position == "fixed") { 282 | } else { 283 | node.style.position = "fixed"; 284 | node.style.bottom = "0px"; 285 | node.style.right = "0px"; 286 | node.style.width = "600px"; 287 | node.style.height = "400px"; 288 | node.style.border = "none"; 289 | node.style.zIndex = "9999"; 290 | } 291 | }) 292 | 293 | } 294 | 295 | var observer = new MutationObserver(function (mutations) { 296 | if (document.querySelectorAll("iframe").length > 0) { 297 | // 移除监听 298 | observer.disconnect(); 299 | // 点击事件监听 300 | document.body.addEventListener('click', function (event) { 301 | var target = event.target; 302 | // 判断是不是a标签 303 | if (target.tagName.toLowerCase() === 'a') { 304 | var href = target.getAttribute('href'); 305 | if (href && href == "##") { 306 | applyLogic(); 307 | event.preventDefault(); 308 | var time = target.innerText; 309 | // 去除[] 310 | time = time.replace(/\[|\]/g, ''); 311 | // 找到当前iframe 312 | var frameUrl = document.querySelectorAll("iframe")[0].getAttribute("src") 313 | // 跳转当前内嵌页面视频进度 314 | dumpInnerVideo(time, frameUrl); 315 | } 316 | } 317 | }, true); 318 | } 319 | }) 320 | 321 | observer.observe(document, { childList: true, subtree: true }); 322 | } else if(currentPageUrl.indexOf("zhihu.com") != -1){ // 知乎话题 323 | injectZhihuTopicQuestionDownButton(currentPageUrl) 324 | } 325 | 326 | 327 | // 跨域通信 监听来自background的消息 328 | chrome.runtime.onMessage.addListener(async function (request, sender, sendResponse) { 329 | 330 | if (request.action === "noticMsg") { 331 | console.log("onMessage request action is " + request.action); 332 | console.log("onMessage current page is " + currentPageUrl); 333 | await invokeSiyuanApi("http://127.0.0.1:6806/api/notification/pushMsg", { 334 | "msg": request.msg, 335 | "timeout": 3000 336 | }); 337 | return true; 338 | } 339 | 340 | // 外部视频跳转 341 | if (request.action === "dumpOuterVideo") { 342 | console.log("onMessage request action is " + request.action); 343 | console.log("onMessage current page is " + currentPageUrl); 344 | // 这里还需要判断一下视频详情页地址是否和当前页面匹配 345 | if (document.URL == request.videoUrl) { 346 | document.querySelector('video').currentTime = request.time; 347 | document.querySelector('video').play(); 348 | sendResponse({ result: "ok" }) 349 | return true; // 保持消息通道打开直到sendResponse被调用 350 | } else { 351 | // 其他页面收到消息后暂停视频 352 | var video = document.querySelector('video'); 353 | if (video) { 354 | video.pause(); 355 | } 356 | return false; 357 | } 358 | } 359 | 360 | // 查询外部视频进度条 361 | if (request.action === "queryOuterVideo") { 362 | console.log("onMessage request action is " + request.action); 363 | console.log("onMessage current page is " + currentPageUrl); 364 | // 判断当前页面的iframe地址是否和request.frameUrl相同 365 | if (document.URL == request.videoUrl) { 366 | sendResponse({ time: document.querySelector('video').currentTime }) 367 | document.querySelector('video').play(); 368 | return true; // 保持消息通道打开直到sendResponse被调用 369 | } 370 | } 371 | 372 | // 外部视频写入截图 373 | if (request.action === "screenOuterInsert" && currentPageUrl.indexOf("/stage/build/desktop") != -1) { 374 | console.log("onMessage request action is " + request.action); 375 | console.log("onMessage current page is " + currentPageUrl); 376 | // 拿到数据直接写入思源 377 | var currentTime = request.currentTime; 378 | var imgUrl = request.imgUrl; 379 | var videoUrl = request.videoUrl; 380 | // 把截图和时间戳插入到思源中 381 | 382 | console.log(currentTime); 383 | console.log(imgUrl); 384 | const videoTimestamp = document.createElement("div"); 385 | 386 | // 获取当前窗口下的datanode 387 | document.querySelectorAll(".fn__flex-1.protyle").forEach(async function (node) { 388 | // 获取class属性值 389 | var className = node.getAttribute("class"); 390 | if (className == "fn__flex-1 protyle") { 391 | // 焦点追加模式 392 | if (!screenDefault) { 393 | var dataNodeId; 394 | if (lastTarget) { 395 | dataNodeId = lastTarget.parentElement.getAttribute("data-node-id"); 396 | } 397 | console.log("dataNodeId is => " + dataNodeId); 398 | if (!dataNodeId) { 399 | // 告警 400 | await invokeSiyuanApi("http://127.0.0.1:6806/api/notification/pushMsg", { 401 | "msg": "请双击输入位置选择插入位置", 402 | "timeout": 3000 403 | }); 404 | } else { 405 | var blockMd = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/getBlockKramdown", { 406 | "id": dataNodeId 407 | }); 408 | var newMd = cleanKramdown(blockMd.data.kramdown) + ` [[${currentTime}]](### "${videoUrl}")` 409 | // 调用更新接口 410 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/updateBlock", { 411 | "data": newMd, 412 | "dataType": "markdown", 413 | "id": dataNodeId 414 | }); 415 | // 插入图片 416 | result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 417 | data: ` ​![image](${imgUrl}) `, 418 | dataType: "markdown", 419 | parentID: dataNodeId 420 | }); 421 | } 422 | } else { 423 | // 从当前节点里找.sb .protyle-background.protyle-background--enable 424 | var parentID = node.querySelector(".protyle-background.protyle-background--enable").getAttribute("data-node-id"); 425 | 426 | // 这里调用一下思源插入内容快的接口 427 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 428 | data: `#### [<<]()[[${currentTime}]](### "${videoUrl}")[>>]():`, 429 | dataType: "markdown", 430 | parentID: parentID, 431 | }); 432 | result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 433 | data: `>`, 434 | dataType: "markdown", 435 | parentID: parentID, 436 | }); 437 | // 这里移动焦点到最新插入的节点 438 | console.log("result is => " + result.data[0].doOperations[0].id); 439 | var newNode = document.querySelector(`[data-node-id="${result.data[0].doOperations[0].id}"]`); 440 | if (newNode) { 441 | node.querySelector(".protyle-content.protyle-content--transition").scrollTop += 1000; 442 | newNode.setAttribute("tabindex", "0"); 443 | newNode.focus(); 444 | } 445 | // 插入图片 446 | result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 447 | data: ` >​![image](${imgUrl}) `, 448 | dataType: "markdown", 449 | parentID: parentID, 450 | }); 451 | } 452 | } 453 | }); 454 | sendResponse({ result: "ok" }); 455 | return true; // 保持消息通道打开直到sendResponse被调用 456 | } 457 | 458 | // 写入截图 iframe写入 459 | if (request.action === "screenInsert" && currentPageUrl.indexOf("/stage/build/desktop") != -1) { 460 | console.log("onMessage request action is " + request.action); 461 | console.log("onMessage current page is " + currentPageUrl); 462 | // 拿到数据直接写入思源 463 | var currentTime = request.currentTime; 464 | var imgUrl = request.imgUrl; 465 | var frameUrl = request.frameUrl; 466 | // 把截图和时间戳插入到思源中 467 | 468 | console.log(currentTime); 469 | console.log(imgUrl); 470 | 471 | // 获取当前窗口下的datanode 472 | document.querySelectorAll(".fn__flex-1.protyle").forEach(async function (node) { 473 | // 获取class属性值 474 | var className = node.getAttribute("class"); 475 | if (className == "fn__flex-1 protyle") { 476 | // 判断当前是哪种模式写入 iframe内嵌 还是外部视频 477 | var iframe = node.querySelector("iframe"); 478 | if (iframe) { 479 | // 焦点追加模式 480 | if (!screenDefault) { 481 | var dataNodeId; 482 | if (lastTarget) { 483 | dataNodeId = lastTarget.parentElement.getAttribute("data-node-id"); 484 | } 485 | console.log("dataNodeId is => " + dataNodeId); 486 | if (!dataNodeId) { 487 | // 告警 488 | await invokeSiyuanApi("http://127.0.0.1:6806/api/notification/pushMsg", { 489 | "msg": "请双击输入位置选择插入位置", 490 | "timeout": 3000 491 | }); 492 | } else { 493 | var blockMd = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/getBlockKramdown", { 494 | "id": dataNodeId 495 | }); 496 | var newMd = cleanKramdown(blockMd.data.kramdown) + ` [[${currentTime}]](## "${frameUrl}")` 497 | // 调用更新接口 498 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/updateBlock", { 499 | "data": newMd, 500 | "dataType": "markdown", 501 | "id": dataNodeId 502 | }); 503 | // 插入图片 504 | result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 505 | data: ` ​![image](${imgUrl}) `, 506 | dataType: "markdown", 507 | parentID: dataNodeId 508 | }); 509 | } 510 | } else { 511 | // 从当前节点里找.sb 512 | var parentID = node.querySelectorAll(".sb")[1].getAttribute("data-node-id"); 513 | 514 | // 这里调用一下思源插入内容快的接口 515 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 516 | data: `#### [<<]()[[${currentTime}]](## "${frameUrl}")[>>]():`, 517 | dataType: "markdown", 518 | parentID: parentID, 519 | }); 520 | result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 521 | data: `>`, 522 | dataType: "markdown", 523 | parentID: parentID, 524 | }); 525 | // 这里移动焦点到最新插入的节点 526 | console.log("result is => " + result.data[0].doOperations[0].id); 527 | var newNode = document.querySelector(`[data-node-id="${result.data[0].doOperations[0].id}"]`); 528 | if (newNode) { 529 | node.querySelector(".protyle-content.protyle-content--transition").scrollTop += 1000; 530 | newNode.setAttribute("tabindex", "0"); 531 | newNode.focus(); 532 | } 533 | // 插入图片 534 | result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 535 | data: `>​![image](${imgUrl})`, 536 | dataType: "markdown", 537 | parentID: parentID, 538 | }); 539 | } 540 | } 541 | } 542 | }); 543 | return true; // 保持消息通道打开直到sendResponse被调用 544 | } 545 | 546 | // 外部视频截图指令 547 | if (request.action === "screenshotOuterVideo") { 548 | console.log("onMessage request action is " + request.action); 549 | console.log("onMessage current page is " + currentPageUrl); 550 | // 判断当前页面的iframe地址是否和request.frameUrl相同 551 | if (document.URL == request.videoUrl) { 552 | // 截图 553 | var video = document.querySelectorAll('video')[0]; 554 | var canvas = document.createElement('canvas'); 555 | var ctx = canvas.getContext('2d'); 556 | canvas.width = video.videoWidth; 557 | canvas.height = video.videoHeight; 558 | ctx.drawImage(video, 0, 0, canvas.width, canvas.height); 559 | var base64Data = canvas.toDataURL('image/png'); 560 | 561 | // 创建一个Blob对象 562 | const arr = base64Data.split(','); 563 | const mime = arr[0].match(/:(.*?);/)[1]; 564 | const bstr = atob(arr[1]); 565 | let n = bstr.length; 566 | const u8arr = new Uint8Array(n); 567 | while (n--) { 568 | u8arr[n] = bstr.charCodeAt(n); 569 | } 570 | const blob = new Blob([u8arr], { type: mime }); 571 | 572 | blob.name = 'screenshot.png'; 573 | blob.lastModifiedDate = new Date(); 574 | 575 | // 创建FormData对象并添加文件 576 | const formData = new FormData(); 577 | formData.append('assetsDirPath', '/assets/'); 578 | // 添加文件,这里我们给文件命名为'screenshot.png' 579 | formData.append('file[]', blob, 'screenshot.png'); 580 | 581 | // 这里直接调用思源上传接口 582 | var uploadResult = await invokeSiyuanUploadApi(formData); 583 | // 获取上传后的图片路径 screenshot.png这个是一个整体 584 | // {"code":0,"msg":"","data":{"errFiles":null,"succMap":{"screenshot.png":"assets/screenshot-20240812122103-liwlec4.png"}}} 585 | // var imgUrl = uploadResult.data.succMap['screenshot.png']; 586 | var imgUrl = Object.values(uploadResult.data.succMap); 587 | if (imgUrl) { 588 | var currentTime = parseVideoTimeFromDuration(document.querySelector('video').currentTime * 1000); 589 | // 这里通过backgroud.js把截图和时间戳转发到content.js 590 | chrome.runtime.sendMessage({ 591 | action: "screenOuterInsert", 592 | imgUrl: imgUrl, 593 | currentTime: currentTime, 594 | videoUrl: request.videoUrl 595 | }, function (response) { 596 | console.log("content.js receive response => " + JSON.stringify(response)); 597 | }); 598 | } else { 599 | console.error("截图失败"); 600 | } 601 | } else { 602 | // 其他页面收到消息后暂停视频 603 | var video = document.querySelector('video'); 604 | if (video) { 605 | video.pause(); 606 | } 607 | } 608 | sendResponse({ result: "ok" }) 609 | return true; // 保持消息通道打开直到sendResponse被调用 610 | } 611 | 612 | 613 | // bilibili 正片页面 注入下载按钮 614 | if (request.action === "injectBilibiliZhengPianButton" && currentPageUrl.indexOf('bilibili.com/bangumi/play') != -1) { 615 | console.log("onMessage request action is " + request.action); 616 | console.log("onMessage current page is " + currentPageUrl); 617 | 618 | console.log(request.data.result.episodes) 619 | if (true) { 620 | // 先移除老的下载按钮 621 | document.querySelectorAll("#CRX-container").forEach(function (item) { 622 | item.remove(); 623 | }) 624 | // 注入正片下载按钮 625 | injectBilibiliZhengPianButton(request.data.result.episodes); 626 | } 627 | sendResponse({ result: "ok" }) 628 | return true; // 保持消息通道打开直到sendResponse被调用 629 | } 630 | 631 | // bilibili 合集页面 注入下载按钮 632 | if (request.action === "injectBilibiliHeJiButton" && currentPageUrl.indexOf('bilibili.com/video') != -1) { 633 | console.log("onMessage request action is " + request.action); 634 | console.log("onMessage current page is " + currentPageUrl); 635 | 636 | console.log(request.data.data.View.ugc_season) 637 | // 订阅合集节点 .second-line_right 独有 638 | var heji = document.querySelector(".subscribe-btn"); 639 | if (heji) { 640 | // 先移除老的下载按钮 641 | document.querySelectorAll("#CRX-container").forEach(function (item) { 642 | item.remove(); 643 | }) 644 | // 注入合集下载按钮 645 | injectBilibiliHeJiButton(request.data.data.View.ugc_season); 646 | } 647 | sendResponse({ result: "ok" }) 648 | return true; // 保持消息通道打开直到sendResponse被调用 649 | } 650 | }); 651 | }); 652 | 653 | /** 654 | * 单视频&&选集页面 注入下载按钮 655 | */ 656 | function injectBilibiliVideoDownButton() { 657 | // 创建一个div容器(可选,如果只需要按钮则不需要) 658 | const crxContainer = document.createElement('div'); 659 | crxContainer.id = 'CRX-container'; 660 | crxContainer.style.position = 'fixed'; // 设置为固定定位 661 | // 顶部垂直居中对齐 662 | crxContainer.style.right = '1%'; 663 | crxContainer.style.top = '100px'; 664 | crxContainer.style.transform = 'translateY(-50%)'; 665 | crxContainer.style.display = 'flex'; 666 | crxContainer.style.alignItems = 'center'; 667 | crxContainer.style.zIndex = '1000'; // 确保它位于其他元素之上 668 | 669 | // 创建并填充按钮 670 | const crxButton = document.createElement('button'); 671 | crxButton.id = 'CRX-container-button'; 672 | crxButton.type = 'button'; 673 | crxButton.style.backgroundColor = 'red'; // 直接在元素上设置样式,而不是通过innerHTML 674 | crxButton.style.width = '100px'; 675 | crxButton.style.height = '42px'; 676 | crxButton.style.zIndex = '2000'; // 确保它位于其他元素之上 677 | crxButton.classList.add('Button', 'FollowButton', 'FEfUrdfMIKpQDJDqkjte', 'Button--primary', 'Button--blue', 'epMJl0lFQuYbC7jrwr_o', 'JmYzaky7MEPMFcJDLNMG'); // 添加类名 678 | 679 | // 判断页面类型 合集、选集、单个视频 680 | 681 | // 视频选集节点 .head-left 独有 682 | var xuanji = document.querySelector(".left"); 683 | // 订阅合集节点 .second-line_right 独有 684 | var heji = document.querySelector(".subscribe-btn"); 685 | if (xuanji) { 686 | // 选集页面 687 | crxButton.textContent = '下载选集'; 688 | // 将按钮添加到div容器中(如果需要的话) 689 | crxContainer.appendChild(crxButton); 690 | // 将容器添加到页面的body开头 691 | document.body.insertBefore(crxContainer, document.body.firstChild); 692 | // 绑定点击事件 693 | crxButton.addEventListener('click', async function () { 694 | console.log('下载选集!'); 695 | // 获取视频标题 696 | var title = document.querySelector(".video-title.special-text-indent").innerText.replace("/", ""); 697 | var author = document.querySelector('meta[itemprop="author"]').getAttribute('content').trim(); 698 | ; 699 | // 这里调用思源接口创建根目录 700 | // var json = { 701 | // "notebook": notebook, 702 | // "path": "/Video-视频库/" + title, 703 | // "markdown": "" 704 | // } 705 | // // 调用思源创建文档api 706 | // await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 707 | 708 | var detailUrl = document.querySelector('meta[itemprop="url"]').getAttribute('content'); 709 | var bvid = detailUrl.split("/")[4] 710 | 711 | // 查询页面.page-num所有节点 712 | document.querySelectorAll(".title-txt").forEach(async function (item, index) { 713 | //
1.东汉末年宦官当道,老郭揭秘曹操身世之谜
解析出1 714 | // var page = item.innerText.split(".")[0]; 715 | var page = index + 1; 716 | // var page = item.innerText.replace("P", "").trim(); 717 | var duration = item.parentElement.nextElementSibling.innerText; 718 | var videoTitle = item.innerText; 719 | var videoUrl = `https://player.bilibili.com/player.html?bvid=${bvid}&page=${page}&high_quality=1&as_wide=1&allowfullscreen=true&autoplay=1`; 720 | // 调用思源接口创建分片文件 721 | var json = { 722 | "notebook": notebook, 723 | "path": "/Video-视频库/" + title + "/" + page + "-" + videoTitle, 724 | "markdown": "" 725 | } 726 | // 调用思源创建文档api 727 | var docRes = await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 728 | // 然后调用思源模版接口惊醒初始化操作 729 | json = { 730 | "id": docRes.data, 731 | "path": pageTemplateUrl 732 | } 733 | var renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/template/render", json) 734 | // 拿到渲染后的markdown 735 | var markdown = renderResult.data.content; 736 | // 替换占位符 作者、时间、时长 737 | markdown = markdown.replace(/{{VideoUrl}}/g, videoUrl) 738 | markdown = markdown.replace(/{{Author}}/g, author) 739 | markdown = markdown.replace(/{{Statue}}/g, "未读") 740 | markdown = markdown.replace(/{{Duration}}/g, duration) 741 | 742 | // 写入数据到思源中 743 | json = { 744 | "dataType": "dom", 745 | "data": markdown, 746 | "nextID": "", 747 | "previousID": "", 748 | "parentID": docRes.data 749 | } 750 | renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/insertBlock", json) 751 | }) 752 | // 移除下载按钮 753 | crxContainer.remove(); 754 | }); 755 | } else if (heji) { 756 | // 合集跳过不处理 通过接口劫持注入 757 | } else { 758 | // 单独视频页面 759 | crxButton.textContent = '下载单视频'; 760 | // 将按钮添加到div容器中(如果需要的话) 761 | crxContainer.appendChild(crxButton); 762 | // 将容器添加到页面的body开头 763 | document.body.insertBefore(crxContainer, document.body.firstChild); 764 | // 绑定点击事件 765 | crxButton.addEventListener('click', async function () { 766 | console.log('下载单视频!'); 767 | // 获取视频标题 768 | var title = document.querySelector(".video-title.special-text-indent").innerText.replace("/", ""); 769 | var author = document.querySelector('meta[itemprop="author"]').getAttribute('content').trim(); 770 | var duration = document.querySelector(".bpx-player-ctrl-time-duration").innerText; 771 | // 这里调用思源接口创建根目录 772 | var json = { 773 | "notebook": notebook, 774 | "path": "/" + title, 775 | "markdown": "" 776 | } 777 | // 调用思源创建文档api 778 | var docRes = await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 779 | // 然后调用思源模版接口惊醒初始化操作 780 | json = { 781 | "id": docRes.data, 782 | "path": pageTemplateUrl 783 | } 784 | var renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/template/render", json) 785 | // 拿到渲染后的markdown 786 | var markdown = renderResult.data.content; 787 | 788 | var detailUrl = document.querySelector('meta[itemprop="url"]').getAttribute('content'); 789 | var bvid = detailUrl.split("/")[4] 790 | 791 | var videoUrl = `https://player.bilibili.com/player.html?bvid=${bvid}&page=1&high_quality=1&as_wide=1&allowfullscreen=true&autoplay=1`; 792 | // 替换占位符 作者、时间、时长 793 | markdown = markdown.replace(/{{VideoUrl}}/g, videoUrl) 794 | markdown = markdown.replace(/{{Author}}/g, author) 795 | markdown = markdown.replace(/{{Statue}}/g, "未读") 796 | markdown = markdown.replace(/{{Duration}}/g, duration) 797 | 798 | // 写入数据到思源中 799 | json = { 800 | "dataType": "dom", 801 | "data": markdown, 802 | "nextID": "", 803 | "previousID": "", 804 | "parentID": docRes.data 805 | } 806 | renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/insertBlock", json) 807 | // 移除下载按钮 808 | crxContainer.remove(); 809 | }); 810 | } 811 | } 812 | 813 | /** 814 | * 正片注入下载按钮 走劫持逻辑 815 | * @param {*} episodes 816 | */ 817 | function injectBilibiliZhengPianButton(episodes) { 818 | // 创建一个div容器(可选,如果只需要按钮则不需要) 819 | const crxContainer = document.createElement('div'); 820 | crxContainer.id = 'CRX-container'; 821 | crxContainer.style.position = 'fixed'; // 设置为固定定位 822 | // 顶部垂直居中对齐 823 | crxContainer.style.right = '1%'; 824 | crxContainer.style.top = '100px'; 825 | crxContainer.style.transform = 'translateY(-50%)'; 826 | crxContainer.style.display = 'flex'; 827 | crxContainer.style.alignItems = 'center'; 828 | crxContainer.style.zIndex = '1000'; // 确保它位于其他元素之上 829 | 830 | // 创建并填充按钮 831 | const crxButton = document.createElement('button'); 832 | crxButton.id = 'CRX-container-button'; 833 | crxButton.type = 'button'; 834 | crxButton.style.backgroundColor = 'red'; // 直接在元素上设置样式,而不是通过innerHTML 835 | crxButton.style.width = '100px'; 836 | crxButton.style.height = '42px'; 837 | crxButton.style.zIndex = '2000'; // 确保它位于其他元素之上 838 | crxButton.classList.add('Button', 'FollowButton', 'FEfUrdfMIKpQDJDqkjte', 'Button--primary', 'Button--blue', 'epMJl0lFQuYbC7jrwr_o', 'JmYzaky7MEPMFcJDLNMG'); // 添加类名 839 | // 单独视频页面 840 | crxButton.textContent = document.querySelector(".mediainfo_mediaTitle__Zyiqh").innerText; 841 | // 将按钮添加到div容器中(如果需要的话) 842 | crxContainer.appendChild(crxButton); 843 | // 将容器添加到页面的body开头 844 | document.body.insertBefore(crxContainer, document.body.firstChild); 845 | crxButton.addEventListener('click', async function () { 846 | console.log('下载正片!'); 847 | // 获取视频标题 848 | var title = crxButton.textContent.replace("/", ""); 849 | var author = document.querySelector('meta[property="og:title"]').getAttribute('content').trim(); 850 | // 这里调用思源接口创建根目录 851 | var json = { 852 | "notebook": notebook, 853 | "path": "/Video-视频库/" + title, 854 | "markdown": "" 855 | } 856 | // 调用思源创建文档api 857 | await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 858 | 859 | // 遍历episodes 860 | episodes.forEach(async function (item, index) { 861 | // 获取视频标题 862 | var videoTitle = item.long_title || item.title; 863 | var duration = parseVideoTimeFromDuration(item.duration); 864 | var bvid = item.bvid; 865 | // 获取视频地址 866 | var videoUrl = `https://player.bilibili.com/player.html?bvid=${bvid}&page=1&high_quality=1&as_wide=1&allowfullscreen=true&autoplay=1`; 867 | // 调用思源接口创建分片文件 868 | json = { 869 | "notebook": notebook, 870 | "path": "/Video-视频库/" + title + "/" + (index + 1) + "-" + videoTitle, 871 | "markdown": "" 872 | } 873 | // 调用思源创建文档api 874 | var docRes = await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 875 | // 然后调用思源模版接口惊醒初始化操作 876 | json = { 877 | "id": docRes.data, 878 | "path": pageTemplateUrl 879 | } 880 | var renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/template/render", json) 881 | // 拿到渲染后的markdown 882 | var markdown = renderResult.data.content; 883 | // 替换占位符 作者、时间、时长 884 | markdown = markdown.replace(/{{VideoUrl}}/g, videoUrl) 885 | markdown = markdown.replace(/{{Author}}/g, author) 886 | markdown = markdown.replace(/{{Statue}}/g, "未读") 887 | markdown = markdown.replace(/{{Duration}}/g, duration) 888 | // 写入数据到思源中 889 | json = { 890 | "dataType": "dom", 891 | "data": markdown, 892 | "nextID": "", 893 | "previousID": "", 894 | "parentID": docRes.data 895 | } 896 | renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/insertBlock", json) 897 | }) 898 | // 移除下载按钮 899 | crxContainer.remove(); 900 | }); 901 | } 902 | 903 | /** 904 | * 把视频时长转换成字符串格式 905 | * 参数单位是毫秒 906 | * @param {*} milliseconds 907 | * @returns 908 | */ 909 | function parseVideoTimeFromDuration(milliseconds) { 910 | // 计算小时数 911 | var hours = Math.floor(milliseconds / (60 * 60 * 1000)); 912 | // 计算剩余的分钟数 913 | var minutes = Math.floor((milliseconds % (60 * 60 * 1000)) / (60 * 1000)); 914 | // 计算剩余的秒数 915 | var seconds = Math.floor((milliseconds % (60 * 1000)) / 1000); 916 | 917 | // 格式化小时、分钟和秒,确保它们是两位数 918 | hours = hours.toString().padStart(2, '0'); 919 | minutes = minutes.toString().padStart(2, '0'); 920 | seconds = seconds.toString().padStart(2, '0'); 921 | 922 | // 根据时长判断并拼接字符串 923 | if (hours > 0) { 924 | return `${hours}:${minutes}:${seconds}`; // xx:yy:zz 925 | } else if (minutes > 0) { 926 | return `${minutes}:${seconds}`; // xx:yy 927 | } else { 928 | // 如果分钟和小时都为0,但秒数可能不为0(尽管在这个特定情况下它会是0,因为至少要有1秒) 929 | // 但为了完整性,我们还是返回秒数(尽管前导0可能看起来不必要) 930 | return `00:${seconds}`; // 00:xx,但注意这个分支实际上不太可能被触发,除非有特别的逻辑需要它 931 | // 或者,如果确实只需要在秒数大于0时才显示,可以改为: 932 | // return seconds > 0 ? `00:${seconds}` : '00:00'; 933 | } 934 | } 935 | 936 | /** 937 | * 合集注入下载按钮 走劫持逻辑 938 | * @param {*} ugc_season 939 | */ 940 | function injectBilibiliHeJiButton(ugc_season) { 941 | // 创建一个div容器(可选,如果只需要按钮则不需要) 942 | const crxContainer = document.createElement('div'); 943 | crxContainer.id = 'CRX-container'; 944 | crxContainer.style.position = 'fixed'; // 设置为固定定位 945 | // 顶部垂直居中对齐 946 | crxContainer.style.right = '1%'; 947 | crxContainer.style.top = '100px'; 948 | crxContainer.style.transform = 'translateY(-50%)'; 949 | crxContainer.style.display = 'flex'; 950 | crxContainer.style.alignItems = 'center'; 951 | crxContainer.style.zIndex = '1000'; // 确保它位于其他元素之上 952 | 953 | // 创建并填充按钮 954 | const crxButton = document.createElement('button'); 955 | crxButton.id = 'CRX-container-button'; 956 | crxButton.type = 'button'; 957 | crxButton.style.backgroundColor = 'red'; // 直接在元素上设置样式,而不是通过innerHTML 958 | crxButton.style.width = '100px'; 959 | crxButton.style.height = '42px'; 960 | crxButton.style.zIndex = '2000'; // 确保它位于其他元素之上 961 | crxButton.classList.add('Button', 'FollowButton', 'FEfUrdfMIKpQDJDqkjte', 'Button--primary', 'Button--blue', 'epMJl0lFQuYbC7jrwr_o', 'JmYzaky7MEPMFcJDLNMG'); // 添加类名 962 | // 单独视频页面 963 | crxButton.textContent = '下载合集'; 964 | // 将按钮添加到div容器中(如果需要的话) 965 | crxContainer.appendChild(crxButton); 966 | // 将容器添加到页面的body开头 967 | document.body.insertBefore(crxContainer, document.body.firstChild); 968 | // 绑定点击事件 969 | crxButton.addEventListener('click', async function () { 970 | console.log('下载合集!'); 971 | // 获取视频标题 972 | var title = ugc_season.title.replace("/", ""); 973 | var author = document.querySelector('meta[itemprop="author"]').getAttribute('content').trim(); 974 | // 这里调用思源接口创建根目录 975 | var json = { 976 | "notebook": notebook, 977 | "path": "/Video-视频库/" + title, 978 | "markdown": "" 979 | } 980 | // 调用思源创建文档api 981 | await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 982 | 983 | // 遍历ugc_season.sections 984 | ugc_season.sections.forEach(async function (item, secIndex) { 985 | // 获取分区标题 986 | var secTitle = item.title; 987 | // 这里调用思源接口创建根目录 988 | json = { 989 | "notebook": notebook, 990 | "path": "/Video-视频库/" + title + "/" + (secIndex + 1) + "-" + secTitle, 991 | "markdown": "" 992 | } 993 | // 调用思源创建文档api 994 | await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 995 | // 遍历item.episodes 996 | item.episodes.forEach(async function (ep, index) { 997 | var bvid = ep.bvid; 998 | var videoTitle = ep.title; 999 | var videoUrl = `https://player.bilibili.com/player.html?bvid=${bvid}&page=1&high_quality=1&as_wide=1&allowfullscreen=true&autoplay=1` 1000 | var duration = parseVideoTimeFromDuration(ep.arc.duration * 1000) 1001 | // 这里调用思源接口创建根目录 1002 | json = { 1003 | "notebook": notebook, 1004 | "path": "/Video-视频库/" + title + "/" + (secIndex + 1) + "-" + secTitle + "/" + (index + 1) + "-" + videoTitle, 1005 | "markdown": "" 1006 | } 1007 | // 调用思源创建文档api 1008 | var docRes = await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 1009 | // 然后调用思源模版接口惊醒初始化操作 1010 | json = { 1011 | "id": docRes.data, 1012 | "path": pageTemplateUrl 1013 | } 1014 | var renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/template/render", json) 1015 | // 拿到渲染后的markdown 1016 | var markdown = renderResult.data.content; 1017 | // 替换markdown占位符 1018 | markdown = markdown.replace(/{{VideoUrl}}/g, videoUrl) 1019 | markdown = markdown.replace(/{{Author}}/g, author) 1020 | markdown = markdown.replace(/{{Statue}}/g, "未读") 1021 | markdown = markdown.replace(/{{Duration}}/g, duration) 1022 | // 写入数据到思源中 1023 | json = { 1024 | "dataType": "dom", 1025 | "data": markdown, 1026 | "nextID": "", 1027 | "previousID": "", 1028 | "parentID": docRes.data 1029 | } 1030 | renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/insertBlock", json) 1031 | }) 1032 | }) 1033 | // 移除按钮 1034 | crxContainer.remove(); 1035 | }); 1036 | } 1037 | 1038 | /** 1039 | * 时间戳按钮注入以及事件绑定 1040 | */ 1041 | function injectVideoJumpButton() { 1042 | // 这里等待#toolbarVIP加载出来再继续执行 1043 | if (document.querySelector("#toolbarVIP") === null || document.querySelector(".protyle-breadcrumb") === null) { 1044 | setTimeout(injectVideoJumpButton, 100); 1045 | return; 1046 | } 1047 | 1048 | // 初始化思源首页的布局 1049 | // initHomeLayout(); 1050 | 1051 | // 精简版按钮 1052 | const simpleDiv = document.createElement('div'); 1053 | simpleDiv.innerHTML = `
🍁
`; 1054 | 1055 | // 精简版保存按钮 1056 | // const simpleSaveDiv = document.createElement('div'); 1057 | // simpleSaveDiv.innerHTML = `
🌻
`; 1058 | 1059 | // 模版插入 1060 | const insert1Div = document.createElement('div'); 1061 | insert1Div.innerHTML = `
🐞
`; 1062 | 1063 | // 自由插入 1064 | const insert2Div = document.createElement('div'); 1065 | insert2Div.innerHTML = `
🐸
`; 1066 | 1067 | const resetDiv = document.createElement('div'); 1068 | resetDiv.innerHTML = `
🪲
`; 1069 | 1070 | const screen1Div = document.createElement('div'); 1071 | screen1Div.innerHTML = `
🐷
`; 1072 | 1073 | const screen2Div = document.createElement('div'); 1074 | screen2Div.innerHTML = `
🐯
`; 1075 | 1076 | // 获取#toolbarVIP元素 1077 | const toolbarVIP = document.getElementById('toolbarVIP'); 1078 | 1079 | // 将新元素添加到#toolbarVIP后面 1080 | toolbarVIP.insertAdjacentElement('afterend', simpleDiv); 1081 | // simpleDiv.insertAdjacentElement('afterend', simpleSaveDiv); 1082 | toolbarVIP.insertAdjacentElement('afterend', insert1Div); 1083 | insert1Div.insertAdjacentElement('afterend', insert2Div); 1084 | insert2Div.insertAdjacentElement('afterend', resetDiv); 1085 | resetDiv.insertAdjacentElement('afterend', screen1Div); 1086 | screen1Div.insertAdjacentElement('afterend', screen2Div); 1087 | 1088 | 1089 | var simpleBtn = document.getElementById('extension-simple-article'); 1090 | // var simpleSaveBtn = document.getElementById('extension-save-simple'); 1091 | var insert1Btn = document.getElementById('extension-video-insert1'); 1092 | var insert2Btn = document.getElementById('extension-video-insert2'); 1093 | var resetBtn = document.getElementById('extension-video-reset'); 1094 | var screen1Btn = document.getElementById('extension-video-screen1'); 1095 | var screen2Btn = document.getElementById('extension-video-screen2'); 1096 | 1097 | // 精简版按钮点击事件 1098 | simpleBtn.addEventListener('click', function () { 1099 | console.log('精简版按钮被点击了!'); 1100 | simpleArticleWindow(); 1101 | }); 1102 | 1103 | 1104 | 1105 | // 重置视频窗口监听事件 1106 | resetBtn.addEventListener('click', function () { 1107 | // 获取当前窗口的iframe的url 1108 | document.querySelectorAll(".fn__flex-1.protyle").forEach(function (node) { 1109 | // 获取class属性值 1110 | var className = node.getAttribute("class") 1111 | if (className == 'fn__flex-1 protyle') { 1112 | // 先判断iframe存不存在 存在调整样式 1113 | var iframe = node.querySelectorAll("iframe")[0]; 1114 | if (iframe) { 1115 | node.querySelectorAll(".iframe-content")[0].style.position = "relative"; 1116 | } else { 1117 | console.log("iframe不存在"); 1118 | } 1119 | // 滚动条移动到最上面 1120 | node.querySelector(".protyle-content.protyle-content--transition").scrollTop = 0; 1121 | } 1122 | }); 1123 | }); 1124 | 1125 | // 默认截图监听事件 1126 | screen1Btn.addEventListener('click', function () { 1127 | console.log('默认截图按钮被点击了!'); 1128 | screenDefault = true; 1129 | screenVideoTime(); 1130 | }); 1131 | 1132 | // 自由截图监听事件 1133 | screen2Btn.addEventListener('click', function () { 1134 | console.log('自由截图按钮被点击了!'); 1135 | setTimeout(function () { 1136 | if (lastTarget && lastRange) { 1137 | let sel = window.getSelection(); 1138 | sel.removeAllRanges(); 1139 | if (lastRange) { 1140 | sel.addRange(lastRange); // 恢复之前保存的光标位置 1141 | } 1142 | screenDefault = false; 1143 | screenVideoTime(); 1144 | } else { 1145 | screenDefault = true; 1146 | screenVideoTime(); 1147 | } 1148 | }, 0); 1149 | }); 1150 | 1151 | // 默认时间戳按钮点击事件 1152 | insert1Btn.addEventListener('click', function () { 1153 | console.log('默认时间戳按钮被点击了!'); 1154 | insertDefault = true; 1155 | insertVideoTime(); 1156 | }); 1157 | 1158 | // 自由时间戳按钮点击事件 1159 | insert2Btn.addEventListener('click', function () { 1160 | console.log('自由时间戳按钮被点击了!'); 1161 | 1162 | setTimeout(function () { 1163 | if (lastTarget && lastRange) { 1164 | let sel = window.getSelection(); 1165 | sel.removeAllRanges(); 1166 | if (lastRange) { 1167 | sel.addRange(lastRange); // 恢复之前保存的光标位置 1168 | } 1169 | insertDefault = false; 1170 | insertVideoTime(); 1171 | } else { 1172 | insertDefault = true; 1173 | insertVideoTime(); 1174 | } 1175 | }, 0); 1176 | }); 1177 | } 1178 | 1179 | 1180 | function initHomeLayout() { 1181 | document.querySelector("#toolbar").style.display = "none"; 1182 | // 隐藏左边菜单栏 hidden visibility: hidden; 1183 | document.querySelector("#dockLeft").style.display = "none"; 1184 | // 隐藏右侧导航条 1185 | document.querySelector("#dockRight").style.display = "none"; 1186 | // 隐藏底部导航条 1187 | document.querySelector("#status").style.display = "none"; 1188 | // 隐藏Tab栏 1189 | document.querySelector(".fn__flex-column.fn__flex.fn__flex-1.layout__wnd--active").querySelector("div").style.display = "none"; 1190 | document.querySelector(".protyle-background__action").style.display = "none"; 1191 | // document.querySelector(".protyle-attr--alias").style.display = "none"; 1192 | // 隐藏menu栏 1193 | document.querySelector(".protyle-breadcrumb").style.display = "none"; 1194 | 1195 | document.querySelector(".b3-list.fn__flex-column").style.display = "none"; 1196 | 1197 | 1198 | 1199 | } 1200 | 1201 | function simpleArticleWindow() { 1202 | // 判断当前是否已经是精简模式 1203 | if(document.querySelector("#status").style.display == "none") { 1204 | // document.querySelector("#toolbar").style.display = "block"; 1205 | document.querySelector("#dockLeft").style.display = "block"; 1206 | document.querySelector("#dockRight").style.display = "block"; 1207 | document.querySelector("#status").style.display = "block"; 1208 | 1209 | // document.querySelector(".fn__flex-column.fn__flex.fn__flex-1.layout__wnd--active").querySelector("div").style.display = "block"; 1210 | // document.querySelector(".protyle-background__action").style.display = "block"; 1211 | // document.querySelector(".protyle-attr--alias").style.display = "block"; 1212 | // document.querySelector(".protyle-breadcrumb").style.display = "block"; 1213 | 1214 | 1215 | // 高亮标注 找出所有span标签 data-type属性是mark的 1216 | document.querySelectorAll('span[data-type="mark"]').forEach(function (node) { 1217 | node.style.borderBottom = "2px solid currentColor"; 1218 | // font-size: larger; 1219 | node.style.fontSize = ""; 1220 | }) 1221 | 1222 | // .protyle-wysiwyg添加color: #1c2222; 1223 | document.querySelector(".protyle-wysiwyg").style.color = ""; 1224 | }else{ 1225 | // 隐藏顶部导航条 1226 | // document.querySelector("#toolbar").style.display = "none"; 1227 | // 隐藏左边菜单栏 hidden visibility: hidden; 1228 | document.querySelector("#dockLeft").style.display = "none"; 1229 | // 隐藏右侧导航条 1230 | document.querySelector("#dockRight").style.display = "none"; 1231 | // 隐藏底部导航条 1232 | document.querySelector("#status").style.display = "none"; 1233 | // 隐藏Tab栏 1234 | document.querySelector(".fn__flex-column.fn__flex.fn__flex-1.layout__wnd--active").querySelector("div").style.display = "none"; 1235 | document.querySelector(".protyle-background__action").style.display = "none"; 1236 | // document.querySelector(".protyle-attr--alias").style.display = "none"; 1237 | // 隐藏menu栏 1238 | document.querySelector(".protyle-breadcrumb").style.display = "none"; 1239 | // 高亮标注 找出所有span标签 data-type属性是mark的 1240 | document.querySelectorAll('span[data-type="mark"]').forEach(function (node) { 1241 | // 修改节点样式 border-bottom: 8px solid currentColor; 1242 | node.style.borderBottom = "6px solid currentColor"; 1243 | // font-size: larger; 1244 | node.style.fontSize = "larger"; 1245 | }) 1246 | 1247 | // 隐藏未标注文字 1248 | // .protyle-wysiwyg添加color: #1c2222; 1249 | document.querySelector(".protyle-wysiwyg").style.color = "#1c2222"; 1250 | } 1251 | 1252 | } 1253 | 1254 | 1255 | function screenVideoTime() { 1256 | // 获取当前窗口的iframe的url 1257 | document.querySelectorAll(".fn__flex-1.protyle").forEach(function (node) { 1258 | // 获取class属性值 1259 | var className = node.getAttribute("class") 1260 | if (className == 'fn__flex-1 protyle') { 1261 | // 判断当前文档树是否展开 如果展开 点击关闭 1262 | // dock__item ariaLabel dock__item--active 1263 | var menuNode = document.querySelector(".dock__item.ariaLabel.dock__item--active"); 1264 | if (menuNode) { 1265 | var dataTitle = menuNode.getAttribute("data-title"); 1266 | if (dataTitle && dataTitle == "大纲") { 1267 | console.log("大纲模式,不处理"); 1268 | } else { 1269 | menuNode.click(); 1270 | } 1271 | } 1272 | 1273 | // 先判断iframe存不存在 存在调整样式 1274 | var iframe = node.querySelectorAll("iframe")[0]; 1275 | if (iframe) { 1276 | // 每次点击时间戳 都要把当前页面iframe固定住 1277 | // .iframe-content样式中 position:relative; 1278 | var position = node.querySelectorAll(".iframe-content")[0].style.position; 1279 | if (position != "fixed") { 1280 | node.querySelectorAll(".iframe-content")[0].style.position = "fixed"; 1281 | // iframe-content的width要和.protyle-wysiwyg.iframe中的width保持一致 1282 | // node.querySelectorAll("iframe")[0].style.removeProperty("width"); 1283 | } 1284 | 1285 | // 先找到对应的iframe 通知backgroud.js转发截图请求 1286 | var frameUrl = node.querySelectorAll("iframe")[0].getAttribute("src") 1287 | chrome.runtime.sendMessage({ action: "screenshot", frameUrl: frameUrl }, function (response) { 1288 | }); 1289 | } else { 1290 | console.log("iframe不存在,分屏模式"); 1291 | // 获取当前窗口首个span[data-href='###']且innerText为http的值 1292 | var videoUrl = node.querySelector("span[data-href='###']").innerText; 1293 | if (videoUrl && videoUrl.indexOf("http") != -1) { 1294 | // 通过backgroud.js 发送截图指令 1295 | chrome.runtime.sendMessage({ action: "screenshotOuterVideo", videoUrl: videoUrl }, function (response) { 1296 | }); 1297 | } 1298 | } 1299 | } 1300 | }); 1301 | } 1302 | 1303 | function insertVideoTime() { 1304 | // 获取当前窗口的iframe的url 1305 | document.querySelectorAll(".fn__flex-1.protyle").forEach(function (node) { 1306 | // 获取class属性值 1307 | var className = node.getAttribute("class") 1308 | if (className == 'fn__flex-1 protyle') { 1309 | // 判断当前文档树是否展开 如果展开 点击关闭 1310 | // dock__item ariaLabel dock__item--active 1311 | var menuNode = document.querySelector(".dock__item.ariaLabel.dock__item--active"); 1312 | if (menuNode) { 1313 | var dataTitle = menuNode.getAttribute("data-title"); 1314 | if (dataTitle && dataTitle == "大纲") { 1315 | console.log("大纲模式,不处理"); 1316 | } else { 1317 | menuNode.click(); 1318 | } 1319 | } 1320 | 1321 | // 判断iframe存不存在 存在调整样式 1322 | var iframe = node.querySelectorAll("iframe")[0]; 1323 | if (iframe) { 1324 | // 每次点击时间戳 都要把当前页面iframe固定住 1325 | // .iframe-content样式中 position:relative; 1326 | // 这里先判断下是不是已经是固定模式了 1327 | var position = node.querySelectorAll(".iframe-content")[0].style.position; 1328 | if (position != "fixed") { 1329 | node.querySelectorAll(".iframe-content")[0].style.position = "fixed"; 1330 | // iframe-content的width要和.protyle-wysiwyg.iframe中的width保持一致 1331 | // node.querySelectorAll("iframe")[0].style.removeProperty("width"); 1332 | } 1333 | 1334 | var frameUrl = node.querySelectorAll("iframe")[0].getAttribute("src"); 1335 | // 发送消息到background.js获取iframe视频时间 1336 | chrome.runtime.sendMessage({ action: "queryInnerIframe", frameUrl: frameUrl }, async function (response) { 1337 | console.log('Received iframe video time :', response.currentTime); 1338 | 1339 | // 焦点追加模式 1340 | if (!insertDefault) { 1341 | var dataNodeId; 1342 | if (lastTarget) { 1343 | dataNodeId = lastTarget.parentElement.getAttribute("data-node-id"); 1344 | } 1345 | console.log("dataNodeId is => " + dataNodeId); 1346 | if (!dataNodeId) { 1347 | // 告警 1348 | await invokeSiyuanApi("http://127.0.0.1:6806/api/notification/pushMsg", { 1349 | "msg": "请双击输入位置选择插入位置", 1350 | "timeout": 3000 1351 | }); 1352 | } else { 1353 | var blockMd = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/getBlockKramdown", { 1354 | "id": dataNodeId 1355 | }); 1356 | var newMd = cleanKramdown(blockMd.data.kramdown) + ` [[${response.currentTime}]](## "${frameUrl}")` 1357 | // 调用更新接口 1358 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/updateBlock", { 1359 | "data": newMd, 1360 | "dataType": "markdown", 1361 | "id": dataNodeId 1362 | }); 1363 | } 1364 | } else { 1365 | // 从当前节点里找.sb 1366 | var parentID = node.querySelectorAll(".sb")[1].getAttribute("data-node-id"); 1367 | // 这里调用一下思源插入内容快的接口 1368 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 1369 | "data": `#### [<<]()[[${response.currentTime}]](## "${frameUrl}")[>>]():`, 1370 | "dataType": "markdown", 1371 | "parentID": parentID 1372 | }); 1373 | result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 1374 | "data": `>`, 1375 | "dataType": "markdown", 1376 | "parentID": parentID 1377 | }); 1378 | // 这里移动焦点到最新插入的节点 1379 | console.log("result is => " + result.data[0].doOperations[0].id) 1380 | var newNode = document.querySelector(`[data-node-id="${result.data[0].doOperations[0].id}"]`) 1381 | if (newNode) { 1382 | node.querySelector(".protyle-content.protyle-content--transition").scrollTop += 1000; 1383 | newNode.setAttribute('tabindex', '0'); 1384 | newNode.focus(); 1385 | } 1386 | } 1387 | }); 1388 | } else { 1389 | console.log("iframe不存在,分屏模式"); 1390 | // 从这里面去拿 1391 | var videoUrl = node.querySelector("span[data-href='###']").innerText; 1392 | // 获取当前窗口首个span[data-href='###']且innerText为http的值 1393 | if (videoUrl && videoUrl.indexOf("http") != -1) { 1394 | // 通过backgroud.js 获取视频页面进度 1395 | chrome.runtime.sendMessage({ action: "queryOuterVideo", videoUrl: videoUrl }, async function (response) { 1396 | // 拿到时间戳 往当前文档插入数据 1397 | console.log('Received iframe video time :', response.currentTime); 1398 | 1399 | // 焦点追加模式 1400 | if (!insertDefault) { 1401 | var dataNodeId; 1402 | if (lastTarget) { 1403 | dataNodeId = lastTarget.parentElement.getAttribute("data-node-id"); 1404 | } 1405 | console.log("dataNodeId is => " + dataNodeId); 1406 | if (!dataNodeId) { 1407 | // 告警 1408 | await invokeSiyuanApi("http://127.0.0.1:6806/api/notification/pushMsg", { 1409 | "msg": "请双击输入位置选择插入位置", 1410 | "timeout": 3000 1411 | }); 1412 | } else { 1413 | var blockMd = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/getBlockKramdown", { 1414 | "id": dataNodeId 1415 | }); 1416 | var newMd = cleanKramdown(blockMd.data.kramdown) + ` [[${response.currentTime}]](### "${videoUrl}")` 1417 | // 调用更新接口 1418 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/updateBlock", { 1419 | "data": newMd, 1420 | "dataType": "markdown", 1421 | "id": dataNodeId 1422 | }); 1423 | } 1424 | } else { 1425 | // 尾部追加模式 在第一个sb节点后面插入 1426 | var parentID = node.querySelector(".protyle-background.protyle-background--enable").getAttribute("data-node-id"); 1427 | // 这里调用一下思源插入内容快的接口 1428 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 1429 | "data": `#### [<<]()[[${response.currentTime}]](### "${videoUrl}")[>>]():`, 1430 | "dataType": "markdown", 1431 | "parentID": parentID 1432 | }); 1433 | result = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/appendBlock", { 1434 | "data": `>`, 1435 | "dataType": "markdown", 1436 | "parentID": parentID 1437 | }); 1438 | // 这里移动焦点到最新插入的节点 1439 | console.log("result is => " + result.data[0].doOperations[0].id) 1440 | var newNode = document.querySelector(`[data-node-id="${result.data[0].doOperations[0].id}"]`) 1441 | if (newNode) { 1442 | node.querySelector(".protyle-content.protyle-content--transition").scrollTop += 1000; 1443 | newNode.setAttribute('tabindex', '0'); 1444 | newNode.focus(); 1445 | } 1446 | } 1447 | }); 1448 | } 1449 | } 1450 | } 1451 | }) 1452 | } 1453 | 1454 | 1455 | function cleanKramdown(kramdownContent) { 1456 | // 使用正则表达式删除所有的 {: ... } 元数据部分 1457 | return kramdownContent.replace(/ *\{:.*?\}/g, '').trim(); 1458 | } 1459 | 1460 | /** 1461 | * 定位视频详情页 1462 | * @param {*} videoUrl 1463 | */ 1464 | function openOuterVideo(videoUrl) { 1465 | // 定位思源视频详情页 存在则定位 不存在则创建 1466 | chrome.runtime.sendMessage({ action: "openOuterVideo", videoUrl: videoUrl }, function (response) { 1467 | }); 1468 | } 1469 | 1470 | 1471 | /** 1472 | * 外部视频时间戳跳转 1473 | * @param {*} time 1474 | * @param {*} videoUrl 1475 | */ 1476 | function dumpOuterVideo(time, videoUrl) { 1477 | chrome.runtime.sendMessage({ action: "dumpOuterVideo", time: time, videoUrl: videoUrl }, function (response) { 1478 | }); 1479 | } 1480 | 1481 | /** 1482 | * 思源页面内嵌视频跳转 1483 | * 1484 | * @param time 时间戳 1485 | * @returns 无返回值,通过回调函数输出响应结果 1486 | */ 1487 | function dumpInnerVideo(time, frameUrl) { 1488 | // 消息先发送到background.js 再由background.js 发送到各个content.js 找到匹配的iframe进行跳转 1489 | chrome.runtime.sendMessage({ action: "dumpInnerVideo", time: time, frameUrl: frameUrl }, function (response) { 1490 | }); 1491 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coriger/siyuan-video-extension/6868bf8be86a0638f243248f47d42c7b66082c39/icon.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "思源笔记:视频笔记插件", 3 | "version": "0.2.6", 4 | "manifest_version": 3, 5 | "description": "思源笔记:视频笔记插件,支持B站、Youtube、百度网盘做视频笔记", 6 | "icons": { 7 | "500": "icon.png" 8 | }, 9 | "action": { 10 | "default_popup": "options.html" 11 | }, 12 | "options_ui": { 13 | "page": "options.html" 14 | }, 15 | "permissions": [ 16 | "storage", 17 | "tabs", 18 | "webRequest" 19 | ], 20 | "host_permissions": [ 21 | "*://*.bilibili.com/*", 22 | "*://pan.baidu.com/*" 23 | ], 24 | "background": { 25 | "service_worker": "background.js" 26 | }, 27 | "content_scripts": [ 28 | { 29 | "matches": ["*://*.bilibili.com/*","*://*/stage/build/desktop/*","*://*.youtube.com/*","*://*.zhihu.com/*","*://pan.baidu.com/*","*://*/plugins/siyuan-blog/*","*://*/supr-blog/*"], 30 | "js": ["zepto.min.js", "common.js", "scripts/baidu_disk.js","scripts/zhihu.js", "scripts/bilibili_iframe.js", "scripts/bilibili_web.js", "scripts/youtube_embed.js", "scripts/youtube_web.js", "scripts/siyuan.js","content.js"], 31 | "all_frames": true 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 思源笔记:视频笔记插件配置 7 | 83 | 84 | 85 |
86 | 87 | 88 | 89 | 90 | 91 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | document.getElementById('config-form').addEventListener('submit', function(event) { 2 | event.preventDefault(); 3 | var token = document.getElementById('token').value; 4 | // 去掉空格 5 | token = token.replace(/\s/g, ''); 6 | var notebook = document.getElementById('notebook').value; 7 | var pageTemplateUrl = document.getElementById('pageTemplateUrl').value; 8 | // 去掉pageTemplateUrl空格 9 | pageTemplateUrl = pageTemplateUrl.replace(/\s/g, ''); 10 | chrome.storage.local.set({ token: token, notebook: notebook, pageTemplateUrl: pageTemplateUrl }, function() { 11 | console.log(notebook, pageTemplateUrl, token); 12 | }); 13 | }); 14 | 15 | 16 | document.addEventListener('DOMContentLoaded', function() { 17 | // 当DOM完全加载后执行 18 | const form = document.getElementById('config-form'); 19 | const tokenInput = document.getElementById('token'); 20 | const notebookSelect = document.getElementById('notebook'); 21 | const pageTemplateUrlInput = document.getElementById('pageTemplateUrl'); 22 | 23 | // 从chrome.storage.local获取数据 24 | chrome.storage.local.get(['token', 'notebook', 'pageTemplateUrl'], function(result) { 25 | // 检查并设置token 26 | if (result.token) { 27 | tokenInput.value = result.token; 28 | // 获取所有笔记本的列表 29 | // 然后根据返回的notebook列表填充notebookSelect的选项 30 | fetch('http://127.0.0.1:6806/api/notebook/lsNotebooks', { 31 | method: 'POST', 32 | headers: { 33 | "Authorization": "token "+result.token, 34 | "Content-Type": "application/json", 35 | }, 36 | body: JSON.stringify({}) 37 | }) 38 | .then(response => response.json()) 39 | .then(data => { 40 | console.log('Fetched notebook list:', data.data.notebooks); 41 | notebookSelect.innerHTML = ''; 42 | data.data.notebooks.forEach(notebook => { 43 | var option = document.createElement('option'); 44 | option.value = notebook.id; 45 | option.text = notebook.name; 46 | if (result.notebook) { 47 | if (option.value === result.notebook) { 48 | option.selected = true; 49 | } 50 | } 51 | notebookSelect.appendChild(option); 52 | }); 53 | }) 54 | } 55 | 56 | // 检查并设置页面模板URL 57 | if (result.pageTemplateUrl) { 58 | pageTemplateUrlInput.value = result.pageTemplateUrl; 59 | } 60 | }) 61 | // 如果需要,还可以添加表单提交事件的处理逻辑 62 | form.addEventListener('submit', function(event) { 63 | event.preventDefault(); // 阻止表单的默认提交行为 64 | // 收集数据并保存到chrome.storage.local或其他需要的操作 65 | }); 66 | }); 67 | 68 | 69 | document.getElementById('token').addEventListener('change', function(event) { 70 | var token = event.target.value; 71 | if (token) { 72 | // 去掉空格 73 | token = token.replace(/\s/g, ''); 74 | // 清理之前的笔记本列表 75 | var notebookSelect = document.getElementById('notebook'); 76 | notebookSelect.innerHTML = ''; 77 | // 发起请求到思源笔记的 API,获取笔记本列表 78 | fetch('http://127.0.0.1:6806/api/notebook/lsNotebooks', { 79 | method: 'POST', 80 | headers: { 81 | "Authorization": "token "+token, 82 | "Content-Type": "application/json", 83 | }, 84 | body: JSON.stringify({}) 85 | }) 86 | .then(response => response.json()) 87 | .then(data => { 88 | console.log('Fetched notebook list:', data.data.notebooks); 89 | var notebookSelect = document.getElementById('notebook'); 90 | notebookSelect.innerHTML = ''; 91 | data.data.notebooks.forEach(notebook => { 92 | var option = document.createElement('option'); 93 | option.value = notebook.id; 94 | option.text = notebook.name; 95 | notebookSelect.appendChild(option); 96 | }); 97 | }) 98 | .catch(error => { 99 | console.error('Error fetching notebook list:', error); 100 | }); 101 | } else { 102 | var notebookSelect = document.getElementById('notebook'); 103 | notebookSelect.innerHTML = ''; 104 | } 105 | }); 106 | 107 | -------------------------------------------------------------------------------- /scripts/baidu_disk.js: -------------------------------------------------------------------------------- 1 | // 百度网盘 2 | if(document.URL.indexOf("pan.baidu.com/disk/main") > -1){ 3 | console.log("load baidu disk js success ~ current url: " + document.URL) 4 | } 5 | 6 | // 监听来自background script的消息 7 | chrome.runtime.onMessage.addListener(async function(request, sender, sendResponse) { 8 | // 只匹配目标url的请求 9 | if(document.URL.indexOf("pan.baidu.com/disk/main") > -1){ 10 | if (request.action === "injectDownloadButton") { 11 | console.log("百度下载按钮注入:",request.data.list) 12 | 13 | // 判断当前主路径 14 | var paths = document.querySelectorAll(".wp-s-pan-file-main__nav-item-title.text-ellip"); 15 | if(paths && paths.length > 0){ 16 | var pathName = paths[paths.length-1].innerText; 17 | // 这里提前检查下data里面的数据 看是否是文件夹 如果都是文件夹就不注入下载按钮了 18 | var existVideo = false; 19 | request.data.list.forEach(async function (item, index) { 20 | // 存在视频文件 21 | if(item.isdir == "0"){ 22 | existVideo = true; 23 | return false; 24 | } 25 | }) 26 | // 注入下载按钮 27 | if(existVideo){ 28 | injectBaiduPanButton(request.data.list,pathName); 29 | } 30 | }else{ 31 | // 移除页面所有button 32 | document.querySelectorAll("#button-container").forEach(function (item) { 33 | item.remove(); 34 | }) 35 | console.log("没有找到主路径,无法注入下载按钮!"); 36 | } 37 | return true; // 保持消息通道打开直到sendResponse被调用 38 | } 39 | } 40 | }) 41 | 42 | 43 | /** 44 | * 百度网盘页面注入下载按钮 45 | * @param {*} data 46 | */ 47 | function injectBaiduPanButton(data,pathName){ 48 | // 创建一个div容器(可选,如果只需要按钮则不需要) 49 | var crxContainer = document.getElementById("button-container"); 50 | if(!crxContainer){ 51 | crxContainer = document.createElement('div'); 52 | crxContainer.id = "button-container"; 53 | crxContainer.style.position = 'fixed'; // 设置为固定定位 54 | // 顶部垂直居中对齐 55 | crxContainer.style.top = '5%'; 56 | // 设置里面的按钮间隔50px 57 | crxContainer.style.padding = '50px'; 58 | // 居中对齐 59 | crxContainer.style.left = '50%'; 60 | crxContainer.style.zIndex = '1000'; // 确保它位于其他元素之上 61 | } 62 | 63 | // 创建并填充按钮 64 | const crxButton = document.createElement('button'); 65 | crxButton.type = 'button'; 66 | crxButton.position = 'absolute'; 67 | crxButton.style.marginLeft = "10px"; 68 | crxButton.style.backgroundColor = 'red'; // 直接在元素上设置样式,而不是通过innerHTML 69 | crxButton.style.width = '64px'; 70 | crxButton.style.height = '28px'; 71 | crxButton.style.zIndex = '1000'; // 确保它位于其他元素之上 72 | // 单独视频页面 73 | crxButton.textContent = pathName.slice(0, 6)+"("+data.length+")"; 74 | // 将按钮添加到div容器中(如果需要的话) 75 | crxContainer.appendChild(crxButton); 76 | // 将容器添加到页面的body开头 77 | document.body.insertBefore(crxContainer, document.body.firstChild); 78 | 79 | crxButton.addEventListener('click', function() { 80 | console.log('下载!'); 81 | // 遍历data 82 | data.forEach(async function (item, index) { 83 | // 获取视频标题 84 | var videoTitle = item.server_filename; 85 | var path = encodeURIComponent(item.path); 86 | // 获取视频地址 87 | var videoUrl = `https://pan.baidu.com/pfile/video?path=${path}`; 88 | console.log(videoTitle+":"+videoUrl); 89 | // 调用思源接口创建分片文件 90 | json = { 91 | "notebook": notebook, 92 | "path": "/"+pathName+"/"+videoTitle, 93 | "markdown":`${videoUrl}` 94 | } 95 | // 调用思源创建文档api 96 | await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd",json) 97 | }) 98 | 99 | // 移除当前按钮 100 | crxButton.remove(); 101 | }); 102 | } -------------------------------------------------------------------------------- /scripts/bilibili_iframe.js: -------------------------------------------------------------------------------- 1 | // B站视频iframe 2 | if (document.URL.indexOf("player.bilibili.com/player.html") > -1) { 3 | // 监听来自background script的消息 4 | chrome.runtime.onMessage.addListener(async function (request, sender, sendResponse) { 5 | // 只匹配目标url的请求 6 | if (document.URL.indexOf("player.bilibili.com/player.html") > -1) { 7 | // 视频跳转指令 8 | if (request.action === "dumpFrameVideo") { 9 | // 判断是否是当前页面 10 | if (request.frameUrl && request.frameUrl == document.URL) { 11 | console.log("bilibili iframe js = > dumpFrameVideo:跳转视频到指定时间", request.time); 12 | document.querySelector("video").currentTime = request.time; 13 | document.querySelector("video").play(); 14 | sendResponse({ result: "ok" }); 15 | return true; 16 | } else { 17 | // 其余收到相同消息的暂停播放 18 | document.querySelector("video").pause(); 19 | return false; 20 | } 21 | } 22 | 23 | // 查询视频进度指令 24 | if (request.action === "queryIframeVideo") { 25 | // 判断当前页面的iframe地址是否和request.frameUrl相同 26 | if (request.frameUrl && request.frameUrl == document.URL) { 27 | sendResponse({ time: document.querySelector("video").currentTime }); 28 | console.log("bilibili iframe js = > queryIframeVideo:查询视频时间", document.querySelector("video").currentTime); 29 | document.querySelector("video").play(); 30 | return true; 31 | } else { 32 | // 其余收到相同消息的暂停播放 33 | document.querySelector("video").pause(); 34 | return false; 35 | } 36 | } 37 | 38 | // 视频截图指令 39 | if (request.action === "screenIframe") { 40 | // 判断当前页面的iframe地址是否和request.frameUrl相同 41 | if (request.frameUrl && request.frameUrl == document.URL) { 42 | // 截图 43 | console.log("bilibili iframe js = > screenIframe:截图"); 44 | var video = document.querySelector("video"); 45 | var canvas = document.createElement("canvas"); 46 | var ctx = canvas.getContext("2d"); 47 | canvas.width = video.videoWidth; 48 | canvas.height = video.videoHeight; 49 | ctx.drawImage(video, 0, 0, canvas.width, canvas.height); 50 | var base64Data = canvas.toDataURL("image/png"); 51 | 52 | // 创建一个Blob对象 53 | const arr = base64Data.split(","); 54 | const mime = arr[0].match(/:(.*?);/)[1]; 55 | const bstr = atob(arr[1]); 56 | let n = bstr.length; 57 | const u8arr = new Uint8Array(n); 58 | while (n--) { 59 | u8arr[n] = bstr.charCodeAt(n); 60 | } 61 | const blob = new Blob([u8arr], { type: mime }); 62 | 63 | blob.name = "screenshot.png"; 64 | blob.lastModifiedDate = new Date(); 65 | 66 | // 创建FormData对象并添加文件 67 | const formData = new FormData(); 68 | formData.append("assetsDirPath", "/assets/"); 69 | // 添加文件,这里我们给文件命名为'screenshot.png' 70 | formData.append("file[]", blob, "screenshot.png"); 71 | 72 | // 这里直接调用思源上传接口 73 | var uploadResult = await invokeSiyuanUploadApi(formData); 74 | // 获取上传后的图片路径 screenshot.png这个是一个整体 75 | // {"code":0,"msg":"","data":{"errFiles":null,"succMap":{"screenshot.png":"assets/screenshot-20240812122103-liwlec4.png"}}} 76 | // var imgUrl = uploadResult.data.succMap["screenshot.png"]; 77 | var imgUrl = Object.values(uploadResult.data.succMap); 78 | if (imgUrl) { 79 | var currentTime = parseVideoTimeFromDuration(document.querySelector("video").currentTime * 1000); 80 | // 这里通过backgroud.js把截图和时间戳转发到content.js 81 | chrome.runtime.sendMessage( 82 | { 83 | action: "screenInsert", 84 | imgUrl: imgUrl, 85 | currentTime: currentTime, 86 | frameUrl: request.frameUrl 87 | }, 88 | function (response) { 89 | // console.log("content.js receive response => " + JSON.stringify(response)); 90 | } 91 | ); 92 | document.querySelector("video").play(); 93 | console.log("bilibili iframe js = > screenIframe:截图成功"); 94 | } else { 95 | console.error("bilibili iframe js = > screenIframe:截图失败"); 96 | } 97 | return true; 98 | } else { 99 | // 其余收到相同消息的暂停播放 100 | document.querySelector("video").pause(); 101 | return false; 102 | } 103 | } 104 | } 105 | }); 106 | } 107 | 108 | -------------------------------------------------------------------------------- /scripts/bilibili_web.js: -------------------------------------------------------------------------------- 1 | // B站正片页面 2 | if(document.URL.indexOf("bilibili.com/bangumi/play") > -1){ 3 | console.log("load bilibili bangumi play js success ~ current url: " + document.URL) 4 | } 5 | 6 | // 监听来自background script的消息 7 | chrome.runtime.onMessage.addListener(async function(request, sender, sendResponse) { 8 | // 只匹配目标url的请求 9 | if(document.URL.indexOf("bilibili.com/bangumi/play") > -1){ 10 | 11 | } 12 | }) -------------------------------------------------------------------------------- /scripts/siyuan.js: -------------------------------------------------------------------------------- 1 | // 思源笔记 2 | if(document.URL.indexOf("/stage/build/desktop") > -1){ 3 | console.log("load siyuan js success ~ current url: " + document.URL) 4 | } 5 | 6 | // 监听来自background script的消息 7 | chrome.runtime.onMessage.addListener(async function(request, sender, sendResponse) { 8 | // 只匹配目标url的请求 9 | if(document.URL.indexOf("/stage/build/desktop") > -1){ 10 | 11 | 12 | } 13 | }) -------------------------------------------------------------------------------- /scripts/youtube_embed.js: -------------------------------------------------------------------------------- 1 | // youtube嵌入视频 2 | if (document.URL.indexOf("youtube.com/embed") > -1) { 3 | // 监听来自background script的消息 4 | chrome.runtime.onMessage.addListener(async function (request, sender, sendResponse) { 5 | // 只匹配目标url的请求 6 | if (document.URL.indexOf("youtube.com/embed") > -1) { 7 | if (request.action === "dumpFrameVideo") { 8 | // 判断是否是当前页面 9 | if (request.frameUrl && request.frameUrl == document.URL) { 10 | console.log("youtube iframe js = > dumpFrameVideo:跳转视频到指定时间", request.time); 11 | // 判断当前播放状态是否为播放中 12 | if (document.querySelector("video").paused) { 13 | // 点击播放按钮 14 | document.querySelector(".ytp-play-button").click(); 15 | } 16 | document.querySelector("video").currentTime = request.time; 17 | document.querySelector("video").play(); 18 | return true; 19 | } else { 20 | // 其余收到相同消息的暂停播放 21 | document.querySelector("video").pause(); 22 | return false; 23 | } 24 | } 25 | 26 | // 查询视频进度指令 27 | if (request.action === "queryIframeVideo") { 28 | // 判断当前页面的iframe地址是否和request.frameUrl相同 29 | if (request.frameUrl && request.frameUrl == document.URL) { 30 | sendResponse({ time: document.querySelector("video").currentTime }); 31 | console.log("youtube iframe js = > queryIframeVideo:查询视频时间", document.querySelector("video").currentTime); 32 | document.querySelector("video").play(); 33 | return true; 34 | } else { 35 | // 其余收到相同消息的暂停播放 36 | document.querySelector("video").pause(); 37 | return false; 38 | } 39 | } 40 | 41 | // 视频截图指令 42 | if (request.action === "screenIframe") { 43 | // 判断当前页面的iframe地址是否和request.frameUrl相同 44 | if (request.frameUrl && request.frameUrl == document.URL) { 45 | // 截图 46 | console.log("youtube iframe js = > screenIframe:截图"); 47 | var video = document.querySelector("video"); 48 | var canvas = document.createElement("canvas"); 49 | var ctx = canvas.getContext("2d"); 50 | canvas.width = video.videoWidth; 51 | canvas.height = video.videoHeight; 52 | ctx.drawImage(video, 0, 0, canvas.width, canvas.height); 53 | var base64Data = canvas.toDataURL("image/png"); 54 | 55 | // 创建一个Blob对象 56 | const arr = base64Data.split(","); 57 | const mime = arr[0].match(/:(.*?);/)[1]; 58 | const bstr = atob(arr[1]); 59 | let n = bstr.length; 60 | const u8arr = new Uint8Array(n); 61 | while (n--) { 62 | u8arr[n] = bstr.charCodeAt(n); 63 | } 64 | const blob = new Blob([u8arr], { type: mime }); 65 | 66 | blob.name = "screenshot.png"; 67 | blob.lastModifiedDate = new Date(); 68 | 69 | // 创建FormData对象并添加文件 70 | const formData = new FormData(); 71 | formData.append("assetsDirPath", "/assets/"); 72 | // 添加文件,这里我们给文件命名为'screenshot.png' 73 | formData.append("file[]", blob, "screenshot.png"); 74 | 75 | // 这里直接调用思源上传接口 76 | var uploadResult = await invokeSiyuanUploadApi(formData); 77 | // 获取上传后的图片路径 screenshot.png这个是一个整体 78 | // {"code":0,"msg":"","data":{"errFiles":null,"succMap":{"screenshot-20240831152040-09diac9.png":"assets/screenshot-20240812122103-liwlec4.png"}}} 79 | // 解析JSON字符串为JavaScript对象 80 | var imgUrl = Object.values(uploadResult.data.succMap); 81 | console.log("截图地址:" + imgUrl); 82 | if (imgUrl) { 83 | var currentTime = parseVideoTimeFromDuration(document.querySelector("video").currentTime * 1000); 84 | // 这里通过backgroud.js把截图和时间戳转发到content.js 85 | chrome.runtime.sendMessage( 86 | { 87 | action: "screenInsert", 88 | imgUrl: imgUrl, 89 | currentTime: currentTime, 90 | frameUrl: request.frameUrl 91 | }, 92 | function (response) { 93 | // console.log("content.js receive response => " + JSON.stringify(response)); 94 | } 95 | ); 96 | document.querySelector("video").play(); 97 | console.log("youtube iframe js = > screenIframe:截图成功"); 98 | } else { 99 | console.error("youtube iframe js = > screenIframe:截图失败"); 100 | } 101 | return true; 102 | } else { 103 | // 其余收到相同消息的暂停播放 104 | document.querySelector("video").pause(); 105 | return false; 106 | } 107 | } 108 | } 109 | }); 110 | } 111 | 112 | -------------------------------------------------------------------------------- /scripts/youtube_web.js: -------------------------------------------------------------------------------- 1 | // youtube视频播放页 2 | if (document.URL.indexOf("youtube.com/watch") > -1 || document.URL.indexOf("youtube.com/playlist") > -1) { 3 | console.log("load youtube_web js success ~ current url: " + document.URL); 4 | } 5 | 6 | // 监听来自background script的消息 7 | chrome.runtime.onMessage.addListener(async function (request, sender, sendResponse) { 8 | // 只匹配目标url的请求 9 | if (document.URL.indexOf("youtube.com/watch") > -1 || document.URL.indexOf("youtube.com/playlist") > -1) { 10 | } 11 | }); 12 | 13 | /** 14 | * 单视频页注入下载按钮 15 | */ 16 | function injectYoutubeVideoDownButton() { 17 | // 创建一个div容器(可选,如果只需要按钮则不需要) 18 | const crxContainer = document.createElement("div"); 19 | crxContainer.id = "CRX-container"; 20 | crxContainer.style.position = "fixed"; // 设置为固定定位 21 | // 顶部垂直居中对齐 22 | crxContainer.style.right = "1%"; 23 | crxContainer.style.top = "100px"; 24 | crxContainer.style.transform = "translateY(-50%)"; 25 | crxContainer.style.display = "flex"; 26 | crxContainer.style.alignItems = "center"; 27 | crxContainer.style.zIndex = "1000"; // 确保它位于其他元素之上 28 | 29 | // 创建并填充按钮 30 | const crxButton = document.createElement("button"); 31 | crxButton.id = "CRX-container-button"; 32 | crxButton.type = "button"; 33 | crxButton.style.backgroundColor = "red"; // 直接在元素上设置样式,而不是通过innerHTML 34 | crxButton.style.width = "100px"; 35 | crxButton.style.height = "42px"; 36 | crxButton.style.zIndex = "2000"; // 确保它位于其他元素之上 37 | crxButton.classList.add("Button", "FollowButton", "FEfUrdfMIKpQDJDqkjte", "Button--primary", "Button--blue", "epMJl0lFQuYbC7jrwr_o", "JmYzaky7MEPMFcJDLNMG"); // 添加类名 38 | 39 | // 单独视频页面 40 | crxButton.textContent = "下载单视频"; 41 | // 将按钮添加到div容器中(如果需要的话) 42 | crxContainer.appendChild(crxButton); 43 | // 将容器添加到页面的body开头 44 | document.body.insertBefore(crxContainer, document.body.firstChild); 45 | // 绑定点击事件 46 | crxButton.addEventListener("click", async function () { 47 | console.log("下载单视频!"); 48 | // 获取视频标题 49 | var title = document.title.trim().replace("/",""); 50 | var author = document.querySelector('.style-scope.ytd-channel-name.complex-string').getAttribute("title").trim(); 51 | var duration = document.querySelector(".ytp-time-duration").innerHTML.trim(); 52 | // 53 | var videoUrl = document.querySelector('link[itemprop="embedUrl"]').getAttribute('href'); 54 | // 调用思源接口创建分片文件 55 | var json = { 56 | "notebook": notebook, 57 | "path": `/Video-视频库/${author}/${title}`, 58 | "markdown":"" 59 | } 60 | // 调用思源创建文档api 61 | var docRes = await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd",json) 62 | // 然后调用思源模版接口惊醒初始化操作 63 | json = { 64 | "id": docRes.data, 65 | "path": pageTemplateUrl 66 | } 67 | var renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/template/render",json) 68 | // 拿到渲染后的markdown 69 | var markdown = renderResult.data.content; 70 | // 替换占位符 作者、时间、时长 71 | markdown = markdown.replace(/{{VideoUrl}}/g,videoUrl) 72 | markdown = markdown.replace(/{{Author}}/g,author) 73 | markdown = markdown.replace(/{{Statue}}/g,"未读") 74 | markdown = markdown.replace(/{{Duration}}/g,duration) 75 | 76 | // 写入数据到思源中 77 | json = { 78 | "dataType": "dom", 79 | "data": markdown, 80 | "nextID": "", 81 | "previousID": "", 82 | "parentID": docRes.data 83 | } 84 | renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/insertBlock",json) 85 | 86 | // 移除下载按钮 87 | crxButton.remove(); 88 | }); 89 | } 90 | 91 | /** 92 | * 视频列表页注入下载按钮 93 | */ 94 | function injectYoutubePlaylistDownButton() { 95 | // 创建一个div容器(可选,如果只需要按钮则不需要) 96 | const crxContainer = document.createElement("div"); 97 | crxContainer.id = "CRX-container"; 98 | crxContainer.style.position = "fixed"; // 设置为固定定位 99 | // 顶部垂直居中对齐 100 | crxContainer.style.right = "1%"; 101 | crxContainer.style.top = "100px"; 102 | crxContainer.style.transform = "translateY(-50%)"; 103 | crxContainer.style.display = "flex"; 104 | crxContainer.style.alignItems = "center"; 105 | crxContainer.style.zIndex = "1000"; // 确保它位于其他元素之上 106 | 107 | // 创建并填充按钮 108 | const crxButton = document.createElement("button"); 109 | crxButton.id = "CRX-container-button"; 110 | crxButton.type = "button"; 111 | crxButton.style.backgroundColor = "red"; // 直接在元素上设置样式,而不是通过innerHTML 112 | crxButton.style.width = "100px"; 113 | crxButton.style.height = "42px"; 114 | crxButton.style.zIndex = "2000"; // 确保它位于其他元素之上 115 | crxButton.classList.add("Button", "FollowButton", "FEfUrdfMIKpQDJDqkjte", "Button--primary", "Button--blue", "epMJl0lFQuYbC7jrwr_o", "JmYzaky7MEPMFcJDLNMG"); // 添加类名 116 | 117 | // 单独视频页面 118 | crxButton.textContent = "下载列表"; 119 | // 将按钮添加到div容器中(如果需要的话) 120 | crxContainer.appendChild(crxButton); 121 | // 将容器添加到页面的body开头 122 | document.body.insertBefore(crxContainer, document.body.firstChild); 123 | // 绑定点击事件 124 | crxButton.addEventListener("click", async function () { 125 | console.log("下载列表"); 126 | // 获取视频标题 127 | var title = document.title.trim().replace("/",""); 128 | var author = document.querySelector(".yt-core-attributed-string__link.yt-core-attributed-string__link--call-to-action-color.yt-core-attributed-string--link-inherit-color").innerHTML.trim().replace("创建者:",""); 129 | // 获取视频列表 130 | var videoList = document.querySelectorAll(".yt-simple-endpoint.style-scope.ytd-playlist-video-renderer"); 131 | // 遍历视频列表 132 | videoList.forEach(async function (item, index) { 133 | // 获取视频标题 134 | var videoTitle = item.getAttribute("title"); 135 | var duration = item.parentElement.parentElement.parentElement.querySelector(".badge-shape-wiz__text").innerHTML.trim(); 136 | // 获取 137 | var videoUrl = "https://www.youtube.com/embed/"+item.getAttribute("href").split("&")[0].split("=")[1]; 138 | 139 | 140 | // 调用思源接口创建分片文件 141 | var json = { 142 | "notebook": notebook, 143 | "path": `/Video-视频库/${author}/${title}/${index+1}-${videoTitle}`, 144 | "markdown":"" 145 | } 146 | // 调用思源创建文档api 147 | var docRes = await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd",json) 148 | // 然后调用思源模版接口惊醒初始化操作 149 | json = { 150 | "id": docRes.data, 151 | "path": pageTemplateUrl 152 | } 153 | var renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/template/render",json) 154 | // 拿到渲染后的markdown 155 | var markdown = renderResult.data.content; 156 | // 替换占位符 作者、时间、时长 157 | markdown = markdown.replace(/{{VideoUrl}}/g,videoUrl) 158 | markdown = markdown.replace(/{{Author}}/g,author) 159 | markdown = markdown.replace(/{{Statue}}/g,"未读") 160 | markdown = markdown.replace(/{{Duration}}/g,duration) 161 | 162 | // 写入数据到思源中 163 | json = { 164 | "dataType": "dom", 165 | "data": markdown, 166 | "nextID": "", 167 | "previousID": "", 168 | "parentID": docRes.data 169 | } 170 | renderResult = await invokeSiyuanApi("http://127.0.0.1:6806/api/block/insertBlock",json) 171 | }) 172 | // 移除下载按钮 173 | crxButton.remove(); 174 | }); 175 | } 176 | -------------------------------------------------------------------------------- /scripts/zhihu.js: -------------------------------------------------------------------------------- 1 | var crawlCount = 0 2 | // 创建一个map 3 | var questionMap = new Map(); 4 | var answerMap = new Map(); 5 | 6 | function reset() { 7 | crawlCount = 0 8 | questionMap = new Map(); 9 | answerMap = new Map(); 10 | } 11 | 12 | /** 13 | * 知乎话题精华问题下载按钮 14 | */ 15 | function injectZhihuTopicQuestionDownButton(url) { 16 | if (url.indexOf('topic') > -1) { 17 | const crxApp = document.createElement('div') 18 | crxApp.id = 'CRX-container' 19 | // 填充CRX-container的内容 20 | crxApp.innerHTML = ` 21 | ` 22 | // 把crxApp插入到class=QuestionHeader-footer-main的div中 23 | document.querySelector('.TopicActions').appendChild(crxApp) 24 | // 点击按钮后进行爬虫处理 25 | const button = document.getElementById('CRX-container') 26 | button.addEventListener('click', function () { 27 | // 重置计数器 28 | reset(); 29 | // 主题精华 30 | // 接口: https://www.zhihu.com/api/v5.1/topics/20069068/feeds/top_activity 31 | var topicId = url.split("/")[4]; 32 | var apiUrl = "https://www.zhihu.com/api/v5.1/topics/" + topicId + "/feeds/top_activity"; 33 | // 获取页面topicName 34 | var topicName = document.querySelector(".TopicMetaCard-title").innerText; 35 | // 调用接口 36 | recursiveFetchTopicQuestion(topicName, [apiUrl]) 37 | }) 38 | } else if (url.indexOf('zhihu.com/question') > -1 && url.indexOf('answer') == -1) { 39 | const crxApp = document.createElement('div') 40 | crxApp.id = 'CRX-container' 41 | // 填充CRX-container的内容 42 | crxApp.innerHTML = ` 43 | ` 44 | // 把crxApp插入到class=QuestionHeader-footer-main的div中 45 | document.querySelector('.QuestionHeader-footer-main').appendChild(crxApp) 46 | // 点击按钮后进行爬虫处理 47 | const button = document.getElementById('CRX-container') 48 | button.addEventListener('click', function () { 49 | // 重置计数器 50 | reset(); 51 | crawlQuestionAnswer(currentPageUrl) 52 | }) 53 | }else if(url.indexOf('zhihu.com/question') > -1 && url.indexOf('answer') > -1){ // 个人回答页 需要跳转到问题页抓取 54 | // 跳转问题页面 55 | const crxApp = document.createElement('div') 56 | crxApp.id = 'CRX-container' 57 | // 填充CRX-container的内容 58 | crxApp.innerHTML = ` 59 | ` 60 | // 把crxApp插入到class=QuestionHeader-footer-main的div中 61 | document.querySelector('.QuestionHeader-footer-main').appendChild(crxApp) 62 | // 点击按钮后进行爬虫处理 63 | const button = document.getElementById('CRX-container') 64 | button.addEventListener('click', function() { 65 | var questionId = currentPageUrl.split("/")[4]; 66 | window.location.href = "https://www.zhihu.com/question/" + questionId; 67 | }) 68 | } 69 | } 70 | 71 | 72 | async function crawlQuestionAnswer(currentPageUrl) { 73 | // 使用request.selector来查询DOM元素 74 | var script_content = document.getElementById("js-initialData").text; 75 | // 解析URL 76 | // 路径的格式总是https://www.zhihu.com/question/655908190,获取问题Id 77 | questionId = currentPageUrl.split("/")[4]; 78 | // 找到script_content里initialState下的question下的answers下的656294274下的next字段 79 | // 把scrip_content转成json格式 80 | jsonData = JSON.parse(script_content); 81 | questionName = document.querySelector(".QuestionHeader-title").innerText; 82 | // 问所需的字段 83 | feedUrl = jsonData["initialState"]["question"]["answers"][questionId]["next"]; 84 | 85 | document.querySelectorAll(".List-item").forEach(function (item, index) { 86 | // 找到meta标签itemprop="url"的content值 87 | var answerUrl = item.querySelector("meta[itemprop='url']").getAttribute("content"); 88 | // https://www.zhihu.com/question/1353125863/answer/36206239923 89 | // 获取回答Id 90 | var answerId = answerUrl.split("/")[4]; 91 | // 找到meta标签itemprop="name"的content值 92 | var author = item.querySelector("meta[itemprop='name']").getAttribute("content"); 93 | var zan; 94 | var zanText = item.querySelector(".Button.VoteButton").getAttribute("aria-label").replace("赞同 ", "").replace("已", ""); 95 | if(zanText.includes("万")){ 96 | zan = parseFloat(zanText.replace("万", ""))*10000; 97 | }else{ 98 | zan = parseInt(zanText); 99 | } 100 | 101 | var mk = htmlToMarkdown(item.querySelector(".RichText.ztext.CopyrightRichText-richText").innerHTML); 102 | // console.log(mk); 103 | answerMap.set(answerId, { 104 | "author": author, 105 | "content": mk, 106 | "zan": zan 107 | }); 108 | }) 109 | 110 | // 从初始URL开始递归调用 111 | recursiveFetch(questionName,currentPageUrl, [feedUrl]) 112 | } 113 | 114 | 115 | async function recursiveFetch(questionName,questionUrl, urls, index = 0) { 116 | if (index >= urls.length) { 117 | // 所有地址都已请求完毕 118 | return; 119 | } 120 | 121 | try { 122 | const currentUrl = urls[index]; 123 | console.log(`Fetching data from ${currentUrl}`); 124 | const data = await fetchData(currentUrl); 125 | 126 | // 处理当前请求的数据 127 | console.log(data); 128 | 129 | if(data && data["data"].length > 0){ 130 | // 假设data.nextUrl是下一个请求的URL 131 | const nextUrl = data["paging"]["next"] 132 | if (nextUrl) { 133 | // 将下一个URL添加到URL列表中 134 | urls.push(nextUrl); 135 | } 136 | is_end = data["paging"]["is_end"]; 137 | currentPage = parseInt(data["paging"]["page"]); 138 | console.log(currentPage + ":" +is_end); 139 | }else{ 140 | is_end = true; 141 | } 142 | 143 | // 超过xx页就停止循环 144 | if (currentPage >= 100 || is_end) { 145 | // 这里统一存储数据 146 | console.log("size:" +answerMap.size); 147 | var answerMapArray = Array.from(answerMap.values()); 148 | answerMapArray.sort(function (a, b) { 149 | return new Date(b["zan"]) - new Date(a["zan"]); 150 | }); 151 | 152 | var str = ""; 153 | // 遍历questionMapArray,获取title和questionUrl 154 | answerMapArray.forEach(function (item, index) { 155 | str += `## ${item.author} [${item.zan}]\n` 156 | str += `${item.content}` 157 | str += `\n --- \n` 158 | }) 159 | // 创建一个json对象 160 | var json = { 161 | "notebook": notebook, 162 | "path": `/知乎/` + questionName + "[" + answerMapArray.length + "]", 163 | "markdown": str 164 | } 165 | // 调用思源创建文档api 166 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 167 | document.getElementById("CRX-container-button").innerText = "抓取完成"; 168 | const link = document.createElement('a'); 169 | link.href = "siyuan://blocks/" + result.data; 170 | link.click(); 171 | return; 172 | }else{ 173 | document.getElementById("CRX-container-button").innerText = "调用:" + urls.length; 174 | } 175 | // 遍历data 176 | const dataList = data["data"]; 177 | console.log(dataList); 178 | // 遍历dataList,这里假设它是一个数组 179 | if (Array.isArray(dataList)) { 180 | dataList.forEach((item, index) => { 181 | if (item["target_type"] == "answer") { 182 | // 读取id 用来去重 183 | var answerId = item["target"]["id"]; 184 | // 点赞数 185 | var zan = parseInt(item["target"]["voteup_count"]); 186 | if (zan > 10) { 187 | console.log("zan is " + zan); 188 | var mk = htmlToMarkdown(`${item["target"]["content"]}`); 189 | 190 | answerMap.set(answerId, { 191 | "author": item["target"]["author"]["name"], 192 | "content": mk, 193 | "zan": zan 194 | }); 195 | } 196 | } 197 | }); 198 | } 199 | // 递归调用以处理下一个URL 200 | await recursiveFetch(questionName,questionUrl, urls, index + 1); 201 | } catch (error) { 202 | console.error('Error fetching data:', error); 203 | } 204 | } 205 | 206 | 207 | async function recursiveFetchTopicQuestion(topicName, urls, index = 0) { 208 | if (index >= urls.length) { 209 | // 所有地址都已请求完毕 210 | return; 211 | } 212 | 213 | try { 214 | const currentUrl = urls[index]; 215 | console.log(`Fetching data from ${currentUrl}`); 216 | const data = await fetchData(currentUrl); 217 | 218 | // 处理当前请求的数据 219 | console.log(data); 220 | 221 | if(data["data"].length > 0){ 222 | // 假设data.nextUrl是下一个请求的URL 223 | const nextUrl = data["paging"]["next"] 224 | if (nextUrl) { 225 | // 将下一个URL添加到URL列表中 226 | urls.push(nextUrl); 227 | } 228 | is_end = data["paging"]["is_end"]; 229 | currentPage = parseInt(data["paging"]["page"]); 230 | }else{ 231 | is_end = true; 232 | } 233 | 234 | // alert(currentPage); 235 | // 超过xx页就停止循环 236 | if (urls.length > 100 || is_end || data["data"].length == 0) { 237 | // 这里进行数据存储 238 | // alert(questionMap.size); 239 | // console.log(questionMap.values()); 240 | // - 1、[外交部称「美方虚化掏空一中原则怂恿支持『台独』分裂活动,将使美国承担难以承受的代价」,释放什么信号?](https://www.zhihu.com/question/534508482)\n- 2、[外交部称「美方虚化掏空一中原则怂恿支持『台独』分裂活动,将使美国承担难以承受的代价」,释放什么信号?](https://www.zhihu.com/question/534508482) 241 | 242 | // 遍历questionMap.values() 根据createTime排序 243 | var questionMapArray = Array.from(questionMap.values()); 244 | questionMapArray.sort(function (a, b) { 245 | return new Date(b["createTime"]) - new Date(a["createTime"]); 246 | }); 247 | 248 | console.log(questionMapArray); 249 | 250 | var str = ""; 251 | // 遍历questionMapArray,获取title和questionUrl 252 | questionMapArray.forEach(function (item, index) { 253 | str += `- ${item.createTime} [${item.title}]` 254 | str += `(${item.questionUrl}) ` 255 | str += `\n` 256 | }) 257 | 258 | // 创建一个json对象 259 | var json = { 260 | "notebook": notebook, 261 | "path": "/知乎/" + topicName + "[" + questionMapArray.length + "]", 262 | "markdown": str 263 | } 264 | // 调用思源创建文档api 265 | var result = await invokeSiyuanApi("http://127.0.0.1:6806/api/filetree/createDocWithMd", json) 266 | document.getElementById("CRX-container-button").innerText = "抓取完成"; 267 | const link = document.createElement('a'); 268 | link.href = "siyuan://blocks/" + result.data; 269 | link.click(); 270 | return; 271 | } else { 272 | // 更新进度 273 | document.getElementById("CRX-container-button").innerText = "调用:" + urls.length; 274 | } 275 | // 遍历data 276 | const dataList = data["data"]; 277 | console.log(dataList); 278 | 279 | // 遍历dataList,这里假设它是一个数组 280 | if (Array.isArray(dataList)) { 281 | dataList.forEach((item, index) => { 282 | if (item["type"] == "topic_feed" && item["target"]["answer_type"] == "NORMAL") { 283 | // 读取id 用来去重 284 | var questionId = item["target"]["question"]["id"]; 285 | // https://www.zhihu.com/question/591337909 286 | questionUrl = "https://www.zhihu.com/question/" + questionId; 287 | title = item["target"]["question"]["title"]; 288 | // 把1546078223这个格式转换成2019-04-13,月和日也要是两位数的,比如'04-13' 这个格式 289 | createTime = formatDate(new Date(parseInt(item["target"]["question"]["created_time"]) * 1000)); 290 | questionMap.set(questionId, { 291 | "questionUrl": questionUrl, 292 | "title": title, 293 | "createTime": createTime 294 | }); 295 | } 296 | }); 297 | } 298 | 299 | // 递归调用以处理下一个URL 300 | await recursiveFetchTopicQuestion(topicName, urls, index + 1); 301 | } catch (error) { 302 | console.error('Error fetching data:', error); 303 | } 304 | } 305 | 306 | 307 | function formatDate(date) { 308 | const year = date.getFullYear(); 309 | const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 月份从0开始,所以需要+1 310 | const day = date.getDate().toString().padStart(2, '0'); 311 | const hours = date.getHours().toString().padStart(2, '0'); 312 | const minutes = date.getMinutes().toString().padStart(2, '0'); 313 | const seconds = date.getSeconds().toString().padStart(2, '0'); 314 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; 315 | } 316 | 317 | function htmlToMarkdown(html) { 318 | let parser = new DOMParser(); 319 | let doc = parser.parseFromString(html, 'text/html'); 320 | let markdown = ''; 321 | let paragraphs = doc.querySelectorAll('p'); 322 | for (let p of paragraphs) { 323 | markdown += p.textContent + '\n\n'; 324 | } 325 | let uls = doc.querySelectorAll('ul'); 326 | for (let ul of uls) { 327 | markdown += '\n'; 328 | let lis = ul.querySelectorAll('li'); 329 | for (let li of lis) { 330 | markdown += '- ' + li.textContent + '\n'; 331 | } 332 | markdown += '\n'; 333 | } 334 | let ols = doc.querySelectorAll('ol'); 335 | let index = 1; 336 | for (let ol of ols) { 337 | markdown += '\n'; 338 | let lis = ol.querySelectorAll('li'); 339 | for (let li of lis) { 340 | markdown += index + '. ' + li.textContent + '\n'; 341 | index++; 342 | } 343 | markdown += '\n'; 344 | } 345 | return markdown; 346 | } -------------------------------------------------------------------------------- /zepto.min.js: -------------------------------------------------------------------------------- 1 | /* Zepto v1.2.0 - zepto event ajax form ie - zeptojs.com/license */ 2 | !function(t,e){"function"==typeof define&&define.amd?define(function(){return e(t)}):e(t)}(this,function(t){var e=function(){function $(t){return null==t?String(t):S[C.call(t)]||"object"}function F(t){return"function"==$(t)}function k(t){return null!=t&&t==t.window}function M(t){return null!=t&&t.nodeType==t.DOCUMENT_NODE}function R(t){return"object"==$(t)}function Z(t){return R(t)&&!k(t)&&Object.getPrototypeOf(t)==Object.prototype}function z(t){var e=!!t&&"length"in t&&t.length,n=r.type(t);return"function"!=n&&!k(t)&&("array"==n||0===e||"number"==typeof e&&e>0&&e-1 in t)}function q(t){return a.call(t,function(t){return null!=t})}function H(t){return t.length>0?r.fn.concat.apply([],t):t}function I(t){return t.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function V(t){return t in l?l[t]:l[t]=new RegExp("(^|\\s)"+t+"(\\s|$)")}function _(t,e){return"number"!=typeof e||h[I(t)]?e:e+"px"}function B(t){var e,n;return c[t]||(e=f.createElement(t),f.body.appendChild(e),n=getComputedStyle(e,"").getPropertyValue("display"),e.parentNode.removeChild(e),"none"==n&&(n="block"),c[t]=n),c[t]}function U(t){return"children"in t?u.call(t.children):r.map(t.childNodes,function(t){return 1==t.nodeType?t:void 0})}function X(t,e){var n,r=t?t.length:0;for(n=0;r>n;n++)this[n]=t[n];this.length=r,this.selector=e||""}function J(t,r,i){for(n in r)i&&(Z(r[n])||L(r[n]))?(Z(r[n])&&!Z(t[n])&&(t[n]={}),L(r[n])&&!L(t[n])&&(t[n]=[]),J(t[n],r[n],i)):r[n]!==e&&(t[n]=r[n])}function W(t,e){return null==e?r(t):r(t).filter(e)}function Y(t,e,n,r){return F(e)?e.call(t,n,r):e}function G(t,e,n){null==n?t.removeAttribute(e):t.setAttribute(e,n)}function K(t,n){var r=t.className||"",i=r&&r.baseVal!==e;return n===e?i?r.baseVal:r:void(i?r.baseVal=n:t.className=n)}function Q(t){try{return t?"true"==t||("false"==t?!1:"null"==t?null:+t+""==t?+t:/^[\[\{]/.test(t)?r.parseJSON(t):t):t}catch(e){return t}}function tt(t,e){e(t);for(var n=0,r=t.childNodes.length;r>n;n++)tt(t.childNodes[n],e)}var e,n,r,i,O,P,o=[],s=o.concat,a=o.filter,u=o.slice,f=t.document,c={},l={},h={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},p=/^\s*<(\w+|!)[^>]*>/,d=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,m=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,g=/^(?:body|html)$/i,v=/([A-Z])/g,y=["val","css","html","text","data","width","height","offset"],x=["after","prepend","before","append"],b=f.createElement("table"),E=f.createElement("tr"),j={tr:f.createElement("tbody"),tbody:b,thead:b,tfoot:b,td:E,th:E,"*":f.createElement("div")},w=/complete|loaded|interactive/,T=/^[\w-]*$/,S={},C=S.toString,N={},A=f.createElement("div"),D={tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},L=Array.isArray||function(t){return t instanceof Array};return N.matches=function(t,e){if(!e||!t||1!==t.nodeType)return!1;var n=t.matches||t.webkitMatchesSelector||t.mozMatchesSelector||t.oMatchesSelector||t.matchesSelector;if(n)return n.call(t,e);var r,i=t.parentNode,o=!i;return o&&(i=A).appendChild(t),r=~N.qsa(i,e).indexOf(t),o&&A.removeChild(t),r},O=function(t){return t.replace(/-+(.)?/g,function(t,e){return e?e.toUpperCase():""})},P=function(t){return a.call(t,function(e,n){return t.indexOf(e)==n})},N.fragment=function(t,n,i){var o,s,a;return d.test(t)&&(o=r(f.createElement(RegExp.$1))),o||(t.replace&&(t=t.replace(m,"<$1>")),n===e&&(n=p.test(t)&&RegExp.$1),n in j||(n="*"),a=j[n],a.innerHTML=""+t,o=r.each(u.call(a.childNodes),function(){a.removeChild(this)})),Z(i)&&(s=r(o),r.each(i,function(t,e){y.indexOf(t)>-1?s[t](e):s.attr(t,e)})),o},N.Z=function(t,e){return new X(t,e)},N.isZ=function(t){return t instanceof N.Z},N.init=function(t,n){var i;if(!t)return N.Z();if("string"==typeof t)if(t=t.trim(),"<"==t[0]&&p.test(t))i=N.fragment(t,RegExp.$1,n),t=null;else{if(n!==e)return r(n).find(t);i=N.qsa(f,t)}else{if(F(t))return r(f).ready(t);if(N.isZ(t))return t;if(L(t))i=q(t);else if(R(t))i=[t],t=null;else if(p.test(t))i=N.fragment(t.trim(),RegExp.$1,n),t=null;else{if(n!==e)return r(n).find(t);i=N.qsa(f,t)}}return N.Z(i,t)},r=function(t,e){return N.init(t,e)},r.extend=function(t){var e,n=u.call(arguments,1);return"boolean"==typeof t&&(e=t,t=n.shift()),n.forEach(function(n){J(t,n,e)}),t},N.qsa=function(t,e){var n,r="#"==e[0],i=!r&&"."==e[0],o=r||i?e.slice(1):e,s=T.test(o);return t.getElementById&&s&&r?(n=t.getElementById(o))?[n]:[]:1!==t.nodeType&&9!==t.nodeType&&11!==t.nodeType?[]:u.call(s&&!r&&t.getElementsByClassName?i?t.getElementsByClassName(o):t.getElementsByTagName(e):t.querySelectorAll(e))},r.contains=f.documentElement.contains?function(t,e){return t!==e&&t.contains(e)}:function(t,e){for(;e&&(e=e.parentNode);)if(e===t)return!0;return!1},r.type=$,r.isFunction=F,r.isWindow=k,r.isArray=L,r.isPlainObject=Z,r.isEmptyObject=function(t){var e;for(e in t)return!1;return!0},r.isNumeric=function(t){var e=Number(t),n=typeof t;return null!=t&&"boolean"!=n&&("string"!=n||t.length)&&!isNaN(e)&&isFinite(e)||!1},r.inArray=function(t,e,n){return o.indexOf.call(e,t,n)},r.camelCase=O,r.trim=function(t){return null==t?"":String.prototype.trim.call(t)},r.uuid=0,r.support={},r.expr={},r.noop=function(){},r.map=function(t,e){var n,i,o,r=[];if(z(t))for(i=0;i=0?t:t+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){null!=this.parentNode&&this.parentNode.removeChild(this)})},each:function(t){return o.every.call(this,function(e,n){return t.call(e,n,e)!==!1}),this},filter:function(t){return F(t)?this.not(this.not(t)):r(a.call(this,function(e){return N.matches(e,t)}))},add:function(t,e){return r(P(this.concat(r(t,e))))},is:function(t){return this.length>0&&N.matches(this[0],t)},not:function(t){var n=[];if(F(t)&&t.call!==e)this.each(function(e){t.call(this,e)||n.push(this)});else{var i="string"==typeof t?this.filter(t):z(t)&&F(t.item)?u.call(t):r(t);this.forEach(function(t){i.indexOf(t)<0&&n.push(t)})}return r(n)},has:function(t){return this.filter(function(){return R(t)?r.contains(this,t):r(this).find(t).size()})},eq:function(t){return-1===t?this.slice(t):this.slice(t,+t+1)},first:function(){var t=this[0];return t&&!R(t)?t:r(t)},last:function(){var t=this[this.length-1];return t&&!R(t)?t:r(t)},find:function(t){var e,n=this;return e=t?"object"==typeof t?r(t).filter(function(){var t=this;return o.some.call(n,function(e){return r.contains(e,t)})}):1==this.length?r(N.qsa(this[0],t)):this.map(function(){return N.qsa(this,t)}):r()},closest:function(t,e){var n=[],i="object"==typeof t&&r(t);return this.each(function(r,o){for(;o&&!(i?i.indexOf(o)>=0:N.matches(o,t));)o=o!==e&&!M(o)&&o.parentNode;o&&n.indexOf(o)<0&&n.push(o)}),r(n)},parents:function(t){for(var e=[],n=this;n.length>0;)n=r.map(n,function(t){return(t=t.parentNode)&&!M(t)&&e.indexOf(t)<0?(e.push(t),t):void 0});return W(e,t)},parent:function(t){return W(P(this.pluck("parentNode")),t)},children:function(t){return W(this.map(function(){return U(this)}),t)},contents:function(){return this.map(function(){return this.contentDocument||u.call(this.childNodes)})},siblings:function(t){return W(this.map(function(t,e){return a.call(U(e.parentNode),function(t){return t!==e})}),t)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(t){return r.map(this,function(e){return e[t]})},show:function(){return this.each(function(){"none"==this.style.display&&(this.style.display=""),"none"==getComputedStyle(this,"").getPropertyValue("display")&&(this.style.display=B(this.nodeName))})},replaceWith:function(t){return this.before(t).remove()},wrap:function(t){var e=F(t);if(this[0]&&!e)var n=r(t).get(0),i=n.parentNode||this.length>1;return this.each(function(o){r(this).wrapAll(e?t.call(this,o):i?n.cloneNode(!0):n)})},wrapAll:function(t){if(this[0]){r(this[0]).before(t=r(t));for(var e;(e=t.children()).length;)t=e.first();r(t).append(this)}return this},wrapInner:function(t){var e=F(t);return this.each(function(n){var i=r(this),o=i.contents(),s=e?t.call(this,n):t;o.length?o.wrapAll(s):i.append(s)})},unwrap:function(){return this.parent().each(function(){r(this).replaceWith(r(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(t){return this.each(function(){var n=r(this);(t===e?"none"==n.css("display"):t)?n.show():n.hide()})},prev:function(t){return r(this.pluck("previousElementSibling")).filter(t||"*")},next:function(t){return r(this.pluck("nextElementSibling")).filter(t||"*")},html:function(t){return 0 in arguments?this.each(function(e){var n=this.innerHTML;r(this).empty().append(Y(this,t,e,n))}):0 in this?this[0].innerHTML:null},text:function(t){return 0 in arguments?this.each(function(e){var n=Y(this,t,e,this.textContent);this.textContent=null==n?"":""+n}):0 in this?this.pluck("textContent").join(""):null},attr:function(t,r){var i;return"string"!=typeof t||1 in arguments?this.each(function(e){if(1===this.nodeType)if(R(t))for(n in t)G(this,n,t[n]);else G(this,t,Y(this,r,e,this.getAttribute(t)))}):0 in this&&1==this[0].nodeType&&null!=(i=this[0].getAttribute(t))?i:e},removeAttr:function(t){return this.each(function(){1===this.nodeType&&t.split(" ").forEach(function(t){G(this,t)},this)})},prop:function(t,e){return t=D[t]||t,1 in arguments?this.each(function(n){this[t]=Y(this,e,n,this[t])}):this[0]&&this[0][t]},removeProp:function(t){return t=D[t]||t,this.each(function(){delete this[t]})},data:function(t,n){var r="data-"+t.replace(v,"-$1").toLowerCase(),i=1 in arguments?this.attr(r,n):this.attr(r);return null!==i?Q(i):e},val:function(t){return 0 in arguments?(null==t&&(t=""),this.each(function(e){this.value=Y(this,t,e,this.value)})):this[0]&&(this[0].multiple?r(this[0]).find("option").filter(function(){return this.selected}).pluck("value"):this[0].value)},offset:function(e){if(e)return this.each(function(t){var n=r(this),i=Y(this,e,t,n.offset()),o=n.offsetParent().offset(),s={top:i.top-o.top,left:i.left-o.left};"static"==n.css("position")&&(s.position="relative"),n.css(s)});if(!this.length)return null;if(f.documentElement!==this[0]&&!r.contains(f.documentElement,this[0]))return{top:0,left:0};var n=this[0].getBoundingClientRect();return{left:n.left+t.pageXOffset,top:n.top+t.pageYOffset,width:Math.round(n.width),height:Math.round(n.height)}},css:function(t,e){if(arguments.length<2){var i=this[0];if("string"==typeof t){if(!i)return;return i.style[O(t)]||getComputedStyle(i,"").getPropertyValue(t)}if(L(t)){if(!i)return;var o={},s=getComputedStyle(i,"");return r.each(t,function(t,e){o[e]=i.style[O(e)]||s.getPropertyValue(e)}),o}}var a="";if("string"==$(t))e||0===e?a=I(t)+":"+_(t,e):this.each(function(){this.style.removeProperty(I(t))});else for(n in t)t[n]||0===t[n]?a+=I(n)+":"+_(n,t[n])+";":this.each(function(){this.style.removeProperty(I(n))});return this.each(function(){this.style.cssText+=";"+a})},index:function(t){return t?this.indexOf(r(t)[0]):this.parent().children().indexOf(this[0])},hasClass:function(t){return t?o.some.call(this,function(t){return this.test(K(t))},V(t)):!1},addClass:function(t){return t?this.each(function(e){if("className"in this){i=[];var n=K(this),o=Y(this,t,e,n);o.split(/\s+/g).forEach(function(t){r(this).hasClass(t)||i.push(t)},this),i.length&&K(this,n+(n?" ":"")+i.join(" "))}}):this},removeClass:function(t){return this.each(function(n){if("className"in this){if(t===e)return K(this,"");i=K(this),Y(this,t,n,i).split(/\s+/g).forEach(function(t){i=i.replace(V(t)," ")}),K(this,i.trim())}})},toggleClass:function(t,n){return t?this.each(function(i){var o=r(this),s=Y(this,t,i,K(this));s.split(/\s+/g).forEach(function(t){(n===e?!o.hasClass(t):n)?o.addClass(t):o.removeClass(t)})}):this},scrollTop:function(t){if(this.length){var n="scrollTop"in this[0];return t===e?n?this[0].scrollTop:this[0].pageYOffset:this.each(n?function(){this.scrollTop=t}:function(){this.scrollTo(this.scrollX,t)})}},scrollLeft:function(t){if(this.length){var n="scrollLeft"in this[0];return t===e?n?this[0].scrollLeft:this[0].pageXOffset:this.each(n?function(){this.scrollLeft=t}:function(){this.scrollTo(t,this.scrollY)})}},position:function(){if(this.length){var t=this[0],e=this.offsetParent(),n=this.offset(),i=g.test(e[0].nodeName)?{top:0,left:0}:e.offset();return n.top-=parseFloat(r(t).css("margin-top"))||0,n.left-=parseFloat(r(t).css("margin-left"))||0,i.top+=parseFloat(r(e[0]).css("border-top-width"))||0,i.left+=parseFloat(r(e[0]).css("border-left-width"))||0,{top:n.top-i.top,left:n.left-i.left}}},offsetParent:function(){return this.map(function(){for(var t=this.offsetParent||f.body;t&&!g.test(t.nodeName)&&"static"==r(t).css("position");)t=t.offsetParent;return t})}},r.fn.detach=r.fn.remove,["width","height"].forEach(function(t){var n=t.replace(/./,function(t){return t[0].toUpperCase()});r.fn[t]=function(i){var o,s=this[0];return i===e?k(s)?s["inner"+n]:M(s)?s.documentElement["scroll"+n]:(o=this.offset())&&o[t]:this.each(function(e){s=r(this),s.css(t,Y(this,i,e,s[t]()))})}}),x.forEach(function(n,i){var o=i%2;r.fn[n]=function(){var n,a,s=r.map(arguments,function(t){var i=[];return n=$(t),"array"==n?(t.forEach(function(t){return t.nodeType!==e?i.push(t):r.zepto.isZ(t)?i=i.concat(t.get()):void(i=i.concat(N.fragment(t)))}),i):"object"==n||null==t?t:N.fragment(t)}),u=this.length>1;return s.length<1?this:this.each(function(e,n){a=o?n:n.parentNode,n=0==i?n.nextSibling:1==i?n.firstChild:2==i?n:null;var c=r.contains(f.documentElement,a);s.forEach(function(e){if(u)e=e.cloneNode(!0);else if(!a)return r(e).remove();a.insertBefore(e,n),c&&tt(e,function(e){if(!(null==e.nodeName||"SCRIPT"!==e.nodeName.toUpperCase()||e.type&&"text/javascript"!==e.type||e.src)){var n=e.ownerDocument?e.ownerDocument.defaultView:t;n.eval.call(n,e.innerHTML)}})})})},r.fn[o?n+"To":"insert"+(i?"Before":"After")]=function(t){return r(t)[n](this),this}}),N.Z.prototype=X.prototype=r.fn,N.uniq=P,N.deserializeValue=Q,r.zepto=N,r}();return t.Zepto=e,void 0===t.$&&(t.$=e),function(e){function h(t){return t._zid||(t._zid=n++)}function p(t,e,n,r){if(e=d(e),e.ns)var i=m(e.ns);return(a[h(t)]||[]).filter(function(t){return t&&(!e.e||t.e==e.e)&&(!e.ns||i.test(t.ns))&&(!n||h(t.fn)===h(n))&&(!r||t.sel==r)})}function d(t){var e=(""+t).split(".");return{e:e[0],ns:e.slice(1).sort().join(" ")}}function m(t){return new RegExp("(?:^| )"+t.replace(" "," .* ?")+"(?: |$)")}function g(t,e){return t.del&&!f&&t.e in c||!!e}function v(t){return l[t]||f&&c[t]||t}function y(t,n,i,o,s,u,f){var c=h(t),p=a[c]||(a[c]=[]);n.split(/\s/).forEach(function(n){if("ready"==n)return e(document).ready(i);var a=d(n);a.fn=i,a.sel=s,a.e in l&&(i=function(t){var n=t.relatedTarget;return!n||n!==this&&!e.contains(this,n)?a.fn.apply(this,arguments):void 0}),a.del=u;var c=u||i;a.proxy=function(e){if(e=T(e),!e.isImmediatePropagationStopped()){e.data=o;var n=c.apply(t,e._args==r?[e]:[e].concat(e._args));return n===!1&&(e.preventDefault(),e.stopPropagation()),n}},a.i=p.length,p.push(a),"addEventListener"in t&&t.addEventListener(v(a.e),a.proxy,g(a,f))})}function x(t,e,n,r,i){var o=h(t);(e||"").split(/\s/).forEach(function(e){p(t,e,n,r).forEach(function(e){delete a[o][e.i],"removeEventListener"in t&&t.removeEventListener(v(e.e),e.proxy,g(e,i))})})}function T(t,n){return(n||!t.isDefaultPrevented)&&(n||(n=t),e.each(w,function(e,r){var i=n[e];t[e]=function(){return this[r]=b,i&&i.apply(n,arguments)},t[r]=E}),t.timeStamp||(t.timeStamp=Date.now()),(n.defaultPrevented!==r?n.defaultPrevented:"returnValue"in n?n.returnValue===!1:n.getPreventDefault&&n.getPreventDefault())&&(t.isDefaultPrevented=b)),t}function S(t){var e,n={originalEvent:t};for(e in t)j.test(e)||t[e]===r||(n[e]=t[e]);return T(n,t)}var r,n=1,i=Array.prototype.slice,o=e.isFunction,s=function(t){return"string"==typeof t},a={},u={},f="onfocusin"in t,c={focus:"focusin",blur:"focusout"},l={mouseenter:"mouseover",mouseleave:"mouseout"};u.click=u.mousedown=u.mouseup=u.mousemove="MouseEvents",e.event={add:y,remove:x},e.proxy=function(t,n){var r=2 in arguments&&i.call(arguments,2);if(o(t)){var a=function(){return t.apply(n,r?r.concat(i.call(arguments)):arguments)};return a._zid=h(t),a}if(s(n))return r?(r.unshift(t[n],t),e.proxy.apply(null,r)):e.proxy(t[n],t);throw new TypeError("expected function")},e.fn.bind=function(t,e,n){return this.on(t,e,n)},e.fn.unbind=function(t,e){return this.off(t,e)},e.fn.one=function(t,e,n,r){return this.on(t,e,n,r,1)};var b=function(){return!0},E=function(){return!1},j=/^([A-Z]|returnValue$|layer[XY]$|webkitMovement[XY]$)/,w={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};e.fn.delegate=function(t,e,n){return this.on(e,t,n)},e.fn.undelegate=function(t,e,n){return this.off(e,t,n)},e.fn.live=function(t,n){return e(document.body).delegate(this.selector,t,n),this},e.fn.die=function(t,n){return e(document.body).undelegate(this.selector,t,n),this},e.fn.on=function(t,n,a,u,f){var c,l,h=this;return t&&!s(t)?(e.each(t,function(t,e){h.on(t,n,a,e,f)}),h):(s(n)||o(u)||u===!1||(u=a,a=n,n=r),(u===r||a===!1)&&(u=a,a=r),u===!1&&(u=E),h.each(function(r,o){f&&(c=function(t){return x(o,t.type,u),u.apply(this,arguments)}),n&&(l=function(t){var r,s=e(t.target).closest(n,o).get(0);return s&&s!==o?(r=e.extend(S(t),{currentTarget:s,liveFired:o}),(c||u).apply(s,[r].concat(i.call(arguments,1)))):void 0}),y(o,t,u,a,n,l||c)}))},e.fn.off=function(t,n,i){var a=this;return t&&!s(t)?(e.each(t,function(t,e){a.off(t,n,e)}),a):(s(n)||o(i)||i===!1||(i=n,n=r),i===!1&&(i=E),a.each(function(){x(this,t,i,n)}))},e.fn.trigger=function(t,n){return t=s(t)||e.isPlainObject(t)?e.Event(t):T(t),t._args=n,this.each(function(){t.type in c&&"function"==typeof this[t.type]?this[t.type]():"dispatchEvent"in this?this.dispatchEvent(t):e(this).triggerHandler(t,n)})},e.fn.triggerHandler=function(t,n){var r,i;return this.each(function(o,a){r=S(s(t)?e.Event(t):t),r._args=n,r.target=a,e.each(p(a,t.type||t),function(t,e){return i=e.proxy(r),r.isImmediatePropagationStopped()?!1:void 0})}),i},"focusin focusout focus blur load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(t){e.fn[t]=function(e){return 0 in arguments?this.bind(t,e):this.trigger(t)}}),e.Event=function(t,e){s(t)||(e=t,t=e.type);var n=document.createEvent(u[t]||"Events"),r=!0;if(e)for(var i in e)"bubbles"==i?r=!!e[i]:n[i]=e[i];return n.initEvent(t,r,!0),T(n)}}(e),function(e){function p(t,n,r){var i=e.Event(n);return e(t).trigger(i,r),!i.isDefaultPrevented()}function d(t,e,n,i){return t.global?p(e||r,n,i):void 0}function m(t){t.global&&0===e.active++&&d(t,null,"ajaxStart")}function g(t){t.global&&!--e.active&&d(t,null,"ajaxStop")}function v(t,e){var n=e.context;return e.beforeSend.call(n,t,e)===!1||d(e,n,"ajaxBeforeSend",[t,e])===!1?!1:void d(e,n,"ajaxSend",[t,e])}function y(t,e,n,r){var i=n.context,o="success";n.success.call(i,t,o,e),r&&r.resolveWith(i,[t,o,e]),d(n,i,"ajaxSuccess",[e,n,t]),b(o,e,n)}function x(t,e,n,r,i){var o=r.context;r.error.call(o,n,e,t),i&&i.rejectWith(o,[n,e,t]),d(r,o,"ajaxError",[n,r,t||e]),b(e,n,r)}function b(t,e,n){var r=n.context;n.complete.call(r,e,t),d(n,r,"ajaxComplete",[e,n]),g(n)}function E(t,e,n){if(n.dataFilter==j)return t;var r=n.context;return n.dataFilter.call(r,t,e)}function j(){}function w(t){return t&&(t=t.split(";",2)[0]),t&&(t==c?"html":t==f?"json":a.test(t)?"script":u.test(t)&&"xml")||"text"}function T(t,e){return""==e?t:(t+"&"+e).replace(/[&?]{1,2}/,"?")}function S(t){t.processData&&t.data&&"string"!=e.type(t.data)&&(t.data=e.param(t.data,t.traditional)),!t.data||t.type&&"GET"!=t.type.toUpperCase()&&"jsonp"!=t.dataType||(t.url=T(t.url,t.data),t.data=void 0)}function C(t,n,r,i){return e.isFunction(n)&&(i=r,r=n,n=void 0),e.isFunction(r)||(i=r,r=void 0),{url:t,data:n,success:r,dataType:i}}function O(t,n,r,i){var o,s=e.isArray(n),a=e.isPlainObject(n);e.each(n,function(n,u){o=e.type(u),i&&(n=r?i:i+"["+(a||"object"==o||"array"==o?n:"")+"]"),!i&&s?t.add(u.name,u.value):"array"==o||!r&&"object"==o?O(t,u,r,n):t.add(n,u)})}var i,o,n=+new Date,r=t.document,s=/)<[^<]*)*<\/script>/gi,a=/^(?:text|application)\/javascript/i,u=/^(?:text|application)\/xml/i,f="application/json",c="text/html",l=/^\s*$/,h=r.createElement("a");h.href=t.location.href,e.active=0,e.ajaxJSONP=function(i,o){if(!("type"in i))return e.ajax(i);var c,p,s=i.jsonpCallback,a=(e.isFunction(s)?s():s)||"Zepto"+n++,u=r.createElement("script"),f=t[a],l=function(t){e(u).triggerHandler("error",t||"abort")},h={abort:l};return o&&o.promise(h),e(u).on("load error",function(n,r){clearTimeout(p),e(u).off().remove(),"error"!=n.type&&c?y(c[0],h,i,o):x(null,r||"error",h,i,o),t[a]=f,c&&e.isFunction(f)&&f(c[0]),f=c=void 0}),v(h,i)===!1?(l("abort"),h):(t[a]=function(){c=arguments},u.src=i.url.replace(/\?(.+)=\?/,"?$1="+a),r.head.appendChild(u),i.timeout>0&&(p=setTimeout(function(){l("timeout")},i.timeout)),h)},e.ajaxSettings={type:"GET",beforeSend:j,success:j,error:j,complete:j,context:null,global:!0,xhr:function(){return new t.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript, application/x-javascript",json:f,xml:"application/xml, text/xml",html:c,text:"text/plain"},crossDomain:!1,timeout:0,processData:!0,cache:!0,dataFilter:j},e.ajax=function(n){var u,f,s=e.extend({},n||{}),a=e.Deferred&&e.Deferred();for(i in e.ajaxSettings)void 0===s[i]&&(s[i]=e.ajaxSettings[i]);m(s),s.crossDomain||(u=r.createElement("a"),u.href=s.url,u.href=u.href,s.crossDomain=h.protocol+"//"+h.host!=u.protocol+"//"+u.host),s.url||(s.url=t.location.toString()),(f=s.url.indexOf("#"))>-1&&(s.url=s.url.slice(0,f)),S(s);var c=s.dataType,p=/\?.+=\?/.test(s.url);if(p&&(c="jsonp"),s.cache!==!1&&(n&&n.cache===!0||"script"!=c&&"jsonp"!=c)||(s.url=T(s.url,"_="+Date.now())),"jsonp"==c)return p||(s.url=T(s.url,s.jsonp?s.jsonp+"=?":s.jsonp===!1?"":"callback=?")),e.ajaxJSONP(s,a);var P,d=s.accepts[c],g={},b=function(t,e){g[t.toLowerCase()]=[t,e]},C=/^([\w-]+:)\/\//.test(s.url)?RegExp.$1:t.location.protocol,N=s.xhr(),O=N.setRequestHeader;if(a&&a.promise(N),s.crossDomain||b("X-Requested-With","XMLHttpRequest"),b("Accept",d||"*/*"),(d=s.mimeType||d)&&(d.indexOf(",")>-1&&(d=d.split(",",2)[0]),N.overrideMimeType&&N.overrideMimeType(d)),(s.contentType||s.contentType!==!1&&s.data&&"GET"!=s.type.toUpperCase())&&b("Content-Type",s.contentType||"application/x-www-form-urlencoded"),s.headers)for(o in s.headers)b(o,s.headers[o]);if(N.setRequestHeader=b,N.onreadystatechange=function(){if(4==N.readyState){N.onreadystatechange=j,clearTimeout(P);var t,n=!1;if(N.status>=200&&N.status<300||304==N.status||0==N.status&&"file:"==C){if(c=c||w(s.mimeType||N.getResponseHeader("content-type")),"arraybuffer"==N.responseType||"blob"==N.responseType)t=N.response;else{t=N.responseText;try{t=E(t,c,s),"script"==c?(1,eval)(t):"xml"==c?t=N.responseXML:"json"==c&&(t=l.test(t)?null:e.parseJSON(t))}catch(r){n=r}if(n)return x(n,"parsererror",N,s,a)}y(t,N,s,a)}else x(N.statusText||null,N.status?"error":"abort",N,s,a)}},v(N,s)===!1)return N.abort(),x(null,"abort",N,s,a),N;var A="async"in s?s.async:!0;if(N.open(s.type,s.url,A,s.username,s.password),s.xhrFields)for(o in s.xhrFields)N[o]=s.xhrFields[o];for(o in g)O.apply(N,g[o]);return s.timeout>0&&(P=setTimeout(function(){N.onreadystatechange=j,N.abort(),x(null,"timeout",N,s,a)},s.timeout)),N.send(s.data?s.data:null),N},e.get=function(){return e.ajax(C.apply(null,arguments))},e.post=function(){var t=C.apply(null,arguments);return t.type="POST",e.ajax(t)},e.getJSON=function(){var t=C.apply(null,arguments);return t.dataType="json",e.ajax(t)},e.fn.load=function(t,n,r){if(!this.length)return this;var a,i=this,o=t.split(/\s/),u=C(t,n,r),f=u.success;return o.length>1&&(u.url=o[0],a=o[1]),u.success=function(t){i.html(a?e("
").html(t.replace(s,"")).find(a):t),f&&f.apply(i,arguments)},e.ajax(u),this};var N=encodeURIComponent;e.param=function(t,n){var r=[];return r.add=function(t,n){e.isFunction(n)&&(n=n()),null==n&&(n=""),this.push(N(t)+"="+N(n))},O(r,t,n),r.join("&").replace(/%20/g,"+")}}(e),function(t){t.fn.serializeArray=function(){var e,n,r=[],i=function(t){return t.forEach?t.forEach(i):void r.push({name:e,value:t})};return this[0]&&t.each(this[0].elements,function(r,o){n=o.type,e=o.name,e&&"fieldset"!=o.nodeName.toLowerCase()&&!o.disabled&&"submit"!=n&&"reset"!=n&&"button"!=n&&"file"!=n&&("radio"!=n&&"checkbox"!=n||o.checked)&&i(t(o).val())}),r},t.fn.serialize=function(){var t=[];return this.serializeArray().forEach(function(e){t.push(encodeURIComponent(e.name)+"="+encodeURIComponent(e.value))}),t.join("&")},t.fn.submit=function(e){if(0 in arguments)this.bind("submit",e);else if(this.length){var n=t.Event("submit");this.eq(0).trigger(n),n.isDefaultPrevented()||this.get(0).submit()}return this}}(e),function(){try{getComputedStyle(void 0)}catch(e){var n=getComputedStyle;t.getComputedStyle=function(t,e){try{return n(t,e)}catch(r){return null}}}}(),e}); 3 | -------------------------------------------------------------------------------- /视频笔记模版.sy.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coriger/siyuan-video-extension/6868bf8be86a0638f243248f47d42c7b66082c39/视频笔记模版.sy.zip --------------------------------------------------------------------------------