-
Notifications
You must be signed in to change notification settings - Fork 74
/
PixivUserBatchDownload.user.js
4209 lines (3916 loc) · 144 KB
/
PixivUserBatchDownload.user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ==UserScript==
// @name PixivUserBatchDownload
// @name:zh-CN P站画师个人作品批量下载工具
// @name:zh-TW P站畫師個人作品批量下載工具
// @name:zh-HK P站畫師個人作品批量下載工具
// @description Batch download pixiv user's images in one key.(Based on Aria2)
// @description:zh-CN 配合Aria2,一键批量下载P站画师的全部作品
// @description:zh-TW 配合Aria2,一鍵批量下載P站畫師的全部作品
// @description:zh-HK 配合Aria2,一鍵批量下載P站畫師的全部作品
// @version 5.21.152
// @author Mapaler <[email protected]>
// @copyright 2016~2024+, Mapaler <[email protected]>
// @namespace http://www.mapaler.com/
// @icon https://www.pixiv.net/favicon.ico
// @homepage https://github.com/Mapaler/PixivUserBatchDownload
// @supportURL https://github.com/Mapaler/PixivUserBatchDownload/issues
// @match *://www.pixiv.net/*
// @exclude *://www.pixiv.net/upload.php*
// @exclude *://www.pixiv.net/messages.php*
// @exclude *://www.pixiv.net/ranking.php*
// @exclude *://www.pixiv.net/info.php*
// @exclude *://www.pixiv.net/ranking_report_user.php*
// @exclude *://www.pixiv.net/setting*
// @exclude *://www.pixiv.net/stacc*
// @exclude *://www.pixiv.net/premium*
// @exclude *://www.pixiv.net/discovery*
// @exclude *://www.pixiv.net/howto*
// @exclude *://www.pixiv.net/idea*
// @exclude *://www.pixiv.net/ads*
// @exclude *://www.pixiv.net/terms*
// @exclude *://www.pixiv.net/novel*
// @exclude *://www.pixiv.net/cate_r18*
// @exclude *://www.pixiv.net/manage*
// @exclude *://www.pixiv.net/report*
// @resource pubd-style https://github.com/Mapaler/PixivUserBatchDownload/raw/master/PixivUserBatchDownload%20ui.css?v=5.20.146
// @require https://cdn.jsdelivr.net/npm/[email protected]/core.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/md5.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/sha256.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/enc-base64.min.js
// @grant window.close
// @grant window.focus
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_addValueChangeListener
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @connect pixiv.net
// @connect pximg.net
// @connect localhost
// @connect 127.0.0.1
// @noframes
// ==/UserScript==
(function() {
'use strict';
//获取当前是否是本地开发状态,自己用 localStorage.setItem("pubd-dev", "1") 既可以开启,localStorage.removeItem("pubd-dev") 关闭
//注意开发状态下为了方便随时调整预览,css 不会自动加载,需要自己用 Stylus 扩展添加 PixivUserBatchDownload ui.css 的内容。
const mdev = Boolean(localStorage.getItem("pubd-dev"));
if (mdev) console.log("GM_info信息:",GM_info); //开发模式时显示meta数据
/*
* 公共变量区
*/
//储存vue框架下P站页面主要内容的DIV位置,现在由程序自行搜索判断,搜索依据为 mainDivSearchCssSelectorArray。
//#root vue的root,源代码中固定存在
//#root>div:nth-of-type(2) 真正存储页面内容的动态 div,搜索条件为root下新增的没有 id 的 node(P站目前是这样)。
//#root>div:nth-of-type(2)>div 再往下由于只有一个单独div,因此再往继续找到有 2 个以上的 node 的时候,记为子 root,其实后面的代码基本不会用到。
let subRoot = null;
//子 root 下面能找到按钮插入点的 node,暨主要内容的 div 而不是顶栏,记为 mainDiv
let mainDiv = null;
//不同页面开始按钮的插入位点
//哪天P站位置改版了,可能就需要手动调整这些路径
//下面的 :scope 指的是 mainDiv,2023年8月9日 当前路径为 #root>div:nth-of-type(2)>div>div:nth-of-type(3)
// 先使用 selectors 字符串搜索,获取失败时使用 fallcack 处理函数
const mainDivSearchCssSelector = [
{selectors: '#spa-contents .user-stats'}, // 手机版用户页
{selectors: '#spa-contents .user-details-card'}, // 手机版作品页
// PC版 单个作品页
{selectors: ':scope aside section:has(button[data-gtm-user-id][data-click-label])',
fallcack: (node) => {
const ele = node.querySelector(':scope main+aside>section div[title]');
return ele?.closest('section');
},
},
//2024年10月11日 目前火狐更新到128后,支持 :has 了,定位全部被导向这一条了,上面一条不起作用了。
// PC版 用户资料页
{selectors: ':scope :has(>div>h1,>div>button[data-gtm-user-id][data-click-label])',
fallcack: (node) => {
const ele = node.querySelector(':scope div[title]:not(a [title])');
return ele?.parentElement?.parentElement?.nextElementSibling;
},
},
];
//搜索页,列表的ul位置(用来显示收藏状态)
const searchListCssPath = ':scope>div>div:nth-of-type(6)>div>section>div:nth-of-type(2)>ul';
//作者页面“主页”按钮的CSS位置(用来获取作者ID)
const userMainPageCssPath = ":scope nav>a";
//作品页,收藏按钮的CSS位置(用来获取当前作品ID)
const artWorkStarCssPath = ":scope main>section>div>div>figcaption>div>div>ul>li:nth-of-type(2)>a";
//作品页,作者头像链接的CSS位置(用来获取作者ID)
const artWorkUserHeadCssPath = ":scope aside>section>h2>div>a";
const scriptVersion = GM_info.script.version.trim(); //本程序的版本
const scriptIcon = GM_info.script.icon64 || GM_info.script.icon; //本程序的图标
const scriptName = (defaultName=>{ //本程序的名称
if (typeof(GM_info) != "undefined") //使用了扩展
{
if (GM_info.script.name_i18n)
{
return GM_info.script.name_i18n[navigator.language.replaceAll("-","_")]; //支持Tampermonkey
} else
{
return GM_info.script.localizedName || //支持Greasemonkey 油猴子 3.x
GM_info.script.name; //支持Violentmonkey 暴力猴
}
}
return defaultName;
})('PixivUserBatchDownload');
const pubd = { //储存程序设置
configVersion: 2, //当前设置版本,用于处理老版本设置的改变
touch: false, //是否为手机版
loggedIn: false, //登录了(未启用)
start: null, //开始按钮指针
menu: null, //菜单指针
dialog: { //窗口们的指针
config: null, //设置窗口
login: null, //登录窗口
refresh_token: null, //刷新token窗口
downthis: null, //下载当前窗口
downillust: null, //下载当前作品窗口
importdata: null, //导入数据窗口(还未开发)
multiple: null, //多画师批量下载窗口(还未开发)
},
oAuth: null, //储存账号密码
downSchemes: [], //储存下载方案
downbreak: false, //是否停止发送Aria2的flag
ajaxTimes: 0, //已经从P站获取信息的次数(用来判断是否要减速)
fastStarList: null, //储存快速收藏的简单数字
starUserlists: [], //储存完整的下载列表
};
//匹配P站内容的正则表达式
const illustPathRegExp = /(\/.+\/\d{4}\/\d{2}\/\d{2}\/\d{2}\/\d{2}\/\d{2}\/((\d+)(?:-([0-9a-zA-Z]+))?(?:_p|_ugoira)))\d+(?:_\w+)?\.([\w\d]+)/i; //P站画作地址 path部分 正则匹配式
const limitingPathRegExp = /(\/common\/images\/(limit_(?:mypixiv|unknown)_\d+))\.([\w\d]+)/i; //P站无权访问作品地址 path部分 正则匹配式
const limitingFilenameExp = /limit_(mypixiv|unknown)/ig; //P站上锁图片文件名正则匹配式
//Header使用
const PixivAppVersion = "6.127.0"; //Pixiv APP的版本
const AndroidVersion = "15.0.0"; //安卓的版本
const UA = `PixivAndroidApp/${PixivAppVersion} (Android ${AndroidVersion}; Android SDK built for x64)`; //向P站请求数据时的UA
//X_Client加密的salt,目前是固定值
const X_Client_Hash_Salt = "28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c";
/*
const X_Client_Hash_Salt = [ //数值写法
0x28,0xC1,0xFD,0xD1,
0x70,0xA5,0x20,0x43,
0x86,0xCB,0x13,0x13,
0xC7,0x07,0x7B,0x34,
0xF8,0x3E,0x4A,0xAF,
0x4A,0xA8,0x29,0xCE,
0x78,0xC2,0x31,0xE0,
0x5B,0x0B,0xAE,0x2C
].map(n=>n.toString(16).toLowerCase()).join('');
*/
const Referer = "https://app-api.pixiv.net/"; //手机app的 Referer,不是这个无法下载图片
const ContentType = "application/x-www-form-urlencoded; charset=UTF-8"; //重要,不要轻易修改
//登录时的固定参数
const client_id = "MOBrBDS8blbauoSck0ZfDbtuzpyT"; //安卓版固定数据
const client_secret = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj"; //安卓版固定数据
let thisPageUserid = null, //当前页面的画师ID
thisPageIllustid = null, //当前页面的作品ID
downIllustMenuId = null, //下载当前作品的菜单的ID(Tampermonker菜单内的指针)
recommendList = null; //推荐作品列表Dom位置
const startDelayAjaxTimes = 100; //开始执行延迟的ajax次数
const ajaxDelayDuration = 1000; //每次延迟的时间
const changeTermwiseCount = 6000; //图片数量大于这个值就从按作者发送切换为按图片发送
/*
* 初始化数据库,这个功能没继续开发了的
*/
if (mdev)
{
const dbName = "PUBD";
var db;
const DBOpenRequest = indexedDB.open(dbName);
DBOpenRequest.onsuccess = function(event) {
db = event.target.result; //DBOpenRequest.result;
console.log("PUBD:数据库已可使用");
};
DBOpenRequest.onerror = function(event) {
// 错误处理
console.log("PUBD:数据库无法启用",event);
};
DBOpenRequest.onupgradeneeded = function(event) {
let db = event.target.result;
// 建立一个对象仓库来存储用户的相关信息,我们选择 user.id 作为键路径(key path)
// 因为 user.id 可以保证是不重复的
let usersStore = db.createObjectStore("users", { keyPath: "user.id" });
// 建立一个索引来通过姓名来搜索用户。名字可能会重复,所以我们不能使用 unique 索引
usersStore.createIndex("name", "user.name", { unique: false });
// 使用账户建立索引,我们确保用户的账户不会重复,所以我们使用 unique 索引
usersStore.createIndex("account", "user.account", { unique: true });
let illustsStore = db.createObjectStore("illusts", { keyPath: "id" });
illustsStore.createIndex("type", "type", { unique: false });
illustsStore.createIndex("userid", "user.id", { unique: false });
// 使用事务的 oncomplete 事件确保在插入数据前对象仓库已经创建完毕
illustsStore.transaction.oncomplete = function(event) {
console.log("PUBD:数据库建立完毕");
};
};
}
/*
* 获取初始状态
*/
//尝试获取旧版网页对象
/*if (typeof(unsafeWindow) != "undefined")
{ //原来的信息-除少部分页面外已失效2020年7月9日
const pixiv = unsafeWindow.pixiv;
}*/
if (typeof(pixiv) != "undefined")
{
if (mdev) console.log("PUBD:本页面存在 pixiv 对象:",pixiv);
thisPageUserid = parseInt(pixiv.context.userId);
if (pixiv.user.loggedIn)
{
pubd.loggedIn = true;
}
if (/touch/i.test(pixiv.touchSourcePath))
{
pubd.touch = true; //新版的手机页面也还是老板结构-2020年7月9日
document.body.classList.add('pubd-touch');
}
}
//尝试获取当前页面画师ID
const metaPreloadData = document.querySelector('#meta-preload-data'); //HTML源代码里有,会被前端删掉的数据
if (metaPreloadData != undefined) //更加新的存在于HTML元数据中的页面信息
{
pubd.loggedIn = true;
if (mdev) console.log("PUBD:本页面抢救出 metaPreloadData 对象:",metaPreloadData);
const preloadData = JSON.parse(metaPreloadData.content);
if (mdev) console.log("PUBD:metaPreloadData 中的 preloadData 元数据:",preloadData);
if (preloadData.user) thisPageUserid = parseInt(Object.keys(preloadData.user)[0]);
if (preloadData.illust) thisPageIllustid = parseInt(Object.keys(preloadData.illust)[0]); //必须判断是否存在,否则会出现can't convert undefined to object错误
}
//获取是否为老的手机版
if (location.host.includes("touch")) //typeof(pixiv.AutoView)!="undefined"
{
pubd.touch = true;
document.body.classList.add('pubd-touch');
}
/*
* 自定义对象区
*/
Date.PixivTimezoneOffset = (date=>{ //国内为 +08:00
const o = date.getTimezoneOffset(); //本地时区差分钟数,正时区为负数
//时区 +差时:差分
return `${Math.trunc(-o/60).toLocaleString(void 0,{
style: "decimal",
minimumIntegerDigits: 2,
signDisplay: "always"
})}:${(o%60).toLocaleString(void 0,{
style: "decimal",
minimumIntegerDigits: 2,
signDisplay: "never"
})}`;
//return `${o<=0?'+':'-'}${[o/60,o%60].map(n=>Math.trunc(Math.abs(n)).toString().padStart(2,'0')).join(':')}`;
})(new Date());
/**
* 生成P站需要的时间格式
* @returns P站时间格式如 "2019-09-03T18:51:40+08:00"
*/
Date.prototype.toPixivString = function() {
let s = this.toJSON();
return s.substring(0,s.indexOf('.')) + Date.PixivTimezoneOffset;
};
//
/**
* 日期格式化
* @param {string} format 格式 y:年,M:月,d:日,h:时,m:分,s:秒
* @returns {string} 格式化的日期字符串
* @see {@link https://www.cnblogs.com/tugenhua0707/p/3776808.html|代码来源}
* @example
* // returns "2023-08-29 21:22:51"
* new Date().format("yyyy-MM-dd hh:mm:ss");
*/
Date.prototype.format = function(format) {
let fmt = format; //复制到新的字符串内
const o = [
["M+" , this.getMonth()+1], //月份
["d+" , this.getDate()], //日
["h+" , this.getHours()], //小时
["m+" , this.getMinutes()], //分
["s+" , this.getSeconds()], //秒
["q+" , Math.floor((this.getMonth()+3)/3)], //季度
["S" , this.getMilliseconds()] //毫秒
];
let regRes;
if(regRes = /(y+)/.exec(fmt)) {
fmt = fmt.replace(regRes[1], (this.getFullYear()+"").substr(4 - regRes[1].length));
}
for (let i = 0; i < o.length; i++) {
let [k, v] = o[i];
if(regRes = new RegExp("("+ k +")").exec(fmt)) {
fmt = fmt.replace(regRes[1], (regRes[1].length===1) ? v : v.toString().padStart(2,'0'));
}
}
return fmt;
}
/*
//一个被收藏的画师
class StarUser{
constructor(id){
const user = this;
user.id = id;
user.infoDone = false;
user.downDone = false;
user.userinfo = null;
user.illusts = null;
}
}
*/
//一个画师收藏列表
class UsersStarList{
constructor(title,userArr = []){
this.title = title;
this.users = new Set(Array.from(userArr));
this.users.delete(null);
}
add(userid)
{
if (isNaN(userid)) userid = parseInt(userid,10);
if (!isNaN(userid) && userid != null) this.users.add(userid);
}
delete(userid)
{
if (isNaN(userid)) userid = parseInt(userid,10);
this.users.delete(userid);
}
has(userid)
{
if (isNaN(userid)) userid = parseInt(userid,10);
return this.users.has(userid);
}
toggle(userid)
{ //切换有无
if (isNaN(userid)) userid = parseInt(userid,10);
const _users = this.users;
if (_users.has(userid) || isNaN(userid) || userid == null)
{
_users.delete(userid);
return false;
}else
{
_users.add(userid);
return true;
}
}
importArray(arr)
{
const arrMaxLength = 500000;
arr = arr.filter(uid=>!isNaN(uid));
if (arr.length>arrMaxLength)
{
alert(`PUBD:收藏用户最多仅允许添加 ${arrMaxLength.toLocaleString()} 个数据。`);
arr = arr.splice(500000); //删除50万以后的
}
const _users = this.users;
arr.forEach(uid=>_users.add(uid));
}
exportArray()
{
return Array.from(this.users);
}
}
//一个本程序使用的headers数据
class HeadersObject{
constructor(importObj){
const header = this;
const timeStr = new Date().toPixivString();
header["App-OS"] = "android";
header["App-OS-Version"] = AndroidVersion;
header["App-Version"] = PixivAppVersion;
header["User-Agent"] = UA;
header["Content-Type"] = ContentType; //重要
header["Referer"] = Referer; // jshint ignore:line
header["X-Client-Hash"] = CryptoJS.MD5(timeStr + X_Client_Hash_Salt).toString();
header["X-Client-Time"] = timeStr;
if (typeof(obj) == "object") Object.assign(this, importObj);
}
}
//储存一项图片列表分析数据的对象
class Works{
constructor(){
this.done = false; //是否分析完毕
this.item = []; //储存图片数据
this.break = false; //储存停止分析的Flag
this.runing = false; //是否正在运行的Flasg
this.next_url = ""; //储存下一页地址(断点续传)
}
};
Math.randomInteger = function(max, min = 0)
{
let _max = Math.max(max, min),
_min = Math.min(max, min);
return this.floor(this.random()*(_max-_min+1)+_min);
}
//认证方案
class oAuth2
{
constructor(existAuth){
this.code_verifier = this.constructor.random_code_verifier();
this.login_time = null;
this.authorization_code = null;
this.auth_data = null;
this.idp_urls = { //默认的综合网址集
"account-edit": "https://accounts.pixiv.net/api/v2/account/edit",
"auth-token": "https://oauth.secure.pixiv.net/auth/token",
"auth-token-redirect-uri": "https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback",
};
if (typeof(existAuth) == "object" && existAuth.auth_data)
{
Object.assign(this, existAuth);
}
}
static random_code_verifier()
{
//OAuth 2.0 客户端应用生成一个43~128位的随机字符串 (code_verifier)并查找它的 SHA256 哈希,这称为 code_challenge。
const codeLen = Math.randomInteger(43, 128); //产生43~128位
const passArray = new Uint8Array(codeLen);
window.crypto.getRandomValues(passArray); //获取符合密码学要求的安全的随机值
const unreservedChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const charsLength = unreservedChars.length - 1;
const codeArray = Array.from(passArray).map(x => unreservedChars.charAt(x/0xFF * charsLength));
return codeArray.join('');
}
get_code_challenge(code_challenge_method)
{
if (code_challenge_method == "S256") //S256
{
const bytes = CryptoJS.SHA256(this.code_verifier);
const base64 = bytes.toString(CryptoJS.enc.Base64);
const base64url = this.constructor.base64_to_base64url(base64);
return base64url;
}
else //plain
{
return this.code_verifier;
}
}
static base64_to_base64url(base64)
{
let base64url = base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
return base64url;
}
refresh_idp_urls(options = {})
{
const thisAuth = this;
//登录的Auth API
GM_xmlhttpRequest({
url: "https://app-api.pixiv.net/idp-urls",
method: "get",
responseType: "text",
headers: new HeadersObject(),
onload: function(response) {
let jo;
try {
jo = JSON.parse(response.responseText);
} catch (e) {
console.error("获取综合网址集失败,返回可能不是JSON格式。", e, response);
if(options.onload_notJson) options.onload_notJson(response.responseText);
return;
}
if (jo.has_error || jo.errors) {
console.error("获取综合网址集失败,返回错误消息", jo);
if(options.onload_hasError) options.onload_hasError(jo);
return;
} else { //登录成功
Object.assign(thisAuth.idp_urls, jo);
console.info("获取综合网址集成功", jo);
if(options.onload) options.onload(jo);
return;
}
},
onerror: function(response) {
console.error("获取登录重定向网址失败,网络请求发生错误", response);
if(options.onerror) options.onerror(response);
return;
}
});
}
get_login_url()
{
const code_challenge_method = "S256"; //P站不支持plain
const loginURL = new URL("https://app-api.pixiv.net/web/v1/login");
loginURL.searchParams.set("code_challenge", this.get_code_challenge(code_challenge_method));
loginURL.searchParams.set("code_challenge_method", code_challenge_method);
loginURL.searchParams.set("client","pixiv-android");
return loginURL;
}
login(authorization_code, options = {})
{
this.authorization_code = authorization_code;
const thisAuth = this;
const postObj = new URLSearchParams();
postObj.set("code_verifier", thisAuth.code_verifier);
postObj.set("code", authorization_code);
postObj.set("grant_type","authorization_code");
postObj.set("redirect_uri",thisAuth.idp_urls["auth-token-redirect-uri"]);
postObj.set("client_id", client_id);
postObj.set("client_secret", client_secret);
postObj.set("include_policy","true");
this.refresh_idp_urls({
onload: function(){
//登录的Auth API
GM_xmlhttpRequest({
url: thisAuth.idp_urls["auth-token"],
method: "post",
responseType: "text",
headers: new HeadersObject(),
data: postObj.toString(),
onload: function(response) {
let jo;
try {
jo = JSON.parse(response.responseText);
} catch (e) {
console.error("登录失败,返回可能不是JSON格式,或本程序异常。", e, response);
if(options.onload_notJson) options.onload_notJson(response.responseText);
return;
}
if (jo.has_error || jo.errors) {
console.error("登录失败,返回错误消息", jo);
if(options.onload_hasError) options.onload_hasError(jo);
return;
} else { //登录成功
thisAuth.auth_data = jo;
thisAuth.login_time = new Date().getTime();
console.info("登录成功", jo);
if(options.onload) options.onload(jo);
return;
}
},
onerror: function(response) {
console.error("登录失败,网络请求发生错误", response);
if(options.onerror) options.onerror(response);
return;
}
});
}
});
}
refresh_token(options = {})
{
const thisAuth = this;
const postObj = new URLSearchParams();
postObj.set("client_id", client_id);
postObj.set("client_secret", client_secret);
postObj.set("grant_type","refresh_token");
postObj.set("refresh_token",thisAuth.auth_data.refresh_token);
postObj.set("include_policy","true");
//登录的Auth API
GM_xmlhttpRequest({
url: thisAuth.idp_urls["auth-token"],
method: "post",
responseType: "text",
headers: new HeadersObject(),
data: postObj.toString(),
onload: function(response) {
let jo;
try {
jo = JSON.parse(response.responseText);
} catch (e) {
console.error("刷新Token失败,返回可能不是JSON格式,或本程序异常。", e, response);
if(options.onload_notJson) options.onload_notJson(response.responseText);
return;
}
if (jo.has_error || jo.errors) {
console.error("刷新Token失败,返回错误消息", jo);
if(options.onload_hasError) options.onload_hasError(jo);
return;
} else { //登录成功
thisAuth.auth_data = jo;
thisAuth.login_time = new Date().getTime();
console.info("刷新Token成功", jo);
if(options.onload) options.onload(jo);
return;
}
},
onerror: function(response) {
console.error("刷新Token失败,网络请求发生错误", response);
if(options.onerror) options.onerror(response);
return;
}
});
}
save()
{
GM_setValue("pubd-oauth", this);
}
}
//一个掩码
var Mask = function(name, logic, content){
this.name = name;
this.logic = logic;
this.content = content;
};
//一个下载方案
var DownScheme = function(name) {
//默认值
this.name = name ? name : "默认方案";
this.rpcurl = "http://localhost:6800/jsonrpc";
this.proxyurl = "";
this.downloadurl = "%{illust.parsedURL.protocol}//%{illust.parsedURL.host}%{illust.parsedURL.path_before_page}%{page}.%{illust.extention}";
this.downfilter = "";
this.savedir = "D:/PixivDownload/";
this.savepath = "%{illust.user.id}/%{illust.filename}%{page}.%{illust.extention}";
this.textout = "%{illust.url_without_page}%{page}.%{illust.extention}\n";
this.masklist = [];
};
DownScheme.prototype.maskAdd = function(name, logic, content) {
var mask = new Mask(name, logic, content);
this.masklist.push(mask);
return mask;
};
DownScheme.prototype.maskRemove = function(index) {
this.masklist.splice(index, 1);
};
DownScheme.prototype.loadFromJson = function(json) {
if (typeof(json) == "string") {
try {
json = JSON.parse(json);
} catch (e) {
console.error("读取的方案数据是字符串,但非JSON。",e);
return false;
}
}
const _this = this;
Object.keys(_this).forEach(function(key){
if (key=="masklist")
{
_this.masklist.length = 0; //清空之前的
json.masklist.forEach(function(mask){
_this.masklist.push(new Mask(mask.name, mask.logic, mask.content));
});
}else if(json[key] != undefined)
{
_this[key] = json[key];
}
});
return true;
};
//创建菜单类
var pubdMenu = function(classname) {
//生成菜单项
function buildMenuItem(title, classname, callback, submenu) {
var item = document.createElement("li");
if (title == 0) //title为0时,只添加一条菜单分割线
{
item.className = "pubd-menu-line" + (classname ? " " + classname : "");
return item;
}
item.className = "pubd-menu-item" + (classname ? " " + classname : "");
//如果有子菜单则添加子菜单
if (typeof(submenu) == "object") {
item.classList.add("pubd-menu-includesub"); //表明该菜单项有子菜单
submenu.classList.add("pubd-menu-submenu"); //表明该菜单是子菜单
//a.addEventListener("mouseenter",function(){callback.show()});
//a.addEventListener("mouseleave",function(){callback.hide()});
item.appendChild(submenu);
item.subitem = submenu;
}else
{
item.subitem = null; //子菜单默认为空
}
//添加链接
var a = item.appendChild(document.createElement("a"));
a.className = "pubd-menu-item-a";
//添加图标
var icon = a.appendChild(document.createElement("i"));
icon.className = "pubd-icon";
//添加文字
var span = a.appendChild(document.createElement("span"));
span.className = "text";
span.innerHTML = title;
//添加菜单操作
if (typeof(callback) == "string") { //为字符串时,当作链接处理
a.target = "_blank";
a.href = callback;
} else if (typeof(callback) == "function") { //为函数时,当作按钮处理
item.addEventListener("click", callback);
//a.onclick = callback;
}
return item;
}
var menu = document.createElement("ul");
menu.className = "pubd-menu display-none" + (classname ? " " + classname : "");
menu.item = [];
//显示该菜单
menu.show = function() {
menu.classList.remove("display-none");
};
menu.hide = function() {
menu.classList.add("display-none");
};
//添加菜单项
menu.add = function(title, classname, callback, submenu) {
var itm = buildMenuItem(title, classname, callback, submenu);
this.appendChild(itm);
this.item.push(itm);
return itm;
};
//鼠标移出菜单时消失
menu.addEventListener("mouseleave", function(e) {
this.hide();
});
return menu;
};
//创建通用对话框类
var Dialog = function(caption, classname, id) {
//构建标题栏按钮
function buildDlgCptBtn(text, classname, callback) {
if (!callback) classname = "";
const btn = document.createElement("a");
btn.className = "dlg-cpt-btn" + (classname ? " " + classname : "");
if (typeof(callback) == "string") {
btn.target = "_blank";
btn.href = callback;
} else {
if (callback)
btn.addEventListener("click", callback);
}
var btnTxt = btn.appendChild(document.createElement("span"));
btnTxt.className = "dlg-cpt-btn-text";
btnTxt.innerHTML = text;
return btn;
}
var dlg = document.createElement("div");
if (id) dlg.id = id;
dlg.className = "pubd-dialog display-none" + (classname ? " " + classname : "");
//添加图标与标题
var cpt = dlg.appendChild(document.createElement("div"));
cpt.className = "caption";
dlg.icon = cpt.appendChild(document.createElement("i"));
dlg.icon.className = "pubd-icon";
var captionDom = cpt.appendChild(document.createElement("span"));
Object.defineProperty(dlg , "caption", {
get() {
return captionDom.textContent;
},
set(str) {
captionDom.innerHTML = str;
}
});
dlg.caption = caption;
//添加标题栏右上角按钮 captionButtons
var cptBtns = dlg.cptBtns = dlg.appendChild(document.createElement("div"));
cptBtns.className = "dlg-cpt-btns";
//添加按钮的函数
cptBtns.add = function(text, classname, callback) {
var btn = buildDlgCptBtn(text, classname, callback);
this.insertBefore(btn, this.firstChild);
return btn;
};
//添加关闭按钮
cptBtns.close = cptBtns.add("X", "dlg-btn-close", (function() {
dlg.classList.add("display-none");
}));
//添加内容区域
var content = dlg.content = dlg.appendChild(document.createElement("div"));
content.className = "dlg-content";
//窗口激活
dlg.active = function() {
if (!this.classList.contains("pubd-dlg-active")) { //如果没有激活的话才执行
var dlgs = document.querySelectorAll(".pubd-dialog"); //获取网页已经载入的所有的窗口
for (var dlgi = 0; dlgi < dlgs.length; dlgi++) { //循环所有窗口
if (dlgs[dlgi] != this)
{
dlgs[dlgi].classList.remove("pubd-dlg-active"); //取消激活
dlgs[dlgi].style.zIndex = parseInt(window.getComputedStyle(dlgs[dlgi], null).getPropertyValue("z-index")) - 1; //从当前网页最终样式获取该窗体z级,并-1.
}
}
this.classList.add("pubd-dlg-active"); //添加激活
this.style.zIndex = ""; //z级归零
}
};
//窗口初始化
dlg.initialise = function() { //窗口初始化默认情况下什么也不做,具体在每个窗口再设置
return;
};
//窗口显示
dlg.show = function(posX, posY, arg) {
if (posX) dlg.style.left = posX + "px"; //更改显示时初始坐标
if (posY) dlg.style.top = posY + "px";
dlg.initialise(arg); //对窗体进行初始化(激活为可见前提前修改窗体内容)
dlg.classList.remove("display-none");
dlg.active(); //激活窗口
};
//窗口隐藏
dlg.hide = function() { //默认情况下等同于关闭窗口
dlg.cptBtns.close.click();
};
//添加鼠标拖拽移动
var drag = dlg.drag = [0, 0]; //[X,Y] 用以储存窗体开始拖动时的鼠标相对窗口坐标差值。
//startDrag(cpt, dlg);
cpt.addEventListener("mousedown", function(e) { //按下鼠标则添加移动事件
var eX = e.pageX>0?e.pageX:0, eY = e.pageY>0?e.pageY:0; //不允许鼠标坐标向上、左超出网页。
drag[0] = eX - dlg.offsetLeft;
drag[1] = eY - dlg.offsetTop;
var handler_mousemove = function(e) { //移动鼠标则修改窗体坐标
var eX = e.pageX>0?e.pageX:0, eY = e.pageY>0?e.pageY:0; //不允许鼠标坐标向上、左超出网页。
dlg.style.left = (eX - drag[0]) + 'px';
dlg.style.top = (eY - drag[1]) + 'px';
};
var handler_mouseup = function(e) { //抬起鼠标则取消移动事件
document.removeEventListener("mousemove", handler_mousemove);
};
document.addEventListener("mousemove", handler_mousemove);
document.addEventListener("mouseup", handler_mouseup, { once: true });
});
//点击窗口任何区域激活窗口
dlg.addEventListener("mousedown", function(e) {
dlg.active();
});
return dlg;
};
//创建框架类
var Frame = function(title, classname) {
var frame = document.createElement("fieldset");
frame.className = "pubd-frame" + (classname ? " " + classname : "");
var caption = frame.appendChild(document.createElement("legend"));
caption.className = "pubd-frame-caption";
caption.textContent = title;
var content = frame.content = frame.appendChild(document.createElement("div"));
content.className = "pubd-frame-content";
frame.rename = function(newName) {
if (typeof(newName) == "string" && newName.length > 0) {
this.querySelector("legend").textContent = newName;
return true;
} else
return false;
};
return frame;
};
//创建带Label的Input类
const LabelInput = function(text, classname, name, type, value, beforeText, title) {
var label = document.createElement("label");
if (text) label.appendChild(document.createTextNode(text));
label.className = classname;
if (title) label.title = title;
var ipt = label.input = document.createElement("input");
ipt.name = name;
ipt.id = ipt.name;
ipt.type = type;
ipt.value = value;
if (beforeText && label.childNodes.length>0)
label.insertBefore(ipt, label.firstChild);
else
label.appendChild(ipt);
return label;
};
//构建错误信息显示模块
const ErrorMsg = function() {
const error_msg_list = document.createElement("ul");
error_msg_list.className = "error-msg-list";
//添加错误显示功能
error_msg_list.clear = function() {
this.innerHTML = ""; //清空当前信息
};
error_msg_list.add = function(arg) {
function addLine(str)
{
const li = document.createElement("li");
li.className = "error-msg-list-item";
li.appendChild(document.createTextNode(str));
return li;
}
const fragment = document.createDocumentFragment();
if (Array.isArray(arg)) //数组
{
arg.forEach(str=>fragment.appendChild(addLine(str)));
}
else //单文本
{
fragment.appendChild(addLine(arg));
}
this.appendChild(fragment);
};
error_msg_list.replace = function(arg) {
this.clear();
this.add(arg);
};
return error_msg_list;
}
//创建进度条类
const Progress = function(classname, align_right) {
const progress = document.createElement("div");
progress.className = "pubd-progress" + (classname ? " " + classname : "");
if (align_right) progress.classList.add("pubd-progress-right");
progress.scaleNum = 0;
const bar = progress.appendChild(document.createElement("div"));
bar.className = "pubd-progress-bar";
const txt = progress.appendChild(document.createElement("span"));
txt.className = "pubd-progress-text";
progress.set = function(scale, pos = 2, str = null) {
const percentStr = (scale * 100).toFixed(pos) + "%"; //百分比的数字
this.scaleNum = Math.min(Math.max(scale, 0), 1);
bar.style.width = percentStr;
txt.textContent = str || percentStr;
};
Object.defineProperty(progress , "scale", {
get() {
return this.scaleNum;
},
set(num) {
this.set(num);
}
});
return progress;
};
//创建 卡片类
function InfoCard(datas) {
var cardDiv = this.dom = document.createElement("div");
cardDiv.className = "pubd-infoCard";
var thumbnailDiv = cardDiv.appendChild(document.createElement("div"));
thumbnailDiv.className = "pubd-infoCard-thumbnail";
var thumbnailImgDom = thumbnailDiv.appendChild(document.createElement("img"));
var infosDlDom = cardDiv.appendChild(document.createElement("dl"));
infosDlDom.className = "pubd-infoCard-dl";
Object.defineProperty(this , "thumbnail", {
get() {
return thumbnailImgDom.src;
},
set(url) {
thumbnailImgDom.src = url;