├── 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 | 
13 |
14 | - Token:在思源笔记`设置-关于`菜单里,找到`API token`,填入
15 | 
16 |
17 | - 笔记本:选择一个数据同步的笔记本
18 |
19 | - 模版文件路径:在思源的数据目录中找到`视频笔记模版.md`文件,拿到完整路径,填入
20 | 
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: `  `,
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: ` > `,
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: `  `,
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: `>`,
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 |
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>$2>")),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=/