├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── bug_report_cn.md │ ├── config.yml │ ├── feature_request.md │ └── feature_request_cn.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG-Early.md ├── CHANGELOG.md ├── LICENSE ├── Privacy Policy.md ├── README-EN.md ├── README-JA.md ├── README-KO.md ├── README-RU.md ├── README-ZH-TW.md ├── README.md ├── dist ├── LICENSE ├── README-EN.md ├── README-JA.md ├── README-KO.md ├── README-RU.md ├── README-ZH-TW.md ├── README.md ├── declarative_net_request_rules.json ├── icon │ ├── logo128.png │ ├── logo16.png │ ├── logo32.png │ └── logo48.png ├── js │ ├── background.js │ ├── background.js.map │ ├── content.js │ └── content.js.map ├── lib │ ├── UPNG.js │ ├── gif.js │ ├── gif.worker.js │ ├── iconfont.js │ ├── jepub.js │ ├── jszip-utils.min.js │ ├── jszip.min.js │ ├── listen_history_change.js │ ├── pako.min.js │ ├── viewer.min.js │ └── whammy.js ├── manifest.json └── style │ ├── showLargerThumbnails.css │ ├── style.css │ └── viewer.min.css ├── notes ├── Chrome 108 版本转换 WebM 失败的问题.md ├── Chrome 下载文件卡住,导致下载器卡住的情况.md ├── Chrome 会替换文件名中的非法字符吗.md ├── Firefox火狐导入Chrome扩展的尝试.md ├── HoneyView 播放某些 APNG 文件不正常的问题.md ├── IDM - command line.md ├── Manifest_V3_调查.md ├── Pixiv 会员屏蔽标签和用户的表现.md ├── Pixiv 插画作品收藏数量排行榜.md ├── chrome.storage.sync.md ├── dlCount 的作用.md ├── getBmkData.js ├── i18n 调研.md ├── images │ ├── 2021-03-25_121957.jpg │ ├── 2021-11-12_143201.png │ ├── 2021-11-12_155844.png │ ├── 2021-12-09_151315.png │ ├── 2021-12-09_174208.png │ ├── 2021-12-09_175558.png │ ├── 2021-12-09_175824.png │ ├── 2021-12-09_180103.png │ ├── 20210217144131.png │ ├── 20210812164537.png │ ├── 20210812164554.png │ ├── 20210812164620.png │ ├── 20210825101408.png │ ├── 20210825101417.png │ ├── 20210902120056.png │ ├── 20210902143848.png │ ├── 20210905153543.png │ ├── 20210905154553.png │ ├── 20210905154604.png │ ├── 20210905154852.png │ ├── 20210905173043.png │ ├── 20210905194915.png │ ├── 20210905200657.png │ ├── 20210905202400.png │ ├── 20210920215721.png │ ├── 20210920223715.png │ ├── 20211112162142.jpg │ ├── 20220322083057.png │ ├── 20220322083130.png │ ├── 20220322083238.png │ ├── 20220322102908.png │ ├── 20220711_034414.png │ ├── 20220711_034446.png │ ├── 20220721_191904.png │ ├── 20220723_180928.png │ ├── 20220728_215342.png │ ├── 20220728_215357.png │ ├── 20220729_000720.png │ ├── 20220810_055730.png │ ├── 20220810_055858.png │ ├── 20220810_055907.png │ ├── 20220810_060652.png │ ├── 20220810_060713.png │ ├── 20220810_060720.png │ ├── 20220810_060943.png │ ├── 20220816_034138.png │ ├── 20220816_173616.png │ ├── 20220816_174812.png │ ├── 20220816_175314.png │ ├── 20220816_180259.png │ ├── 20220816_181944.png │ ├── 20220828_214621.jpg │ ├── 20220828_214739.jpg │ ├── 20220828_223101.jpg │ ├── 20220828_223146.jpg │ ├── 20220828_224343.png │ ├── 20220828_224420.png │ ├── 20221030_040331.png │ ├── 20221030_041027.png │ ├── 20221105_010142.png │ ├── 202211100345.png │ ├── 20221111_222653.jpg │ ├── 20221111_223249.jpg │ ├── 20221203_223104.png │ ├── 20230226_025746.png │ ├── 20230226_025751.png │ ├── 20230524_120618.png │ ├── 20230524_123414.png │ ├── 20230708_203452.png │ ├── 20230723_191733.png │ ├── 20230725_125246.png │ ├── 20230725_125300.png │ ├── 20230725_125316.png │ ├── 20230725_125334.png │ ├── 20230726_014448.png │ ├── 20230726_213719.png │ ├── 20230727_000034.png │ ├── 20230727_120756.png │ ├── 20230727_124806.png │ ├── 20230727_130647.png │ ├── 20230727_130754.png │ ├── 20230727_151505.png │ ├── 20230812_122305.png │ ├── 20230812_122537.png │ ├── 20230812_154158.png │ ├── 20230814_210404.png │ ├── 20230814_212346.png │ ├── 20230824_111023.png │ ├── 20230824_111220.png │ ├── 20230824_111238.png │ ├── 20230826_091630.png │ ├── 20230826_091643.png │ ├── 20230826_091722.png │ ├── 20231109_035624.jpg │ ├── 20231109_043455.jpg │ ├── 20231113_025022.png │ ├── 20231113_025028.png │ ├── 20240322_160029.png │ ├── 20240322_161838.jpg │ ├── 20240330_012002.png │ ├── 20240409_181838.jpg │ ├── 20240409_182151.jpg │ ├── 20240409_182510.jpg │ ├── 20240409_182541.jpg │ ├── 20240409_182547.jpg │ ├── 20240409_182555.jpg │ ├── 20240409_212755.jpg │ ├── 20240506_232810.png │ ├── 20240721_233414.jpg │ ├── 20240721_233646.jpg │ ├── 20240814_033710.png │ ├── 20241213_003309.webp │ ├── 20241213_003324.webp │ ├── 20241213_010822.webp │ ├── 20241213_010914.webp │ ├── 20241228_213411.webp │ ├── 20241229_125606.webp │ ├── 20241229_125617.webp │ ├── 20241229_125835.png │ ├── 20241229_125840.png │ ├── 2025-03-28 0327.png │ ├── 20250111_231011.png │ ├── 20250111_231051.png │ ├── 20250326_002652.png │ ├── 20250326_035405.png │ ├── 20250330_200109.png │ ├── 20250408_194742.webp │ ├── bg.webp │ ├── bg_1920.jpg │ ├── chrome-108-webp.png │ ├── edge-107-webp.png │ ├── ui-en-0.png │ ├── ui-en-1.png │ ├── ui-ja-0.png │ ├── ui-ja-1.png │ ├── ui-ko-0.png │ ├── ui-ko-1.png │ ├── ui-ru-0.png │ ├── ui-ru-1.png │ ├── ui-tw-0.png │ ├── ui-tw-1.png │ ├── ui-zh-0.png │ ├── ui-zh-1.png │ └── 作品发表时间的数据 │ │ ├── image1.png │ │ ├── image2.png │ │ ├── image3.png │ │ ├── image4.png │ │ ├── 每隔 10000 个作品的发表时间之差.txt │ │ └── 每隔 1000000 个作品的发表时间之差 .txt ├── novel.md ├── onDeterminingFilename.md ├── pixiv 的快捷键.md ├── popup.md ├── rankingPage.md ├── recaptcha_enterprise_score_token 添加关注的用户时的验证码.md ├── recordtxt.7z ├── searchPage.md ├── sl 字段.md ├── xRestrict 字段.md ├── 一些国产套壳浏览器使用本程序的情况.md ├── 一些已知问题.md ├── 下载器运行流程.md ├── 下载器遇到的一些网络请求错误.md ├── 下载小说里的内嵌图片.md ├── 下载状态的处理.md ├── 从书签中移除被删除(404)的作品.md ├── 优化生成文件名的效率.md ├── 优化转换动图的性能.md ├── 作品收藏数量的增长速度.md ├── 作品缩略图的选择器列表-图像作品.md ├── 作品缩略图的选择器列表-小说作品.md ├── 使用 nginx 反代理访问 pixiv 的情况.md ├── 保存系列小说的设定资料.md ├── 动图转换的内存占用.md ├── 动态切换界面语言.md ├── 去重.md ├── 商店介绍文本.md ├── 在 Yandex 浏览器(安卓)上使用.md ├── 在后台脚本中管理关注用户的列表.md ├── 多图作品里的图片格式只会是同一种.md ├── 大量抓取时被 Pixiv 限制的情况的测试.md ├── 定制版-特供版本分支需要修改的地方.md ├── 对移动端浏览器进行优化.md ├── 导入下载记录时的效率-花费的时间.md ├── 小说封面图片的一些研究.md ├── 小说搜索页面的“以系列为单位显示”.md ├── 尝试转换动图时避免页面卡顿,失败了.md ├── 当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录 │ ├── 20240718_215904.jpg │ ├── 20240718_215911.jpg │ ├── 20240718_220435.jpg │ ├── 20240718_220525.jpg │ ├── 20240718_221541.jpg │ ├── 20240718_221612.jpg │ ├── 20240718_222417.jpg │ ├── 20240718_222808.jpg │ ├── 20240718_223521.jpg │ └── 一个含有很多图片,总体积很大的系列小说记录.md ├── 扩展升级到 Manifest V3.md ├── 批量关注用户太频繁导致账户被封禁限制.md ├── 抓取 tag 搜索页面、收藏页面遇到限制.md ├── 抓取作品的发布时间数据.md ├── 抓取结果体积太大的问题.md ├── 抓取结果数量太多时,导出到文件会失败的问题.md ├── 收藏作品时因为 429 导致失败的问题.md ├── 收藏页面的加载时间太久的问题.md ├── 文件体积的调查.md ├── 文件名中不可以使用的字符.md ├── 断点续传.md ├── 新版“已关注用户的最新作品”页面.md ├── 日文 tag 和翻译的 tag 在搜索时的差别.md ├── 显示更大的缩略图.md ├── 显示更大的缩略图导致首页关注的新作品滚动过头的问题.md ├── 替换方形缩略图.md ├── 杂记.md ├── 测试 Chrome 中 IndexedDB 的储存容量上限.md ├── 测试用网址.md ├── 测试边抓取边下载的方式花费的时间.md ├── 浏览量和收藏量的比例.md ├── 添加设置项-add setting item.md ├── 添加默认背景图片的调查.md ├── 用户分布.md ├── 用户屏蔽设定-mute.md ├── 第三方库.md ├── 系统磁盘剩余空间不足导致下载失败-status 0-错误代码0.md ├── 记录 Pixiv 的变化.md ├── 调查使用 ids API 批量获取用户作品数据的可行性.md ├── 转换动图为 GIF 格式时,不同质量的调查.md ├── 过度访问警告.md ├── 进度条未找到的问题.md ├── 选择下载的图片尺寸-调查.md ├── 页面处于后台时,定时器的延迟问题.md ├── 页面处于后台时,转换动图的时间.md ├── 预览动图.md ├── 首页顶部滚动区域的问题.md └── 高亮关注的用户.md ├── pack.js ├── package.json ├── src ├── declarative_net_request_rules.json ├── manifest.json ├── static │ ├── icon │ │ ├── logo128.png │ │ ├── logo16.png │ │ ├── logo32.png │ │ └── logo48.png │ ├── lib │ │ ├── UPNG.js │ │ ├── gif.js │ │ ├── gif.worker.js │ │ ├── iconfont.js │ │ ├── jepub.js │ │ ├── jszip-utils.min.js │ │ ├── jszip.min.js │ │ ├── listen_history_change.js │ │ ├── pako.min.js │ │ ├── viewer.min.js │ │ └── whammy.js │ └── style │ │ └── viewer.min.css ├── style │ ├── DoNotDownloadLastFewImages.less │ ├── Loading.less │ ├── UseDifferentNameRuleIfWorkHasTagWarp.less │ ├── blockAds.less │ ├── blockTagsForSpecificUser.less │ ├── centerPanel.less │ ├── deleteWorks.less │ ├── imageViewer.less │ ├── input.less │ ├── log.less │ ├── msgBox.less │ ├── outputWrap.less │ ├── previewWorkDetailPanel.less │ ├── rightButtons.less │ ├── search.css │ ├── search.less │ ├── selectWork.less │ ├── setUserName.less │ ├── showLargerThumbnails.css │ ├── showLargerThumbnails.less │ ├── style.css │ ├── style.less │ └── var.less └── ts │ ├── API.ts │ ├── ArtworkThumbnail.ts │ ├── BG.ts │ ├── BoldKeywords.ts │ ├── Bookmark.ts │ ├── CenterPanel.ts │ ├── CheckNewVersion.ts │ ├── CheckUnsupportBrowser.ts │ ├── Colors.ts │ ├── Config.ts │ ├── ConvertUgoira │ ├── ConvertUgoira.ts │ ├── ToAPNG.ts │ ├── ToGIF.ts │ ├── ToWebM.ts │ └── convertUgoiraImages.worker.ts │ ├── CopyToClipboard.ts │ ├── EVT.ts │ ├── FileName.ts │ ├── FindHorizontalImageWrap.ts │ ├── HighlightFollowingUsers.ts │ ├── ImageViewer.d.ts │ ├── ImageViewer.ts │ ├── InitPage.ts │ ├── Input.ts │ ├── Lang.ts │ ├── ListenPageSwitch.ts │ ├── Loading.ts │ ├── Log.ts │ ├── ManageFollowing.ts │ ├── Mask.ts │ ├── MsgBox.ts │ ├── NovelThumbnail.ts │ ├── OpenCenterPanel.ts │ ├── PageType.ts │ ├── PreviewUgoira.ts │ ├── PreviewWork.ts │ ├── PreviewWorkDetailInfo.ts │ ├── RemoveBlockedUsersWork.ts │ ├── RemoveWorksTagsInBookmarks.ts │ ├── ReplaceSquareThumb.ts │ ├── RequestSponsorship.ts │ ├── SelectWork.ts │ ├── SetTimeoutWorker.ts │ ├── SetUserName.ts │ ├── ShowDownloadBtnOnThumbOnDesktop.ts │ ├── ShowDownloadBtnOnThumbOnMobile.ts │ ├── ShowHelp.ts │ ├── ShowLargerThumbnails.ts │ ├── ShowNotification.ts │ ├── ShowOriginSizeImage.ts │ ├── ShowWhatIsNew.ts │ ├── ShowZoomBtnOnThumb.ts │ ├── Theme.ts │ ├── Tip.ts │ ├── Toast.ts │ ├── Token.ts │ ├── Tools.ts │ ├── UnBookmarkWorks.ts │ ├── WorkThumbnail.ts │ ├── WorkToolBar.ts │ ├── background.ts │ ├── content.ts │ ├── crawl │ ├── CrawlArgument.d.ts │ ├── CrawlResult.d.ts │ ├── InitPageBase.ts │ ├── InitRequestPage.ts │ ├── InitUnsupportedPage.ts │ ├── StopCrawl.ts │ ├── TimedCrawl.ts │ └── VipSearchOptimize.ts │ ├── crawlArtworkPage │ ├── CrawlRecommendWorks.ts │ ├── InitAreaRankingPage.ts │ ├── InitArtworkPage.ts │ ├── InitArtworkSeriesPage.ts │ ├── InitBookmarkDetailPage.ts │ ├── InitDiscoverPage.ts │ ├── InitNewArtworkPage.ts │ ├── InitPixivisionPage.ts │ ├── InitRankingArtworkPage.ts │ └── InitSearchArtworkPage.ts │ ├── crawlMixedPage │ ├── CrawlTagList.ts │ ├── InitBookmarkLegacyPage.ts │ ├── InitBookmarkNewPage.ts │ ├── InitBookmarkPage.ts │ ├── InitFollowingPage.ts │ ├── InitHomePage.ts │ ├── InitUnlistedPage.ts │ ├── InitUserPage.ts │ └── QuickCrawl.ts │ ├── crawlNovelPage │ ├── GetNovelGlossarys.ts │ ├── InitNewNovelPage.ts │ ├── InitNovelPage.ts │ ├── InitNovelSeriesPage.ts │ ├── InitRankingNovelPage.ts │ └── InitSearchNovelPage.ts │ ├── download │ ├── BookmarkAfterDL.ts │ ├── CheckWarningMessage.ts │ ├── Download.ts │ ├── DownloadControl.ts │ ├── DownloadInterval.ts │ ├── DownloadNovelCover.ts │ ├── DownloadNovelEmbeddedImage.ts │ ├── DownloadOnClickBookmark.ts │ ├── DownloadOnClickLike.ts │ ├── DownloadRecord.ts │ ├── DownloadStates.ts │ ├── DownloadType.d.ts │ ├── ExportLST.ts │ ├── ExportResult.ts │ ├── ExportResult2CSV.ts │ ├── ImportResult.ts │ ├── MakeNovelFile.ts │ ├── MergeNovel.ts │ ├── ProgressBar.ts │ ├── Resume.ts │ ├── SaveWorkDescription.ts │ ├── SaveWorkMeta.ts │ ├── ShowConvertCount.ts │ ├── ShowDownloadStates.ts │ ├── ShowRemainingDownloadOnTitle.ts │ ├── ShowSkipCount.ts │ ├── ShowTotalResultOnTitle.ts │ └── showStatusOnTitle.ts │ ├── filter │ ├── BlackandWhiteImage.ts │ ├── BlockTagsForSpecificUser.ts │ ├── Filter.ts │ ├── Mute.ts │ └── WorkPublishTime.ts │ ├── langText.ts │ ├── output │ ├── OutputPanel.ts │ ├── PreviewFileName.ts │ └── ShowURLs.ts │ ├── pageFunciton │ ├── BookmarkAllWorks.ts │ ├── BookmarksAddTag.ts │ ├── DeleteWorks.ts │ ├── DestroyManager.ts │ ├── DisplayThumbnailListOnMultiImageWorkPage.ts │ ├── FastScreen.ts │ ├── QuickBookmark.ts │ ├── RemoveWorksOfFollowedUsersOnSearchPage.ts │ ├── SaveAvatarIcon.ts │ ├── SaveAvatarImage.ts │ └── SaveUserCover.ts │ ├── setting │ ├── ConvertOldSettings.ts │ ├── DoNotDownloadLastFewImages.ts │ ├── Form.ts │ ├── FormHTML.ts │ ├── FormSettings.ts │ ├── InvisibleSettings.ts │ ├── NameRuleManager.ts │ ├── Options.ts │ ├── SaveNamingRule.ts │ ├── Settings.ts │ ├── SettingsForm.d.ts │ └── UseDifferentNameRuleIfWorkHasTag.ts │ ├── showDownloadBtnOnThumb.ts │ ├── store │ ├── CacheWorkData.ts │ ├── IdListWithPageNo.ts │ ├── SaveArtworkData.ts │ ├── SaveNovelData.ts │ ├── States.ts │ ├── Store.ts │ ├── StoreType.d.ts │ ├── WorkPublishTimeIllusts.ts │ └── WorkPublishTimeNovels.ts │ └── utils │ ├── CreateCSV.ts │ ├── DateFormat.ts │ ├── IndexedDB.ts │ ├── SecretSignal.ts │ ├── Utils.ts │ └── imageToIcon.ts ├── tsconfig.json └── webpack.conf.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/**/* 2 | static/**/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 6, 5 | sourceType: 'module', 6 | ecmaFeatures: { 7 | modules: true, 8 | }, 9 | }, 10 | env: { 11 | browser: true, 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | extends: [ 15 | 'plugin:@typescript-eslint/eslint-recommended', 16 | 'prettier/@typescript-eslint', 17 | 'prettier', 18 | ], 19 | rules: { 20 | 'no-console': 'off', 21 | 'no-useless-catch': 'off', 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | 14 | patreon: xuejianxianzun 15 | custom: https://afdian.com/a/xuejianxianzun 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 'Report a problem' 4 | title: '[BUG]' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Question self-test** 11 | 12 | Please follow the steps below first to see if the problem is resolved: 13 | 14 | 1. If the browser is not Chrome or Edge, please use Chrome or Edge browser to install this extension. 15 | 2. If your Chrome or Edge browser is not the latest version, please update your browser. 16 | 3. If this extension is not the latest version, please update to the latest version. The latest version number: ![version](https://img.shields.io/github/v/release/xuejianxianzun/PixivBatchDownloader) 17 | 4. Try refreshing the problematic tab, or restart the browser. 18 | 5. Search for the keyword of the problem in the issues list of this repository to see if someone has already reported this problem. 19 | 20 | Before you report a bug, please make sure you have performed the above checks. 21 | 22 | **Bug Details** 23 | 24 | **How to reproduce this problem? ** 25 | 26 | **Diagnostic Information** 27 | 28 | When the problem occurs, the URL of the page: (required) 29 | 30 | Screenshot of the problem: (optional) 31 | 32 | Also, if necessary, you can export the settings of the downloader, or export the crawling results, and add them as attachments. 33 | 34 | **Your operating system and browser version number** 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告 Bug - 中文模板 3 | about: '报告问题' 4 | title: '[BUG]' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **问题自检** 11 | 12 | 请首先按照以下步骤操作,观察问题是否可以解决: 13 | 14 | 1. 如果浏览器不是 Chrome 或者 Edge,请使用 Chrome 或 Edge 浏览器安装本扩展程序。 15 | 2. 如果你的 Chrome 或 Edge 浏览器不是最新版本,请更新浏览器。 16 | 3. 如果本扩展程序不是最新版本,请更新到最新版本。最新版本号:![version](https://img.shields.io/github/v/release/xuejianxianzun/PixivBatchDownloader) 17 | 4. 尝试刷新出现问题的标签页,或者重启浏览器。 18 | 5. 在本仓库的 issues 列表里搜索问题的关键字,查看是否已经有人报告过此问题。 19 | 20 | 在你报告 Bug 之前,请确认你执行了上述检查。 21 | 22 | **Bug 详情** 23 | 24 | **如何重现这个问题?** 25 | 26 | **诊断信息** 27 | 28 | 发生问题时,页面的 URL:(必须) 29 | 30 | 问题截图:(可选) 31 | 32 | 另外,如果有必要,你可以导出下载器的设置,或抓取结果,并添加到附件。 33 | 34 | **你的操作系统和浏览器版本号** 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What functionality do you want?** 11 | 12 | **What is the purpose to solve?** 13 | 14 | **Does other software have such functions for reference?** 15 | 16 | **Additional context** 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_cn.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 需要新功能 - 中文模板 3 | about: 提出新的功能请求 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **你需要什么功能?** 11 | 12 | **目的是解决什么问题?** 13 | 14 | **其他软件是否有此类功能可以参考?** 15 | 16 | **附加描述** 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # base 2 | .DS_Store 3 | node_modules 4 | 5 | # Log files 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | *.sw? 18 | 19 | # lock 20 | yarn.lock 21 | package-lock.json 22 | 23 | # build file 24 | *.zip 25 | /dist-offline 26 | /dist-special-DelMemory 27 | /dist-special-Hongye 28 | /dist-special-wolun 29 | /dist-special-cao 30 | *special* 31 | 32 | # 当把 dist 目录离线安装为浏览器扩展时,浏览器会自动创建 _metadata 目录。这个目录不需要上传和打包 33 | _metadata 34 | 35 | .eslintcache 36 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | 2 | package.json 3 | LICENSE 4 | /static 5 | /dist/lib 6 | 7 | .DS_Store 8 | .eslintignore 9 | .gitignore 10 | .prettierignore 11 | .eslintcache 12 | .history 13 | *.lock 14 | *.zip 15 | *.md 16 | *.yml 17 | *.png 18 | *.map 19 | *.jpg 20 | *.png 21 | 22 | yarn-error.log -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "es5", 5 | "tabWidth": 2, 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /Privacy Policy.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy for Powerful Pixiv Downloader 2 | 3 | This program is a browser extension and is a download tool for the website pixiv.net. 4 | 5 | This program will request some URLs on pixiv.net according to the user's operation. 6 | 7 | This program will not modify the user's account settings, Nor will the user's information be sent to other websites. 8 | 9 | ## Use pixiv token 10 | 11 | pixiv.net will generate a token for the logged-in user, some network requests need to use the token. 12 | 13 | This program will store the user's token locally, which is used when some requests need to be accompanied by a token. 14 | 15 | The user's token will not be sent to sites other than pixiv.net. 16 | 17 | ## Use pixiv cookies 18 | 19 | pixiv.net will generate cookies for users to save some information about users. 20 | 21 | This program automatically attaches cookies when sending a request. 22 | 23 | This program does not create cookies. 24 | 25 | This program will not modify or store user cookies. 26 | 27 | ## No ads 28 | 29 | This program does not serve ads. 30 | 31 | ## Does not track users 32 | 33 | This program does not track user activities. 34 | 35 | -------------- 36 | 37 | 2021/04/20 -------------------------------------------------------------------------------- /dist/declarative_net_request_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "priority": 1, 5 | "action": { 6 | "type": "modifyHeaders", 7 | "responseHeaders": [ 8 | { 9 | "header": "Access-Control-Allow-Origin", 10 | "operation": "set", 11 | "value": "*" 12 | } 13 | ] 14 | }, 15 | "condition": { 16 | "urlFilter": "||pximg.net", 17 | "resourceTypes": ["xmlhttprequest", "image"] 18 | } 19 | }, 20 | { 21 | "id": 2, 22 | "priority": 1, 23 | "action": { 24 | "type": "modifyHeaders", 25 | "responseHeaders": [ 26 | { 27 | "header": "Access-Control-Allow-Origin", 28 | "operation": "set", 29 | "value": "*" 30 | } 31 | ] 32 | }, 33 | "condition": { 34 | "urlFilter": "||pximg.cat", 35 | "resourceTypes": ["xmlhttprequest", "image"] 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /dist/icon/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/dist/icon/logo128.png -------------------------------------------------------------------------------- /dist/icon/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/dist/icon/logo16.png -------------------------------------------------------------------------------- /dist/icon/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/dist/icon/logo32.png -------------------------------------------------------------------------------- /dist/icon/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/dist/icon/logo48.png -------------------------------------------------------------------------------- /dist/lib/listen_history_change.js: -------------------------------------------------------------------------------- 1 | let _wr = function (type) { 2 | let orig = history[type] 3 | return function () { 4 | let rv = orig.apply(this, arguments) 5 | let e = new Event(type) 6 | e.arguments = arguments 7 | window.dispatchEvent(e) 8 | return rv 9 | } 10 | } 11 | history.pushState = _wr('pushState') 12 | history.replaceState = _wr('replaceState') 13 | -------------------------------------------------------------------------------- /notes/Chrome 108 版本转换 WebM 失败的问题.md: -------------------------------------------------------------------------------- 1 | # Chrome 108 版本转换 WebM 失败的问题 2 | 3 | 昨晚有用户反馈动图转换失败,我试了下还真是,所有动图在转换为 WebM 格式时都会失败,报错 `Frame 2 has a different width`. 4 | 5 | ![](./images/20221203_223104.png) 6 | 7 | 经过测试,我昨天刚更新的 Chrome 108 版本出现了此错误,但是 Edge 107 版本一切正常。 8 | 9 | 我还特意安装了 360 极速浏览器 X(内核为 95),也能正常转换。所以这是 Chrome 108 版本导致的问题。 10 | 11 | 由于这个问题对下载影响非常严重(转换出错的文件会卡住下载线程,导致下载无法继续),所以我决定尽快修复,发布新版本。 12 | 13 | 我先用谷歌搜索 Chrome 108 版本对 WebP 的编码做了什么改动,但是什么信息都没找到,看来只好自己尝试了。过程中走了不少弯路,浪费了很多时间。但是找到真相后,一切却又很简单。 14 | 15 | ## 问题原因 16 | 17 | 下载器先把图片转换为 DataURL,然后传递给编码器。 18 | 19 | ```js 20 | canvas.toDataURL('image/webp', 0.9) 21 | encoder.add(url, delay) 22 | ``` 23 | 24 | 我在不同浏览器里获取同一个图片的 DataURL,然后粘贴到地址栏打开图片,再右键保存为 webp 图像文件。 25 | 26 | 对比文件体积,发现 Chrome 108 保存的图片体积比 Edge 107 小了 80 btye。 27 | 28 | 用十六进制查看器打开这两个图片文件进行对比,发现以前版本的图片里有 80 byte 的空白字符,而 Chrome 108 则没有。 29 | 30 | Edge 107: 31 | 32 | ![](./images/edge-107-webp.png) 33 | 34 | Chrome 108: 35 | 36 | ![](./images/chrome-108-webp.png) 37 | 38 | 所以解决思路已经有了,就是检测浏览器版本,如果大于 108 则进行特殊处理。 39 | 40 | ## 对编码器里代码的研究 41 | 42 | 编码器库文件是 whammy.js,在 `compile` 方法里,会用 `atob` 函数把下载器传递的 DataURL 转换成二进制数据。 43 | 44 | ```js 45 | var str = atob(frame.image.slice(23)) 46 | // frame.image 就是 DataURL,这里去掉了开头的 data:image/webp;base64, 标记,然后转换 47 | ``` 48 | 49 | 这行代码生成的就是图片文件的二进制数据,它的内容和上面我在浏览器里保存成的 webp 图像是一样的。 50 | 51 | 虽然 Chrome 108 移除了 80 byte 的空白字符,不过把这里的二进制数据保存成图片依然是没有问题的。 52 | 53 | 问题在于转换成 WebM 视频的时候,不能直接使用图片数据,要从其中移除一部分内容(图片的 EXIF 信息等)。 54 | 55 | ```js 56 | var p1 = str.substr(0, 15) 57 | var p2 = str.substr(577) 58 | str = p1 + p2 59 | ``` 60 | 61 | 以前第二部分的偏移量是 `577`,现在由于新版本 Chrome 移除了 80 byte 的内容,所以把 `577` 减去 80 变成 `497` 就行了。 62 | 63 | 不过这里不能直接改成 `479`,需要先检测浏览器的版本号,如果大于等于 108 则使用 `497`,否则不变。 64 | 65 | 这样修改之后,问题马上解决了。 66 | 67 | ps:实际上我走了很多弯路,一开始虽然也是从 DataURL 开始着手研究,但是没想起来保存成图片文件,而是顺着代码去研究编码器里面是怎么对传入的 DataURL 进行转换和操作的,给我整的头晕脑胀也没找到确切原因。后来我发现两个浏览器生成的 DataURL 长度不一样,这才想起保存图片,对比体积差异,进而对比文件内容,一下子搞清楚了。 68 | 69 | 果然找对思路很重要啊,白白浪费俩小时了。 70 | 71 | 这个问题和解决的代码我也提交到了 whammy.js 的 issues 里。 72 | 73 | https://github.com/antimatter15/whammy/issues/72 74 | -------------------------------------------------------------------------------- /notes/Chrome 下载文件卡住,导致下载器卡住的情况.md: -------------------------------------------------------------------------------- 1 | ## Chrome 下载文件卡住,导致下载器卡住的情况 2 | 3 | 我偶尔会遇到这种情况。 4 | 5 | ### 表现 6 | 7 | 1. 下载器的下载进度卡住,并且不会自动暂停重试。 8 | 2. 手动暂停,再开始下载,依然会直接卡住,无法继续下载。 9 | 3. 查看 Chrome 的下载管理,发现最近一个下载的文件始终在卡着,不能完成。如图: 10 | 11 | ![](./images/20210825101408.png) 12 | 13 | 上图的文件的 url 是正常的,复制 url 在地址栏打开,可以看到这个图片。 14 | 15 | 此外,在下载文件夹里也可以看到这个图片的缓存文件 `.tmp` 结尾的文件,看起来缓存文件已经创建好了(可以打开查看图片),但是没有保存成目标文件。 16 | 17 | ### 处理方法 18 | 19 | 1. 关闭 Chrome 浏览器。点击关闭按钮,会提示仍然有文件在下载: 20 | 21 | ![](./images/20210825101417.png) 22 | 23 | 选择“退出”。 24 | 25 | 2. 之后可以在任务管理器里看到,仍然存在一个 `chrome.exe` 进程。可能这就是负责下载的进程。必须先把这个进程结束掉,才能重新启动 Chrome,否则 Chrome 会无法启动。 26 | 27 | 3. 重新启动 Chrome 之后,在 Pixiv 页面继续下载。 28 | 29 | ### 推测原因 30 | 31 | 这个问题不能稳定复现,我也不能确定原因。扩展管理里也没有错误信息。 32 | 33 | 出现这个问题时,文件是完整的,并且在硬盘上创建了缓存文件,但是下载却卡住了。 34 | 35 | 我推测主要的原因可能出在硬盘上,比如硬盘 I/O 繁忙,或者出现了某些错误。 36 | 37 | 如果硬盘没出问题的话,那就是 Chrome 的偶然性 bug 了吧。 -------------------------------------------------------------------------------- /notes/Chrome 会替换文件名中的非法字符吗.md: -------------------------------------------------------------------------------- 1 | 我记得用 Chrome 的 downloads 扩展 API 下载文件时,如果文件名中有非法字符,Chrome 会抛出一个错误,并且文件会保存失败。 2 | 3 | 不过今天 2020/10/07 我发现用 a 标签的 download 属性下载文件时,Chrome 自动替换了非法字符! 4 | 5 | 当时的情况是下载这个作品:https://www.pixiv.net/artworks/79349882 6 | 7 | 文件名 `m|` 里有个竖线是不能做文件名的。但是保存成功了,我一看,Chrome 自动把竖线 `|` 替换成了下划线 `_`。 8 | 9 | 这是一个让人省心的地方。不知道 downloads 扩展是否也会自动替换非法字符。还没对此进行测试。 -------------------------------------------------------------------------------- /notes/Firefox火狐导入Chrome扩展的尝试.md: -------------------------------------------------------------------------------- 1 | 前两天看到文章说 Firefox 支持导入 Chrome 的扩展程序,但本质是从火狐自己的扩展商店里匹配对应的扩展。我试了试果然如此。 2 | 3 | 从 Chrome 导入扩展: 4 | 5 | ![](./images/20230826_091630.png) 6 | 7 | 提示 32 个里只匹配到了 5 个: 8 | 9 | ![](./images/20230826_091643.png) 10 | 11 | 只有这几个被导入成功,看来火狐还是没法直接使用 Chrome 的扩展。 12 | 13 | ![](./images/20230826_091722.png) -------------------------------------------------------------------------------- /notes/HoneyView 播放某些 APNG 文件不正常的问题.md: -------------------------------------------------------------------------------- 1 | # HoneyView 播放某些 APNG 文件不正常的问题 2 | 3 | 日期:2023/02/26 4 | 5 | 在 2022/12/26 的一项修改 `替换第三方库 pako.js 为 UZIP.js` 导致生成的 APNG 文件体积比以前大一些,文件内容自然也有细微的不同。 6 | 7 | 今天有用户报告问题,把下面这个动图转换为 APNG 时,用 HoneyView 不能正常播放。我试了试确实如此。 8 | 9 | https://www.pixiv.net/artworks/95767772 10 | 11 | 这个动图源文件有 33 帧 jpg 图像,转换后应该也有 33 帧图像,但是 HoneyView 在播放时却只会循环播放前 11 帧。 12 | 13 | ![](./images/20230226_025746.png) 14 | 15 | 那么这个 APNG 文件真的有问题吗?我用其他方法打开这个图像,但是都很正常: 16 | 17 | 1. 把图片拖到 Chrome 浏览器中打开,播放正常 18 | 2. 安装另一个图片查看软件 XnViewMP,播放正常,并且帧数显示确实是 33 帧 19 | 3. 用软件 APNG Disassembler (https://apngdis.sourceforge.net/) 提取这个 APNG 里的图像,能正常提取出来 33 个 PNG 图像 20 | 4. 用 HxD (十六进制查看器)打开这个图像,找到保存帧数的位置,数值确实是 33。 21 | 22 | 综上,这个 APNG 图像在技术上应该是符合标准的,HoneyView 播放异常应该是 HoneyView 自己的问题。 23 | 24 | 我本想向 HoneyView 报告这个问题,但是我在它的官网看到如下提示: 25 | 26 | >由于内部原因,除了最低程度的安全性修补外,将不再对 Honeyview 进行更新。 因此,请您理解:目前很难再在 Honeyview 中添加新功能。 对此造成的不便,我们深表歉意。 27 | 28 | 我又看了 HoneyView 上次更新的时间已经是半年前了。 29 | 30 | 我感觉即使这个问题被他们确认,也可能不会修复。或者即使修复,也可能需要在很久之后才会发布新版本。所以我不如把下载器生成 APNG 的代码改回以前的版本,这样解决起来更快。 31 | 32 | ----------- 33 | 34 | 现在我已经把 UZIP.js 改成了 pako.min.js,生成的文件可以正常在 HoneyView 里播放。 35 | 36 | ![](./images/20230226_025751.png) 37 | 38 | 39 | -------------------------------------------------------------------------------- /notes/Manifest_V3_调查.md: -------------------------------------------------------------------------------- 1 | 今天的日期:2021/09/02。Chrome 稳定版的内核是 93。 2 | 3 | 我更新了这个下载器的清单版本(升级到 Manifest V3),但是 V3 需要 Chromium 内核版本 ≥ 88 才可以用。 4 | 5 | 目前下载器的迁移已经完成,但是 V3 的支持情况不够好,导致我决定不发布它。 6 | 7 | ## 国内套壳浏览器的情况 8 | 9 | 可能有一些国内用户在使用套壳浏览器,它们的内核版本是否支持 V3 呢?我去看了一下目前还在更新的国产套壳浏览器,结论是: 10 | 11 | 目前只有傲游浏览器可以支持。 12 | 13 | 下面为各个套壳浏览器的内核版本:(由高到低排序) 14 | 15 | | 浏览器 | 内核版本 | 16 | | ------------- | -------- | 17 | | 傲游浏览器 | 89 | 18 | | 云起浏览器 | 87 | 19 | | 搜狗浏览器 | 87 | 20 | | 360急速浏览器 | 86 | 21 | | 极速浏览器 | 86 | 22 | | 百分(Cent)浏览器 | 86 | 23 | | 华为浏览器 | 85 | 24 | | 星愿浏览器 | 80 | 25 | | 猎豹浏览器 | 79 | 26 | | 猎豹浏览器 | 79 | 27 | | QQ浏览器 | 70 | 28 | | 2345浏览器 | 69 | 29 | 30 | 版本号最低的只有 69、70,蚌埠住了。 31 | 32 | 目前只有傲游浏览器能支持 V3,但是它安装和启动的过程中共弹出了 2 次 UAC,而且启动之后风扇很响,不知道这是否和它号称自己为“区块链浏览器”有关,反正我不喜欢。 33 | 34 | 我测试了某个浏览器,它不支持 V3,加载扩展时提示: 35 | 36 | `Invalid value for 'web_accessible_resources[0]'.` 37 | 38 | ![](./images/20210902120056.png) 39 | 40 | 此外国内的用户即使使用的是 Google Chrome 浏览器,但是有少部分用户可能无法自动更新。 41 | 42 | ## 手机上的 Yandex 浏览器 43 | 44 | 手机上的 Yandex 浏览器最新版本的内核虽然已经是 88,但是尝试让它加载 V3 版本的下载器,却出现错误提示: 45 | 46 | ![](./images/20210902143848.png) 47 | 48 | 所以如果下载器升级到 V3,手机上的用户就无法使用下载器了。这个影响比较大,也是我不发布这个版本的主要原因。 -------------------------------------------------------------------------------- /notes/Pixiv 会员屏蔽标签和用户的表现.md: -------------------------------------------------------------------------------- 1 | Pixiv 会员可以在用户设置里屏蔽标签和用户。 2 | 3 | https://www.pixiv.net/settings/viewing/mute?type=tag 4 | 5 | ![](./images/20241229_125835.png) 6 | 7 | https://www.pixiv.net/settings/viewing/mute?type=user 8 | 9 | ![](./images/20241229_125840.png) 10 | 11 | 普通会员只可设置1个屏蔽对象,高级会员的用户最高可设置500个屏蔽对象。 12 | 13 | 屏蔽标签后,不会显示该标签的作品: 14 | 15 | ![](./images/20241229_125617.webp) 16 | 17 | 屏蔽用户后,在其主页里会把作品显示为“屏蔽中”;在其他页面里也不会显示他的作品。 18 | 19 | ![](./images/20241229_125606.webp) 20 | 21 | **注意:** Pixiv 的 API 会照常返回数据(如同没有屏蔽),只是在前端显示时进行了处理。 22 | 23 | 下载器会获取用户的屏蔽设置,对抓取结果进行过滤。 24 | 25 | 26 | -------------------------------------------------------------------------------- /notes/chrome.storage.sync.md: -------------------------------------------------------------------------------- 1 | # chrome.storage.sync 2 | 3 | 文档: https://developer.chrome.com/docs/extensions/reference/storage/#property-sync 4 | 5 | `chrome.storage.sync` 和 `localStorage` 有一些区别: 6 | 7 | - `chrome.storage.sync` 是异步的(`localStorage` 是同步的) 8 | - `chrome.storage.sync` 可以储存对象(`localStorage` 只能储存为字符串格式) 9 | - 存取数据的语法和结果都有区别 10 | 11 | 例如对于数据: 12 | 13 | ```js 14 | const data = { 15 | name: 'saber' 16 | } 17 | ``` 18 | 19 | ## set 20 | 21 | `localStorage` 的写法是两个参数: 22 | 23 | ```js 24 | localStorage.setItem('data', data) 25 | ``` 26 | 27 | `chrome.storage.sync` 的写法是一个对象: 28 | 29 | ```js 30 | chrome.storage.sync.set({ 31 | data: { 32 | name: 'saber' 33 | } 34 | // 或者简写为 data 35 | }) 36 | ``` 37 | 38 | ## get 39 | 40 | `localStorage` 的结果不包含 key `data`: 41 | 42 | ```js 43 | localStorage.getItem('data') 44 | 45 | // return 46 | { 47 | name: 'saber' 48 | } 49 | // 可以直接使用 result 50 | ``` 51 | 52 | `chrome.storage.sync` 包含 key `data`: 53 | ```js 54 | chrome.storage.sync.get('data') 55 | 56 | // return 57 | { 58 | data: { 59 | name: 'saber' 60 | } 61 | } 62 | // 需要通过 result['data'] 使用 63 | ``` 64 | 65 | 我觉得 `chrome.storage.sync` 的 get、set 在设计上是好的,但是如果每次只操作一条数据,数据的格式多嵌套了一层挺麻烦的。 66 | 67 | ## 使用 promise 68 | 69 | `chrome.storage.sync` 的 get、set 需要在清单版本为 V3 的前提下才能使用 promise 作为返回值。 70 | 71 | 现在本扩展程序因为浏览器兼容性的原因没有升级到 V3,所以只能使用回调函数。 72 | 73 | ## 体积限制 74 | 75 | `chrome.storage.sync` 一共可以储存: 76 | - 最多 512 条数据(项目) 77 | - 每条数据的体积上限是 8 KiB 78 | - 所有数据的体积上限是 100 KiB 79 | 80 | `chrome.storage.sync` 能储存的数据的体积很小,所以只适合储存配置和标记,其他数据还是要储存在其他地方。 81 | 82 | ## 写入次数限制 83 | 84 | MAX_WRITE_OPERATIONS_PER_MINUTE 85 | 86 | 每分钟最多执行 120 次写入操作。超出限制就会报错: 87 | 88 | ``` 89 | Unchecked runtime.lastError: This request exceeds the MAX_WRITE_OPERATIONS_PER_MINUTE quota. 90 | ``` 91 | 92 | 此外,每小时最多执行 1800 次写入操作,超出限制也会报错。 93 | 94 | -------------------------------------------------------------------------------- /notes/dlCount 的作用.md: -------------------------------------------------------------------------------- 1 | # dlCount 的作用 2 | 3 | 在下载器内部的抓取结果里,保存有 `dlCount` 属性。它用来记录从这个作品里下载前几张图片。 4 | 5 | 一个插画和漫画作品里可以有多张图片,如果用户启用了“多图作品只下载前几张图片”,下载器可以只下载前几张。 6 | 7 | # 用于生成抓取结果 8 | 9 | 例如一个作品有 10 张图片,用户设置了只下载前 2 张,这个作品的 `dlCount` 就是 `2`。 10 | 11 | 当 `addResult` 方法添加抓取结果时,因为这个作品的 `dlCount` 是 `2`,所以下载器会生成这个作品前 2 张图片的数据,也就是生成 2 个结果。 12 | 13 | # 用于生成文件名 14 | 15 | 当 `getFileName` 方法生成文件名时,如果用户启用了“为每个作品创建单独的文件夹”,就要读取抓取结果里的 `dlCount` 属性,判断是否应该因为此设置而创建一层文件夹。 16 | 17 | # 补充 18 | 19 | 把 `dlCount` 保存在抓取结果里有些显得有些怪异,因为它并不是作品数据的一部分,而是一个用于其他目的的属性。 20 | 21 | 但是这是必要的,因为 `dlCount` 的值受“多图作品只下载前几张图片”设置的影响,但这个设置可能会被用户改变。 22 | 23 | 如果抓取时和下载时的该设置不同,那么生成的 `dlCount` 也不同,这就会出现问题。 24 | 25 | -------- 26 | 27 | 例如,用户设置了下载前 2 张图片,并且如果一个作品里下载的文件数量 > 1,就创建单独的文件夹。 28 | 29 | 那么 `dlCount` 是 `2` 的作品应该创建单独的文件夹。 30 | 31 | 但是在下载时,用户可能修改设置,只下载前 1 张图片。此时如果计算 `dlCount`,那么结果是 1,也就不应该创建单独的文件夹。 32 | 33 | 这样就产生了问题。 34 | 35 | 所以 `dlCount` 应该以生成抓取结果时为准,而不是在以后动态计算。 36 | 37 | 所以下载器必须在生成抓取结果时,把 `dlCount` 保存在抓取结果里,以备以后使用。 38 | -------------------------------------------------------------------------------- /notes/getBmkData.js: -------------------------------------------------------------------------------- 1 | // 定时抓取当前页面的作品的数据,获取其发表后经过的时间和收藏数量 2 | 3 | // 一小时的毫秒数 4 | const oneHour = 1 * 60 * 60 * 1000 5 | 6 | // 每次抓取的间隔时间(等同于每小时抓取几次) 7 | const delay = oneHour / 4 8 | 9 | // 从地址栏获取作品 id 10 | function getIllustId() { 11 | const test = /artworks\/(\d*\d)/.exec(location.href) 12 | if (test && test.length > 1) { 13 | return test[1] 14 | } 15 | 16 | return alert('getIllustId failed') 17 | } 18 | 19 | function getBMK() { 20 | const id = getIllustId() 21 | const url = `https://www.pixiv.net/ajax/illust/${id}` 22 | return new Promise((resolve, reject) => { 23 | fetch(url, { 24 | method: 'get', 25 | credentials: 'same-origin', 26 | }) 27 | .then((response) => { 28 | if (response.ok) { 29 | return response.json() 30 | } else { 31 | // 第一种异常,请求成功但状态不对 32 | reject({ 33 | status: response.status, 34 | statusText: response.statusText, 35 | }) 36 | } 37 | }) 38 | .then((data) => { 39 | // 计算发表之后经过了多少小时 40 | const nowTime = new Date().getTime() 41 | const createTime = new Date(data.body.createDate).getTime() 42 | // 计算小时数,保留小数点后 1 位 43 | const hourResult = 44 | Math.round(((nowTime - createTime) / oneHour) * 10) / 10 45 | 46 | const bmk = data.body.bookmarkCount 47 | console.log('hour', hourResult, 'bmk', bmk) 48 | resolve(bmk) 49 | }) 50 | .catch((error) => { 51 | // 第二种异常,请求失败 52 | reject(error) 53 | }) 54 | }) 55 | } 56 | 57 | // 立刻抓取一次 58 | getBMK() 59 | 60 | // 定时抓取 61 | setInterval(() => { 62 | getBMK() 63 | }, delay) 64 | -------------------------------------------------------------------------------- /notes/i18n 调研.md: -------------------------------------------------------------------------------- 1 | pixiv 用户可以修改网站上显示的语言,而 chrome 扩展的 i18n 却不会检查页面语言,而是依据操作系统的区域设置、或者浏览器的语言设置来决定。 2 | 3 | pixiv 页面可以显示为: 4 | 5 | - 简体中文 6 | - 繁体中文 7 | - 日本语 8 | - 韩语 9 | - 英语 10 | 11 | 设想用户的操作系统是中文,但他可以随时修改 pixiv 显示的语言。目前下载器显示的语言和 pixiv (用户选择)的一致。 12 | 13 | 如果使用了 i18n,下载器将始终只能显示为中文,不会跟随用户的选择而变化。这样就失去了灵活性。 14 | 15 | 一些测试结果: 16 | 17 | | 系统语言 | html标记 | i18n结果 | 18 | | :------: | :----------: | :------: | 19 | | 简体中文 | lang="zh-CN" | 简体中文 | 20 | | 简体中文 | lang="en" | 简体中文 | 21 | | 简体中文 | lang="ja" | 简体中文 | 22 | 23 | 用户群投票:(2020-01-17) 24 | 25 | | 和操作系统一致 | 和p站语言一致 | 26 | | :------------: | :-----------: | 27 | | 13 | 12 | 28 | 29 | 结论:维持现状,不进行 i18n。 -------------------------------------------------------------------------------- /notes/images/2021-03-25_121957.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2021-03-25_121957.jpg -------------------------------------------------------------------------------- /notes/images/2021-11-12_143201.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2021-11-12_143201.png -------------------------------------------------------------------------------- /notes/images/2021-11-12_155844.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2021-11-12_155844.png -------------------------------------------------------------------------------- /notes/images/2021-12-09_151315.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2021-12-09_151315.png -------------------------------------------------------------------------------- /notes/images/2021-12-09_174208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2021-12-09_174208.png -------------------------------------------------------------------------------- /notes/images/2021-12-09_175558.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2021-12-09_175558.png -------------------------------------------------------------------------------- /notes/images/2021-12-09_175824.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2021-12-09_175824.png -------------------------------------------------------------------------------- /notes/images/2021-12-09_180103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2021-12-09_180103.png -------------------------------------------------------------------------------- /notes/images/20210217144131.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210217144131.png -------------------------------------------------------------------------------- /notes/images/20210812164537.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210812164537.png -------------------------------------------------------------------------------- /notes/images/20210812164554.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210812164554.png -------------------------------------------------------------------------------- /notes/images/20210812164620.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210812164620.png -------------------------------------------------------------------------------- /notes/images/20210825101408.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210825101408.png -------------------------------------------------------------------------------- /notes/images/20210825101417.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210825101417.png -------------------------------------------------------------------------------- /notes/images/20210902120056.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210902120056.png -------------------------------------------------------------------------------- /notes/images/20210902143848.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210902143848.png -------------------------------------------------------------------------------- /notes/images/20210905153543.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210905153543.png -------------------------------------------------------------------------------- /notes/images/20210905154553.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210905154553.png -------------------------------------------------------------------------------- /notes/images/20210905154604.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210905154604.png -------------------------------------------------------------------------------- /notes/images/20210905154852.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210905154852.png -------------------------------------------------------------------------------- /notes/images/20210905173043.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210905173043.png -------------------------------------------------------------------------------- /notes/images/20210905194915.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210905194915.png -------------------------------------------------------------------------------- /notes/images/20210905200657.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210905200657.png -------------------------------------------------------------------------------- /notes/images/20210905202400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210905202400.png -------------------------------------------------------------------------------- /notes/images/20210920215721.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210920215721.png -------------------------------------------------------------------------------- /notes/images/20210920223715.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20210920223715.png -------------------------------------------------------------------------------- /notes/images/20211112162142.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20211112162142.jpg -------------------------------------------------------------------------------- /notes/images/20220322083057.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220322083057.png -------------------------------------------------------------------------------- /notes/images/20220322083130.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220322083130.png -------------------------------------------------------------------------------- /notes/images/20220322083238.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220322083238.png -------------------------------------------------------------------------------- /notes/images/20220322102908.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220322102908.png -------------------------------------------------------------------------------- /notes/images/20220711_034414.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220711_034414.png -------------------------------------------------------------------------------- /notes/images/20220711_034446.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220711_034446.png -------------------------------------------------------------------------------- /notes/images/20220721_191904.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220721_191904.png -------------------------------------------------------------------------------- /notes/images/20220723_180928.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220723_180928.png -------------------------------------------------------------------------------- /notes/images/20220728_215342.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220728_215342.png -------------------------------------------------------------------------------- /notes/images/20220728_215357.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220728_215357.png -------------------------------------------------------------------------------- /notes/images/20220729_000720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220729_000720.png -------------------------------------------------------------------------------- /notes/images/20220810_055730.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220810_055730.png -------------------------------------------------------------------------------- /notes/images/20220810_055858.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220810_055858.png -------------------------------------------------------------------------------- /notes/images/20220810_055907.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220810_055907.png -------------------------------------------------------------------------------- /notes/images/20220810_060652.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220810_060652.png -------------------------------------------------------------------------------- /notes/images/20220810_060713.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220810_060713.png -------------------------------------------------------------------------------- /notes/images/20220810_060720.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220810_060720.png -------------------------------------------------------------------------------- /notes/images/20220810_060943.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220810_060943.png -------------------------------------------------------------------------------- /notes/images/20220816_034138.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220816_034138.png -------------------------------------------------------------------------------- /notes/images/20220816_173616.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220816_173616.png -------------------------------------------------------------------------------- /notes/images/20220816_174812.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220816_174812.png -------------------------------------------------------------------------------- /notes/images/20220816_175314.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220816_175314.png -------------------------------------------------------------------------------- /notes/images/20220816_180259.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220816_180259.png -------------------------------------------------------------------------------- /notes/images/20220816_181944.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220816_181944.png -------------------------------------------------------------------------------- /notes/images/20220828_214621.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220828_214621.jpg -------------------------------------------------------------------------------- /notes/images/20220828_214739.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220828_214739.jpg -------------------------------------------------------------------------------- /notes/images/20220828_223101.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220828_223101.jpg -------------------------------------------------------------------------------- /notes/images/20220828_223146.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220828_223146.jpg -------------------------------------------------------------------------------- /notes/images/20220828_224343.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220828_224343.png -------------------------------------------------------------------------------- /notes/images/20220828_224420.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20220828_224420.png -------------------------------------------------------------------------------- /notes/images/20221030_040331.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20221030_040331.png -------------------------------------------------------------------------------- /notes/images/20221030_041027.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20221030_041027.png -------------------------------------------------------------------------------- /notes/images/20221105_010142.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20221105_010142.png -------------------------------------------------------------------------------- /notes/images/202211100345.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/202211100345.png -------------------------------------------------------------------------------- /notes/images/20221111_222653.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20221111_222653.jpg -------------------------------------------------------------------------------- /notes/images/20221111_223249.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20221111_223249.jpg -------------------------------------------------------------------------------- /notes/images/20221203_223104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20221203_223104.png -------------------------------------------------------------------------------- /notes/images/20230226_025746.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230226_025746.png -------------------------------------------------------------------------------- /notes/images/20230226_025751.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230226_025751.png -------------------------------------------------------------------------------- /notes/images/20230524_120618.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230524_120618.png -------------------------------------------------------------------------------- /notes/images/20230524_123414.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230524_123414.png -------------------------------------------------------------------------------- /notes/images/20230708_203452.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230708_203452.png -------------------------------------------------------------------------------- /notes/images/20230723_191733.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230723_191733.png -------------------------------------------------------------------------------- /notes/images/20230725_125246.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230725_125246.png -------------------------------------------------------------------------------- /notes/images/20230725_125300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230725_125300.png -------------------------------------------------------------------------------- /notes/images/20230725_125316.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230725_125316.png -------------------------------------------------------------------------------- /notes/images/20230725_125334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230725_125334.png -------------------------------------------------------------------------------- /notes/images/20230726_014448.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230726_014448.png -------------------------------------------------------------------------------- /notes/images/20230726_213719.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230726_213719.png -------------------------------------------------------------------------------- /notes/images/20230727_000034.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230727_000034.png -------------------------------------------------------------------------------- /notes/images/20230727_120756.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230727_120756.png -------------------------------------------------------------------------------- /notes/images/20230727_124806.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230727_124806.png -------------------------------------------------------------------------------- /notes/images/20230727_130647.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230727_130647.png -------------------------------------------------------------------------------- /notes/images/20230727_130754.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230727_130754.png -------------------------------------------------------------------------------- /notes/images/20230727_151505.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230727_151505.png -------------------------------------------------------------------------------- /notes/images/20230812_122305.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230812_122305.png -------------------------------------------------------------------------------- /notes/images/20230812_122537.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230812_122537.png -------------------------------------------------------------------------------- /notes/images/20230812_154158.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230812_154158.png -------------------------------------------------------------------------------- /notes/images/20230814_210404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230814_210404.png -------------------------------------------------------------------------------- /notes/images/20230814_212346.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230814_212346.png -------------------------------------------------------------------------------- /notes/images/20230824_111023.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230824_111023.png -------------------------------------------------------------------------------- /notes/images/20230824_111220.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230824_111220.png -------------------------------------------------------------------------------- /notes/images/20230824_111238.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230824_111238.png -------------------------------------------------------------------------------- /notes/images/20230826_091630.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230826_091630.png -------------------------------------------------------------------------------- /notes/images/20230826_091643.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230826_091643.png -------------------------------------------------------------------------------- /notes/images/20230826_091722.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20230826_091722.png -------------------------------------------------------------------------------- /notes/images/20231109_035624.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20231109_035624.jpg -------------------------------------------------------------------------------- /notes/images/20231109_043455.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20231109_043455.jpg -------------------------------------------------------------------------------- /notes/images/20231113_025022.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20231113_025022.png -------------------------------------------------------------------------------- /notes/images/20231113_025028.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20231113_025028.png -------------------------------------------------------------------------------- /notes/images/20240322_160029.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240322_160029.png -------------------------------------------------------------------------------- /notes/images/20240322_161838.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240322_161838.jpg -------------------------------------------------------------------------------- /notes/images/20240330_012002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240330_012002.png -------------------------------------------------------------------------------- /notes/images/20240409_181838.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240409_181838.jpg -------------------------------------------------------------------------------- /notes/images/20240409_182151.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240409_182151.jpg -------------------------------------------------------------------------------- /notes/images/20240409_182510.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240409_182510.jpg -------------------------------------------------------------------------------- /notes/images/20240409_182541.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240409_182541.jpg -------------------------------------------------------------------------------- /notes/images/20240409_182547.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240409_182547.jpg -------------------------------------------------------------------------------- /notes/images/20240409_182555.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240409_182555.jpg -------------------------------------------------------------------------------- /notes/images/20240409_212755.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240409_212755.jpg -------------------------------------------------------------------------------- /notes/images/20240506_232810.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240506_232810.png -------------------------------------------------------------------------------- /notes/images/20240721_233414.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240721_233414.jpg -------------------------------------------------------------------------------- /notes/images/20240721_233646.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240721_233646.jpg -------------------------------------------------------------------------------- /notes/images/20240814_033710.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20240814_033710.png -------------------------------------------------------------------------------- /notes/images/20241213_003309.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241213_003309.webp -------------------------------------------------------------------------------- /notes/images/20241213_003324.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241213_003324.webp -------------------------------------------------------------------------------- /notes/images/20241213_010822.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241213_010822.webp -------------------------------------------------------------------------------- /notes/images/20241213_010914.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241213_010914.webp -------------------------------------------------------------------------------- /notes/images/20241228_213411.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241228_213411.webp -------------------------------------------------------------------------------- /notes/images/20241229_125606.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241229_125606.webp -------------------------------------------------------------------------------- /notes/images/20241229_125617.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241229_125617.webp -------------------------------------------------------------------------------- /notes/images/20241229_125835.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241229_125835.png -------------------------------------------------------------------------------- /notes/images/20241229_125840.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20241229_125840.png -------------------------------------------------------------------------------- /notes/images/2025-03-28 0327.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/2025-03-28 0327.png -------------------------------------------------------------------------------- /notes/images/20250111_231011.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20250111_231011.png -------------------------------------------------------------------------------- /notes/images/20250111_231051.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20250111_231051.png -------------------------------------------------------------------------------- /notes/images/20250326_002652.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20250326_002652.png -------------------------------------------------------------------------------- /notes/images/20250326_035405.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20250326_035405.png -------------------------------------------------------------------------------- /notes/images/20250330_200109.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20250330_200109.png -------------------------------------------------------------------------------- /notes/images/20250408_194742.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/20250408_194742.webp -------------------------------------------------------------------------------- /notes/images/bg.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/bg.webp -------------------------------------------------------------------------------- /notes/images/bg_1920.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/bg_1920.jpg -------------------------------------------------------------------------------- /notes/images/chrome-108-webp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/chrome-108-webp.png -------------------------------------------------------------------------------- /notes/images/edge-107-webp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/edge-107-webp.png -------------------------------------------------------------------------------- /notes/images/ui-en-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-en-0.png -------------------------------------------------------------------------------- /notes/images/ui-en-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-en-1.png -------------------------------------------------------------------------------- /notes/images/ui-ja-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-ja-0.png -------------------------------------------------------------------------------- /notes/images/ui-ja-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-ja-1.png -------------------------------------------------------------------------------- /notes/images/ui-ko-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-ko-0.png -------------------------------------------------------------------------------- /notes/images/ui-ko-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-ko-1.png -------------------------------------------------------------------------------- /notes/images/ui-ru-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-ru-0.png -------------------------------------------------------------------------------- /notes/images/ui-ru-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-ru-1.png -------------------------------------------------------------------------------- /notes/images/ui-tw-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-tw-0.png -------------------------------------------------------------------------------- /notes/images/ui-tw-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-tw-1.png -------------------------------------------------------------------------------- /notes/images/ui-zh-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-zh-0.png -------------------------------------------------------------------------------- /notes/images/ui-zh-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/ui-zh-1.png -------------------------------------------------------------------------------- /notes/images/作品发表时间的数据/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/作品发表时间的数据/image1.png -------------------------------------------------------------------------------- /notes/images/作品发表时间的数据/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/作品发表时间的数据/image2.png -------------------------------------------------------------------------------- /notes/images/作品发表时间的数据/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/作品发表时间的数据/image3.png -------------------------------------------------------------------------------- /notes/images/作品发表时间的数据/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/images/作品发表时间的数据/image4.png -------------------------------------------------------------------------------- /notes/images/作品发表时间的数据/每隔 1000000 个作品的发表时间之差 .txt: -------------------------------------------------------------------------------- 1 | 24448713 2 | 11585263 3 | 8595874 4 | 6682302 5 | 6056363 6 | 5421947 7 | 5255027 8 | 5182467 9 | 4461469 10 | 4051395 11 | 4241938 12 | 4246662 13 | 3927786 14 | 4049462 15 | 3977171 16 | 3751805 17 | 3517158 18 | 3400867 19 | 3611974 20 | 3660983 21 | 3605833 22 | 3885538 23 | 4062106 24 | 4202123 25 | 3734542 26 | 3461046 27 | 3642451 28 | 3788087 29 | 3807417 30 | 3524501 31 | 3792128 32 | 4035814 33 | 3683950 34 | 3708420 35 | 3508451 36 | 3831416 37 | 3819019 38 | 3622942 39 | 4052661 40 | 4454252 41 | 4183441 42 | 3976217 43 | 4125231 44 | 4346301 45 | 4303471 46 | 4152969 47 | 4758455 48 | 4849708 49 | 4663465 50 | 4811636 51 | 4948848 52 | 4839877 53 | 4971483 54 | 5282714 55 | 4271589 56 | 4712838 57 | 4967081 58 | 5132813 59 | 4958137 60 | 5506179 61 | 5291583 62 | 5185242 63 | 5410645 64 | 5394531 65 | 4882531 66 | 5735281 67 | 5726555 68 | 5386278 69 | 5223833 70 | 5601724 71 | 5263115 72 | 5564402 73 | 5116689 74 | 4893501 75 | 5210506 76 | 5205703 77 | 5078922 78 | 5095772 79 | 4757135 80 | 4250210 81 | 3893641 82 | 3307496 83 | 3896379 84 | 3813763 85 | 3964716 86 | 4077532 87 | 3732054 88 | 3668541 89 | 3713804 90 | 3813694 91 | 3750245 92 | 3652445 93 | 3525293 94 | 3808814 95 | 4035150 96 | 3595420 97 | 3623016 98 | 3727972 99 | 3682195 100 | 3758799 101 | 3526035 102 | 3637166 -------------------------------------------------------------------------------- /notes/pixiv 的快捷键.md: -------------------------------------------------------------------------------- 1 | 在作品页面里: 2 | 3 | - `v` 查看大图 4 | - `b` 收藏作品(如果已经收藏,则是编辑收藏) 5 | - `c` 发表评论(定位到评论输入框) 6 | - `j` 当处于页面滚动区域处于图片的**上方**时,定位到图片区域 7 | - `k` 当处于页面滚动区域处于图片的**下方**一定范围内时,定位到图片区域 -------------------------------------------------------------------------------- /notes/popup.md: -------------------------------------------------------------------------------- 1 | 本扩展没有 popup,所以点击扩展图标有两种效果: 2 | 3 | 1. 在不可用的网站,无反应 4 | 2. 在可用的网站,显示下载器面板 5 | 6 | 最近商店的评论区有个人说点图标没反应,还给了差评。我想可能是没有在 pixiv 就点击了图标导致的。 7 | 8 | 虽然我很无奈,但是我尝试用 popup 来显示提示:只能在 pixiv.net 使用。 9 | 10 | 但是尝试之后,我发现**设置 popup 会导致点击扩展图标不会显示下载器面板**。 11 | 12 | 原因: 13 | 14 | 点击扩展图标打开下载面板是依赖于 `chrome.browserAction.onClicked` 事件: 15 | 16 | ```JavaScript 17 | chrome.browserAction.onClicked.addListener(function (tab) { 18 | // 打开下载面板 19 | chrome.tabs.sendMessage(tab.id!, { 20 | msg: 'click_icon', 21 | }) 22 | }) 23 | ``` 24 | 25 | 但是如果设置了 popup,就不会触发 `chrome.browserAction.onClicked` 事件,所以也就无法通过点击图标打开下载器面板。(不过仍然可以通过页面上的下载按钮打开下载器面板)。 26 | 27 | 官网是这么写的: 28 | 29 | onClicked: Fired when a browser action icon is clicked. **Does not fire if the browser action has a popup.** 30 | 31 | ----------------- 32 | 33 | 动态设置 popup 也存在这个问题。 34 | 35 | ```JavaScript 36 | chrome.browserAction.onClicked.addListener(function (tab) { 37 | // 如果不可用,则用 popup 显示提示 38 | if (!checkEnable(tab.url)) { 39 | chrome.browserAction.setPopup({ 40 | popup: 'popup.html', 41 | }) 42 | } 43 | }) 44 | ``` 45 | 46 | 如果默认不添加 popup,而是当用户在不可用的网站点击扩展图标时动态设置 popup,也有问题。因为一旦设置过一次 popup,那么后续就不会再触发 `onClicked` 事件,而是始终显示 popup。那么之后再 pixiv 页面点击图标也是只会显示 popup,而不会打开下载面板。 47 | 48 | 所以尝试一番之后我放弃了用 popup 显示提示的想法。 49 | 50 | ------------ 51 | 52 | 题外话:为什么下载器一直以来都没有把设置选项放在 popup 里,或者单独做一个设置页面(通过点击扩展图标打开设置页面)。 53 | 54 | 这是因为目前的做法:把设置项直接显示在页面上,可以让每个页面使用不同的设置,并且不会互相影响。在浏览 pixiv 的时候,用户经常会打开多个页面,每个页面里的选项可以自由修改并且不会影响到其他页面,是很有用的。 55 | 56 | 如果把设置选项放到 popup 里或者是一个单独的设置页面里,那么要让各个页面的设置能够互不影响,恐怕实现起来会很麻烦,所以我没有这样做。 57 | 58 | 不过现在确实存在一个问题,就是设置项越来越多,看起来会密密麻麻的。 -------------------------------------------------------------------------------- /notes/rankingPage.md: -------------------------------------------------------------------------------- 1 | 排行榜的 api 其实就是在排行榜页面的网址后面加上 `p` 数和 `format=json`。如下: 2 | 3 | ``` 4 | // 页面网址 5 | https://www.pixiv.net/ranking.php?mode=daily&content=illust&date=20191118 6 | 7 | // api 8 | https://www.pixiv.net/ranking.php?mode=daily&content=illust&date=20191118&p=2&format=json 9 | ``` 10 | 11 | ## mode 12 | 13 | - daily 14 | - daily_r18 15 | - weekly 16 | - weekly_r18 17 | - monthly 18 | - rookie 19 | - male_r18 20 | - female_r18 21 | - r18g 22 | 23 | ## content 24 | 25 | - 空,综合 26 | - illust 27 | - ugoira 28 | - manga 29 | 30 | ## date 31 | 32 | YYYYMMDD 格式的日期,如: 33 | 34 | 20191118 35 | 36 | ## p 37 | 38 | 数字 39 | 40 | ## format 41 | 42 | - json -------------------------------------------------------------------------------- /notes/recordtxt.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/recordtxt.7z -------------------------------------------------------------------------------- /notes/xRestrict 字段.md: -------------------------------------------------------------------------------- 1 | 作品数据中有一项 `xRestrict`,以前没注意它是什么意思,现在测试了一番,觉得它表示作品的年龄限制。 2 | 3 | ` 4 | xRestrict: 0 | 1 | 2 5 | ` 6 | 7 | - 0 全年龄 8 | - 1 R-18 9 | - 2 R-18G 10 | 11 | 这个标记在所有类型的作品里都是可用的(包括小说)。 12 | 13 | ----------- 14 | 15 | 测试过程: 16 | 17 | 在 [综合今日排行榜](https://www.pixiv.net/ranking.php?mode=daily) 的**一般**分类中,抓取所有作品,`xRestrict` 皆为 `0`。 18 | 19 | 在 [综合R-18每日排行榜](https://www.pixiv.net/ranking.php?mode=daily_r18) 中,抓取所有作品,`xRestrict` 皆为 `1`。 20 | 21 | 在 [综合R-18G排行榜](https://www.pixiv.net/ranking.php?mode=r18g) 中,抓取所有作品,`xRestrict` 皆为 `2`。 22 | 23 | ----------- 24 | 25 | 在 [小说 本周排行榜](https://www.pixiv.net/novel/ranking.php?mode=weekly) 的**一般**分类中,抓取所有作品,`xRestrict` 皆为 `0`。 26 | 27 | 在 [小说 R-18本周排行榜](https://www.pixiv.net/novel/ranking.php?mode=weekly_r18) 的**一般**分类中,抓取所有作品,`xRestrict` 皆为 `1`。 28 | 29 | 在 [小说 R-18G排行榜](https://www.pixiv.net/novel/ranking.php?mode=r18g) 的**一般**分类中,抓取所有作品,`xRestrict` 皆为 `2`。 30 | -------------------------------------------------------------------------------- /notes/一些已知问题.md: -------------------------------------------------------------------------------- 1 | # 已知问题 2 | 3 | ## 进度条显示总体积为 0 KiB 4 | 5 | 偶尔会发生这个情况: 6 | 7 | 下载器的某个进度条看上去是正常的,下载也没有出现问题,除了一个问题:这个文件的总体积显示为 0 KiB。 8 | 9 | 这个问题的原因在于 Pixiv。当下载器从 Pixiv 下载图片时,如果这次 HTTP 的返回头里没有 `content-length` 标签,就会出现这个问题。 10 | 11 | 这是随机的,所有图片文件都可能出现这个问题。 12 | 13 | 即使对于同一个文件,也有可能这次下载是正常的,下一次下载就出现这个问题:没有 `content-length` 标签。 14 | 15 | ```js 16 | xhr.onprogress = function(event) { 17 | // event.loaded - how many bytes downloaded 18 | // event.lengthComputable = true if the server sent Content-Length header 19 | // event.total - total number of bytes (if lengthComputable) 20 | } 21 | ``` 22 | 23 | 下载器使用 xhr 对象的 `event.total` 来作为文件的总体积。如果缺少 `content-length` 标签,就会出现这个问题。 -------------------------------------------------------------------------------- /notes/下载器运行流程.md: -------------------------------------------------------------------------------- 1 | # 判断页面类型 2 | 3 | # 初始化中间面板 4 | 5 | # 添加设置表单 6 | 7 | # 初始化设置表单 8 | 9 | # 初始化抓取功能 10 | 11 | 由 `InitxxxxPage` 类执行,初始化的 `init` 方法包含以下步骤: 12 | 13 | - `options.showAllOption() `显示所有选项 14 | - `setFormOption `设置选项的值,隐藏不需要的选项 15 | - `appendCenterBtns `添加中间的按钮,绑定它们的功能 16 | - `appendElseEl `添加中间按钮之外的其他元素 17 | - `initElse `添加各个页面额外的初始化步骤,什么都可以放 18 | 19 | 当页面类型变化时,`InitxxxxPage` 会调用 `destroy` 方法,移除添加的元素,重置公用状态,解绑事件等。 20 | 21 | # 抓取作品 22 | 23 | 抓取时的一般流程如下: 24 | 25 | - `readyCrawl` 检查是否可以开始抓取 26 | - `nextStep` 可以开始抓取时,进入下一个流程,默认进入 `getIdList`。这里是一个钩子,可以改变流程,但是多数情况下最后还是会进入 `getIdList`。 27 | - `getIdList` 获取作品 id 列表,不包含作品的详细信息。此方法由各个页面的抓取器具体定义,不具有通用性。过程中一般会使用过滤器 filter。 28 | - `getIdListFinished` 作品 id 列表获取完毕 29 | - `resetGetIdListStatus` 可以在这里重置一些 `getIdList` 使用的数据 30 | - `getWorksData` 获取作品的详细数据,所有页面通用。过程中会使用过滤器 filter。 31 | - `afterGetWorksData` 每当执行完一次 `getWorksData`,都会进入这个方法。目前用来检查是否抓取完毕。 32 | - `crawlFinished` 获取作品的详细数据完毕 33 | - `sortResult` 可以对抓取结果进行排序 34 | 35 | # 下载器下载文件 36 | 37 | 下载要保存的文件。如有必要,会对动图进行转换。 38 | 39 | # 浏览器保存文件 40 | 41 | 下载器把下载好的文件数据发送给浏览器,浏览器保存到硬盘上。 42 | -------------------------------------------------------------------------------- /notes/下载器遇到的一些网络请求错误.md: -------------------------------------------------------------------------------- 1 | 下载器遇到的一些网络请求错误 2 | 3 | ## 超时 4 | 5 | 下载器的很多请求都是使用 `fetch`,并且没有设置超时时间。 6 | 7 | **在使用代理软件的时候,可能不会发生超时错误。** 下面的请求即使经过了 1.4 min 也没有超时。 8 | 9 | ![](./images/20210812164537.png) 10 | 11 | 这可能是因为代理的存在,此时浏览器是与代理软件通信的,而不是与 pixiv 通信。所以当代理软件在保持着这个请求时,浏览器永远不会认为这个请求会超时。(除非代理软件告诉浏览器请求超时了) 12 | 13 | 在下图中,浏览器通过代理发送请求,在请求还未返回时直接关闭代理。过一会儿之后浏览器显示请求超时。这应该是因为浏览器得不到代理软件的回应,所以判断这个请求超时了。 14 | 15 | ![](./images/20210812164554.png) 16 | 17 | ![](./images/20210812164620.png) 18 | 19 | 推测:使用代理并且代理正常工作时,浏览器可能不会发生超时错误。 20 | 21 | 如果请求途中断开代理,以及不使用代理(直连)时,会发生超时错误。 22 | -------------------------------------------------------------------------------- /notes/下载小说里的内嵌图片.md: -------------------------------------------------------------------------------- 1 | # 下载小说里的内嵌图片 2 | 3 | 小说里可以插入(内嵌)2 种图片: 4 | 5 | 1. 直接上传图片 6 | 2. 引用一个图像作品的 id 7 | 8 | 在 pixiv 的页面上,小说显示出来后,内嵌的图片上都有一个超链接。 9 | - 上传的图片:img 不是原图,是压缩过的。超链接是原图。 10 | - 引用的图片:img 是引用的作品里的第一张图片的原图。超链接是作品网址。 11 | 12 | 不过下载器不需要关心页面上显示出来的这些内容。 13 | 14 | ## 示例小说 15 | 16 | https://www.pixiv.net/novel/show.php?id=18016618 17 | 18 | 作品数据里的关键数据如下: 19 | 20 | ```js 21 | body: { 22 | content: `塔菲,你个臭粉毛,哼啊啊啊啊啊 23 | 24 | 上传的插画 25 | [uploadedimage:13309543] 26 | 27 | 通过作品 id 插入的插画 28 | [pixivimage:99381250]`, 29 | 30 | textEmbeddedImages: { 31 | "13309543": { 32 | "novelImageId": "13309543", 33 | "sl": "0", 34 | "urls": { 35 | "240mw": "https://i.pximg.net/c/240x480_80/novel-cover-master/img/2022/07/24/06/19/26/tei345516131740_1fd8644bc2575f1f1384075aab1f0bed_master1200.jpg", 36 | "480mw": "https://i.pximg.net/c/480x960/novel-cover-master/img/2022/07/24/06/19/26/tei345516131740_1fd8644bc2575f1f1384075aab1f0bed_master1200.jpg", 37 | "1200x1200": "https://i.pximg.net/c/1200x1200/novel-cover-master/img/2022/07/24/06/19/26/tei345516131740_1fd8644bc2575f1f1384075aab1f0bed_master1200.jpg", 38 | "128x128": "https://i.pximg.net/c/128x128/novel-cover-master/img/2022/07/24/06/19/26/tei345516131740_1fd8644bc2575f1f1384075aab1f0bed_square1200.jpg", 39 | "original": "https://i.pximg.net/novel-cover-original/img/2022/07/24/06/19/26/tei345516131740_1fd8644bc2575f1f1384075aab1f0bed.png" 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | ## 直接上传的图片 47 | 48 | `[uploadedimage:13309543]` 这种标记是上传的图片。在 `body.textEmbeddedImages` 里有对应的数据,通过 `urls` 对象取出里面储存的网址即可。 49 | 50 | ## 引用作品的图片 51 | 52 | `[pixivimage:99381250]` 这种标记是引用作品的图片。小说数据里没有它对应的网址,需要下载器自行获取其网址。 53 | 54 | 此外,pixiv 允许引用作品中某一张特定的图片,例如 `[pixivimage:99760571-1]` 最后面的 1 就是第 1 张图片。(等于是这个作品的 p0)。 55 | 56 | 有这个特征的小说如:https://www.pixiv.net/novel/show.php?id=17968738 57 | 58 | ## 小说保存格式为 EPUB 时 59 | 60 | EPUB 可以内嵌图片,所以我把图片转换为 DataURL,使用 img 标签保存在 EPUB 里。 61 | 62 | ## 小说保存格式为 TXT 时 63 | 64 | 因为 TXT 不能内嵌图片,所以图片会随 txt 文件一同下载和保存。 65 | -------------------------------------------------------------------------------- /notes/下载状态的处理.md: -------------------------------------------------------------------------------- 1 | # 状态说明 2 | 3 | `DownloadStates` 类保存有每个下载任务的状态: 4 | 5 | ``` 6 | // -1 未开始下载 7 | // 0 下载中 8 | // 1 下载完成 9 | ``` 10 | 11 | # 下载前 12 | 13 | 如果是全新的下载,则把所有任务的状态设为 `-1`。 14 | 15 | 如果之前暂停了下载,或者是由断点续传恢复的下载,则需要继续下载。此时调用 `downloadStates.resume()`,将“下载中” `0` 复位到“未开始下载” `-1 `,然后进入下载。 16 | 17 | # 下载中 18 | 19 | `DownloadControl` 类通过 `new Download` 创建一个下载任务,并把这个任务的状态改为 `0`。 20 | 21 | 之后在下载过程中: 22 | 23 | - 跳过下载的,先触发 `EVT.events.skipSaveFile`,`DownloadControl` 监听到这个事件后会触发 `EVT.events.downloadSucccess` 事件,视为已下载。状态变为 `1`。 24 | - Download 里文件下载失败的,触发 `EVT.events.downloadError` 事件。状态不变。 25 | - Download 文件下载成功的,会把文件传递给后台保存到硬盘里,之后 `DownloadControl` 根据后台是否成功保存了文件,触发 `EVT.events.downloadSucccess` 事件(状态变为 `1`)或者 `EVT.events.saveFileError` 事件(状态复位成 `-1`,再次创建这个下载任务)。 26 | -------------------------------------------------------------------------------- /notes/优化生成文件名的效率.md: -------------------------------------------------------------------------------- 1 | # 优化生成文件名的效率 2 | 3 | ## 新旧代码对比 4 | 5 | 旧代码在执行 `getFileName` 方法时,每次都会在其内部创建一个函数表达式: 6 | 7 | ```js 8 | function getFileName () { 9 | // ... 10 | const generateFileName = (rule) => {...} 11 | // ... 12 | } 13 | ``` 14 | 15 | 新代码把这个函数表达式改为在函数外的函数声明,直接调用: 16 | 17 | ```js 18 | function generateFileName (rule, cfg) { 19 | // ... 20 | } 21 | 22 | function getFileName () { 23 | // ... 24 | generateFileName(rule, cfg) 25 | // ... 26 | } 27 | ``` 28 | 29 | 在新代码里,`generateFileName` 已经事先声明好了,在 `getFileName` 里面直接调用即可。 30 | 31 | 这样能提高多少效率呢? 32 | 33 | ## 测试 34 | 35 | 抓取 `初音ミク` tag 1000 页,约 60000 个作品,共 123317 条数据。 36 | 37 | 遍历生成文件名: 38 | 39 | ```js 40 | console.time('filename') 41 | for (const data of store.result) { 42 | fileName.getFileName(data) 43 | } 44 | console.timeEnd('filename') 45 | ``` 46 | 47 | 多次执行此代码。 48 | 49 | 旧代码花费的时间平均在 2061 ms 左右: 50 | 51 | ``` 52 | 1996.89892578125 ms 53 | 2080.978759765625 ms 54 | 2128.549072265625 ms 55 | 2034.5361328125 ms 56 | 2069.7451171875 ms 57 | ``` 58 | 59 | 新代码花费的时间平均在 1864 ms 左右: 60 | 61 | ``` 62 | 1919.650146484375 ms 63 | 1847.028076171875 ms 64 | 1852.77880859375 ms 65 | 1865.02099609375 ms 66 | 1837.669189453125 ms 67 | ``` 68 | 69 | 遍历 12 万条数据生成文件名,新代码比旧代码节约了 200 ms。大概节约了 10% 的时间。 70 | 71 | 考虑到生成文件名时的运算比较多,这次的改动只是其中的一小部分,所以提升 10% 的效率也可以令人满意了。 72 | 73 | 当然,生成文件名本来花费的时间也就不多,这点优化聊胜于无。 74 | -------------------------------------------------------------------------------- /notes/优化转换动图的性能.md: -------------------------------------------------------------------------------- 1 | # 优化转换动图的性能 2 | 3 | ## 两个测试样本 4 | 5 | 样本1:(体积稍小)https://www.pixiv.net/artworks/99831960 6 | 7 | zip 源文件体积:7.36MB 8 | 9 | 样本2:(体积较大)https://www.pixiv.net/artworks/99829091 10 | 11 | zip 源文件体积:20.50MB 12 | 13 | ## 缩短提取图片数据的时间 14 | 15 | 以前下载器使用 zip.js 库从 zip 文件里提取图片数据。它返回的数据是 base64URL 字符串。 16 | 17 | zip.js 库是一个泛用库,它不能针对 Pixiv 的压缩包进行优化。 18 | 19 | 我在制作预览动图的功能时,实现了从 zip 文件里提取图片数据的方法,这是专门对 Pixiv 的压缩包编写的,所以速度更快。 20 | 21 | 我用自己编写的方法替换了 zip.js,节约了提取图片所需的时间。 22 | 23 | 旧版本的提取图片所需时间:(多次测试取大概的平均值) 24 | - 样本1:1000 ms 25 | - 样本2:1900 ms 26 | 27 | 优化之后: 28 | - 样本1:170 ms 29 | - 样本2:800 ms 30 | 31 | ## 缩短动图转换为 webm 的时间 32 | 33 | 之前在编码 webm 时,我传递给编码器的数据是绘制了图片的 Canvas 对象。但是这会导致编码器做一些重复工作,浪费了时间。 34 | 35 | 现在我把图像转换为 webp 格式的 DataURL,然后传递给编码器。对于现在使用的编码器来说,这样是最快的。 36 | 37 | 结合上面“缩短提取图片数据的时间”的优化,转换动图所需的时间有明显的减少。 38 | 39 | 转换时间指的是:从动图的 zip 文件下载完成之后开始,截止到动图转换完毕的时间。 40 | 41 | 旧版本的转换时间:(掐秒表,多次测试后的平均值) 42 | - 样本1:5 s 43 | - 样本2:29 s 44 | 45 | 优化之后: 46 | - 样本1:3.6 s 47 | - 样本2:22 s 48 | 49 | ## 减少了动图转换为 webm 的内存使用量 50 | 51 | 这里仅测试样本 2,因为它的体积比较大。 52 | 53 | 之前在编码 webm 时,由于编码器做了一些重复工作,内存使用量会略多一些。 54 | 55 | 而且旧版本有个很大的问题,在编码时间过去大约一半时(十几秒之后),会有数秒钟的时间段,内存占用会迅速多出一个 GB,令人震惊。等这个动图下载完毕后,内存才会回落。 56 | 57 | 新版本没有这个问题,也就是内存使用量不会突然大幅增加。在整个转换过程中,内存的增加量只有 200 MB 左右。 58 | 59 | 旧版本瞬间占用大量内存的问题,无疑会影响系统整体性能,也会影响这个网页的稳定性。有些用户在下载大量动图时,页面偶尔会崩溃,新版本里这样的异常情况应该会明显减少。 60 | 61 | ## 优化了动图转换为 apng 的代码 62 | 63 | 仅仅优化了代码。没有明显的性能提升。 64 | 65 | ## 缩短动图转换为 gif 的时间 66 | 67 | 优化了转换为 gif 的代码,并且转换时间也缩短了一些。 68 | 69 | 旧版本的转换时间: 70 | - 样本1:6.6 s 71 | - 样本2:42 s 72 | 73 | 优化之后: 74 | - 样本1:5.5 s 75 | - 样本2:39 s 76 | 77 | -------------------------------------------------------------------------------- /notes/作品缩略图的选择器列表-图像作品.md: -------------------------------------------------------------------------------- 1 | # 作品缩略图的选择器列表-图像作品 2 | 3 | `div[width="90"]` 小尺寸作品列表,主要出现在:作品页面右侧,画师头像下面的 3 个作品。 4 | 5 | `div[width="104"]` 小尺寸作品列表,主要出现在:搜索页面显示的几个热门作品。 6 | 7 | `div[width="112"]` 小尺寸作品列表,主要出现在:鼠标放在画师名字上之后出现的画师的 3 个作品。 8 | 9 | `div[width="118"]` 小尺寸作品列表,主要出现在:鼠标点击顶部搜索框之后,搜索框下拉区域的推荐作品。 10 | 11 | `div[width="136"]` 小尺寸作品列表,主要出现在: 12 | 13 | 1. 作品页内作品大图下方的作品列表 14 | 2. 收藏作品后出现的推荐作品 15 | 16 | `div[width="131"]` 小尺寸作品列表,主要出现在: 17 | 18 | 关注画师之后,底部出现的画师列表里的作品 19 | 20 | `div[width="184"]` 中尺寸作品列表,全站通用 21 | 22 | `div[width="288"]` 大尺寸作品列表,主要出现在首页“每日排行榜”区域 23 | 24 | ----------- 25 | 26 | `._work` 旧版页面的通用作品列表,基本上所有旧版页面都在使用,但是尺寸有所不同 27 | 28 | ----------- 29 | 30 | ~~`figure > div` (不再使用)范围:~~ 31 | 32 | 1. 作品页面里的大图区域(不需要预览这个大图) 33 | 2. 关注用户的新作品页面(此页面改版了,不需要这个选择器了) 34 | 35 | ----------- 36 | 37 | `._work.item` 小尺寸作品列表,主要出现在: 38 | 旧版页面(如关注的用户的新作品页面)里鼠标放在画师名字上之后出现的画师的 3 个作品。 39 | 40 | ----------- 41 | 42 | `li>div>div:first-child` 43 | 44 | 约稿页面里的作品缩略图。 45 | -------------------------------------------------------------------------------- /notes/作品缩略图的选择器列表-小说作品.md: -------------------------------------------------------------------------------- 1 | # 作品缩略图的选择器列表-小说作品 2 | 3 | 因为添加了“点击收藏按钮时下载作品”的功能,所以我打算获取小说的缩略图(主要是为了获取它的收藏按钮)。 4 | 5 | 这或许对“手动选择作品”也有帮助。因为现在在手动选择作品时,要求鼠标点击的元素包含作品链接,但是小说的链接就只有封面和标题,在其他部分点击的话是没效果的。如果能够从缩略图获取,或许能改善这个问题。 6 | 7 | 1. 由于 Pixiv 新版元素的 className 是动态生成的,所以不应该依赖具体的 className。 8 | 2. 由于要从动态添加的元素里获取缩略图,所以选择器路径应该尽量短一些。 9 | 3. 有些选择器会选择到图像作品,需要注意加以区别。 10 | 4. 如果有些页面只有一种列表,应该手动指定它所使用的选择器,以免被其他选择器错误的选择 11 | 12 | | 选择器 | 对应元素 | 13 | | ----------------------------------- | -------------------------------------------------------------------------------------------- | 14 | | `section li>div` | 新版页面里大部分小说列表(没特别说明的都用这个) | 15 | | `li>div[size="392"]` | 用户主页的小说列表、关注的用户的新作小说列表 div,与上面的选择器相同但带有 `size="392"` 属性 | 16 | | `nav>div>div` | 小说页面内,正文下面横排的 3 个小说列表 | 17 | | `div.gtm-novel-work-recommend-link` | 小说页面内,相关作品里的小说列表 | 18 | | `li[size="1"]>div` | 首页里的大部分小说列表 | 19 | | `div._ranking-item` | 小说排行榜里的列表 | 20 | | `section ul>div` | 关注页面里的小说列表 | 21 | | `section ul>li` | 系列小说页面里的小说列表 | 22 | | `div[size="496"]` | 发现页面里收藏小说后,下面弹出的推荐作品 | 23 | 24 | 小说缩略图的选取比图像作品的缩略图要麻烦,因为小说的很多 `li`、`div` 没有特殊标记,不像作品元素很多都有 width `div[width="90"]`。 25 | 26 | --------------- 27 | 28 | 已经测试没有问题的页面: 29 | 30 | - 首页 31 | - 用户主页 32 | - 小说页面内 33 | - 系列小说页面 34 | - 小说排行榜 35 | - 小说搜索页面 36 | - 发现页面 37 | - 已关注用户的新作品 38 | - 大家的新作品 39 | - 关注页面 40 | - 约稿页面 41 | 42 | 所有含有小说的页面测试完毕。 43 | -------------------------------------------------------------------------------- /notes/使用 nginx 反代理访问 pixiv 的情况.md: -------------------------------------------------------------------------------- 1 | 国内一些用户使用 Nginx 反代理来访问 pixiv。 2 | 3 | [Pixiv-Nginx](https://github.com/mashirozx/Pixiv-Nginx) 4 | 5 | 这种方式会把图片网址的域名 `pximg.net` 重定向到 `pixiv.cat`(因为后者没有被国内屏蔽,可以正常加载)。 6 | 7 | 此外,也有其他一些 pixiv 反代理网站可用,如 `pximg.moezx.cc`。 8 | 9 | 之前 nginx 反代理的用户无法使用下载器进行下载,现在下载器里添加了对 `pixiv.cat` 的支持,可以进行下载了。(没有添加其他代理域名的原因是:1. 其他域名使用的人少;2. 添加太多主机权限会导致扩展难以通过 Chrome 应用商店的审核。 10 | 11 | **注意:** 12 | 1. `pixiv.cat` 的图片加载速度可能比较慢,所以下载线程应该设置的小一些。 13 | 2. 某些用户的 nginx 反代理使用的域名不是 `pixiv.cat`,此时下载器仍然无法进行下载。你可以从下载器里复制 url,把`pximg.net` 替换成你的反向代理域名,用其他下载软件进行下载。 14 | 15 | 附:反代理导致下载器无法下载图片的错误信息如图: 16 | 17 | ![](./images/2021-03-25_121957.jpg) -------------------------------------------------------------------------------- /notes/动图转换的内存占用.md: -------------------------------------------------------------------------------- 1 | ## 动图转换的内存占用 2 | 3 | 动图转换时的内存占用量,影响因素比较多,以下实验仅作为一个参考。 4 | 5 | ---------- 6 | 7 | 从今日动图排行榜里,下载100个动图,转换为 webm,同时转换4个,记录内存占用情况。 8 | 9 | https://www.pixiv.net/ranking.php?mode=daily&content=ugoira 10 | 11 | 计算量为页面内存 + 显存的增加量。 12 | 13 | 转换前增加量 0G 14 | 转换中最大 页面增加了 0.85G,GPU 增加了 0.8G 15 | 转换完 页面增加了 0.45G,GPU 增加了 0.4G 16 | 17 | 转换中,当一个动图转换完成之后,内存是会被回收的,但是内存占用还是会缓慢增加。转换完成之后内存就基本不再降低,或者降低的不多。 18 | -------------------------------------------------------------------------------- /notes/去重.md: -------------------------------------------------------------------------------- 1 | # 设计目的 2 | 3 | 下载文件之后储存下载记录;下载文件之前可以查询下载记录,判断文件是否重复。 4 | 5 | 在下载面板上增加设置项目: 6 | 7 | - 启用去重 deduplication 8 | - 去重策略?宽松 严格 dupliStrategy strict loose 9 | - 清空去重数据? clearDownloadRecords 10 | 11 | 宽松策略是只通过 id 判断是否重复,严格策略是同时结合文件名进行判断。 12 | 13 | # 数据的存储方式: 14 | 15 | ## 数据库 16 | 17 | 去重数据(其实就是下载记录)单独存储在一个 IndexedDB 数据库中。 18 | 19 | ## 表 20 | 21 | 考虑到查询效率,这里做了分表。 22 | 23 | 根据 pixiv 作品 id 开头的数字,分成了 1-9 共 9 个表。当存储或查询时,根据作品 id 去对应的表里查询。 24 | 25 | 这样做是为了减少每个表里的数据量,这样在查询时可以避免不必要的查询,提高效率。 26 | 27 | 不过现实不会那么理想,因为数据的分布可能不是平均的。大家最常下载的应该是最近的作品,目前 pixiv 作品的 id 是 8000 w 多,所以表 8 可能存储的数据最多。 28 | 29 | ## 数据 30 | 31 | 每下载成功一个文件,添加它对应的下载记录。(下载失败的文件、跳过下载的文件不存储到下载记录里) 32 | 33 | 记录的数据如: 34 | 35 | ``` 36 | { 37 | id:'82432085', 38 | n:'光崎/82432085_p0.png' 39 | } 40 | ``` 41 | 42 | `id` 字段是作品 id。 43 | 44 | `n` 字段是生成的文件名。文件名可以很长,其长度不好估算。 45 | 46 | 假设一条数据占据的存储空间为 `500 B` 左右(往大了算),那么 `1 GiB` 空间可以存储约 `200 w` 条记录。 47 | 48 | 目前 pixiv 的作品 id 是 8 位数。如果只算图片作品,每个分表的数据最大量可以达到约 1111w。如果加上小说,那么会更多一些。不过小说的 id 总数比图片的少很多。 49 | 50 | # IndexedDB 相关知识 51 | 52 | ## 额度控制 53 | 54 | 浏览器的本地存储的额度控制参考这里: 55 | 56 | https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria 57 | 58 | 根据这里面的说法,如果本地存储占据的硬盘空间达到上限,浏览器会依次清理掉最久没有被使用的源的数据。似乎一个域名就是一个源(子域名不同则视为不同的网站)。浏览器清理存储空间的时候是按源为单位的,所以如果清理到 pixiv.net 的话,其下所有数据库都会被删除。 59 | 60 | ## 由用户清理数据 61 | 62 | 当用户清除浏览数据时,选择 **Cookie 及其它网站数据** 会清理本地存储,包括 localstorage、WebSQL、IndexedDB 等。 63 | 64 | 本程序的去重功能设置里,提供了一个按钮,可以清除保存的去重数据。 -------------------------------------------------------------------------------- /notes/在 Yandex 浏览器(安卓)上使用.md: -------------------------------------------------------------------------------- 1 | # 在 Yandex 浏览器(安卓)上调试扩展程序 2 | 3 | 我们可以输入网址打开[扩展管理页面](browser://extensions),加载扩展程序: 4 | 5 | browser://extensions 6 | 7 | 这会打开文件浏览器,我们需要找到并选择扩展程序的 manifest.json 文件,把扩展程序加载到 Yandex 浏览器中。 8 | 9 | 官方文档:https://yandex.com/support/browser-mobile-android-phone/personal-settings/extensions.html#developers2 10 | 11 | # Manifest version 3 支持 12 | 13 | 2022/11/11 14 | 15 | Yandex 浏览器最新版本仍然不支持 Manifest version 3 的扩展程序。 16 | 17 | 离线加载 V3 的扩展程序会出现错误: 18 | 19 | ![](./images/20221111_223249.jpg) 20 | 21 | 为了保险起见,我还得测试一下它是否支持在线安装 V3 的扩展程序。 22 | 23 | 这个扩展程序是 V3 的: https://chrome.google.com/webstore/detail/gppongmhjkpfnbhagpmjfkannfbllamg 24 | 25 | ![](./images/20221111_222653.jpg) 26 | 27 | 结果 Yandex 直接显示不兼容,无法安装。 28 | 29 | 所以我对 Yandex 已经没有念想了,年底必须升级到 V3,它不支持我也没办法。 30 | 31 | # 显示警告 32 | 33 | 下载器即将对 Android 上的 Yandex 浏览器发出警告,因为它不能支持 Manifest version 3。 34 | 35 | 我的安卓手机上的 Yandex 浏览器的 UA: 36 | 37 | `Mozilla/5.0 (Linux; arm_64; Android 10; MI 8 Lite) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 YaBrowser/22.11.0.224.00 SA/3 Mobile Safari/537.36` 38 | 39 | 当 Yandex 浏览器启用桌面模式后的 UA: 40 | 41 | `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 YaBrowser/22.11.0.224.00 SA/3 Safari/537.36` 42 | 43 | 不要检查 Android 字样,因为桌面模式下可能没有 Android 标识。 44 | -------------------------------------------------------------------------------- /notes/多图作品里的图片格式只会是同一种.md: -------------------------------------------------------------------------------- 1 | Pixiv 的多图作品的数据里,只有第一张图片的 urls,没有后续图片的 urls。 2 | 3 | ![](./images/20250111_231051.png) 4 | 5 | 所以下载器把后续图片的格式视为与第一张图片相同,即所有图片要么全是 jpg 格式,要么全是 png 格式。不会出现 jpg 和 png 格式混合的情况。 6 | 7 | 那么这个做法对不对呢?刚才我验证了一下,是对的。 8 | 9 | 我尝试在投稿时同时上传 jpg 和 png 图片,看到了如下提示: 10 | 11 | **有幾種不同圖片檔案格式。投稿時不同格式的圖片檔案會自動轉換成JPEG。** 12 | 13 | 这就保证了所有图片都会是相同的格式。 14 | 15 | ![](./images/20250111_231011.png) 16 | -------------------------------------------------------------------------------- /notes/定制版-特供版本分支需要修改的地方.md: -------------------------------------------------------------------------------- 1 | 建立新分支 2 | 3 | 0. `manifest.json` 修改包名,在后面加上版本标识字符 4 | 5 | ```json 6 | "name": "Powerful Pixiv Downloader Test", 7 | ``` 8 | 9 | 1. `package.json` 修改 css 编译路径 10 | 11 | ```json 12 | `"less": "lessc ./src/style/style.less ./dist-special-test/style/style.css"` 13 | ``` 14 | 15 | 2. `webpack.conf.js` 修改 js 编译路径 16 | 17 | ```js 18 | path: path.resolve(__dirname, 'dist-special-test/js'), 19 | ``` 20 | 21 | 3. `tsconfig.json` 修改 ts 输出目录,但不是必须 22 | 23 | ```json 24 | "outDir": "./dist-special-test/js" 25 | ``` 26 | 27 | 4. `pack.js` 修改 zip 文件名,以及 dist 文件夹名称。 28 | 29 | ```js 30 | const packName = 'powerfulpixivdownloader-special-test' 31 | const distPath = './dist-special-test' 32 | ``` 33 | 34 | 5. `Config.ts` 在 appName 后面加上定制版的名字,如 35 | 36 | ```ts 37 | static readonly appName = 'Powerful Pixiv Downloader Test' 38 | ``` 39 | 40 | 6. `.gitignore` 里面排除其他 dist 文件夹,避免追踪其他 dist 文件夹 41 | 42 | ``` 43 | # build file 44 | *.zip 45 | /dist/** 46 | /dist-offline/** 47 | /dist-special-HongYe/** 48 | /dist-special-cao/** 49 | /dist-special-DelMemory/** 50 | ``` 51 | 52 | 特殊分支里不能排除 `*special*` 否则会排除该分支自己的文件夹。 -------------------------------------------------------------------------------- /notes/对移动端浏览器进行优化.md: -------------------------------------------------------------------------------- 1 | # 对移动端浏览器进行优化 2 | 3 | 下载器是基于 pixiv 的桌面端网站开发的,一些功能并没有对移动端页面进行优化。现在打算优化一下。 4 | 5 | ## 通过 UA 判断是否处于移动端模式 6 | 7 | Kiwi 浏览器移动端的 UA 是这样的: 8 | 9 | ``` 10 | Mozilla/5.0 (Linux; Android 10; MI 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36 11 | ``` 12 | 13 | 切换为桌面版网站时是这样的: 14 | 15 | ``` 16 | Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 17 | ``` 18 | 19 | 两个主要区别: 20 | 1. 移动端的操作系统是 Android,桌面端是 Linux 21 | 2. 移动端有 Mobile 标志,桌面端没有 22 | 23 | 可以通过 `navigator.userAgent.includes('Mobile')` 判断是否是移动端,并且 Chrome 浏览器在切换为移动端调试时也有这个标记,例如: 24 | 25 | ``` 26 | Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Mobile Safari/537.36 27 | ``` 28 | 29 | ## 优化下载器一些面板的宽度 30 | 31 | 之前下载器面板宽度在竖屏时通常是超出屏幕宽度的,难以使用。 32 | 33 | 现在适配了宽度。包括主面板、消息提示面板、输出面板、轻提示。 34 | 35 | ## 识别主题颜色 36 | 37 | 移动端的主题颜色是 body 上的 class,如果含有 dark 就是夜间模式,如果没有 dark 就是白色模式。 38 | 39 | 现在进行了适配。 40 | 41 | ## 在作品页面内,点击收藏按钮、点赞按钮时下载作品 42 | 43 | 之前在移动端页面点击收藏按钮、点赞按钮时,下载器无法监听到对应事件,这是因为下载器压根没有获取到对应元素。 44 | 45 | 现在我在 `WorkToolBar` 类里添加 `getElementsOnMobile` 方法,处理移动端的工具栏按钮,使这两个功能正常了。 46 | 47 | ## 快速收藏按钮及联动效果 48 | 49 | 修复了在移动端快速收藏按钮功能不正常的问题,以及导致其他相关按钮样式或功能异常的问题。 50 | 51 | ## 点击作品缩略图里的收藏按钮时下载作品 52 | 53 | 移动端的作品缩略图选择器以及收藏按钮选择器都与桌面端不同。现在适配了,所以此功能已经修复。 54 | 55 | ## 在缩略图上显示下载按钮 56 | 57 | 之前移动端的“在缩略图右上角显示下载按钮”功能无效,因为按钮是当鼠标移动到缩略图上时才会显示。但是移动端没有鼠标移动事件,导致按钮始终不会显示。 58 | 59 | 现在为每个作品都添加了一个始终显示的缩略图,解决了此问题。 60 | 61 | ## 手动选择作品 62 | 63 | 之前手动选择作品在移动端存在异常,某些作品的缩略图元素可能无法选择,而且点击到作品缩略图时会进入作品,导致页面跳转,这使得此功能几乎无法使用。 64 | 65 | 现在修复了此问题,可以选择所有作品了。 66 | 67 | 至于页面跳转的问题,之前是对作品缩略图绑定 `click` 事件,配合 `ev.preventDefault()`,但是阻止默认事件无效。 68 | 69 | 现在改为使用 `touchend` 事件,可以阻止跳转了。 70 | 71 | 不过还存在一些小问题,比如选择了一些作品,然后跳转页面,新页面里含有已选择的作品。这些作品应该能够被识别并且显示已选择的对号图标,但是现在有时不会显示。不过这个问题不太大,我也懒得修。 72 | 73 | ## 获取 token 74 | 75 | 移动端获取 token 的字符串前缀不同,导致之前获取不到 token,现在修复。 76 | 77 | ## 提示 Kiwi 浏览器不能创建文件夹的 bug 78 | 79 | 当下载器识别到是移动端时,会在下载时显示这个提示。 80 | 81 | 目前我不能精准识别 Kiwi 浏览器,所以其他浏览器的移动端页面可能也会看到这个提示。 82 | 83 | ## 高亮已关注的用户 84 | 85 | 之前在移动端不能高亮已关注的用户,原因是此功能需要先获取登录用户的 id,移动端由于网页代码不同没获取到,现在修复。 86 | 87 | ## 显示更大的缩略图导致右侧一列被隐藏 88 | 89 | 在移动端的很多页面都是 2 个作品横排,有些页面是 3 个作品横排,已经够大了,所以不需要显示更大的缩略图了。 90 | 91 | 正好此功能会导致显示出现问题,所以在移动端禁用了此功能。 92 | 93 | ## 其他小问题 94 | 95 | 96 | -------------------------------------------------------------------------------- /notes/小说封面图片的一些研究.md: -------------------------------------------------------------------------------- 1 | # 小说封面图片的一些研究 2 | 3 | ## 尺寸 4 | 5 | Pixiv 会对上传的小说封面图片进行裁剪,使其最大宽高不超过 600*600 px。 6 | 7 | 封面图都是 jpg 格式,平均体积大约在 200KB 左右。 8 | 9 | 目前无法获取未被裁剪的原图。 10 | 11 | ## URL 12 | 13 | 所有小说封面的 URL 都是不同的。 14 | 15 | 首先,有很多小说用的是系统提供的封面图片,但是 pixiv 会在图片上叠加标题文字,合成一张新的图片,所以这些图片都是不同的图片。既然图片文件都不一样,URL 自然也不一样。 16 | 17 | 其次,如果我们上传自己制作的封面,那么 pixiv 不会叠加文字,但是如果我们把这个封面上传到多个小说里,它们的 URL 也是不同的。 18 | 19 | 下面是我做的测试,把一个自己做的封面上传到两个小说里,URL 的如下: 20 | 21 | ```js 22 | "https://i.pximg.net/c/600x600/novel-cover-master/img/2022/07/16/16/40/08/ci17967034_6f6b742681ac1ff7f0effbcb7f94a67a_master1200.jpg", 23 | "https://i.pximg.net/c/600x600/novel-cover-master/img/2022/07/16/16/39/28/ci17967032_adbd09bd505b471af19e2bda9b5b0c36_master1200.jpg", 24 | ``` 25 | 26 | 可以看到 URL 的格式是固定的,在 `novel-cover-master/img/` 后面有一些内容: 27 | 1. 图片被上传时的日期,精确到秒 `2022/07/16/16/40/08/` 28 | 2. 作品 id `ci17967034` 29 | 3. 还有一串随机字符。 30 | 31 | 可以看到封面 URL 里有一些唯一性的标记,所以不同小说的封面图片,其 URL 必定是不同的。 -------------------------------------------------------------------------------- /notes/小说搜索页面的“以系列为单位显示”.md: -------------------------------------------------------------------------------- 1 | # 小说搜索页面的“以系列为单位显示” 2 | 3 | 2022/08/28 4 | 5 | 小说搜索页面有一个选项:“以系列为单位显示”。 6 | 7 | 我不清楚它是什么时候出现的,也没有在意。但是最近 Pixiv 又在偷偷改版。说偷偷是因为它每次改版都是灰度测试,只有一小部分用户会先改为新版,这使得改版导致的问题不能及时被我知道。 8 | 9 | 今天有个用户说他抓取小说时,数量不对,我远程到他的电脑上才找到原因。 10 | 11 | ## 默认值的变化 12 | 13 | 现在,大部分用户的这个选项是默认关闭的。然而,Pixiv 正在对此进行修改,有一些用户的这个选项是默认开启的。 14 | 15 | 如果没有在 URL 中指定此设置的值,那么对于同样的网址,这两种用户看到的作品数量是不同的: 16 | 17 | https://www.pixiv.net/tags/%E3%81%8F%E3%81%99%E3%81%90%E3%82%8A/novels?work_lang=zh-cn 18 | 19 | 我看到的(“以系列为单位显示”是关闭的): 20 | 21 | ![](./images/20220828_214621.jpg) 22 | 23 | 出现问题的用户所看到的(“以系列为单位显示”是开启的): 24 | 25 | ![](./images/20220828_214739.jpg) 26 | 27 | 新版用户打开此网址时,“以系列为单位显示”直接就是开启的。 28 | 29 | ## 开启与关闭此设置的搜索结果 30 | 31 | ![](./images/20220828_223101.jpg) 32 | 33 | 以现在的搜索结果为例,未开启“以系列为单位显示”时,每个作品都是单独显示的。 34 | 35 | 其中前 3 个是系列小说里的,第 4 个是单篇小说。 36 | 37 | 开启“以系列为单位显示”之后,pixiv 返回的搜索结果不再以小说为单位,而是以系列为单位: 38 | 39 | ![](./images/20220828_223146.jpg) 40 | 41 | 每个搜索结果都会标明是“系列”还是“单篇”。 42 | 43 | 系列小说只会在搜索结果中显示 1 个结果,它显示的文字、超链接都是系列页面的,而不是里面的小说。 44 | 45 | 单篇小说显示的文字和超链接没有变。 46 | 47 | 所以开启“以系列为单位显示”之后,显示的结果数量比关闭时要少。因为此时显示的数量其实是系列数量+单篇数量,而非小说作品的数量。 48 | 49 | ## 对抓取的影响 50 | 51 | 抓取时也是如此,如果不指定“以系列为单位显示”的状态,服务器返回的数据也是不同的。 52 | 53 | - 对于旧版用户,服务器会返回“以系列为单位显示”关闭时的结果; 54 | - 对于新版用户,服务器会返回“以系列为单位显示”开启时的结果; 55 | 56 | 关闭时: 57 | 58 | ![](./images/20220828_224343.png) 59 | 60 | 开启时: 61 | 62 | ![](./images/20220828_224420.png) 63 | 64 | **注意:** 开启时,返回的结果的数据格式与关闭时是不同的。 65 | 66 | 对于系列作品,返回的数据是系列数据,而不是里面的小说的数据。 67 | 68 | 可以通过 `isOneShot` 属性判断这个结果是系列还是单篇,如果为 `true` 则是单篇,`false` 是系列。 69 | 70 | ## gs 71 | 72 | 在 URL 中,`gs` 表示“以系列为单位显示”是否启用。 73 | 74 | - `0` 表示关闭“以系列为单位显示”; 75 | - `1` 表示开启“以系列为单位显示”; 76 | 77 | ## 对此变化的处理 78 | 79 | 下载器在请求数据时,将会始终设置 `gs=0`,也就是始终关闭“以系列为单位显示”。因为这样获取的才是小说数据。 80 | 81 | `gs=1` 时返回的数据里有些是系列数据,而非小说数据,所以是不能使用的。 82 | -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_215904.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_215904.jpg -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_215911.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_215911.jpg -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_220435.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_220435.jpg -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_220525.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_220525.jpg -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_221541.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_221541.jpg -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_221612.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_221612.jpg -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_222417.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_222417.jpg -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_222808.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_222808.jpg -------------------------------------------------------------------------------- /notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_223521.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/notes/当小说保存为 EPUB 格式,且有很多内嵌图片,总体积很大时,存在严重性能问题的记录/20240718_223521.jpg -------------------------------------------------------------------------------- /notes/批量关注用户太频繁导致账户被封禁限制.md: -------------------------------------------------------------------------------- 1 | # 批量关注用户太频繁导致账户被封禁限制 2 | 3 | 在上版本里,虽然下载器批量关注用户时已经主动减慢了添加速度,大约 2.5 秒发送一个请求。但是添加关注的请求达到一定数量时, `/bookmark_add.php` API 返回 403 状态码,并且用户被 Pixiv 限制,无法恢复正常。并且封禁时也没有事先发送站内通知消息。 4 | 5 | ![](./images/20230723_191733.png) 6 | 7 | 至于达到封禁限制时,这个数量是多少,我尚不清楚。 8 | 9 | 现在我尝试进行如下处理: 10 | 1. 把添加关注的速度降低到 3 秒一个。 11 | 2. 在新增 1000 个关注之后自动停止执行。 12 | 13 | 执行完成后没有被限制,可以照常进行关注用户等操作。这个方案通过。 14 | 15 | ![](./images/20230727_120756.png) 16 | 17 | 18 | -------------- 19 | 20 | 不过并非所有用户都会遇到这个问题,一些注册时间较晚的用户,`/bookmark_add.php` API 无法直接使用,而是需要先获取 recaptcha_enterprise_score_token,下载器为了解决这个问题,花费了更多时间去处理,发送一个请求最少需要 6 秒,而且同样设置了 1000 个的限制,不会导致被封禁。 21 | -------------------------------------------------------------------------------- /notes/抓取结果体积太大的问题.md: -------------------------------------------------------------------------------- 1 | # 抓取结果体积太大的问题 2 | 3 | 有人分享了一个抓取结果:824 MB (865,027,197 字节) 4 | 5 | https://drive.google.com/file/d/1-cOcIVoU6Jvr8hzsuWCjT9XKQC6zTUxR/view?usp=sharing 6 | 7 | 导入的时候报错 JSON parse error! 8 | 9 | ## 原因:文件体积太大 10 | 11 | 我猜测是文件体积太大的原因。 12 | 13 | 我导出了一个只有 233 个抓取结果的 json 文件,导入并读取为 text 之后,获取 string 的 Byte length,和文件体积一模一样: 14 | 15 | 文件体积是 297 KB (304,602 字节),读取后 string 的内存字节数也是 304602。 16 | 17 | 那么这么 824 MB (865,027,197 字节) 读取后的长度是多少呢?输出了 0: 18 | 19 | ![](./images/20230708_203452.png) 20 | 21 | 可见此时并没有读取成功。 22 | 23 | 之后我还试了个 453485855 字节的文件,读取后也和原文件字节数相同。 24 | 25 | MDN 上说 V8 的 string 长度限制是 1GB,但是 824MB 没有超过 1GB,所以我推测长度限制是 512MB。 26 | 27 | ## 尝试分割文件 28 | 29 | 我用 VSCode 和 Sublime Text 打开这个文件都失败了。 30 | 31 | VSCode 报错 invalid string length,ST 读取有进度但是非常之慢,我没有耐心等到它结束。 32 | 33 | 用记事本打开卡死了。 34 | 35 | 下载了一个分割文件的软件,也提示文件体积太大,打不开。 36 | 37 | 我又尝试用 NodeJS 的命令行读取文件,也提示体积太大: 38 | 39 | ```js 40 | const itemsArray = require('./result.json'); 41 | Uncaught Error: Cannot create a string longer than 0x1fffffe8 characters 42 | ``` 43 | 44 | 提示体积不能超过 512MB。 45 | 46 | ## 使用代码分割 47 | 48 | 然后我又尝试了个流式读取的库: 49 | 50 | https://www.npmjs.com/package/stream-json 51 | 52 | 我本以为它会把抓取结果分成若干部分返回,但可能由于抓取结果是一个数组,所以它把所有数据读取为 1 个结果返回了。 53 | 54 | 所以它读取完也需要一些时间,好在不是很久,而且它确实能读取到所有数据,这样就可以对数据进行操作了。 55 | 56 | 抓取结果的数据格式如下: 57 | 58 | ```js 59 | [{}, {}, {}, {}, {}, ] 60 | ``` 61 | 62 | 读取后的 data 是个 Object,格式如下:(如果源数据格式不一样,可能结果也不一样,我没试) 63 | 64 | ```js 65 | { 66 | key: 0, 67 | value: [{}, {}, {}, {}, {}, ] 68 | } 69 | ``` 70 | 71 | `data.value` 就是抓取结果的原始数组。 72 | 73 | 最后成功了,我以 100000 条数据为单位,把数据保存成了多个小文件。 74 | 75 | 代码地址: 76 | 77 | https://github.com/xuejianxianzun/split-json 78 | 79 | 代码在执行过程中使用了 4 GB 内存。 80 | 81 | 需要注意的是,在保存数据时,单批数据的体积小于 512 MB 也有可能失败。在一开始,我以 200000 条数据为单位,第二批数据字符串化之后的体积是 296 MB (311,144,446 字节),NodeJS 报错了: 82 | 83 | `Reached heap limit Allocation failed - JavaScript heap out of memory`` 84 | 85 | JavaScript 堆内存不足,这可能是执行到这里时,NodeJS 所申请的内存不够用了,所以执行失败了。 86 | 87 | 之后我把批次设置为 100000,单个文件体积约 100 - 150 MB,没有报错了。 88 | 89 | -------------------------------------------------------------------------------- /notes/收藏作品时因为 429 导致失败的问题.md: -------------------------------------------------------------------------------- 1 | # 收藏作品时因为 429 导致失败的问题 2 | 3 | 1. 因为大量抓取导致触发 429 限制时,收藏功能也会受到影响,收藏的请求也会返回 429 状态码。 4 | 2. 收藏请求本身也被 pixiv 算在 429 限制里,即使不抓取,只是在短时间内进行大量收藏,也可能会触发 429 限制。 5 | 6 | 针对第二点,我通过实验进行过验证。 7 | 8 | 首先下载某个画师的 4 页作品,共 192 个。 9 | 10 | 之后开启“不下载重复文件”,开启“下载之后收藏作品”,再次点击开始下载按钮。 11 | 12 | 此时,文件会以极快的速度下载完成,所以下载进度非常快,随之发出了大量的收藏请求。 13 | 14 | 当完成 180/192 个收藏请求之后,剩余的收藏返回了 429 状态码。这表明单纯的大量收藏也可能会引起 429 错误。 15 | 16 | ------------- 17 | 18 | 现在当下载器发出的收藏请求遇到 429 错误时,会在几分钟后重试,重新发送这个收藏请求。 19 | 20 | 如果重试时没有再遇到 429 错误,是可以成功的。 -------------------------------------------------------------------------------- /notes/收藏页面的加载时间太久的问题.md: -------------------------------------------------------------------------------- 1 | # 收藏页面的加载时间太久的问题 2 | 3 | 在 2023 年 1 月初,有人问我为什么在 pixiv 的收藏页面,点击一个标签后需要等待很久才会显示对应的作品。 4 | 5 | 我在我的收藏页面试了下,确实需要很久(20 秒以上),我向 pixiv 发送了一个反馈: 6 | 7 | ------------------- 8 | 9 | 收藏页面的加载总是花费太长时间 10 | 11 | https://www.pixiv.net/users/9460149/bookmarks/artworks 12 | 13 | 在我的收藏页面中,点击任何一个标签,都需要至少 20 秒以上的时间才能加载出内容。 14 | 15 | 我录制了网络活动,发现两个主要问题: 16 | 17 | 1. 点击标签后,需要等待数秒钟,才会发起网络请求。为什么不是立刻? 18 | 2. 在前几个请求中,总会有一个请求卡住(pending),这个请求是不固定的,有时是 google-analytics.com 的脚本,有时是 google.com.hk 的脚本,最常见的情况是 pixon.ads-pixiv.net 的脚本…… 总之肯定会卡住几秒钟。为什么这些请求这么慢? 19 | 3. 我周围其他的 pixiv 用户有同样的问题。 20 | 21 | 我是中国区用户,使用代理访问 pixiv。网络没有异常,在收藏页面之外的其他地方一切正常。 22 | 23 | 这是录屏文件: 24 | https://saber.love/f/2023-01-04%2018-27-32.mkv 25 | 26 | ------------------- 27 | 28 | 几天后 pixiv事务局 回信: 29 | 30 | 此次非常感谢您与我们提供信息。 31 | 32 | 非常抱歉您所告知内容可能是由于注册的书签标签数量很多。 33 | 34 | 我们会在调查后依次进行应对。 35 | 36 | 还请理解,应对时需要花费一些时间。 37 | 38 | 另外也请谅解,当应对结束,我们无法单独告知。 39 | 40 | 编辑编辑收藏,减少编辑收藏数量可能会改善此情况,烦请您尝试。 41 | 42 | ▼想要编辑收藏 43 | 44 | https://www.pixiv.help/hc/articles/235585448 45 | 46 | ------------------- 47 | 48 | 看来主要问题在于我为这些收藏的作品添加的 **标签数量很多**。但是编辑页面并没有直接删除某个标签的功能,这使得就算用户想删除一些标签,也很麻烦。(目前只能点击一个标签,然后把它所属的作品全部取消收藏,才能达到删除标签的效果)。 49 | 50 | 我现在在我的收藏页面又试了几次,从点击标签到加载完毕只需要 13 秒,比之前快了几秒,不知道是 pixiv 已经做了优化,还是因为其他原因(比如时间段、服务器负载等)?以后还得观察。 51 | 52 | 如果过段时间情况没有改善,我可能需要添加一个“移除收藏作品的标签”的功能来解决此问题。 53 | -------------------------------------------------------------------------------- /notes/文件体积的调查.md: -------------------------------------------------------------------------------- 1 | 这是一份粗略的调查,仅供参考。 2 | 3 | ### 目的 4 | 5 | 了解 Pixiv **最近一段时间**的图片的体积情况。 6 | 7 | 现在时间:2020/09/18 8 | 9 | ### 调查 1 10 | 11 | #### 取样 12 | 13 | 下载 2020 年 9 月 16 日的排行榜(无 R-18),每个作品下载其第一张图片。 14 | 15 | #### 结果 16 | 17 | 共 500 张图片,总体积 1.0 GB。 18 | 19 | #### 细分结果 20 | 21 | - 0 MB - 1 MB(1024 KB)的有 244 个 22 | - 1 MB - 2 MB(2048 KB)的有 131 个 23 | - 2 MB - 3 MB(3072 KB)的有 42 个 24 | - 3 MB - 4 MB(4096 KB)的有 22 个 25 | - 4 MB - 5 MB(5120 KB)的有 16 个 26 | - 5 MB - 10 MB(10240 KB)的有 25 个 27 | - 10 MB 以上的有 20 个 28 | 29 | #### 概论 30 | 31 | 最近的图片的平均体积约为 2 MB。 32 | 33 | 按体积划分,1 MB以下的约为 49%,1 MB以上的约为 51%。 34 | 35 | -------------- 36 | 37 | ### 调查 2 38 | 39 | #### 取样 40 | 41 | 下载月排行榜(无 R-18),每个作品下载其第一张图片。 42 | 43 | 范围: 8/19/2020~9/17/2020 44 | 45 | #### 结果 46 | 47 | 共 500 张图片,总体积 865 MB。 48 | 49 | #### 细分结果 50 | 51 | - 0 MB - 1 MB(1024 KB)的有 268 个 52 | - 1 MB - 2 MB(2048 KB)的有 106 个 53 | - 2 MB - 3 MB(3072 KB)的有 45 个 54 | - 3 MB - 4 MB(4096 KB)的有 31 个 55 | - 4 MB - 5 MB(5120 KB)的有 15 个 56 | - 5 MB - 10 MB(10240 KB)的有 28 个 57 | - 10 MB 以上的有 7 个 58 | 59 | #### 概论 60 | 61 | 最近的图片的平均体积约为 1.73 MB。 62 | 63 | 按体积划分,1 MB以下的约为 54%,1 MB以上的约为 46%。 -------------------------------------------------------------------------------- /notes/文件名中不可以使用的字符.md: -------------------------------------------------------------------------------- 1 | - [文件名中不可使用的字符](#文件名中不可使用的字符) 2 | - [Windows 不允许使用的符号](#windows-不允许使用的符号) 3 | - [Chrome 不允许使用的符号](#chrome-不允许使用的符号) 4 | - [Window 保留名称](#window-保留名称) 5 | 6 | # 文件名中不可使用的字符 7 | 8 | 本文中的“文件名”包含: 9 | 10 | 1. 文件夹名称(即路径) 11 | 2. 文件名称(不包含 `.` 分隔符和后缀名) 12 | 13 | ## Windows 不允许使用的符号 14 | 15 | ``` 16 | \ 17 | / 18 | : 19 | ? 20 | " 21 | < 22 | > 23 | * 24 | | 25 | ``` 26 | 27 | 以上符号不能出现在文件名中。下载器会把它们替换为对应的全角符号。 28 | 29 | ## Chrome 不允许使用的符号 30 | 31 | ``` 32 | ~ 33 | . (注 1) 34 | ``` 35 | 36 | Windows 允许 `~` 出现在文件名中,但是 Chrome 不允许。 37 | 38 | window 允许 `.` 出现在文件名中,但是 Chrome 要求文件名**开头和结尾都不能使用 `.`**(文件名中间可以使用)。 39 | 40 | 下载器会把以上符号它们替换为对应的全角符号。 41 | 42 | ## Window 保留名称 43 | 44 | Windows 中有一些保留名称: 45 | 46 | - `CON` Keyboard and display 47 | - `PRN` System list device, usually a parallel port 48 | - `AUX` Auxiliary device, usually a serial port 49 | - `NUL` Bit-bucket device 50 | - `COM1` First serial communications port 51 | - `LPT1` First parallel printer port 52 | - `LPT2` Second parallel printer port 53 | - `LPT3` Third parallel printer port 54 | - `COM2` Second serial communications port 55 | - `COM3` Third serial communications port 56 | - `COM4` Fourth serial communications port 57 | 58 | 它们不区分大小写。 59 | 60 | 它们**不可以单独作为文件名**,必须搭配其他合法字符一起使用。 61 | 62 | 如果作为文件名,那么后缀名前面的部分需要单独处理。如 `con.txt` 是不合法的。 63 | 64 | 对于其处理方法,目前的决定是在其后添加特定的后缀字符。暂定为 `[downloader_add]`。 65 | 66 | Window 保留名称这不适合替换成全角符号,那样会导致用户无法用修改后的结果搜索到出处。 67 | 68 | 测试用例: 69 | 70 | 1. 用户名为保留名字的情况,如果只使用命名规则 `{user}` 作为文件名(或路径名),则需要加以处理。 71 | 72 | - [用户名为 con](https://www.pixiv.net/users/470827) 73 | - [用户名为 CON](https://www.pixiv.net/users/3860015) 74 | - [用户名为 aux](https://www.pixiv.net/users/25310111) 75 | 76 | 2. 页面 tag 为保留名字的情况,如果只使用命名规则 `{p_tag}` 作为文件名(或路径名),则需要加以处理。 77 | 78 | - [页面 tag 为 PRN](https://www.pixiv.net/tags/PRN/artworks?s_mode=s_tag) -------------------------------------------------------------------------------- /notes/日文 tag 和翻译的 tag 在搜索时的差别.md: -------------------------------------------------------------------------------- 1 | 用同一个日文 tag 的不同翻译去搜索,得到的结果并不相同。 2 | 3 | ### 例 1 4 | 5 | 以儿童卫士 夏色祭 为例: 6 | 7 | - 日文 tag `夏色まつり` 3,235 作品 8 | - 中文翻译 `夏色祭` 3,371 作品 9 | - 英文翻译 `Natsuiro Matsuri` 3,235 作品 10 | 11 | ### 例 2 12 | 13 | 碧蓝航线 14 | 15 | - 日文 `アズールレーン` 127,885 作品 16 | - 中文 `碧蓝航线` 138,323 作品 17 | - 英文 `AzurLane` 10,146 作品 18 | 19 | ### 结论 20 | 21 | 日文 tag 的搜索数量较多,中文翻译的 tag 最多,英文翻译的 tag 很少。 22 | 23 | 为什么中文的搜索结果最多?我的推测是:有些作品添加了中文的 tag,但没有添加对应的日文的 tag。所以搜日文 tag 时搜不到这些作品。 -------------------------------------------------------------------------------- /notes/显示更大的缩略图.md: -------------------------------------------------------------------------------- 1 | # 显示更大的缩略图 2 | 3 | pixiv 显示的缩略图在很多页面上都显得比较小。这里研究下怎么让缩略图变大。 4 | 5 | 首先使用用户主页的作品列表来做测试。 6 | 7 | https://www.pixiv.net/users/483730 8 | 9 | ## 步骤 10 | 11 | 首先要修改作品列表外部的容器的宽度设置为自适应。 12 | 13 | 它默认的宽度是 `width: 1224px;`。要设置它的宽度为 auto,并且 `min-width: 1224px;`。 14 | 15 | 然后再设置每个作品缩略图元素的宽度为 15%。 16 | 17 | 因为 pixiv 在多数页面上,一行显示 6 个作品缩略图,彼此之间还有间距,所以设置它们的宽度为 15% 并且 `min-width: 540px;`,让它们的宽度自适应。 18 | 19 | 最后设置缩略图里的一些元素,取消它们的宽高限制。(它们默认设置了宽高为 184px,现在设为 auto)。 20 | 21 | ## 如何生成样式表 22 | 23 | pixiv 页面使用了模板编辑器生成,元素的 className 是可能会变的,所以如何生成样式表就成了个问题。 24 | 25 | ### 直接使用 className 26 | 27 | ```css 28 | .fDaNEv{ 29 | width: 15%; 30 | } 31 | 32 | .jDiQFZ{ 33 | width: 100%; 34 | } 35 | ``` 36 | 37 | 直接使用页面上的 className,这样很简单。但是如果 className 变了,那么这个功能就失效了,必须修改代码,上传到应用商店更新,这样才能修复。 38 | 39 | ### 使用 js 获取每个元素 40 | 41 | 考虑使用 js 和 XPath 来获取某些特定元素,例如 42 | 43 | 获取列表外部容器: 44 | 45 | ```js 46 | let root = document.querySelector('#root') 47 | let r2 = document.evaluate("/html/body/div[2]/div[2]/div[2]/div/div[2]/div[3]/div/div", document, null, XPathResult.ANY_TYPE, null); 48 | r2.iterateNext() 49 | ``` 50 | 51 | 这样做非常麻烦,但是比较稳定。 52 | 53 | 它的复杂程度比上个方法麻烦很多: 54 | 55 | 1. 要使用 js 获取指定元素。但是由于 pixiv 页面里使用 className 作为选择器不可靠,所以不能用 className,这样选取特定元素很麻烦。 56 | 2. 不同页面类型里的 DOM 元素结构不一样,所以在不同页面里的获取方法可能也不同。这和 css 不一样,一套 css 在所有页面都能用。 57 | 3. 如果页面代码变化了,也有可能需要修改代码。 58 | 59 | 目前打算先用纯 css 的方法。 60 | 61 | ## 横图 62 | 63 | 一排显示 6 个缩略图的话,竖图显示效果很好,但是横图就不好了,因为宽度比较小,横图显示的也就比较小。和竖图相比较就很不平衡。 64 | 65 | 一个想法是把横图当做 2 个竖图处理,就是把它的宽度设置为竖图的 2 倍。这样效果确实好,但是实现起来比较麻烦。 66 | 67 | 目前实现了这个功能,但是只处理了常用的区域的横图。一些不常用区域的横图并没有处理,因为情况太多,处理起来很麻烦。 68 | 69 | -------------- 70 | 71 | li[size="1"] 72 | 73 | 它里面的第 3 - 5 层子元素才是 div 184 74 | 75 | - 画师主页的 76 | - 作品页面下面的相关作品 77 | - 收藏页面的作品 78 | - 主页里一排 6 个的 79 | - 发现页面的作品 80 | - 关注的用户的新作品页面 -------------------------------------------------------------------------------- /notes/杂记.md: -------------------------------------------------------------------------------- 1 | 卸载扩展并重新安装,不会丢失下载记录。只是会丢失下载器设置。 2 | 3 | 每隔一段时间,需要更新作品发布日期的数据。根据目前的数据统计,每 10 小时就有 10000 个新的图像作品,这会增加一条数据。每 43 天左右会增加 100 条数据。 4 | -------------------------------------------------------------------------------- /notes/测试用网址.md: -------------------------------------------------------------------------------- 1 | [动图转换](https://www.pixiv.net/member.php?id=16274829) 2 | 3 | [插画和小说混合](https://www.pixiv.net/users/7836791) 4 | 5 | [动图和插画混合](https://www.pixiv.net/users/848141) 6 | 7 | [正方形图片](https://www.pixiv.net/artworks/86426057) 8 | 9 | [体积大的图片](https://www.pixiv.net/users/5224036) 10 | -------------------------------------------------------------------------------- /notes/添加设置项-add setting item.md: -------------------------------------------------------------------------------- 1 | # 添加选项 2 | 3 | ## 添加 html 文本 4 | 5 | 如果它是一个表单项: 6 | 7 | 在 `src\ts\modules\setting\FormHTML.ts` 添加这个选项的 html 元素。需要对 checkbox、radio、input 等元素设置 name 属性。 8 | 9 | 在 `src\ts\modules\setting\Form.d.ts` 添加声明。需要从哪些元素上获取值,就添加对应的声明。 10 | 11 | 如果它不是一个表单项,请跳过上面两步。 12 | 13 | ## 使选项能够被获取 14 | 15 | 在 `src\ts\modules\setting\Settings.ts` 里的 `interface XzSetting` 里设置选项的类型声明。 16 | 17 | 在 `src\ts\modules\setting\Settings.ts` 里的 `settings` 里添加选项的默认值。 18 | 19 | ### 如果它是一个表单项 20 | 21 | 然后在 `src\ts\modules\setting\FormSettings.ts` 里: 22 | 23 | 在 `ListenChange` 方法里保存这个设置的值。(直接使用封装好的方法) 24 | 25 | 在 `restoreFormSettings` 方法里恢复这个设置的值。(直接使用封装好的方法) 26 | 27 | ### 如果它不是一个表单项 28 | 29 | 你需要在其他模块里自己控制它的读取、修改。 30 | 31 | ## 使用 32 | 33 | 在其它组件里 `import { settings } from '/src/ts/modules/setting/Settings'`,然后使用 `settings.name` 获取选项的值。 34 | 35 | 可以参考这个模块里的说明。 -------------------------------------------------------------------------------- /notes/添加默认背景图片的调查.md: -------------------------------------------------------------------------------- 1 | # 添加默认背景图片的调查 2 | 3 | 当用户开启“背景图片”时,由于下载器没有自带背景图片,所以此时用户看不到背景图片。必须由用户手动选择一张图片来设置为背景图片。 4 | 5 | 我想给下载器附带一个默认的背景图片。这就是 `notes/images/bg.webp`,是一个免版权的月亮素材,体积只有 6KB,可以放到下载器的资源里。 6 | 7 | ![](./images/bg.webp) 8 | 9 | 图片来源:https://pixabay.com/zh/photos/moon-full-moon-sky-night-sky-lunar-1859616/ 10 | 11 | 但是是否有这个必要呢?我在 2 群和 5 群调查了下大家有没有开启“背景图片”,结果如下: 12 | 13 | 共 43 人回复,其中只有 2 人开启了“背景图片”。还有一部分人根本不知道有这个功能。 14 | 15 | 大约只有 5% 的人开启了背景图片,所以添加默认背景图片的意义不大。现在我取消了这个计划。 16 | 17 | -------------------------------------------------------------------------------- /notes/用户分布.md: -------------------------------------------------------------------------------- 1 | # 用户分布 2 | 3 | 在 Chrome 商店后台的统计数据中,用户的语言占比前几名是: 4 | - 简体中文 5 | - 英语 6 | - 日语 7 | - 繁体中文 8 | 9 | 用户区域(我猜可能是 IP)占比前几名是: 10 | - 日本 11 | - 美国 12 | - 中国 13 | - 台湾 14 | - 香港 15 | - 印尼 16 | 17 | 我猜测用户语言能够反映出用户的真实区域。 18 | 19 | 而“用户区域”可能是有些国人挂着日本、美国的代理,他们的区域也就变成代理的区域,所以不准确。 20 | 21 | # 转换数据 22 | 23 | 最近一个月的数据:2022年6月21日 - 2022年7月20日 24 | 25 | 每日展示次数 2300,日安装量 140,日卸载量 34。 26 | 27 | 每周用户数 时间跨度:在过去 7 天内加载过该内容的 Chrome 浏览器大概数量。包括该内容处于已启用、已停用和未知状态的情况。 -------------------------------------------------------------------------------- /notes/第三方库.md: -------------------------------------------------------------------------------- 1 | # 第三方库 2 | 3 | 按照功能分类: 4 | 5 | # 生成 GIF 图像 6 | 7 | - gif.js 8 | - gif.worker.js 9 | 10 | https://github.com/jnordberg/gif.js 11 | 12 | # 生成 APNG 图像 13 | 14 | UPNG.js 15 | 16 | https://github.com/photopea/UPNG.js 17 | 18 | ~~它依赖 UZIP.js 来压缩图像数据。(https://github.com/photopea/UZIP.js)~~ 19 | 20 | 它依赖 pako.min.js 来压缩图像数据。(https://github.com/nodeca/pako) 21 | 22 | # 生成 WEBM 视频 23 | 24 | whammy.js 25 | 26 | https://github.com/antimatter15/whammy 27 | 28 | 我对这个文件进行了修改,以修复 Chrome 108 开始的转换失败问题。 29 | 30 | # 生成 EPUB 文件 31 | 32 | js-epub-maker.js 33 | 34 | https://github.com/bbottema/js-epub-maker 35 | 36 | 我对这个文件进行了修改,以解除它对 handlebars.min.js 的依赖(后者会导致 unsafe-eval 问题)。 37 | 38 | 它又依赖下面两个库: 39 | 40 | - lib/jszip-utils.min.js 41 | - lib/jszip.min.js 42 | 43 | https://github.com/Stuk/jszip/tree/main/dist 44 | 45 | # 图片查看器 46 | 47 | viewer.min.js 48 | 49 | -------------------------------------------------------------------------------- /notes/记录 Pixiv 的变化.md: -------------------------------------------------------------------------------- 1 | # 记录 Pixiv 的变化 2 | 3 | 4 | ## 作品页面底部的相关作品的变化 5 | 6 | 2022/04/12 7 | 8 | 之前作品页面的底部会显示“相关作品”。但是现在发生了一些变化,有时候作品的底部显示的不是“相关作品”,而是显示“推荐作品”,数量固定为 18 个。 9 | 10 | 这可能与这个作品是否被收藏有关,但是目前并不确定。 11 | -------------------------------------------------------------------------------- /notes/进度条未找到的问题.md: -------------------------------------------------------------------------------- 1 | 有用户报告了下面这个报错截图,看来是操作下载进度条时,某个进度条未找到。我觉得有点奇怪,暂且判断一下,未找到的情况下就不执行后续代码。具体原因尚不清楚。 2 | 3 | ![](./images/20210217144131.png) -------------------------------------------------------------------------------- /notes/选择下载的图片尺寸-调查.md: -------------------------------------------------------------------------------- 1 | # 插画、漫画 2 | 3 | ## 尺寸 4 | 5 | 插画、漫画的图片有五种尺寸: 6 | 7 | ``` 8 | urls: { 9 | mini: string 10 | thumb: string 11 | small: string 12 | regular: string 13 | original: string 14 | } 15 | ``` 16 | 17 | 18 | - mini 最小尺寸,有些多图作品只有第一张图有此尺寸。总是 48*48 的正方形,所以可能会裁剪掉图片的一些区域。 19 | - thumb 缩略图,有些多图作品只有第一张图有此尺寸。总是 250*250 的正方形,所以可能会裁剪掉图片的一些区域。 20 | - small 较小尺寸,所有图片都有此尺寸。尺寸上限为 540*540。如果原图不是正方形,会保留图片比例缩小到此范围。 21 | - regular 普通尺寸,所有图片都有此尺寸。尺寸上限为 1200*1200。如果原图不是正方形,会保留图片比例缩小到此范围。 22 | - original 原图,所有图片都有此尺寸 23 | 24 | 所以,为了确保能对所有图片生效,只能选择以下三个尺寸: 25 | 26 | - small 27 | - regular 28 | - original 29 | 30 | 为什么没有 mini 和 thumb 选项: 31 | 32 | 1. 多图作品可能只有第一张图有这两个尺寸,后续图片没有这两个尺寸。所以保持一致,不使用这两个尺寸。 33 | 2. 这两个尺寸总是固定大小的正方形,很可能会裁剪图片,导致看不到图片的全貌。 34 | 35 | ## 例子 36 | 37 | https://www.pixiv.net/artworks/84773373 38 | 39 | ``` 40 | mini: "https://i.pximg.net/c/48x48/custom-thumb/img/2020/10/03/18/39/30/84773373_p0_custom1200.jpg" 41 | original: "https://i.pximg.net/img-original/img/2020/10/03/18/39/30/84773373_p0.png" 42 | regular: "https://i.pximg.net/img-master/img/2020/10/03/18/39/30/84773373_p0_master1200.jpg" 43 | small: "https://i.pximg.net/c/540x540_70/img-master/img/2020/10/03/18/39/30/84773373_p0_master1200.jpg" 44 | thumb: "https://i.pximg.net/c/250x250_80_a2/custom-thumb/img/2020/10/03/18/39/30/84773373_p0_custom1200.jpg" 45 | ``` 46 | 47 | # 动图 48 | 49 | ## 尺寸 50 | 51 | 动图也有上面五种尺寸,但是都是静态的。为了保留动态效果,不能采用这五种尺寸。 52 | 53 | 在 ugoira_meta 里,有两种尺寸: 54 | 55 | ``` 56 | originalSrc: "https://i.pximg.net/img-zip-ugoira/img/2020/10/12/10/19/38/84965766_ugoira1920x1080.zip" 57 | src: "https://i.pximg.net/img-zip-ugoira/img/2020/10/12/10/19/38/84965766_ugoira600x600.zip" 58 | ``` 59 | 60 | - originalSrc 动图源文件压缩包 61 | - src 图片尺寸缩小到不超过 600*600 的压缩包。 62 | 63 | 打开动图页面时,会显示 src 尺寸的,点击它才会显示原图尺寸。所以 src 尺寸在使用意义上对应插画、漫画的 regular 尺寸。 64 | 65 | ## 对应关系 66 | 67 | 动图压缩包的尺寸需要映射到插画、漫画的尺寸。 68 | 69 | - small 动图使用 src 尺寸 70 | - regular 动图使用 src 尺寸 71 | - original 动图使用 originalSrc 尺寸 72 | -------------------------------------------------------------------------------- /notes/页面处于后台时,定时器的延迟问题.md: -------------------------------------------------------------------------------- 1 | # 页面处于后台时,定时器的延迟问题 2 | 3 | “处于后台”指这个标签页被完全遮挡的状态,包括切换到其他标签页和切换到其他全屏程序。 4 | 5 | *页面处于前台,且关闭显示器时,依然是前台状态,不会变成后台。* 6 | 7 | 根据 MDN 的文档:为了优化后台 tab 的加载损耗(以及降低耗电量),在未被激活的 tab 中定时器的最小延时限制为 1 S(1000 ms)。 8 | 9 | 但实际上,定时器可能被延迟更久,我做了一些测试: 10 | 11 | ## 测试 12 | 13 | 设置 300 ms 后执行下一次抓取,当页面处于前台时,时间符合预期。但是当页面处于后台时,时间变成了 800 ms。 14 | 15 | ```js 16 | window.setTimeout(()=>{ 17 | console.timeEnd('setTimeout') 18 | this.getWorksData() 19 | }, 300) 20 | console.time('setTimeout') 21 | ``` 22 | 23 | ![](./images/20220816_181944.png) 24 | 25 | ----------------- 26 | 27 | 将延迟时间设置为 800 ms 时测试,当页面处于后台时,时间变成了 1700 - 1800 ms。 28 | 29 | ![](./images/20220816_173616.png) 30 | 31 | 2.25 倍 32 | 33 | ----------------- 34 | 35 | 将延迟时间设置为 1200 ms 时测试,当页面处于后台时,时间变成了 1600 - 1800 ms。 36 | 37 | ![](./images/20220816_174812.png) 38 | 39 | 1.5 倍 40 | 41 | ## 数据汇总 42 | 43 | 我又进行了更多的测试,数据如下: 44 | 45 | | 设置的延迟 | 页面处于后台时的延迟 | 46 | | ---------- | -------------------- | 47 | | 0 | 10 | 48 | | 50 | 800 | 49 | | 100 | 800 | 50 | | 300 | 800 | 51 | | 700 | 800 | 52 | | 800 | 1800 | 53 | | 1200 | 1800 | 54 | | 1800 | 2800 | 55 | | 2200 | 2800 | 56 | | 2800 | 3800 | 57 | | 3000 | 3800 | 58 | | 3900 | 4800 | 59 | | 5000 | 5800 | 60 | | 60000 | 60800 | 61 | 62 | 如果把延迟时间设置的非常小,比如 0,即使页面处于后台,延迟时间也依然很小。除此之外,都会大幅增加延迟时间。 63 | 64 | 很明显,页面处于后台时,定时器的执行时间总是和 **800 ms** 有关。 65 | 66 | 如果设置的延迟时间的毫秒部分小于 `800`,就会把毫秒部分设置为 `800`。 67 | 68 | 如果设置的延迟时间的毫秒部分大于等于 `800`,就会把当前延迟时间设置为 `下一秒 + 800 ms`。 69 | 70 | 所以,延迟时间的毫秒数部分不应该大于等于 800,因为这会导致后台时的实际延迟时间增加约 1 秒,变慢许多。 71 | -------------------------------------------------------------------------------- /notes/页面处于后台时,转换动图的时间.md: -------------------------------------------------------------------------------- 1 | # 页面处于后台时,转换动图的时间 2 | 3 | 2022/08/31 4 | 5 | 之前在转换动图时,如果页面被隐藏了,下载器会显示一条提示: 6 | 7 | ``` 8 | 这个标签页正在转换动图。如果这个标签页被隐藏了,转换速度可能会变慢。 9 | ``` 10 | 11 | 但是现在标签页处于后台时,动图转换速度似乎没有明显的降低,所以我移除了这个提示。 12 | 13 | ## 分析 14 | 15 | 之前在转换动图时,如果页面被隐藏了,转换速度会明显变慢(主要是 WebM 格式)。 16 | 17 | 我的猜测是因为定时器的存在。对于 WebM 视频来说,之前在转换过程中有第三方库的定时器 setTimeout,所以当页面处于后台时,转换速度会降低。 18 | 19 | 但是后来我直接向编码库传递转换好的 DataURL,它就不会执行定时器了。所以即使页面处于后台,转换速度也不会有明显的增加。 20 | 21 | 其实还是会增加一点,但是增加的幅度不多,不需要专门用消息框去提醒用户。 22 | 23 | 下面进行测试,转换单个动图,输出其转换花费的时间。 24 | 25 | 测试的动图作品: 26 | 27 | https://www.pixiv.net/artworks/100875971 28 | 29 | 21.3 MB,123 张图片,每张分辨率 640*360,177 KB。 30 | 31 | 一种测试方式是始终处于该标签页,另一种是按下快速下载按钮后,就立即切换到其他页面。 32 | 33 | 每种方式都至少执行 3 次,取平均值。(测试平台是我的电脑,其他人可能根据性能不同有不同的结果) 34 | 35 | ## WebM 36 | 37 | ```js 38 | console.time('webm') 39 | file = await convertUgoira.webm(file, arg.result.ugoiraInfo) 40 | console.timeEnd('webm') 41 | ``` 42 | 43 | - 前台时:6873 ms 44 | - 后台时:7814 ms 45 | 46 | 可以看到时间增加了约 1 秒,可以接受。 47 | 48 | ## GIF 49 | 50 | ```js 51 | console.time('gif') 52 | file = await convertUgoira.gif(file, arg.result.ugoiraInfo) 53 | console.timeEnd('gif') 54 | ``` 55 | 56 | - 前台时:8202 ms 57 | - 后台时:8451 ms 58 | 59 | 时间几乎没有增加,偶尔还比在前台时更快。 60 | 61 | ## APNG 62 | 63 | ```js 64 | console.time('apng') 65 | file = await convertUgoira.apng(file, arg.result.ugoiraInfo) 66 | console.timeEnd('apng') 67 | ``` 68 | 69 | - 前台时:15467 ms 70 | - 后台时:16034 ms 71 | 72 | 时间增加了约 0.6 秒,可以接受。 73 | -------------------------------------------------------------------------------- /pack.js: -------------------------------------------------------------------------------- 1 | // build 命令 2 | const fs = require('fs') 3 | const path = require('path') 4 | const copy = require('recursive-copy') 5 | const archiver = require('archiver') 6 | 7 | const packName = 'powerfulpixivdownloader-online' 8 | const distPath = './dist' 9 | 10 | // 复制一些文件到发布目录 11 | async function copys() { 12 | return new Promise(async (resolve, reject) => { 13 | // 复制 static 文件夹的内容 14 | await copy('./src/static', distPath, { 15 | overwrite: true, 16 | }).catch(function (error) { 17 | console.error('Copy failed: ' + error) 18 | reject() 19 | }) 20 | 21 | // 复制 src 目录里需要的文件 22 | await copy('./src', distPath, { 23 | overwrite: true, 24 | filter: ['manifest.json', 'declarative_net_request_rules.json'], 25 | }) 26 | 27 | // 复制根目录一些文件 28 | await copy('./', distPath, { 29 | overwrite: true, 30 | filter: ['README*.md', 'LICENSE'], 31 | }).then(function (results) { 32 | resolve() 33 | console.log('Copy success') 34 | }) 35 | }) 36 | } 37 | 38 | // 打包发布目录 39 | function pack() { 40 | const zipName = path.resolve(__dirname, packName + '.zip') 41 | const output = fs.createWriteStream(zipName) 42 | 43 | const archive = archiver('zip', { 44 | zlib: { level: 9 }, // Sets the compression level. 45 | }) 46 | 47 | archive.on('error', function (err) { 48 | throw err 49 | }) 50 | 51 | archive.on('finish', () => { 52 | console.log(`Pack success`) 53 | }) 54 | 55 | // pipe archive data to the file 56 | archive.pipe(output) 57 | 58 | // 添加文件夹 59 | archive.directory(distPath, packName) 60 | 61 | archive.finalize() 62 | } 63 | 64 | // 构建 65 | async function build() { 66 | await copys() 67 | pack() 68 | } 69 | 70 | build() 71 | 72 | console.log('Start pack') 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "powerful-pixiv-downloader", 3 | "version": "17.6.0", 4 | "description": "强大的 Pixiv 批量下载器。Powerful Pixiv batch downloader. 強力な Pixiv ダウンローダー。", 5 | "repository": "https://github.com/xuejianxianzun/PixivBatchDownloader", 6 | "author": "xuejianxianzun", 7 | "license": "GPL-3.0-or-later", 8 | "scripts": { 9 | "fmt": "prettier -c --write \"src/ts/**\" && prettier -c --write \"src/style/**\"", 10 | "ts": "webpack --config ./webpack.conf.js", 11 | "less": "lessc ./src/style/style.less ./dist/style/style.css && lessc ./src/style/showLargerThumbnails.less ./dist/style/showLargerThumbnails.css", 12 | "pre-build": "npm run fmt && npm run ts && npm run less", 13 | "build": "npm run pre-build && node ./pack.js" 14 | }, 15 | "devDependencies": { 16 | "@types/chrome": "0.0.154", 17 | "@types/node": "^13.13.5", 18 | "@types/har-format": "1.2.8", 19 | "@typescript-eslint/eslint-plugin": "^2.7.0", 20 | "@typescript-eslint/parser": "^2.7.0", 21 | "archiver": "^3.1.1", 22 | "eslint": "^6.6.0", 23 | "eslint-config-prettier": "^6.5.0", 24 | "eslint-plugin-prettier": "^3.1.1", 25 | "less": "^4.1.3", 26 | "prettier": "^3.0.0", 27 | "recursive-copy": "^2.0.10", 28 | "ts-loader": "^6.2.1", 29 | "ts-node": "^8.5.2", 30 | "typescript": "^3.9.10", 31 | "viewerjs": "^1.4.0", 32 | "webpack": "^5.88.1", 33 | "webpack-cli": "^5.1.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/declarative_net_request_rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "priority": 1, 5 | "action": { 6 | "type": "modifyHeaders", 7 | "responseHeaders": [ 8 | { 9 | "header": "Access-Control-Allow-Origin", 10 | "operation": "set", 11 | "value": "*" 12 | } 13 | ] 14 | }, 15 | "condition": { 16 | "urlFilter": "||pximg.net", 17 | "resourceTypes": ["xmlhttprequest", "image"] 18 | } 19 | }, 20 | { 21 | "id": 2, 22 | "priority": 1, 23 | "action": { 24 | "type": "modifyHeaders", 25 | "responseHeaders": [ 26 | { 27 | "header": "Access-Control-Allow-Origin", 28 | "operation": "set", 29 | "value": "*" 30 | } 31 | ] 32 | }, 33 | "condition": { 34 | "urlFilter": "||pximg.cat", 35 | "resourceTypes": ["xmlhttprequest", "image"] 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /src/static/icon/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/src/static/icon/logo128.png -------------------------------------------------------------------------------- /src/static/icon/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/src/static/icon/logo16.png -------------------------------------------------------------------------------- /src/static/icon/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/src/static/icon/logo32.png -------------------------------------------------------------------------------- /src/static/icon/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuejianxianzun/PixivBatchDownloader/8e7cb6913153cddab08e21b77257ca5f0319d0f5/src/static/icon/logo48.png -------------------------------------------------------------------------------- /src/static/lib/listen_history_change.js: -------------------------------------------------------------------------------- 1 | let _wr = function (type) { 2 | let orig = history[type] 3 | return function () { 4 | let rv = orig.apply(this, arguments) 5 | let e = new Event(type) 6 | e.arguments = arguments 7 | window.dispatchEvent(e) 8 | return rv 9 | } 10 | } 11 | history.pushState = _wr('pushState') 12 | history.replaceState = _wr('replaceState') 13 | -------------------------------------------------------------------------------- /src/style/Loading.less: -------------------------------------------------------------------------------- 1 | @import './var.less'; 2 | 3 | #xzLoadingWrap { 4 | position: fixed; 5 | z-index: 2147483647; 6 | left: 0; 7 | right: 0; 8 | top: 0; 9 | bottom: 0; 10 | margin: auto; 11 | width: 96px; 12 | height: 96px; 13 | border-radius: 16px; 14 | font-size: 48px; 15 | display: none; 16 | // display: flex; 17 | background-color: #fff; 18 | color: @beautifyBlue; 19 | justify-content: center; 20 | align-items: center; 21 | opacity: 0.7; 22 | pointer-events: none; 23 | 24 | .iconWrap { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | animation: rotate360 1.2s infinite; 29 | } 30 | } 31 | 32 | .theme-dark { 33 | &#xzLoadingWrap { 34 | background: #333; 35 | color: #fff; 36 | } 37 | } 38 | 39 | @keyframes rotate360 { 40 | from { 41 | transform: rotate(0deg); 42 | } 43 | to { 44 | transform: rotate(360deg); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/style/blockAds.less: -------------------------------------------------------------------------------- 1 | #header-banner.ad, 2 | ._2vNejsc, 3 | ._3M6FtEB, 4 | ._3jgsYyw, 5 | ._premium-lead-promotion-banner, 6 | ._1N-LC6t, 7 | ._premium-lead-tag-search-bar, 8 | .ad-bigbanner, 9 | .ad-footer, 10 | .ad-multiple_illust_viewer, 11 | .ads_anchor, 12 | .ads_area, 13 | .adsbygoogle, 14 | .popular-introduction-overlay, 15 | .ui-fixed-container aside, 16 | iframe[name='header'], 17 | iframe[name='footer'], 18 | iframe[name='rectangle'], 19 | ._2vGmESI, 20 | section.ad, 21 | aside .sc-LzLtN, 22 | iframe[name='500x500'], 23 | iframe[name='expandedFooter'] { 24 | display: none !important; 25 | z-index: -999 !important; 26 | width: 0 !important; 27 | height: 0 !important; 28 | opacity: 0 !important; 29 | } 30 | -------------------------------------------------------------------------------- /src/style/blockTagsForSpecificUser.less: -------------------------------------------------------------------------------- 1 | .blockTagsForSpecificUserWrap { 2 | input::-webkit-input-placeholder, 3 | textarea::-webkit-input-placeholder { 4 | color: #999; 5 | } 6 | 7 | .icon { 8 | font-size: 1.2em; 9 | } 10 | 11 | .textButton { 12 | border: none; 13 | background: none; 14 | outline: none; 15 | padding: 1px 3px; 16 | cursor: pointer; 17 | color: #666; 18 | &.add { 19 | padding: 1px 8px; 20 | } 21 | } 22 | 23 | .controlBar { 24 | padding: 5px 0; 25 | color: #666; 26 | } 27 | 28 | .addWrap { 29 | display: none; 30 | 31 | button.cancel .icon { 32 | font-size: 1.3em; 33 | } 34 | } 35 | 36 | .settingItem { 37 | display: flex; 38 | align-items: center; 39 | padding: 5px 0; 40 | .inputItem { 41 | margin-right: 10px; 42 | .label { 43 | color: #444; 44 | } 45 | &.uid { 46 | input { 47 | width: auto; 48 | max-width: 100px; 49 | } 50 | } 51 | &.tags { 52 | flex: 1; 53 | display: flex; 54 | max-width: 320px; 55 | input { 56 | flex: 1; 57 | } 58 | } 59 | span { 60 | padding-right: 10px; 61 | } 62 | } 63 | 64 | .btns { 65 | & button:first-child { 66 | display: inline-block; 67 | margin-right: 6px; 68 | } 69 | 70 | button[data-updaterule] .icon { 71 | font-size: 1.4em; 72 | } 73 | } 74 | } 75 | } 76 | 77 | .theme-dark { 78 | &.blockTagsForSpecificUserWrap { 79 | div, 80 | span { 81 | color: #fff !important; 82 | } 83 | 84 | input::-webkit-input-placeholder, 85 | textarea::-webkit-input-placeholder { 86 | color: #ddd; 87 | } 88 | } 89 | } 90 | 91 | .xzBG { 92 | .blockTagsForSpecificUserWrap { 93 | div, 94 | span { 95 | color: #fff !important; 96 | } 97 | 98 | input::-webkit-input-placeholder, 99 | textarea::-webkit-input-placeholder { 100 | color: #ddd; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/style/deleteWorks.less: -------------------------------------------------------------------------------- 1 | #deleteWorkEl { 2 | box-sizing: border-box; 3 | position: fixed; 4 | z-index: 10000; 5 | left: 0; 6 | top: 0; 7 | width: 24px; 8 | height: 24px; 9 | border-radius: 50%; 10 | background: red; 11 | pointer-events: none; 12 | display: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/style/input.less: -------------------------------------------------------------------------------- 1 | .XZInputWrap { 2 | position: fixed; 3 | z-index: 99999; 4 | left: 0; 5 | right: 0; 6 | top: 20vh; 7 | bottom: 0; 8 | margin: 0 auto auto; 9 | display: flex; 10 | flex-direction: column; 11 | background-color: #fff; 12 | box-shadow: 0 0 15px @boxShadowBlue; 13 | padding: 20px 20px; 14 | max-width: 90vw; 15 | height: max-content; 16 | font-size: 16px; 17 | border-radius: 8px; 18 | 19 | .XZInputInstruction { 20 | display: flex; 21 | color: #333; 22 | margin: 0px 0 10px 0; 23 | line-height: 24px; 24 | } 25 | 26 | .XZInputContainer { 27 | display: flex; 28 | flex-wrap: nowrap; 29 | align-items: center; 30 | 31 | .XZInput { 32 | color: #333; 33 | border: 1px solid #0096fa; 34 | box-sizing: border-box; 35 | font-size: 16px; 36 | line-height: 24px; 37 | flex-basis: 600px; 38 | flex-grow: 0; 39 | padding: 2px 8px; 40 | border-radius: 6px; 41 | font-family: @MobileFontFamily; 42 | } 43 | 44 | input.XZInput { 45 | height: 36px; 46 | } 47 | 48 | textarea.XZInput { 49 | height: auto; 50 | } 51 | 52 | .XZInputButton { 53 | margin-left: 10px; 54 | flex-shrink: 0; 55 | background-color: #0096fa; 56 | color: #fff; 57 | height: 36px; 58 | line-height: 36px; 59 | padding: 0 25px; 60 | border: none; 61 | border-radius: 18px; 62 | cursor: pointer; 63 | font-size: 14px; 64 | font-weight: bold; 65 | 66 | &.cancel { 67 | background-color: #e49d00; 68 | } 69 | } 70 | } 71 | } 72 | 73 | .theme-dark.XZInputWrap { 74 | background-color: #333; 75 | box-shadow: 2px 3px 8px #000; 76 | 77 | .XZInputInstruction { 78 | color: rgba(255, 255, 255, 0.84); 79 | } 80 | 81 | .XZInput { 82 | background-color: #4c4c4c; 83 | color: rgba(255, 255, 255, 0.84); 84 | } 85 | } 86 | 87 | .mobile.XZInputWrap { 88 | padding: 14px 14px; 89 | font-size: 14px; 90 | 91 | .XZInputInstruction { 92 | margin: 0px 0 8px 0; 93 | line-height: 22px; 94 | } 95 | 96 | .XZInput { 97 | font-size: 14px; 98 | padding: 2px 5px; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/style/log.less: -------------------------------------------------------------------------------- 1 | @import './var.less'; 2 | 3 | .logWrap { 4 | font-family: @pixivFontFamily; 5 | position: relative; 6 | z-index: 1; 7 | width: 100%; 8 | color: #333; 9 | background: #fff; 10 | } 11 | 12 | .logContent { 13 | width: 100%; 14 | max-width: 950px; 15 | max-height: 200px; 16 | overflow-y: scroll; 17 | margin: 0 auto; 18 | padding: 10px 0; 19 | font-size: 14px; 20 | 21 | br { 22 | display: inline; 23 | } 24 | 25 | a { 26 | color: @themeBlue; 27 | } 28 | } 29 | 30 | .theme-dark { 31 | &.logWrap { 32 | background: #222; 33 | color: #fff; 34 | } 35 | } 36 | 37 | .xzBG { 38 | &.logWrap { 39 | background: #222; 40 | color: #fff; 41 | text-shadow: #003247 0px 0px 2px; 42 | 43 | .logContent * { 44 | color: #fff !important; 45 | } 46 | 47 | .xzBGLayer { 48 | filter: blur(2px); 49 | } 50 | } 51 | } 52 | 53 | .logWrap.mobile { 54 | font-size: 16px; 55 | font-family: @MobileFontFamily; 56 | padding: 10px 10px; 57 | } 58 | -------------------------------------------------------------------------------- /src/style/msgBox.less: -------------------------------------------------------------------------------- 1 | .xz_msg_box { 2 | background: #fff; 3 | box-shadow: 0 0 15px @boxShadowBlue; 4 | position: fixed; 5 | z-index: 9999; 6 | min-width: 500px; 7 | max-width: 700px; 8 | padding: 30px; 9 | border-radius: 12px; 10 | top: 15%; 11 | left: 50%; 12 | transform: translate(-50%); 13 | text-align: center; 14 | 15 | p { 16 | margin: 0; 17 | } 18 | 19 | .title { 20 | font-size: 17px; 21 | font-weight: bold; 22 | color: @themeBlue; 23 | } 24 | 25 | .blue { 26 | color: @themeBlue; 27 | } 28 | 29 | .content { 30 | padding: 0; 31 | margin: 20px 0; 32 | max-height: 60vh; 33 | overflow: auto; 34 | font-size: 14px; 35 | line-height: 1.7; 36 | color: #555; 37 | text-align: left; 38 | } 39 | 40 | .btn { 41 | cursor: pointer; 42 | background: @themeBlue; 43 | box-shadow: @boxShadowBlue 0px 0px 2px; 44 | color: #fff; 45 | border: none; 46 | text-align: center; 47 | padding: 7px 20px; 48 | border-radius: 6px; 49 | &:hover { 50 | font-weight: bold; 51 | } 52 | } 53 | 54 | a { 55 | color: @themeBlue; 56 | } 57 | } 58 | 59 | .theme-dark { 60 | &.xz_msg_box { 61 | background: #222; 62 | box-shadow: @darkBoxShadow 0px 0px 15px; 63 | 64 | .title { 65 | color: #fff; 66 | } 67 | 68 | .content { 69 | color: #eee; 70 | text-shadow: 1px 1px black; 71 | } 72 | } 73 | } 74 | 75 | .xzBG { 76 | &.xz_msg_box { 77 | background: #222; 78 | box-shadow: @darkBoxShadow 0px 0px 15px; 79 | color: #fff; 80 | transition: none; 81 | 82 | * { 83 | color: #fff; 84 | } 85 | 86 | p { 87 | color: #fff !important; 88 | } 89 | 90 | a { 91 | color: #c7e9ff !important; 92 | } 93 | 94 | .content { 95 | text-shadow: #003247 0px 0px 2px; 96 | } 97 | 98 | .xzBGLayer { 99 | filter: blur(1px); 100 | border-radius: 12px; 101 | } 102 | } 103 | } 104 | 105 | .xz_msg_box.mobile { 106 | min-width: inherit; 107 | width: 86vw; 108 | max-width: inherit; 109 | padding: 5vw 3vw; 110 | top: 6vh; 111 | font-size: 16px; 112 | font-family: @MobileFontFamily; 113 | } 114 | -------------------------------------------------------------------------------- /src/style/rightButtons.less: -------------------------------------------------------------------------------- 1 | .rightButton { 2 | display: flex; 3 | display: none; 4 | justify-content: center; 5 | align-items: center; 6 | border: none; 7 | border-radius: 3px; 8 | color: #fff; 9 | box-sizing: border-box; 10 | width: 30px; 11 | height: 32px; 12 | padding: 3px; 13 | right: 0; 14 | text-align: center; 15 | cursor: pointer; 16 | position: fixed; 17 | z-index: 1000; 18 | font-size: 18px; 19 | 20 | &:hover { 21 | opacity: 0.8; 22 | } 23 | 24 | & .icon { 25 | vertical-align: middle; 26 | } 27 | } 28 | 29 | #openCenterPanelBtn { 30 | top: 15%; 31 | background: #80b9f7; 32 | background: -webkit-linear-gradient(top, #bce4ff 0%, #35adfd 100%); 33 | } 34 | 35 | #quickCrawlBtn { 36 | top: 21%; 37 | background: #0096fa; 38 | background: -webkit-linear-gradient(top, #47b4ff 0%, #027fd2 100%); 39 | font-size: 17px; 40 | } 41 | -------------------------------------------------------------------------------- /src/style/selectWork.less: -------------------------------------------------------------------------------- 1 | @import './var.less'; 2 | 3 | @keyframes moveRight { 4 | from { 5 | left: -100vw; 6 | } 7 | to { 8 | left: -98vw; 9 | } 10 | } 11 | 12 | @keyframes moveBottom { 13 | from { 14 | top: -100vh; 15 | } 16 | to { 17 | top: -98vh; 18 | } 19 | } 20 | 21 | #selectWorkEl { 22 | box-sizing: border-box; 23 | position: fixed; 24 | z-index: 1000; 25 | left: 0; 26 | top: 0; 27 | width: 20px; 28 | height: 20px; 29 | border-radius: 50%; 30 | background: @beautifyBlue; 31 | display: none; 32 | pointer-events: none; 33 | 34 | &::before { 35 | content: ''; 36 | display: block; 37 | position: absolute; 38 | width: 200vw; 39 | left: -100vw; 40 | top: 10px; 41 | height: 1px; 42 | background-image: linear-gradient( 43 | to right, 44 | @themeBlue 0%, 45 | @themeBlue 50%, 46 | transparent 50% 47 | ); 48 | background-size: 8px 1px; 49 | background-repeat: repeat-x; 50 | animation: moveRight 2s linear infinite; 51 | } 52 | 53 | &::after { 54 | content: ''; 55 | display: block; 56 | position: absolute; 57 | height: 200vh; 58 | top: -100vh; 59 | left: 10px; 60 | width: 1px; 61 | background-image: linear-gradient( 62 | to bottom, 63 | @themeBlue 0%, 64 | @themeBlue 50%, 65 | transparent 50% 66 | ); 67 | background-size: 1px 8px; 68 | background-repeat: repeat-y; 69 | animation: moveBottom 1s linear infinite; 70 | } 71 | } 72 | 73 | .selectedWorkFlag { 74 | position: absolute; 75 | z-index: 1000; 76 | margin: auto; 77 | top: 0; 78 | bottom: 0; 79 | left: 0; 80 | right: 0; 81 | width: 60px; 82 | height: 60px; 83 | font-size: 54px; 84 | border-radius: 50%; 85 | text-align: center; 86 | .icon { 87 | color: @beautifyBlue; 88 | vertical-align: baseline; 89 | } 90 | &::after { 91 | content: ''; 92 | display: block; 93 | position: absolute; 94 | width: 60%; 95 | height: 60%; 96 | border-radius: 50%; 97 | background-color: #fff; 98 | left: 12px; 99 | top: 12px; 100 | z-index: -1; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/style/var.less: -------------------------------------------------------------------------------- 1 | @themeBlue: #0ea8ef; 2 | @beautifyBlue: #29b3f3; 3 | @boxShadowBlue: #2ca6df; 4 | @darkBoxShadow: #10506e; 5 | @pixivFontFamily: 6 | win-bug-omega, 7 | system-ui, 8 | -apple-system, 9 | 'Segoe UI', 10 | Roboto, 11 | Ubuntu, 12 | Cantarell, 13 | 'Noto Sans', 14 | 'Hiragino Kaku Gothic ProN', 15 | Meiryo, 16 | sans-serif; 17 | @MobileFontFamily: sans-serif; 18 | -------------------------------------------------------------------------------- /src/ts/BoldKeywords.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from './EVT' 2 | import { settings } from './setting/Settings' 3 | 4 | class BoldKeywords { 5 | constructor(wrap: HTMLElement) { 6 | this.wrap = wrap 7 | this.bindEvent() 8 | this.setClassName() 9 | } 10 | 11 | private wrap: HTMLElement 12 | private readonly className = 'showBlobKeywords' 13 | 14 | private bindEvent() { 15 | window.addEventListener(EVT.list.settingChange, (ev: CustomEventInit) => { 16 | const data = ev.detail.data as any 17 | if (data.name === 'boldKeywords') { 18 | this.setClassName() 19 | } 20 | }) 21 | } 22 | 23 | private setClassName() { 24 | this.wrap.classList[settings.boldKeywords ? 'add' : 'remove']( 25 | this.className 26 | ) 27 | } 28 | } 29 | 30 | export { BoldKeywords } 31 | -------------------------------------------------------------------------------- /src/ts/CheckNewVersion.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from './EVT' 2 | import { Utils } from './utils/Utils' 3 | 4 | // 检查新版本 5 | class CheckNewVersion { 6 | constructor() { 7 | this.checkNew() 8 | } 9 | 10 | private async checkNew() { 11 | if (!Utils.isPixiv()) { 12 | return 13 | } 14 | // 读取上一次检查的时间,如果超过指定的时间,则检查 GitHub 上的信息 15 | const timeName = 'xzUpdateTime' 16 | const verName = 'xzGithubVer' 17 | const interval = 1000 * 60 * 30 // 30 分钟检查一次 18 | 19 | const lastTime = localStorage.getItem(timeName) 20 | if (!lastTime || new Date().getTime() - parseInt(lastTime) > interval) { 21 | // 获取最新的 releases 信息 22 | const latest = await fetch( 23 | 'https://api.github.com/repos/xuejianxianzun/PixivBatchDownloader/releases/latest' 24 | ) 25 | const latestJson = await latest.json() 26 | const latestVer = latestJson.name 27 | // 保存 GitHub 上的版本信息 28 | localStorage.setItem(verName, latestVer) 29 | // 保存本次检查的时间戳 30 | localStorage.setItem(timeName, new Date().getTime().toString()) 31 | } 32 | 33 | // 获取本地扩展的版本号 34 | const manifest = await fetch(chrome.runtime.getURL('manifest.json')) 35 | const manifestJson = await manifest.json() 36 | const manifestVer = manifestJson.version 37 | 38 | // 比较大小 39 | const latestVer = localStorage.getItem(verName) 40 | if (!latestVer) { 41 | return 42 | } 43 | if (this.bigger(latestVer, manifestVer)) { 44 | EVT.fire('hasNewVer') 45 | } 46 | } 47 | 48 | // 传入两个版本号字符串,比较第一个是否比第二个大 49 | private bigger(a: string, b: string) { 50 | const _a = a.split('.') 51 | const _b = b.split('.') 52 | 53 | // 分别比较每一个版本号字段,从主版本号比较到子版本号 54 | for (let i = 0; i < _a.length; i++) { 55 | if (_b[i] === undefined) { 56 | break 57 | } 58 | // 一旦某个版本号不相等,就立即返回结果 59 | if (Number.parseInt(_a[i]) > Number.parseInt(_b[i])) { 60 | return true 61 | } else if (Number.parseInt(_a[i]) < Number.parseInt(_b[i])) { 62 | return false 63 | } 64 | } 65 | 66 | return false 67 | } 68 | } 69 | 70 | new CheckNewVersion() 71 | -------------------------------------------------------------------------------- /src/ts/Colors.ts: -------------------------------------------------------------------------------- 1 | enum Colors { 2 | // 通用颜色 3 | white = '#fff', 4 | black = '#000', 5 | red = '#f00', 6 | theme = '#0ea8ef', 7 | 8 | // 带有语义的字体颜色 9 | textSuccess = '#00BD17', 10 | textWarning = '#d27e00', 11 | textError = '#f00', 12 | 13 | // 背景颜色 14 | // 稍暗,适合在颜色区域的面积较大时使用 15 | bgBlue = '#0ea8ef', 16 | bgGreen = '#14ad27', 17 | bgYellow = '#e49d00', 18 | bgRed = '#f33939', 19 | 20 | // 带有语义的背景颜色 21 | // 稍亮,适合在小区域使用 22 | bgBrightBlue = '#29b3f3', 23 | bgSuccess = '#00BD17', 24 | bgWarning = '#e49d00', 25 | bgError = '#f00', 26 | } 27 | 28 | export { Colors } 29 | -------------------------------------------------------------------------------- /src/ts/Config.ts: -------------------------------------------------------------------------------- 1 | // 定义一些常量 2 | // 用户不可以修改这里的配置 3 | class Config { 4 | /**使用输出面板显示内容时,如果文件数量大于这个值,就不再显示内容,而是保存到 txt 文件 */ 5 | static readonly outputMax = 5000 6 | /**同时下载的文件数量的最大值 */ 7 | static readonly downloadThreadMax = 6 8 | /**下载某个文件出错时,最大重试次数 */ 9 | static readonly retryMax = 10 10 | /**作品类型所对应的字符串名称 */ 11 | static readonly worksTypeName = ['Illustration', 'Manga', 'Ugoira', 'Novel'] 12 | /**程序名 */ 13 | static readonly appName = 'Powerful Pixiv Downloader' 14 | /**下载器储存设置时使用的 key name */ 15 | static readonly settingStoreName = 'xzSetting' 16 | /**按收藏数量过滤作品时,预设的最大收藏数量 */ 17 | static readonly BookmarkCountLimit = 9999999 18 | /**Pixiv 作品总数量上限 */ 19 | static readonly worksNumberLimit = 9999999999 20 | /**当抓取被 pixiv 限制,返回了空数据时,等待这个时间之后再继续抓取 */ 21 | static readonly retryTime = 200000 22 | /**浏览器是否处于移动端模式 */ 23 | static readonly mobile = navigator.userAgent.includes('Mobile') 24 | } 25 | 26 | export { Config } 27 | -------------------------------------------------------------------------------- /src/ts/ConvertUgoira/ToAPNG.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from '../EVT' 2 | import { UgoiraInfo } from '../crawl/CrawlResult' 3 | 4 | declare const UPNG: any 5 | 6 | class ToAPNG { 7 | public async convert( 8 | ImageBitmapList: ImageBitmap[], 9 | info: UgoiraInfo 10 | ): Promise { 11 | return new Promise(async (resolve, reject) => { 12 | const width = ImageBitmapList[0].width 13 | const height = ImageBitmapList[0].height 14 | const canvas = document.createElement('canvas') 15 | const ctx = canvas.getContext('2d', { 16 | willReadFrequently: true, 17 | })! as CanvasRenderingContext2D 18 | canvas.width = width 19 | canvas.height = height 20 | 21 | // 添加帧数据 22 | let arrayBuffList: ArrayBuffer[] = [] 23 | ImageBitmapList.forEach((imageBitmap) => { 24 | ctx.drawImage(imageBitmap, 0, 0) 25 | // 从画布获取图像绘制后的 Uint8ClampedArray buffer 26 | const buff = ctx.getImageData(0, 0, width, height).data.buffer 27 | arrayBuffList.push(buff) 28 | }) 29 | const delayList = info.frames.map((frame) => frame.delay) 30 | 31 | // 编码 32 | // https://github.com/photopea/UPNG.js/#encoder 33 | const pngFile = UPNG.encode( 34 | arrayBuffList, 35 | width, 36 | height, 37 | 0, 38 | delayList 39 | ) as ArrayBuffer 40 | 41 | arrayBuffList = null as any 42 | 43 | const blob = new Blob([pngFile], { 44 | type: 'image/vnd.mozilla.apng', 45 | }) 46 | 47 | EVT.fire('convertSuccess') 48 | 49 | resolve(blob) 50 | }) 51 | } 52 | } 53 | 54 | const toAPNG = new ToAPNG() 55 | export { toAPNG } 56 | -------------------------------------------------------------------------------- /src/ts/ConvertUgoira/ToWebM.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from '../EVT' 2 | import { UgoiraInfo } from '../crawl/CrawlResult' 3 | 4 | declare const Whammy: any 5 | 6 | class ToWebM { 7 | public async convert( 8 | ImageBitmapList: ImageBitmap[], 9 | info: UgoiraInfo 10 | ): Promise { 11 | return new Promise(async (resolve, reject) => { 12 | const width = ImageBitmapList[0].width 13 | const height = ImageBitmapList[0].height 14 | const canvas = document.createElement('canvas') 15 | const ctx = canvas.getContext('2d')! 16 | canvas.width = width 17 | canvas.height = height 18 | 19 | // 创建视频编码器 20 | const encoder = new Whammy.Video() 21 | 22 | // 添加帧数据 23 | ImageBitmapList.forEach((imageBitmap, index) => { 24 | ctx.drawImage(imageBitmap, 0, 0) 25 | // 把图像转换为 webp 格式的 DataURL,这样 webm 编码器内部可以直接使用,不需要进行一些重复的操作 26 | // https://github.com/antimatter15/whammy#basic-usage 27 | const url = canvas.toDataURL('image/webp', 0.9) 28 | encoder.add(url, info.frames![index].delay) 29 | }) 30 | 31 | // 编码视频 32 | encoder.compile(false, (video: Blob) => { 33 | EVT.fire('convertSuccess') 34 | resolve(video) 35 | }) 36 | }) 37 | } 38 | } 39 | 40 | const toWebM = new ToWebM() 41 | export { toWebM } 42 | -------------------------------------------------------------------------------- /src/ts/ConvertUgoira/convertUgoiraImages.worker.ts: -------------------------------------------------------------------------------- 1 | interface EventData { 2 | format: 'png' | 'webp' 3 | ImageBitmapList: ImageBitmap[] 4 | width: number 5 | height: number 6 | } 7 | 8 | onmessage = async (ev) => { 9 | const canvas = new OffscreenCanvas(ev.data.width, ev.data.height) 10 | const ctx = canvas.getContext('2d')! 11 | 12 | if (ev.data.format === 'png') { 13 | const result: ArrayBuffer[] = [] 14 | ev.data.ImageBitmapList.forEach((ImageBitmap: ImageBitmap) => { 15 | ctx.drawImage(ImageBitmap, 0, 0, ev.data.width, ev.data.height) 16 | // 从画布获取图像绘制后的 Uint8ClampedArray buffer 17 | const buffer = ctx.getImageData(0, 0, ev.data.width, ev.data.height).data 18 | .buffer 19 | result.push(buffer) 20 | }) 21 | ;(self as unknown as Worker).postMessage({ 22 | result, 23 | }) 24 | } 25 | 26 | if (ev.data.format === 'webp') { 27 | const result: string[] = [] 28 | ev.data.ImageBitmapList.forEach(async (ImageBitmap: ImageBitmap) => { 29 | ctx.drawImage(ImageBitmap, 0, 0, ev.data.width, ev.data.height) 30 | const blob = await canvas.convertToBlob({ 31 | type: 'image/webp', 32 | quality: 0.9, 33 | }) 34 | const dataURL = new FileReaderSync().readAsDataURL(blob) 35 | result.push(dataURL) 36 | }) 37 | ;(self as unknown as Worker).postMessage({ 38 | result, 39 | width: ev.data.width, 40 | height: ev.data.height, 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ts/CopyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import { lang } from './Lang' 2 | import { toast } from './Toast' 3 | 4 | interface ClipboardItem { 5 | readonly types: string[] 6 | readonly presentationStyle: 'unspecified' | 'inline' | 'attachment' 7 | getType(): Promise 8 | } 9 | 10 | interface ClipboardItemData { 11 | [mimeType: string]: Blob | string | Promise 12 | } 13 | 14 | declare var ClipboardItem: { 15 | prototype: ClipboardItem 16 | new (itemData: ClipboardItemData): ClipboardItem 17 | } 18 | 19 | interface Clipboard { 20 | read(): Promise 21 | write(data: ClipboardItem[]): Promise 22 | } 23 | 24 | class CopyToClipboard { 25 | static setClipboard(text: string): Promise { 26 | return new Promise((resolve, reject) => { 27 | const type = 'text/plain' 28 | const blob = new Blob([text], { type }) 29 | const data = [new ClipboardItem({ [type]: blob })] 30 | 31 | ;(window.navigator.clipboard as unknown as Clipboard).write(data).then( 32 | () => { 33 | toast.success(lang.transl('_已复制到剪贴板')) 34 | resolve() 35 | }, 36 | () => { 37 | toast.error(lang.transl('_写入剪贴板失败')) 38 | reject() 39 | } 40 | ) 41 | }) 42 | } 43 | } 44 | 45 | export { CopyToClipboard } 46 | -------------------------------------------------------------------------------- /src/ts/ListenPageSwitch.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from './EVT' 2 | 3 | // 监听页面的无刷新切换 4 | class ListenPageSwitch { 5 | constructor() { 6 | this.supportListenHistory() 7 | this.listenPageSwitch() 8 | } 9 | 10 | // 为监听 url 变化的事件提供支持 11 | private supportListenHistory() { 12 | const s = document.createElement('script') 13 | const url = chrome.runtime.getURL('lib/listen_history_change.js') 14 | s.src = url 15 | document.head.appendChild(s) 16 | } 17 | 18 | // 无刷新切换页面时派发事件 19 | private listenPageSwitch() { 20 | // 点击浏览器的前进或后退按钮会触发 popstate 事件 21 | // 点击链接进入一个 url 不同的页面是 pushState 操作 22 | // 现在还没有遇到 replaceState 操作 23 | ;['pushState', 'popstate', 'replaceState'].forEach((item) => { 24 | window.addEventListener(item, () => { 25 | EVT.fire('pageSwitch') 26 | }) 27 | }) 28 | } 29 | } 30 | 31 | new ListenPageSwitch() 32 | -------------------------------------------------------------------------------- /src/ts/Loading.ts: -------------------------------------------------------------------------------- 1 | import { theme } from './Theme' 2 | 3 | // loading 图标 4 | class Loading { 5 | private id = 'xzLoadingWrap' 6 | 7 | private readonly html = ` 8 |
9 |
10 | 13 |
14 |
` 15 | 16 | private _show = false 17 | 18 | public set show(val: boolean) { 19 | this._show = val 20 | this._show ? this.showEl() : this.hiddenEl() 21 | } 22 | 23 | public get show() { 24 | return this._show 25 | } 26 | 27 | private create() { 28 | document.body.insertAdjacentHTML('beforeend', this.html) 29 | const el = document.body.querySelector('#' + this.id) as HTMLDivElement 30 | theme.register(el) 31 | return el 32 | } 33 | 34 | private getEl() { 35 | let el = document.body.querySelector('#' + this.id) 36 | if (el) { 37 | return el as HTMLDivElement 38 | } else { 39 | return this.create() 40 | } 41 | } 42 | 43 | private showEl() { 44 | this.getEl().style.display = 'flex' 45 | } 46 | 47 | private hiddenEl() { 48 | this.getEl().style.display = 'none' 49 | } 50 | } 51 | 52 | const loading = new Loading() 53 | export { loading } 54 | -------------------------------------------------------------------------------- /src/ts/Mask.ts: -------------------------------------------------------------------------------- 1 | // 遮罩层 2 | class Mask { 3 | private id = 'xzMaskWrap' 4 | 5 | private _show = false 6 | 7 | public set show(val: boolean) { 8 | this._show = val 9 | this._show ? this.showMask() : this.hiddenMask() 10 | } 11 | 12 | public get show() { 13 | return this._show 14 | } 15 | 16 | private create() { 17 | const mask = document.createElement('div') 18 | mask.id = this.id 19 | return document.body.appendChild(mask) 20 | } 21 | 22 | private getMskEl() { 23 | let mask = document.body.querySelector('#' + this.id) 24 | if (mask) { 25 | return mask as HTMLDivElement 26 | } else { 27 | return this.create() 28 | } 29 | } 30 | 31 | private showMask() { 32 | this.getMskEl().style.display = 'flex' 33 | } 34 | 35 | private hiddenMask() { 36 | this.getMskEl().style.display = 'none' 37 | } 38 | } 39 | 40 | const mask = new Mask() 41 | export { mask } 42 | -------------------------------------------------------------------------------- /src/ts/OpenCenterPanel.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from './EVT' 2 | import { lang } from './Lang' 3 | 4 | // 页面右侧的按钮,点击可以打开中间面板 5 | class OpenCenterPanel { 6 | constructor() { 7 | this.addBtn() 8 | this.show() 9 | this.bindEvents() 10 | } 11 | 12 | private btn: HTMLButtonElement = document.createElement('button') 13 | 14 | private addBtn() { 15 | this.btn = document.createElement('button') 16 | this.btn.classList.add('rightButton') 17 | this.btn.id = 'openCenterPanelBtn' 18 | this.btn.setAttribute('data-xztitle', '_显示控制面板') 19 | this.btn.innerHTML = `` 22 | document.body.append(this.btn) 23 | lang.register(this.btn) 24 | } 25 | 26 | private bindEvents() { 27 | // 这里阻止事件冒泡是为了配合 CenterPanel 的“点击页面其他部分隐藏 CenterPanel”的效果 28 | this.btn.addEventListener('click', (e) => { 29 | const ev = e || window.event 30 | ev.stopPropagation() 31 | EVT.fire('openCenterPanel') 32 | }) 33 | 34 | window.addEventListener(EVT.list.centerPanelClosed, () => { 35 | this.show() 36 | }) 37 | 38 | window.addEventListener(EVT.list.centerPanelOpened, () => { 39 | this.hide() 40 | }) 41 | } 42 | 43 | private show() { 44 | this.btn.style.display = 'flex' 45 | } 46 | 47 | private hide() { 48 | this.btn.style.display = 'none' 49 | } 50 | } 51 | 52 | new OpenCenterPanel() 53 | -------------------------------------------------------------------------------- /src/ts/RemoveWorksTagsInBookmarks.ts: -------------------------------------------------------------------------------- 1 | import { lang } from './Lang' 2 | import { log } from './Log' 3 | import { toast } from './Toast' 4 | import { states } from './store/States' 5 | import { bookmark, WorkBookmarkData } from './Bookmark' 6 | import { msgBox } from './MsgBox' 7 | 8 | // 移除本页面中所有作品的标签 9 | class RemoveWorksTagsInBookmarks { 10 | public async start(list: WorkBookmarkData[]) { 11 | if (list.length === 0) { 12 | toast.error(lang.transl('_没有数据可供使用')) 13 | log.error(lang.transl('_没有数据可供使用')) 14 | return 15 | } 16 | 17 | states.busy = true 18 | 19 | const total = list.length.toString() 20 | log.log(lang.transl('_当前作品个数', total)) 21 | 22 | let number = 0 23 | for (const item of list) { 24 | try { 25 | const status = await bookmark.add( 26 | item.workID.toString(), 27 | item.type, 28 | [], 29 | false, 30 | item.private, 31 | true 32 | ) 33 | 34 | if (status === 403) { 35 | msgBox.error( 36 | `Add bookmark: ${item.workID}, Error: 403 Forbidden, ${lang.transl( 37 | '_你的账号已经被Pixiv限制' 38 | )}` 39 | ) 40 | break 41 | } 42 | } catch (error) { 43 | // 处理自己收藏的作品时可能遇到错误。最常见的错误就是作品被删除了,获取作品数据时会产生 404 错误 44 | // 但是也可能出现其他错误,比如因为请求太多而出现 429 错误。因为 429 错误需要等待几分钟后才能重试,这里偷懒不再重试 45 | } 46 | number++ 47 | log.log(`${number} / ${total}`, 1, false) 48 | } 49 | 50 | const msg = 51 | lang.transl('_移除本页面中所有作品的标签') + ' ' + lang.transl('_完成') 52 | log.success(msg) 53 | toast.success(msg, { 54 | position: 'topCenter', 55 | }) 56 | states.busy = false 57 | } 58 | } 59 | 60 | const removeWorksTagsInBookmarks = new RemoveWorksTagsInBookmarks() 61 | export { removeWorksTagsInBookmarks } 62 | -------------------------------------------------------------------------------- /src/ts/RequestSponsorship.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from './EVT' 2 | import { lang } from './Lang' 3 | import { msgBox } from './MsgBox' 4 | import { setSetting, settings } from './setting/Settings' 5 | 6 | class RequestSponsorship { 7 | constructor() { 8 | window.addEventListener(EVT.list.settingInitialized, () => { 9 | // 赋予初始值 10 | if (settings.requestSponsorshipTime === 0) { 11 | setSetting( 12 | 'requestSponsorshipTime', 13 | new Date().getTime() + this.interval 14 | ) 15 | } 16 | 17 | window.setTimeout(() => { 18 | this.check() 19 | }, 10000) 20 | }) 21 | } 22 | 23 | // 30 * 24 * 60 * 60 * 1000 24 | private readonly interval = 2592000000 25 | 26 | private check() { 27 | const now = new Date().getTime() 28 | if (now >= settings.requestSponsorshipTime) { 29 | msgBox.once('request sponsorship', lang.transl('_赞助方式提示'), 'show', { 30 | title: lang.transl('_赞助我'), 31 | }) 32 | 33 | setSetting('requestSponsorshipTime', now + this.interval) 34 | } 35 | } 36 | } 37 | 38 | new RequestSponsorship() 39 | -------------------------------------------------------------------------------- /src/ts/SetTimeoutWorker.ts: -------------------------------------------------------------------------------- 1 | interface ListData { 2 | id: number 3 | time: number 4 | callback: Function | null 5 | } 6 | 7 | class SetTimeoutWorker { 8 | constructor() { 9 | this.createWorker() 10 | } 11 | 12 | // 因为 worker 的代码很短,所以直接储存在这里,避免从网络加载导致的延迟问题 13 | private readonly workerCode = `onmessage = (ev) => { 14 | setTimeout(() => { 15 | postMessage({ 16 | id: ev.data.id 17 | }) 18 | }, ev.data.time) 19 | }` 20 | 21 | private worker!: Worker 22 | 23 | private createWorker() { 24 | const blob = new Blob([this.workerCode]) 25 | this.worker = new Worker(URL.createObjectURL(blob)) 26 | 27 | this.worker.addEventListener('message', (ev) => { 28 | const id = ev.data.id as number 29 | if (this.list[id].callback !== null) { 30 | this.list[id].callback!() 31 | this.clear(id) 32 | } 33 | }) 34 | } 35 | 36 | private list: ListData[] = [] 37 | 38 | private timerId = 0 39 | 40 | public set(callback: Function, time: number) { 41 | const data = { 42 | id: this.timerId, 43 | time, 44 | callback, 45 | } 46 | this.list.push(data) 47 | this.timerId++ 48 | 49 | this.worker.postMessage({ 50 | id: data.id, 51 | time, 52 | }) 53 | 54 | return data.id 55 | } 56 | 57 | public clear(id: number) { 58 | this.list[id].callback = null 59 | } 60 | } 61 | 62 | const setTimeoutWorker = new SetTimeoutWorker() 63 | export { setTimeoutWorker } 64 | -------------------------------------------------------------------------------- /src/ts/ShowHelp.ts: -------------------------------------------------------------------------------- 1 | import { lang } from './Lang' 2 | import { Config } from './Config' 3 | import { msgBox } from './MsgBox' 4 | import { settings, setSetting, SettingKeys } from './setting/Settings' 5 | 6 | // 用消息框显示一次性的提示 7 | class ShowHelp { 8 | public show(settingKey: SettingKeys, msg: string, title?: string) { 9 | if (settings[settingKey] === true) { 10 | setSetting(settingKey, false) 11 | msgBox.show(msg, { 12 | title: title ? title : Config.appName + ' Help', 13 | btn: lang.transl('_我知道了'), 14 | }) 15 | } 16 | } 17 | } 18 | 19 | const showHelp = new ShowHelp() 20 | export { showHelp } 21 | -------------------------------------------------------------------------------- /src/ts/ShowNotification.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from './EVT' 2 | import { lang } from './Lang' 3 | import { settings } from './setting/Settings' 4 | import { states } from './store/States' 5 | import { store } from './store/Store' 6 | import { Tools } from './Tools' 7 | 8 | class ShowNotification { 9 | constructor() { 10 | this.iconURL = chrome.runtime.getURL('icon/logo128.png') 11 | this.bindEvents() 12 | } 13 | 14 | private iconURL = '' 15 | 16 | private bindEvents() { 17 | // 当用户开启“下载完成后显示通知”的提示时,请求权限 18 | window.addEventListener(EVT.list.settingChange, (ev: CustomEventInit) => { 19 | const data = ev.detail.data as any 20 | if (data.name === 'showNotificationAfterDownloadComplete' && data.value) { 21 | this.requstPremission() 22 | } 23 | }) 24 | 25 | // 当下载任务完毕时,显示通知 26 | window.addEventListener(EVT.list.downloadComplete, () => { 27 | window.setTimeout(() => { 28 | // 如果抓取标签列表没有完成,则不显示通知 29 | // 在一次抓取多个标签时,当最后一个标签下载完之后会解除 crawlTagList 状态,这时可以显示一条通知 30 | // 如果有等待下载的任务,则不显示通知 31 | if ( 32 | settings.showNotificationAfterDownloadComplete && 33 | !states.crawlTagList && 34 | store.waitingIdList.length === 0 35 | ) { 36 | this.show(lang.transl('_下载完毕2'), Tools.getPageTitle()) 37 | } 38 | }, 0) 39 | }) 40 | } 41 | 42 | public async show(title: string, text: string) { 43 | await this.requstPremission() 44 | new Notification(title, { 45 | body: text, 46 | // 不设置 tag。如果设置了相同的 tag,那么新的通知会覆盖旧的通知,导致如果有多个页面下载完毕,用户只能看到最后一个页面的通知 47 | // tag: 'PowerfulPixivDownloader', 48 | icon: this.iconURL, 49 | }) 50 | } 51 | 52 | private requstPremission() { 53 | if (Notification.permission !== 'granted') { 54 | return Notification.requestPermission() 55 | } 56 | } 57 | } 58 | 59 | new ShowNotification() 60 | -------------------------------------------------------------------------------- /src/ts/ShowWhatIsNew.ts: -------------------------------------------------------------------------------- 1 | import { lang } from './Lang' 2 | import { Config } from './Config' 3 | import { msgBox } from './MsgBox' 4 | import { Utils } from './utils/Utils' 5 | import { EVT } from './EVT' 6 | import { setSetting, settings } from './setting/Settings' 7 | 8 | // 显示最近更新内容 9 | class ShowWhatIsNew { 10 | constructor() { 11 | this.bindEvents() 12 | } 13 | 14 | private flag = '17.6.0' 15 | 16 | private bindEvents() { 17 | window.addEventListener(EVT.list.settingInitialized, () => { 18 | // 消息文本要写在 settingInitialized 事件回调里,否则它们可能会被翻译成错误的语言 19 | let msg = ` 20 | 💡${lang.transl('_为下载器的设置项添加了更多提示')} 21 |
22 | 🗑️${lang.transl('_移除设置项')}${lang.transl( 23 | '_添加命名标记前缀' 24 | )} 25 |
26 | 🗑️${lang.transl('_移除设置项')}${lang.transl( 27 | '_隐藏浏览器底部的下载栏' 28 | )} 29 | ` 30 | 31 | // 32 | // ✨${lang.transl('_新增设置项')}: 33 | // ✨${lang.transl('_新增功能')}: 34 | // ${lang.transl('_下载间隔')} 35 | // 36 | // 🗑${lang.transl('_移除设置项')}${lang.transl('_隐藏浏览器底部的下载栏')} 37 | 38 | // ${lang.transl( 39 | // '_你可以在更多选项卡的xx分类里找到它', 40 | // lang.transl('_下载') 41 | // )} 42 | 43 | //
44 | //
45 | // ${lang.transl('_该功能默认启用')} 46 | // ${lang.transl('_修复已知问题')} 47 | // ${lang.transl('_优化性能和用户体验')} 48 | // ${lang.transl('_其他优化')} 49 | // ${lang.transl('_为下载器的设置项添加了更多提示')} 50 | 51 | // 在更新说明的下方显示赞助提示 52 | msg += ` 53 |
54 |
55 | ${lang.transl('_赞助方式提示')}` 56 | 57 | this.show(msg) 58 | }) 59 | } 60 | 61 | private show(msg: string) { 62 | if (Utils.isPixiv() && settings.whatIsNewFlag !== this.flag) { 63 | msgBox.show(msg, { 64 | title: Config.appName + ` ${lang.transl('_最近更新')}`, 65 | btn: lang.transl('_我知道了'), 66 | }) 67 | setSetting('whatIsNewFlag', this.flag) 68 | } 69 | } 70 | } 71 | 72 | new ShowWhatIsNew() 73 | -------------------------------------------------------------------------------- /src/ts/Tip.ts: -------------------------------------------------------------------------------- 1 | interface MouseArg { 2 | type: number 3 | x: number 4 | y: number 5 | } 6 | 7 | // 给下载器的界面元素添加提示文本,当鼠标移动到元素上时会显示提示 8 | // 如果要给某个元素添加提示,先给它添加 has_tip 的 className,然后用 data-tip 设置提示内容 9 | class Tip { 10 | constructor() { 11 | this.addTipEl() 12 | this.bindEvents() 13 | } 14 | private tipEl!: HTMLDivElement 15 | 16 | private addTipEl() { 17 | this.tipEl = document.createElement('div') 18 | this.tipEl.id = 'tip' 19 | document.body.append(this.tipEl) 20 | } 21 | 22 | private bindEvents() { 23 | const tips = document.querySelectorAll( 24 | '.has_tip' 25 | ) as NodeListOf 26 | for (const el of tips) { 27 | for (const ev of ['mouseenter', 'mouseleave']) { 28 | el.addEventListener(ev, (event) => { 29 | const e = (event || window.event) as MouseEvent 30 | const text = el.dataset.tip 31 | this.showTip(text, { 32 | type: ev === 'mouseenter' ? 1 : 0, 33 | x: e.clientX, 34 | y: e.clientY, 35 | }) 36 | }) 37 | } 38 | } 39 | } 40 | 41 | // 显示中间面板上的提示。参数 mouse 指示鼠标是移入还是移出,并包含鼠标坐标 42 | private showTip(text: string | undefined, mouse: MouseArg) { 43 | if (!text) { 44 | throw new Error('No tip text.') 45 | } 46 | 47 | if (mouse.type === 1) { 48 | this.tipEl.innerHTML = text 49 | this.tipEl.style.left = mouse.x + 30 + 'px' 50 | this.tipEl.style.top = mouse.y - 30 + 'px' 51 | this.tipEl.style.display = 'block' 52 | } else if (mouse.type === 0) { 53 | this.tipEl.style.display = 'none' 54 | } 55 | } 56 | } 57 | 58 | new Tip() 59 | -------------------------------------------------------------------------------- /src/ts/UnBookmarkWorks.ts: -------------------------------------------------------------------------------- 1 | import { API } from './API' 2 | import { lang } from './Lang' 3 | import { log } from './Log' 4 | import { toast } from './Toast' 5 | import { token } from './Token' 6 | import { states } from './store/States' 7 | import { WorkBookmarkData } from './Bookmark' 8 | import { setTimeoutWorker } from './SetTimeoutWorker' 9 | import { Config } from './Config' 10 | import { settings } from './setting/Settings' 11 | 12 | class UnBookmarkWorks { 13 | public async start(list: WorkBookmarkData[]) { 14 | log.warning(lang.transl('_取消收藏作品')) 15 | if (list.length === 0) { 16 | toast.error(lang.transl('_没有数据可供使用')) 17 | log.error(lang.transl('_没有数据可供使用')) 18 | return 19 | } 20 | 21 | states.busy = true 22 | 23 | const total = list.length 24 | log.log(lang.transl('_当前作品个数', total.toString())) 25 | 26 | // 尚不清楚 deleteBookmark 使用的 API 是否会被计入 429 限制里 27 | // 当操作的作品数量大于一页(48 个作品)时,使用慢速抓取 28 | const slowMode = total > 48 29 | 30 | let progress = 0 31 | 32 | for (const item of list) { 33 | try { 34 | await this.waitSlowMode(slowMode) 35 | await API.deleteBookmark(item.bookmarkID, item.type, token.token) 36 | } catch (error) { 37 | // 处理自己收藏的作品时可能遇到错误。最常见的错误就是作品被删除了,获取作品数据时会产生 404 错误 38 | // 对于出错的作品直接跳过,不需要对其执行任何操作 39 | // 不过这种作品无法被删除,执行完毕后还是会留在收藏里 40 | } 41 | progress++ 42 | log.log(`${progress} / ${total}`, 1, false) 43 | } 44 | 45 | const msg = lang.transl('_取消收藏作品') + ' ' + lang.transl('_完成') 46 | log.success(msg) 47 | toast.success(msg, { 48 | position: 'topCenter', 49 | }) 50 | states.busy = false 51 | } 52 | 53 | private waitSlowMode(slowMode: boolean): Promise { 54 | return new Promise((resolve) => { 55 | if (!slowMode) { 56 | return resolve() 57 | } else { 58 | setTimeoutWorker.set(() => { 59 | return resolve() 60 | }, settings.slowCrawlDealy) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | const unBookmarkWorks = new UnBookmarkWorks() 67 | export { unBookmarkWorks } 68 | -------------------------------------------------------------------------------- /src/ts/content.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * project: Powerful Pixiv Downloader 3 | * author: xuejianxianzun; 雪见仙尊 4 | * license: GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt 5 | * Github: https://github.com/xuejianxianzun/PixivBatchDownloader 6 | * Releases: https://github.com/xuejianxianzun/PixivBatchDownloader/releases 7 | * Wiki: https://xuejianxianzun.github.io/PBDWiki 8 | * Website: https://pixiv.download/ 9 | * E-mail: xuejianxianzun@gmail.com 10 | */ 11 | 12 | import './Lang' 13 | import './Theme' 14 | import './store/States' 15 | import './setting/Settings' 16 | import './setting/InvisibleSettings' 17 | import './ListenPageSwitch' 18 | import './CenterPanel' 19 | import './setting/Form' 20 | import './setting/DoNotDownloadLastFewImages' 21 | import './setting/UseDifferentNameRuleIfWorkHasTag' 22 | import './ReplaceSquareThumb' 23 | import './InitPage' 24 | import './crawlMixedPage/QuickCrawl' 25 | import './download/DownloadControl' 26 | import './download/Resume' 27 | import './Tip' 28 | import './PreviewWork' 29 | import './ShowLargerThumbnails' 30 | import './ShowZoomBtnOnThumb' 31 | import './showDownloadBtnOnThumb' 32 | import './RemoveBlockedUsersWork' 33 | import './output/OutputPanel' 34 | import './output/PreviewFileName' 35 | import './output/ShowURLs' 36 | import './download/ExportResult2CSV' 37 | import './download/ExportResult' 38 | import './download/ImportResult' 39 | import './download/ExportLST' 40 | import './download/MergeNovel' 41 | import './download/SaveWorkMeta' 42 | import './download/SaveWorkDescription' 43 | import './download/showStatusOnTitle' 44 | import './download/ShowTotalResultOnTitle' 45 | import './download/ShowRemainingDownloadOnTitle' 46 | import './download/DownloadOnClickLike' 47 | import './HighlightFollowingUsers' 48 | // import './CheckNewVersion' 49 | import './ShowWhatIsNew' 50 | import './CheckUnsupportBrowser' 51 | import './ShowNotification' 52 | import './RequestSponsorship' 53 | -------------------------------------------------------------------------------- /src/ts/crawl/CrawlArgument.d.ts: -------------------------------------------------------------------------------- 1 | export type userWorksType = 'illusts' | 'manga' | 'novels' 2 | 3 | export type tagPageFlag = 'illusts' | 'manga' | 'illustmanga' | 'novels' 4 | 5 | // 大家的新作品的 API 参数 6 | export interface NewIllustOption { 7 | lastId: string 8 | limit: string 9 | type: string 10 | r18: string 11 | } 12 | 13 | // 排行榜的 API 参数 14 | export interface RankingOption { 15 | mode: string 16 | p: number 17 | worksType?: string 18 | date?: string 19 | } 20 | 21 | // 搜索 api 使用的选项(过滤选项) 22 | export interface SearchOption { 23 | order?: string 24 | type?: string 25 | wlt?: string 26 | hlt?: string 27 | ratio?: string 28 | tool?: string 29 | s_mode?: string 30 | mode?: string 31 | scd?: string 32 | ecd?: string 33 | blt?: string 34 | bgt?: string 35 | [key: string]: string | undefined 36 | } 37 | -------------------------------------------------------------------------------- /src/ts/crawl/InitRequestPage.ts: -------------------------------------------------------------------------------- 1 | import { options } from '../setting/Options' 2 | import { InitPageBase } from './InitPageBase' 3 | 4 | // 投稿页面 5 | class InitRequestPage extends InitPageBase { 6 | constructor() { 7 | super() 8 | this.init() 9 | } 10 | 11 | protected initAny() { 12 | // 为作品容器添加自定义 className,让显示更大的缩率图功能不那么容易失效 13 | const allSection = document.querySelectorAll('section') 14 | for (const section of allSection) { 15 | if (section.parentElement?.nodeName == 'DIV') { 16 | section.parentElement.classList.add('requestContainer') 17 | } 18 | } 19 | } 20 | 21 | protected addCrawlBtns() {} 22 | 23 | protected setFormOption() { 24 | options.hideOption([1]) 25 | } 26 | } 27 | 28 | export { InitRequestPage } 29 | -------------------------------------------------------------------------------- /src/ts/crawl/InitUnsupportedPage.ts: -------------------------------------------------------------------------------- 1 | import { options } from '../setting/Options' 2 | import { InitPageBase } from './InitPageBase' 3 | 4 | // 初始化不支持的页面类型 5 | class InitUnsupportedPage extends InitPageBase { 6 | constructor() { 7 | super() 8 | this.init() 9 | } 10 | 11 | // 在不支持的页面类型里,不会添加专门用于当前页面的抓取按钮 12 | // 只会由 SelectWork 模块添加通用的“手动抓取”功能 13 | protected addCrawlBtns() {} 14 | 15 | protected setFormOption() { 16 | options.hideOption([1]) 17 | } 18 | } 19 | 20 | export { InitUnsupportedPage } 21 | -------------------------------------------------------------------------------- /src/ts/crawl/StopCrawl.ts: -------------------------------------------------------------------------------- 1 | import { Colors } from '../Colors' 2 | import { EVT } from '../EVT' 3 | import { lang } from '../Lang' 4 | import { log } from '../Log' 5 | import { toast } from '../Toast' 6 | import { Tools } from '../Tools' 7 | import { states } from '../store/States' 8 | 9 | class StopCrawl { 10 | constructor() { 11 | this.addBtn() 12 | this.bindEvents() 13 | } 14 | private btn!: HTMLButtonElement 15 | 16 | private addBtn() { 17 | this.btn = Tools.addBtn('stopCrawl', Colors.bgRed, '_停止抓取') 18 | this.hide() 19 | 20 | this.btn.addEventListener('click', () => { 21 | this.hide() 22 | const msg = lang.transl('_已停止抓取') 23 | log.error(msg) 24 | toast.error(msg) 25 | EVT.fire('stopCrawl') 26 | states.stopCrawl = true 27 | }) 28 | } 29 | 30 | private bindEvents() { 31 | window.addEventListener(EVT.list.crawlStart, () => { 32 | this.show() 33 | }) 34 | 35 | const hiddenEvents = [EVT.list.crawlComplete, EVT.list.stopCrawl] 36 | hiddenEvents.forEach((evt) => { 37 | window.addEventListener(evt, () => { 38 | this.hide() 39 | }) 40 | }) 41 | } 42 | 43 | private hide() { 44 | this.btn.style.display = 'none' 45 | } 46 | 47 | private show() { 48 | this.btn.style.display = 'flex' 49 | } 50 | } 51 | 52 | new StopCrawl() 53 | -------------------------------------------------------------------------------- /src/ts/crawlArtworkPage/InitBookmarkDetailPage.ts: -------------------------------------------------------------------------------- 1 | // 初始化 bookmark_detail 页面 2 | import { InitPageBase } from '../crawl/InitPageBase' 3 | import { Colors } from '../Colors' 4 | import { lang } from '../Lang' 5 | import { Tools } from '../Tools' 6 | import { options } from '../setting/Options' 7 | import { API } from '../API' 8 | import { store } from '../store/Store' 9 | 10 | class InitBookmarkDetailPage extends InitPageBase { 11 | constructor() { 12 | super() 13 | this.init() 14 | } 15 | 16 | protected addCrawlBtns() { 17 | Tools.addBtn( 18 | 'crawlBtns', 19 | Colors.bgBlue, 20 | '_抓取相似图片', 21 | '_抓取相似图片' 22 | ).addEventListener( 23 | 'click', 24 | () => { 25 | this.readyCrawl() 26 | }, 27 | false 28 | ) 29 | } 30 | 31 | protected initAny() {} 32 | 33 | protected setFormOption() { 34 | // 个数/页数选项的提示 35 | options.setWantPageTip({ 36 | text: '_抓取多少作品', 37 | tip: '_想要获取多少个作品', 38 | rangTip: `1 - ${this.maxCount}`, 39 | min: 1, 40 | max: this.maxCount, 41 | }) 42 | } 43 | 44 | protected getWantPage() { 45 | this.crawlNumber = this.checkWantPageInputGreater0(this.maxCount, false) 46 | } 47 | 48 | // 获取相似的作品列表 49 | protected async getIdList() { 50 | let data = await API.getRecommenderData( 51 | Tools.getIllustId(), 52 | this.crawlNumber 53 | ) 54 | 55 | for (const id of data.recommendations) { 56 | store.idList.push({ 57 | type: 'illusts', 58 | id: id.toString(), 59 | }) 60 | } 61 | 62 | this.getIdListFinished() 63 | } 64 | } 65 | export { InitBookmarkDetailPage } 66 | -------------------------------------------------------------------------------- /src/ts/crawlArtworkPage/InitDiscoverPage.ts: -------------------------------------------------------------------------------- 1 | // 初始化发现页面 2 | import { InitPageBase } from '../crawl/InitPageBase' 3 | import { Colors } from '../Colors' 4 | import { lang } from '../Lang' 5 | import { Tools } from '../Tools' 6 | import { options } from '../setting/Options' 7 | import { store } from '../store/Store' 8 | 9 | class InitDiscoverPage extends InitPageBase { 10 | constructor() { 11 | super() 12 | this.init() 13 | } 14 | 15 | protected addCrawlBtns() { 16 | Tools.addBtn( 17 | 'crawlBtns', 18 | Colors.bgBlue, 19 | '_抓取当前作品', 20 | '_抓取当前作品Title' 21 | ).addEventListener('click', () => { 22 | this.readyCrawl() 23 | }) 24 | } 25 | 26 | protected setFormOption() { 27 | options.hideOption([1]) 28 | } 29 | 30 | protected getWantPage() {} 31 | 32 | protected getIdList() { 33 | // 在发现页面,直接获取页面上显示的作品,不需要获取列表页 34 | if (location.pathname.includes('/novel')) { 35 | // 小说页面 36 | const allWork = document.querySelectorAll( 37 | '.gtm-novel-work-recommend-link' 38 | ) 39 | allWork.forEach((div) => { 40 | const a = div.querySelector('a') 41 | if (a) { 42 | const id = Tools.getNovelId(a.href) 43 | store.idList.push({ 44 | type: 'novels', 45 | id, 46 | }) 47 | } 48 | }) 49 | } else { 50 | // 插画漫画页面 51 | const allLink = document.querySelectorAll( 52 | 'div[size="184"] a' 53 | ) as NodeListOf 54 | // 获取已有作品的 id 55 | allLink.forEach((a) => { 56 | const id = Tools.getIllustId(a.href) 57 | store.idList.push({ 58 | type: 'illusts', 59 | id, 60 | }) 61 | }) 62 | } 63 | this.getIdListFinished() 64 | } 65 | } 66 | export { InitDiscoverPage } 67 | -------------------------------------------------------------------------------- /src/ts/crawlMixedPage/InitBookmarkLegacyPage.ts: -------------------------------------------------------------------------------- 1 | // 初始化旧版收藏页面 2 | // 该页面类型已不复存在,这个类仅做占位之用。 3 | // 在 PageType 里不能删除 BookmarkLegacy 枚举成员,因为有些代码里硬编码了它的枚举值 3 4 | // 例如:settings.wantPageArr[pageType.type] 5 | // settings.nameRuleForEachPageType[pageType.type] 6 | import { InitPageBase } from '../crawl/InitPageBase' 7 | 8 | class InitBookmarkLegacyPage extends InitPageBase { 9 | constructor() { 10 | super() 11 | this.init() 12 | } 13 | } 14 | export { InitBookmarkLegacyPage } 15 | -------------------------------------------------------------------------------- /src/ts/crawlMixedPage/InitUnlistedPage.ts: -------------------------------------------------------------------------------- 1 | //初始化 Unlisted 作品页 2 | import { InitPageBase } from '../crawl/InitPageBase' 3 | import { Colors } from '../Colors' 4 | import { options } from '../setting/Options' 5 | import { Tools } from '../Tools' 6 | import { Utils } from '../utils/Utils' 7 | import { IDData } from '../store/StoreType' 8 | import { store } from '../store/Store' 9 | 10 | class InitUnlistedPage extends InitPageBase { 11 | constructor() { 12 | super() 13 | this.init() 14 | } 15 | 16 | protected addCrawlBtns() { 17 | Tools.addBtn('crawlBtns', Colors.bgBlue, '_抓取此作品').addEventListener( 18 | 'click', 19 | () => { 20 | this.readyCrawl() 21 | } 22 | ) 23 | } 24 | 25 | protected setFormOption() { 26 | options.hideOption([1]) 27 | } 28 | 29 | protected destroy() { 30 | Tools.clearSlot('crawlBtns') 31 | Tools.clearSlot('otherBtns') 32 | } 33 | 34 | protected nextStep() { 35 | this.getIdList() 36 | } 37 | 38 | protected getIdList() { 39 | const workId = Utils.getURLPathField(window.location.pathname, 'unlisted') 40 | const isNovel = window.location.href.includes('/novel') 41 | const idData: IDData = { 42 | type: isNovel ? 'novels' : 'illusts', 43 | id: workId, 44 | } 45 | store.idList = [idData] 46 | 47 | this.getIdListFinished() 48 | } 49 | } 50 | 51 | export { InitUnlistedPage } 52 | -------------------------------------------------------------------------------- /src/ts/crawlNovelPage/GetNovelGlossarys.ts: -------------------------------------------------------------------------------- 1 | import { API } from '../API' 2 | import { GlossaryItem, NovelSeriesGlossaryItem } from '../crawl/CrawlResult' 3 | 4 | interface GlossaryResult { 5 | id: string 6 | seriesId: string 7 | name: string 8 | items: GlossaryItem[] 9 | } 10 | 11 | class GetNovelGlossarys { 12 | /**获取系列小说的设定资料 */ 13 | public async getGlossarys( 14 | seriesId: string | number 15 | ): Promise { 16 | return new Promise(async (resolve, reject) => { 17 | // 先获取设定资料的分类、每条设定资料的简略数据 18 | // 注意此时每条设定资料缺少 detail 数据(此时为 null) 19 | const glossaryData = await API.getNovelSeriesGlossary(seriesId) 20 | const result = glossaryData.body.categories as unknown as GlossaryResult[] 21 | 22 | if (result.length === 0) { 23 | return resolve(result) 24 | } 25 | 26 | // 请求每条设定资料的详细数据 27 | for (const categorie of result) { 28 | for (const item of categorie.items) { 29 | const data = await API.getNovelSeriesGlossaryItem( 30 | item.seriesId, 31 | item.id 32 | ) 33 | item.detail = data.body.item.detail 34 | } 35 | } 36 | 37 | return resolve(result) 38 | }) 39 | } 40 | 41 | /**把设定资料用特定格式存储起来 */ 42 | public storeGlossaryText(data: GlossaryResult[]) { 43 | const array: string[] = [] 44 | for (const categorie of data) { 45 | array.push(categorie.name) 46 | array.push('\n\n') 47 | 48 | for (const item of categorie.items) { 49 | array.push(item.name) 50 | array.push('\n') 51 | array.push(item.overview) 52 | array.push('\n\n') 53 | if (item.detail) { 54 | array.push(item.detail) 55 | array.push('\n\n') 56 | } 57 | array.push('----------------------------------------') 58 | array.push('\n\n') 59 | } 60 | } 61 | if (array.length > 0) { 62 | return array.join('') + '\n\n' 63 | } 64 | return '' 65 | } 66 | } 67 | 68 | const getNovelGlossarys = new GetNovelGlossarys() 69 | export { getNovelGlossarys } 70 | -------------------------------------------------------------------------------- /src/ts/download/DownloadNovelCover.ts: -------------------------------------------------------------------------------- 1 | import { lang } from '../Lang' 2 | import { log } from '../Log' 3 | import { Utils } from '../utils/Utils' 4 | 5 | class DownloadNovelCover { 6 | /**下载小说的封面图片 7 | * 8 | * 默认是正常下载小说的情况,可以设置为合并系列小说的情况 9 | */ 10 | public async download( 11 | coverURL: string, 12 | novelName: string, 13 | action: 'downloadNovel' | 'mergeNovel' = 'downloadNovel' 14 | ) { 15 | log.log(lang.transl('_下载封面图片'), 1, false, 'downloadNovelCover') 16 | 17 | const url = await this.getCoverBolbURL(coverURL) 18 | let coverName = Utils.replaceSuffix(novelName, coverURL) 19 | 20 | // 合并系列小说时,文件直接保存在下载目录里,封面图片也保存在下载目录里 21 | // 所以要替换掉封面图路径里的斜线 22 | if (action === 'mergeNovel') { 23 | coverName = Utils.replaceUnsafeStr(coverName) 24 | } 25 | this.sendDownload(url, coverName) 26 | } 27 | 28 | // 生成封面图片的 Blob URL 29 | private async getCoverBolbURL(coverURL: string): Promise { 30 | return new Promise(async (resolve, reject) => { 31 | const res = await fetch(coverURL, { 32 | method: 'get', 33 | credentials: 'same-origin', 34 | }) 35 | const blob = await res.blob() 36 | const url = URL.createObjectURL(blob) 37 | return resolve(url) 38 | }) 39 | } 40 | 41 | private sendDownload(url: string, name: string) { 42 | chrome.runtime.sendMessage({ 43 | msg: 'save_novel_cover_file', 44 | fileUrl: url, 45 | fileName: name, 46 | }) 47 | } 48 | } 49 | 50 | const downloadNovelCover = new DownloadNovelCover() 51 | export { downloadNovelCover } 52 | -------------------------------------------------------------------------------- /src/ts/download/DownloadOnClickBookmark.ts: -------------------------------------------------------------------------------- 1 | import { WorkTypeString } from '../store/StoreType' 2 | import { EVT } from '../EVT' 3 | import { settings } from '../setting/Settings' 4 | import { states } from '../store/States' 5 | import { toast } from '../Toast' 6 | import { Colors } from '../Colors' 7 | import { lang } from '../Lang' 8 | import { workToolBar } from '../WorkToolBar' 9 | import { pageType } from '../PageType' 10 | import { Tools } from '../Tools' 11 | import { artworkThumbnail } from '../ArtworkThumbnail' 12 | import { novelThumbnail } from '../NovelThumbnail' 13 | 14 | // 点击作品的收藏按钮时,下载这个作品 15 | class DownloadOnClickBookmark { 16 | constructor() { 17 | this.bindEvents() 18 | } 19 | 20 | public bindEvents() { 21 | // 在作品缩略图上点击收藏按钮时,下载这个作品 22 | artworkThumbnail.onClickBookmarkBtn((el: HTMLElement, id: string) => { 23 | if (!id) { 24 | id = Tools.findWorkIdFromElement(el, 'illusts') 25 | } 26 | this.send(id) 27 | }) 28 | 29 | novelThumbnail.onClickBookmarkBtn((el: HTMLElement, id: string) => { 30 | if (!id || id === '0') { 31 | id = Tools.findWorkIdFromElement(el, 'novels') 32 | console.log(id) 33 | } 34 | this.send(id, 'novels') 35 | }) 36 | 37 | // 在作品页面里点击收藏按钮时,下载这个作品 38 | workToolBar.register( 39 | ( 40 | toolbar: HTMLDivElement, 41 | pixivBMKDiv: HTMLDivElement, 42 | likeBtn: HTMLButtonElement 43 | ) => { 44 | pixivBMKDiv.addEventListener('click', () => { 45 | if (pageType.type === pageType.list.Artwork) { 46 | this.send(Tools.getIllustId(window.location.href)) 47 | } 48 | 49 | if (pageType.type === pageType.list.Novel) { 50 | this.send(Tools.getNovelId(window.location.href), 'novels') 51 | } 52 | }) 53 | } 54 | ) 55 | } 56 | 57 | /**发送作品的 id 和类型,抓取并下载这个作品 58 | * 59 | * @type 默认值是 'illusts' 60 | */ 61 | public send(id: string, type: WorkTypeString = 'illusts') { 62 | if (settings.downloadOnClickBookmark) { 63 | EVT.fire('crawlIdList', [ 64 | { 65 | id, 66 | type, 67 | }, 68 | ]) 69 | } 70 | } 71 | } 72 | 73 | const downloadOnClickBookmark = new DownloadOnClickBookmark() 74 | export { downloadOnClickBookmark } 75 | -------------------------------------------------------------------------------- /src/ts/download/DownloadOnClickLike.ts: -------------------------------------------------------------------------------- 1 | import { Colors } from '../Colors' 2 | import { EVT } from '../EVT' 3 | import { lang } from '../Lang' 4 | import { pageType } from '../PageType' 5 | import { settings } from '../setting/Settings' 6 | import { states } from '../store/States' 7 | import { WorkTypeString } from '../store/StoreType' 8 | import { toast } from '../Toast' 9 | import { Tools } from '../Tools' 10 | import { workToolBar } from '../WorkToolBar' 11 | 12 | // 在作品页面里点赞时,下载这个作品 13 | class DownloadOnClickLike { 14 | constructor() { 15 | this.bindEvents() 16 | } 17 | 18 | private bindEvents() { 19 | workToolBar.register( 20 | ( 21 | toolbar: HTMLDivElement, 22 | pixivBMKDiv: HTMLDivElement, 23 | likeBtn: HTMLButtonElement 24 | ) => { 25 | likeBtn.addEventListener('click', () => { 26 | if (pageType.type === pageType.list.Artwork) { 27 | this.send(Tools.getIllustId(window.location.href)) 28 | } 29 | 30 | if (pageType.type === pageType.list.Novel) { 31 | this.send(Tools.getNovelId(window.location.href), 'novels') 32 | } 33 | }) 34 | } 35 | ) 36 | } 37 | 38 | /**发送作品的 id 和类型,抓取并下载这个作品 39 | * 40 | * @type 默认值是 'illusts' 41 | */ 42 | private send(id: string, type: WorkTypeString = 'illusts') { 43 | if (settings.downloadOnClickLike) { 44 | EVT.fire('crawlIdList', [ 45 | { 46 | id, 47 | type, 48 | }, 49 | ]) 50 | } 51 | } 52 | } 53 | 54 | new DownloadOnClickLike() 55 | -------------------------------------------------------------------------------- /src/ts/download/DownloadStates.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from '../EVT' 2 | import { store } from '../store/Store' 3 | 4 | // 每个任务会在数组中的对应位置用一个数字表示它的下载状态。数字和含义: 5 | // -1 未开始下载 6 | // 0 下载中 7 | // 1 下载完成 8 | type DLStatesI = (-1 | 0 | 1)[] 9 | 10 | // 下载状态列表 11 | class DownloadStates { 12 | constructor() { 13 | this.bindEvents() 14 | } 15 | 16 | public states: DLStatesI = [] 17 | 18 | private bindEvents() { 19 | // 初始化下载状态 20 | const evs = [EVT.list.crawlComplete, EVT.list.resultChange] 21 | for (const ev of evs) { 22 | window.addEventListener(ev, () => { 23 | this.init() 24 | }) 25 | } 26 | } 27 | 28 | // 创建新的状态列表 29 | public init() { 30 | this.states = new Array(store.result.length).fill(-1) 31 | } 32 | 33 | // 统计下载完成的数量 34 | public downloadedCount() { 35 | let count = 0 36 | const length = this.states.length 37 | for (let i = 0; i < length; i++) { 38 | if (this.states[i] === 1) { 39 | count++ 40 | } 41 | } 42 | return count 43 | } 44 | 45 | // 接受传入的状态数据 46 | // 目前只有在恢复下载的时候使用 47 | public replace(states: DLStatesI) { 48 | this.states = states 49 | } 50 | 51 | // 恢复之前的下载任务 52 | // 这会把之前的“下载中”标记复位到“未开始下载”,以便再次下载 53 | public resume() { 54 | const length = this.states.length 55 | for (let i = 0; i < length; i++) { 56 | if (this.states[i] === 0) { 57 | this.setState(i, -1) 58 | } 59 | } 60 | } 61 | 62 | // 获取第一个“未开始下载”标记的索引 63 | public getFirstDownloadItem() { 64 | const length = this.states.length 65 | for (let i = 0; i < length; i++) { 66 | if (this.states[i] === -1) { 67 | this.setState(i, 0) 68 | return i 69 | } 70 | } 71 | return undefined 72 | } 73 | 74 | // 设置已下载列表中的标记 75 | public setState(index: number, value: -1 | 0 | 1) { 76 | this.states[index] = value 77 | } 78 | 79 | public clear() { 80 | this.states = [] 81 | } 82 | } 83 | 84 | const downloadStates = new DownloadStates() 85 | export { downloadStates, DLStatesI } 86 | -------------------------------------------------------------------------------- /src/ts/download/DownloadType.d.ts: -------------------------------------------------------------------------------- 1 | import { Result } from '../store/StoreType' 2 | 3 | export interface TaskList { 4 | [id: string]: { 5 | index: number 6 | progressBarIndex: number 7 | } 8 | } 9 | 10 | export interface downloadArgument { 11 | id: string 12 | result: Result 13 | index: number 14 | progressBarIndex: number 15 | taskBatch: number 16 | } 17 | 18 | // 前台向后台发送的任务信息 19 | export interface SendToBackEndData { 20 | msg: string 21 | fileUrl: string 22 | fileName: string 23 | id: string 24 | taskBatch: number 25 | } 26 | 27 | // 浏览器下载时每个任务的信息 28 | export interface DonwloadSuccessData { 29 | url: string 30 | id: string 31 | tabId: number 32 | uuid: boolean 33 | } 34 | 35 | export interface DonwloadSkipData { 36 | id: string 37 | reason: 38 | | 'duplicate' 39 | | 'size' 40 | | 'color' 41 | | 'widthHeight' 42 | | '404' 43 | | '500' 44 | | 'excludedType' 45 | } 46 | 47 | // 所有任务的信息 48 | export interface DonwloadListData { 49 | [key: number]: DonwloadSuccessData | null 50 | } 51 | 52 | // 下载完成后返回的信息 53 | export interface DownloadedMsg { 54 | msg: string 55 | data: DonwloadSuccessData 56 | err?: string 57 | } 58 | -------------------------------------------------------------------------------- /src/ts/download/ExportLST.ts: -------------------------------------------------------------------------------- 1 | import { Tools } from '../Tools' 2 | import { store } from '../store/Store' 3 | import { fileName } from '../FileName' 4 | import { lang } from '../Lang' 5 | import { Utils } from '../utils/Utils' 6 | import { toast } from '../Toast' 7 | 8 | // 输出 lst 文件 9 | class ExportLST { 10 | constructor() { 11 | this.bindEvents() 12 | } 13 | 14 | private readonly separate = '?/' // 分隔符 15 | private readonly CRLF = '\r\n' // 换行符 16 | 17 | private bindEvents() { 18 | window.addEventListener( 19 | 'keydown', 20 | (ev) => { 21 | if (ev.altKey && ev.code === 'KeyL') { 22 | this.createLst() 23 | } 24 | }, 25 | false 26 | ) 27 | } 28 | 29 | private createLst() { 30 | if (store.result.length === 0) { 31 | toast.error(lang.transl('_没有可用的抓取结果')) 32 | return 33 | } 34 | 35 | const array: string[] = [] 36 | for (const data of store.result) { 37 | array.push(data.original + this.separate + fileName.createFileName(data)) 38 | } 39 | 40 | const result = array.join(this.CRLF) 41 | const blob = new Blob([result]) 42 | const url = URL.createObjectURL(blob) 43 | const name = Tools.getPageTitle() + '.lst' 44 | 45 | Utils.downloadFile(url, name) 46 | } 47 | } 48 | 49 | new ExportLST() 50 | -------------------------------------------------------------------------------- /src/ts/download/ExportResult.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from '../EVT' 2 | import { Tools } from '../Tools' 3 | import { store } from '../store/Store' 4 | import { lang } from '../Lang' 5 | import { Utils } from '../utils/Utils' 6 | import { toast } from '../Toast' 7 | import { log } from '../Log' 8 | 9 | class ExportResult { 10 | constructor() { 11 | this.bindEvents() 12 | } 13 | 14 | private bindEvents() { 15 | window.addEventListener(EVT.list.exportResult, () => { 16 | this.output() 17 | }) 18 | } 19 | 20 | private async output() { 21 | if (store.result.length === 0) { 22 | toast.error(lang.transl('_没有可用的抓取结果')) 23 | return 24 | } 25 | 26 | const resultList = await Utils.json2BlobSafe(store.result) 27 | for (const result of resultList) { 28 | Utils.downloadFile( 29 | result.url, 30 | `result-total ${result.total}-${Utils.replaceUnsafeStr( 31 | Tools.getPageTitle() 32 | )}-${Utils.replaceUnsafeStr( 33 | store.crawlCompleteTime.toLocaleString() 34 | )}.json` 35 | ) 36 | } 37 | 38 | const msg = lang.transl('_导出成功') 39 | log.success(msg) 40 | toast.success(msg) 41 | } 42 | } 43 | 44 | new ExportResult() 45 | -------------------------------------------------------------------------------- /src/ts/download/ShowConvertCount.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from '../EVT' 2 | import { lang } from '../Lang' 3 | 4 | // 显示正在转换的文件数量 5 | class ShowConvertCount { 6 | constructor(el: HTMLElement) { 7 | this.el = el 8 | lang.register(this.el) 9 | this.bindEvents() 10 | } 11 | 12 | private el: HTMLElement // 显示提示文本的容器 13 | 14 | private bindEvents() { 15 | window.addEventListener(EVT.list.convertChange, (ev: CustomEventInit) => { 16 | const count = ev.detail.data 17 | if (count > 0) { 18 | lang.updateText(this.el, '_转换任务提示', count.toString()) 19 | } else { 20 | this.el.textContent = '' 21 | lang.updateText(this.el, '') 22 | } 23 | }) 24 | } 25 | } 26 | 27 | export { ShowConvertCount } 28 | -------------------------------------------------------------------------------- /src/ts/download/ShowDownloadStates.ts: -------------------------------------------------------------------------------- 1 | import { Colors } from '../Colors' 2 | import { EVT } from '../EVT' 3 | import { lang } from '../Lang' 4 | 5 | // 显示下载状态 6 | class ShowDownloadStates { 7 | constructor(el: HTMLElement) { 8 | this.el = el 9 | this.bindEvents() 10 | } 11 | 12 | private el: HTMLElement 13 | 14 | private bindEvents() { 15 | for (const ev of [ 16 | EVT.list.crawlComplete, 17 | EVT.list.resultChange, 18 | EVT.list.resume, 19 | ]) { 20 | window.addEventListener(ev, () => { 21 | this.setText('_未开始下载') 22 | }) 23 | } 24 | 25 | window.addEventListener(EVT.list.downloadStart, () => { 26 | this.setText('_正在下载中') 27 | }) 28 | 29 | window.addEventListener(EVT.list.downloadPause, () => { 30 | this.setText('_已暂停', '#f00') 31 | }) 32 | 33 | window.addEventListener(EVT.list.downloadStop, () => { 34 | this.setText('_已停止', '#f00') 35 | }) 36 | 37 | window.addEventListener(EVT.list.downloadComplete, () => { 38 | this.setText('_下载完毕', Colors.textSuccess) 39 | }) 40 | } 41 | 42 | private setText(textFlag: string, color: string = Colors.bgBlue) { 43 | lang.updateText(this.el, textFlag) 44 | this.el.style.color = color 45 | } 46 | } 47 | 48 | export { ShowDownloadStates } 49 | -------------------------------------------------------------------------------- /src/ts/download/ShowRemainingDownloadOnTitle.ts: -------------------------------------------------------------------------------- 1 | import { store } from '../store/Store' 2 | import { states } from '../store/States' 3 | import { EVT } from '../EVT' 4 | 5 | // 在网页标题上显示剩余下载数量 6 | class ShowRemainingDownloadOnTitle { 7 | constructor() { 8 | this.bindEvents() 9 | } 10 | 11 | private bindEvents() { 12 | window.setInterval(() => { 13 | this.show() 14 | }, 500) 15 | 16 | const removeStrEvents = [ 17 | EVT.list.downloadStop, 18 | EVT.list.downloadComplete, 19 | EVT.list.crawlStart, 20 | ] 21 | 22 | for (const evt of removeStrEvents) { 23 | window.addEventListener(evt, () => { 24 | this.removeStr() 25 | }) 26 | } 27 | } 28 | 29 | // 生成新的字符串 30 | private createStr() { 31 | if (store.remainingDownload > 0) { 32 | return ` ${store.remainingDownload} ` 33 | } 34 | return '' 35 | } 36 | 37 | // 保存缓存的字符串,后面会需要在标题中查找缓存的字符串 38 | private str = this.createStr() 39 | 40 | // 检查标题中是否有下载状态的 flag 41 | // 如果没有,就不会显示剩余数量 42 | private checkStatusFlag() { 43 | return document.title.indexOf(']') > 0 44 | } 45 | 46 | private show() { 47 | if (!states.downloading || !this.checkStatusFlag()) { 48 | return 49 | } 50 | 51 | // 先移除旧的字符串,然后添加新的字符串 52 | const title = this.removeStr(document.title) 53 | 54 | this.str = this.createStr() 55 | 56 | if (!this.str || !title) { 57 | return 58 | } 59 | 60 | document.title = title.replace(']', ']' + this.str) 61 | } 62 | 63 | private removeStr(): undefined 64 | // 如果传入字符串,则不直接修改 document.title,以提高性能 65 | private removeStr(titleStr: string): string 66 | private removeStr(titleStr?: string) { 67 | if (!this.str) { 68 | return 69 | } 70 | 71 | if (titleStr) { 72 | return titleStr.replace(this.str, '') 73 | } else { 74 | document.title = document.title.replace(this.str, '') 75 | } 76 | } 77 | } 78 | 79 | new ShowRemainingDownloadOnTitle() 80 | -------------------------------------------------------------------------------- /src/ts/download/ShowSkipCount.ts: -------------------------------------------------------------------------------- 1 | import { EVT } from '../EVT' 2 | import { lang } from '../Lang' 3 | 4 | // 显示跳过下载的文件数量 5 | class ShowSkipCount { 6 | constructor(el: HTMLElement) { 7 | this.el = el 8 | lang.register(this.el) 9 | this.bindEvents() 10 | } 11 | 12 | private count = 0 // 跳过下载的数量 13 | private el: HTMLElement // 显示提示文本的容器 14 | 15 | private bindEvents() { 16 | window.addEventListener(EVT.list.crawlStart, () => { 17 | this.reset() 18 | }) 19 | 20 | window.addEventListener(EVT.list.downloadStop, () => { 21 | // 重置计数但不清空提示文字,因为用户还需要看 22 | this.count = 0 23 | }) 24 | 25 | window.addEventListener(EVT.list.skipDownload, () => { 26 | this.addCount() 27 | }) 28 | 29 | window.addEventListener(EVT.list.downloadStart, () => { 30 | if (this.count === 0) { 31 | this.reset() 32 | } 33 | }) 34 | 35 | window.addEventListener(EVT.list.resultChange, () => { 36 | this.reset() 37 | }) 38 | 39 | window.addEventListener(EVT.list.downloadComplete, () => { 40 | // 重置计数但不清空提示文字,因为用户还需要看 41 | this.count = 0 42 | }) 43 | } 44 | 45 | private addCount() { 46 | this.count++ 47 | lang.updateText(this.el, '_已跳过n个文件', this.count.toString()) 48 | } 49 | 50 | private reset() { 51 | this.count = 0 52 | lang.updateText(this.el, '') 53 | } 54 | } 55 | 56 | export { ShowSkipCount } 57 | -------------------------------------------------------------------------------- /src/ts/download/ShowTotalResultOnTitle.ts: -------------------------------------------------------------------------------- 1 | import { store } from '../store/Store' 2 | import { states } from '../store/States' 3 | import { EVT } from '../EVT' 4 | 5 | // 抓取阶段,在网页标题上显示抓取到的结果数量 6 | class ShowTotalResultOnTitle { 7 | constructor() { 8 | this.bindEvents() 9 | } 10 | 11 | private enable = false 12 | 13 | private bindEvents() { 14 | const enableEvts = [EVT.list.crawlStart, EVT.list.resultChange] 15 | 16 | enableEvts.forEach((evt) => { 17 | window.addEventListener(evt, () => { 18 | this.removeStr() 19 | this.enable = true 20 | }) 21 | }) 22 | 23 | const disableEvts = [ 24 | EVT.list.downloadStart, 25 | EVT.list.downloadPause, 26 | EVT.list.downloadStop, 27 | ] 28 | 29 | disableEvts.forEach((evt) => { 30 | window.addEventListener(evt, () => { 31 | this.removeStr() 32 | this.enable = false 33 | }) 34 | }) 35 | 36 | window.setInterval(() => { 37 | this.enable && this.show() 38 | }, 500) 39 | } 40 | 41 | // 生成新的字符串 42 | private createStr() { 43 | if (store.result.length > 0) { 44 | return ` ${store.result.length} ` 45 | } 46 | return '' 47 | } 48 | 49 | // 保存缓存的字符串,后面会需要在标题中查找缓存的字符串 50 | private str = this.createStr() 51 | 52 | // 检查标题中是否有下载状态的 flag 53 | // 如果没有,就不会显示剩余数量 54 | private checkStatusFlag() { 55 | return document.title.indexOf(']') > 0 56 | } 57 | 58 | private show() { 59 | if (states.downloading || !this.checkStatusFlag()) { 60 | return 61 | } 62 | 63 | // 先移除旧的字符串,然后添加新的字符串 64 | const title = this.removeStr(document.title) 65 | 66 | this.str = this.createStr() 67 | 68 | if (!this.str || !title) { 69 | return 70 | } 71 | 72 | document.title = title.replace(']', ']' + this.str) 73 | } 74 | 75 | // 如果传入字符串,则不直接修改 document.title,以提高性能 76 | private removeStr(titleStr?: string) { 77 | if (!this.str) { 78 | return 79 | } 80 | 81 | if (titleStr) { 82 | return titleStr.replace(this.str, '') 83 | } else { 84 | document.title = document.title.replace(this.str, '') 85 | } 86 | } 87 | } 88 | 89 | new ShowTotalResultOnTitle() 90 | -------------------------------------------------------------------------------- /src/ts/output/ShowURLs.ts: -------------------------------------------------------------------------------- 1 | import { store } from '../store/Store' 2 | import { EVT } from '../EVT' 3 | import { lang } from '../Lang' 4 | import { settings } from '../setting/Settings' 5 | import { toast } from '../Toast' 6 | import { Config } from '../Config' 7 | 8 | // 显示 url 9 | class ShowURLs { 10 | constructor() { 11 | this.bindEvents() 12 | } 13 | 14 | private bindEvents() { 15 | window.addEventListener(EVT.list.showURLs, () => { 16 | this.showURLs() 17 | }) 18 | } 19 | 20 | private showURLs() { 21 | const urls: string[] = [] 22 | const size = settings.imageSize 23 | for (const data of store.result) { 24 | // 只输出图片文件的 url 25 | // 小说文件没有固定的 url 所以不输出 26 | if (data.type !== 3) { 27 | urls.push(data[size]) 28 | } 29 | } 30 | 31 | if (store.result.length === 0 || urls.length === 0) { 32 | return toast.error(lang.transl('_没有可用的抓取结果')) 33 | } 34 | 35 | let result = '' 36 | if (store.result.length < Config.outputMax) { 37 | result = urls.join('
') 38 | } else { 39 | result = urls.join('\n') 40 | } 41 | 42 | EVT.fire('output', { 43 | content: result, 44 | title: '_复制url', 45 | }) 46 | } 47 | } 48 | 49 | new ShowURLs() 50 | -------------------------------------------------------------------------------- /src/ts/pageFunciton/DestroyManager.ts: -------------------------------------------------------------------------------- 1 | import { pageType } from '../PageType' 2 | import { EVT } from '../EVT' 3 | 4 | // 管理所有页面的销毁事件 5 | // 页面把自己的 destory 函数注册到这个类里,当页面类型变化时会自动执行对应 6 | class DestroyManager { 7 | constructor() { 8 | this.bindEvents() 9 | } 10 | 11 | private list: Map = new Map() 12 | private lastType = pageType.type 13 | 14 | private bindEvents() { 15 | window.addEventListener(EVT.list.pageSwitchedTypeChange, () => { 16 | const fun = this.list.get(this.lastType) 17 | fun && fun() 18 | 19 | this.lastType = pageType.type 20 | }) 21 | } 22 | 23 | // 接收 destory 函数,并关联到对应的页面类型 24 | public register(fun: Function) { 25 | this.list.set(pageType.type, fun) 26 | } 27 | } 28 | 29 | const destroyManager = new DestroyManager() 30 | export { destroyManager } 31 | -------------------------------------------------------------------------------- /src/ts/pageFunciton/DisplayThumbnailListOnMultiImageWorkPage.ts: -------------------------------------------------------------------------------- 1 | import { theme } from '../Theme' 2 | import { Tools } from '../Tools' 3 | import { pageType } from '../PageType' 4 | import { settings } from '../setting/Settings' 5 | import { EVT } from '../EVT' 6 | import { ImageViewer } from '../ImageViewer' 7 | 8 | class DisplayThumbnailListOnMultiImageWorkPage { 9 | constructor() { 10 | this.bindEvents() 11 | } 12 | 13 | private readonly ID = 'viewerWarpper' 14 | private readonly insertTarget = 'main figcaption' 15 | private waitTimer: number | undefined 16 | 17 | private bindEvents() { 18 | window.addEventListener(EVT.list.pageSwitch, () => { 19 | this.init() 20 | }) 21 | 22 | window.addEventListener(EVT.list.settingChange, (ev: CustomEventInit) => { 23 | const data = ev.detail.data as any 24 | if (data.name === 'displayThumbnailListOnMultiImageWorkPage') { 25 | data.value ? this.init() : this.remove() 26 | } 27 | }) 28 | } 29 | 30 | private init() { 31 | this.remove() 32 | 33 | if (!settings.displayThumbnailListOnMultiImageWorkPage) { 34 | return 35 | } 36 | 37 | if ( 38 | pageType.type !== pageType.list.Artwork && 39 | pageType.type !== pageType.list.Unlisted 40 | ) { 41 | return 42 | } 43 | 44 | window.setTimeout(() => { 45 | this.display() 46 | }, 0) 47 | } 48 | 49 | private remove() { 50 | // 删除之前创建的元素,因为切换页面时它不会被自动清理 51 | document.querySelector(`#${this.ID}`)?.remove() 52 | window.clearTimeout(this.waitTimer) 53 | } 54 | 55 | private async display() { 56 | // 等待要插入的目标元素生成 57 | const target = document.querySelector(this.insertTarget) 58 | if (!target) { 59 | this.waitTimer = window.setTimeout(() => { 60 | this.display() 61 | }, 300) 62 | return 63 | } 64 | 65 | // 把缩略图列表添加到页面上 66 | this.remove() 67 | const viewer = new ImageViewer({ 68 | workId: Tools.getIllustId(), 69 | imageNumber: 2, 70 | }) 71 | const wrap = await viewer.init() 72 | if (wrap) { 73 | wrap.id = this.ID 74 | theme.register(wrap) 75 | wrap.style.display = 'block' 76 | target.insertAdjacentElement('beforebegin', wrap) 77 | } 78 | } 79 | } 80 | 81 | new DisplayThumbnailListOnMultiImageWorkPage() 82 | -------------------------------------------------------------------------------- /src/ts/pageFunciton/SaveAvatarIcon.ts: -------------------------------------------------------------------------------- 1 | import { lang } from '../Lang' 2 | import { API } from '../API' 3 | import { log } from '../Log' 4 | import { Tools } from '../Tools' 5 | import { EVT } from '../EVT' 6 | import { img2ico } from '../utils/imageToIcon' 7 | import { Utils } from '../utils/Utils' 8 | import { toast } from '../Toast' 9 | 10 | // 保存用户头像为图标 11 | class SaveAvatarIcon { 12 | constructor() { 13 | this.bindEvents() 14 | } 15 | 16 | private bindEvents() { 17 | window.addEventListener(EVT.list.saveAvatarIcon, () => { 18 | this.saveAvatarIcon() 19 | }) 20 | } 21 | 22 | private async saveAvatarIcon() { 23 | const userId = Tools.getCurrentPageUserID() 24 | const userProfile = await API.getUserProfile(userId) 25 | const bigImg = userProfile.body.imageBig // imageBig 并不是头像原图,而是裁剪成 170 px 的尺寸 26 | const fullSizeImg = bigImg.replace('_170', '') // 去掉 170 标记,获取头像图片的原图 27 | 28 | // 生成 ico 文件 29 | // 尺寸固定为 256,因为尺寸更小的图标如 128,在 windows 资源管理器里会被缩小到 48 显示 30 | const blob = await img2ico.convert({ 31 | size: [256], 32 | source: fullSizeImg, 33 | shape: 'fillet', 34 | bleed: true, 35 | }) 36 | 37 | // 直接保存到下载文件夹 38 | const url = URL.createObjectURL(blob) 39 | const name = `${userProfile.body.name}_${userId}_icon.ico` 40 | Utils.downloadFile(url, name) 41 | 42 | const msg = '✓ ' + lang.transl('_保存用户头像为图标') 43 | log.success(msg) 44 | toast.success(msg) 45 | EVT.fire('closeCenterPanel') 46 | } 47 | } 48 | 49 | new SaveAvatarIcon() 50 | -------------------------------------------------------------------------------- /src/ts/pageFunciton/SaveAvatarImage.ts: -------------------------------------------------------------------------------- 1 | import { lang } from '../Lang' 2 | import { API } from '../API' 3 | import { log } from '../Log' 4 | import { Tools } from '../Tools' 5 | import { EVT } from '../EVT' 6 | import { Utils } from '../utils/Utils' 7 | import { toast } from '../Toast' 8 | 9 | // 保存用户头像 10 | class SaveAvatarImage { 11 | constructor() { 12 | this.bindEvents() 13 | } 14 | 15 | private bindEvents() { 16 | window.addEventListener(EVT.list.saveAvatarImage, () => { 17 | this.saveAvatarImage() 18 | }) 19 | } 20 | 21 | private async saveAvatarImage() { 22 | const userId = Tools.getCurrentPageUserID() 23 | const userProfile = await API.getUserProfile(userId) 24 | const imageURL = userProfile.body.imageBig 25 | 26 | // 提取图片的后缀名 27 | const arr = imageURL.split('.') 28 | const ext = arr[arr.length - 1] 29 | 30 | // imageBig 并不是头像原图,而是裁剪成 170 px 的尺寸 31 | // 如果是 gif 格式,则不生成其大图 url,因为生成的大图是静态图。不知道 gif 头像是否有大图,以及其 url 是什么样的 32 | // 如果是其他格式,则去掉 170 标记,获取头像图片的原图 33 | const fullSizeImgURL = 34 | ext === 'gif' ? imageURL : imageURL.replace('_170', '') 35 | 36 | // 加载文件 37 | const img = await fetch(fullSizeImgURL) 38 | const blob = await img.blob() 39 | 40 | // 直接保存到下载文件夹 41 | const url = URL.createObjectURL(blob) 42 | const name = `${userProfile.body.name}_${userId}_avatar.${ext}` 43 | Utils.downloadFile(url, name) 44 | 45 | const msg = '✓ ' + lang.transl('_保存用户头像') 46 | log.success(msg) 47 | toast.success(msg) 48 | EVT.fire('closeCenterPanel') 49 | } 50 | } 51 | 52 | new SaveAvatarImage() 53 | -------------------------------------------------------------------------------- /src/ts/pageFunciton/SaveUserCover.ts: -------------------------------------------------------------------------------- 1 | import { lang } from '../Lang' 2 | import { API } from '../API' 3 | import { log } from '../Log' 4 | import { Tools } from '../Tools' 5 | import { EVT } from '../EVT' 6 | import { Utils } from '../utils/Utils' 7 | import { toast } from '../Toast' 8 | 9 | // 保存用户封面图片 10 | class SaveUserCover { 11 | constructor() { 12 | this.bindEvents() 13 | } 14 | 15 | private bindEvents() { 16 | window.addEventListener(EVT.list.saveUserCover, () => { 17 | this.saveUserCover() 18 | }) 19 | } 20 | 21 | private async saveUserCover() { 22 | const userId = Tools.getCurrentPageUserID() 23 | const userProfile = await API.getUserProfile(userId) 24 | const bgData = userProfile.body.background 25 | if (bgData === null) { 26 | return toast.error(lang.transl('_没有数据可供使用')) 27 | } 28 | 29 | const bgUrl = bgData.url 30 | 31 | if (!bgUrl) { 32 | return toast.error(lang.transl('_没有数据可供使用')) 33 | } 34 | 35 | // 加载文件 36 | const img = await fetch(bgUrl) 37 | const blob = await img.blob() 38 | 39 | // 提取后缀名 40 | const arr = bgUrl.split('.') 41 | const ext = arr[arr.length - 1] 42 | 43 | // 直接保存到下载文件夹 44 | const url = URL.createObjectURL(blob) 45 | const name = `${userProfile.body.name}_${userId}_cover.${ext}` 46 | Utils.downloadFile(url, name) 47 | 48 | const msg = '✓ ' + lang.transl('_保存用户封面') 49 | log.success(msg) 50 | toast.success(msg) 51 | EVT.fire('closeCenterPanel') 52 | } 53 | } 54 | 55 | new SaveUserCover() 56 | -------------------------------------------------------------------------------- /src/ts/setting/ConvertOldSettings.ts: -------------------------------------------------------------------------------- 1 | interface Data { 2 | [key: string]: { 3 | [key: string]: string 4 | } 5 | } 6 | 7 | // 为了兼容以前的版本的设置,把旧的设置值转换为新版本的设置值 8 | class ConvertOldSettings { 9 | // 旧设置和新设置的对应关系 10 | // 为了集中管理,便于使用,写到了一个对象里 11 | private readonly data: Data = { 12 | ratio: { 13 | '0': 'square', 14 | '1': 'horizontal', 15 | '2': 'vertical', 16 | '3': 'userSet', 17 | }, 18 | idRange: { 19 | '1': '>', 20 | '2': '<', 21 | }, 22 | widthTag: { 23 | '1': 'yes', 24 | '-1': 'no', 25 | }, 26 | restrict: { 27 | '1': 'yes', 28 | '-1': 'no', 29 | }, 30 | userSetLang: { 31 | '-1': 'auto', 32 | '0': 'zh-cn', 33 | '1': 'ja', 34 | '2': 'en', 35 | '3': 'zh-tw', 36 | '4': 'ko', 37 | }, 38 | } 39 | 40 | // 传递需要转换的设置的键值 41 | public convert(key: string, value: string): string { 42 | const map = this.data[key] 43 | // 如果这是一个可以转换的设置 44 | if (map) { 45 | // 如果传递的值是旧的设置值,则能够获取到新的设置值 46 | // 如果传递的值已经是新的设置值,则获取到的是 undefined ,此时不需要转换 47 | const newValue = map[value] 48 | if (newValue !== undefined) { 49 | return newValue 50 | } 51 | } 52 | 53 | return value 54 | } 55 | } 56 | 57 | const convertOldSettings = new ConvertOldSettings() 58 | 59 | export { convertOldSettings } 60 | -------------------------------------------------------------------------------- /src/ts/setting/InvisibleSettings.ts: -------------------------------------------------------------------------------- 1 | import { settings, setSetting, SettingKeys } from './Settings' 2 | import { secretSignal } from '../utils/SecretSignal' 3 | import { log } from '../Log' 4 | import { toast } from '../Toast' 5 | 6 | type Cfg = { 7 | [key in SettingKeys]?: string[] 8 | } 9 | 10 | // 管理不可见的设置。通过预设的按键,切换其开关状态 11 | class InvisibleSettings { 12 | constructor() { 13 | this.register() 14 | } 15 | 16 | // ppdss: Powerful Pixiv Downloader Secret Settings 17 | private readonly cfg: Cfg = { 18 | createFolderBySl: ['ppdss1', 'switchsl', 'kaiguansl'], 19 | downloadUgoiraFirst: ['ppdss2', 'dlugoirafirst', 'qw111'], 20 | } 21 | 22 | private register() { 23 | for (const [name, codes] of Object.entries(this.cfg)) { 24 | for (const code of codes!) { 25 | secretSignal.register(code, () => { 26 | this.onChange(name as SettingKeys) 27 | }) 28 | } 29 | } 30 | } 31 | 32 | private onChange(name: SettingKeys) { 33 | const nowValue = settings[name] 34 | if (typeof nowValue !== 'boolean') { 35 | return 36 | } 37 | 38 | // 如果查找到了符合的记录,则反转这个设置的值 39 | const newValue = !settings[name] 40 | setSetting(name, newValue) 41 | 42 | // 显示提示信息 43 | if (settings[name]) { 44 | const msg = name + ' On' 45 | log.success(msg) 46 | toast.success(msg) 47 | } else { 48 | const msg = name + ' Off' 49 | log.warning(msg) 50 | toast.warning(msg) 51 | } 52 | } 53 | } 54 | 55 | new InvisibleSettings() 56 | -------------------------------------------------------------------------------- /src/ts/showDownloadBtnOnThumb.ts: -------------------------------------------------------------------------------- 1 | import { Config } from './Config' 2 | import { ShowDownloadBtnOnThumbOnDesktop } from './ShowDownloadBtnOnThumbOnDesktop' 3 | import { ShowDownloadBtnOnThumbOnMobile } from './ShowDownloadBtnOnThumbOnMobile' 4 | 5 | // 在图片作品的缩略图上显示下载按钮,点击按钮可以直接下载这个作品 6 | class ShowDownloadBtnOnThumb { 7 | constructor() { 8 | Config.mobile 9 | ? new ShowDownloadBtnOnThumbOnMobile() 10 | : new ShowDownloadBtnOnThumbOnDesktop() 11 | } 12 | } 13 | 14 | new ShowDownloadBtnOnThumb() 15 | -------------------------------------------------------------------------------- /src/ts/store/CacheWorkData.ts: -------------------------------------------------------------------------------- 1 | import { ArtworkData } from '../crawl/CrawlResult' 2 | 3 | // 本程序有多个模块需要在抓取流程之外获取作品数据 4 | // 为了避免重复发起请求,以及解决浏览器有时候不读取缓存的问题,所以在这里缓存一些作品数据 5 | // 即使下载器获取过某个作品的数据,但是以后再次请求时,浏览器也有可能不会读取缓存,而是重新发起请求。 6 | class CacheWorkData { 7 | private cache: ArtworkData[] = [] 8 | // 一个图像作品的数据大约是 5 KB 9 | private readonly max = 100 10 | 11 | public set(data: ArtworkData) { 12 | if (this.has(data.body.id)) { 13 | return 14 | } 15 | if (this.cache.length >= this.max) { 16 | this.cache.shift() 17 | } 18 | this.cache.push(data) 19 | } 20 | 21 | public get(id: string) { 22 | return this.cache.find((val) => val.body.id === id) 23 | } 24 | 25 | public has(id: string) { 26 | return this.cache.some((val) => val.body.id === id) 27 | } 28 | } 29 | 30 | const cacheWorkData = new CacheWorkData() 31 | export { cacheWorkData } 32 | -------------------------------------------------------------------------------- /src/ts/store/IdListWithPageNo.ts: -------------------------------------------------------------------------------- 1 | import { store } from './Store' 2 | import { IDData } from './StoreType' 3 | import { Utils } from '../utils/Utils' 4 | 5 | type IDDataWithPageNo = IDData & { 6 | page: number 7 | } 8 | 9 | // 这是为了解决抓取多个列表页面时,获得的 id 数据顺序混乱的问题 10 | // 原因:如果下载器在获取列表页时(getIdList),有多个页面作品列表,并且同时抓取多个页面 11 | // 例如同时抓取 1、2、3 页,由于网络请求耗时不固定,所以完成顺序可能是乱的,如 2、3、1 12 | // 这样直接保存 id 列表也是乱的 13 | // 这个类会保留每个 id 所处的页码。抓取完成后可以把这些 id 按页码顺序排列,保证 id 的顺序和在页码里的顺序一致 14 | 15 | // 这个类现在已经不使用了,因为先在下载器在抓取列表页时,始终只使用 1 个请求 16 | // 而非之前的同时发起多个请求 17 | // 所以不需要这个类了 18 | class IdListWithPageNo { 19 | // 存储 id 列表,按 pageId 不同分别存储 20 | private allList: { 21 | [pageId: number]: IDDataWithPageNo[] 22 | } = {} 23 | 24 | // 添加一条记录 25 | public add(pageId: number, idData: IDData, page: number) { 26 | if (this.allList[pageId] === undefined) { 27 | this.allList[pageId] = [] 28 | } 29 | this.allList[pageId].push({ 30 | id: idData.id, 31 | type: idData.type, 32 | page: page, 33 | }) 34 | } 35 | 36 | // 清空记录 37 | public clear(pageId: number) { 38 | if (this.allList[pageId]) { 39 | delete this.allList[pageId] 40 | } 41 | } 42 | 43 | // 排序 44 | private sort(pageId: number) { 45 | if (this.allList[pageId]) { 46 | this.allList[pageId].sort(Utils.sortByProperty('page', 'asc')) 47 | } 48 | } 49 | 50 | // 转储到 store.idList 里 51 | // 自动排序 52 | // 转储之后自动清空 53 | public store(pageId: number) { 54 | if (this.allList[pageId]) { 55 | this.sort(pageId) 56 | 57 | for (const data of this.allList[pageId]) { 58 | store.idList.push({ 59 | id: data.id, 60 | type: data.type, 61 | }) 62 | } 63 | 64 | this.clear(pageId) 65 | } 66 | } 67 | 68 | // 如果没有值,返回的就是 undefined 69 | public get(pageId: number) { 70 | return this.allList[pageId] 71 | } 72 | } 73 | 74 | const idListWithPageNo = new IdListWithPageNo() 75 | export { idListWithPageNo } 76 | -------------------------------------------------------------------------------- /src/ts/utils/DateFormat.ts: -------------------------------------------------------------------------------- 1 | // 格式化日期(和时间) 2 | class DateFormat { 3 | // format 参数可以由以下格式组合: 4 | /* 5 | YYYY 6 | YY 7 | MM 8 | MMM 9 | MMMM 10 | DD 11 | hh 12 | mm 13 | ss 14 | */ 15 | // 区分大小写;可以添加空格或其他符号;不要使用上面未包含的格式。 16 | // 参考资料: 17 | // https://www.w3.org/TR/NOTE-datetime 18 | // https://en.wikipedia.org/wiki/Date_format_by_country 19 | public static format( 20 | date: string | number | Date, 21 | format: string = 'YYYY-MM-DD' 22 | ) { 23 | // 生成年、月、日、时、分、秒 24 | const _date = new Date(date) 25 | const YYYY = _date.getFullYear().toString() 26 | const YY = YYYY.substring(YYYY.length - 2, YYYY.length) 27 | const MM = (_date.getMonth() + 1).toString().padStart(2, '0') 28 | const MMM = this.months[_date.getMonth()] 29 | const MMMM = this.Months[_date.getMonth()] 30 | const DD = _date.getDate().toString().padStart(2, '0') 31 | const hh = _date.getHours().toString().padStart(2, '0') 32 | const mm = _date.getMinutes().toString().padStart(2, '0') 33 | const ss = _date.getSeconds().toString().padStart(2, '0') 34 | // 对格式字符串进行替换 35 | let r = format 36 | r = r.replace('YYYY', YYYY) 37 | r = r.replace('YY', YY) 38 | r = r.replace('MMMM', MMMM) 39 | r = r.replace('MMM', MMM) 40 | r = r.replace('MM', MM) 41 | r = r.replace('DD', DD) 42 | r = r.replace('hh', hh) 43 | r = r.replace('mm', mm) 44 | r = r.replace('ss', ss) 45 | 46 | return r 47 | } 48 | 49 | private static readonly months = [ 50 | 'Jan', 51 | 'Feb', 52 | 'Mar', 53 | 'Apr', 54 | 'May', 55 | 'Jun', 56 | 'Jul', 57 | 'Aug', 58 | 'Sept', 59 | 'Oct', 60 | 'Nov', 61 | 'Dec', 62 | ] 63 | 64 | private static readonly Months = [ 65 | 'January', 66 | 'February', 67 | 'March', 68 | 'April', 69 | 'May', 70 | 'June', 71 | 'July', 72 | 'August', 73 | 'September', 74 | 'October', 75 | 'November', 76 | 'December', 77 | ] 78 | } 79 | 80 | export { DateFormat } 81 | -------------------------------------------------------------------------------- /src/ts/utils/SecretSignal.ts: -------------------------------------------------------------------------------- 1 | // 可以用字母和数字组成隐藏口令,注册到这个模块,当用户输入隐藏口令时执行回调函数 2 | class SecretSignal { 3 | constructor() { 4 | this.bindEvents() 5 | } 6 | 7 | private list: { 8 | code: string 9 | cb: Function 10 | }[] = [] 11 | 12 | // 允许的按键:字母键,数字键,小键盘数字键 13 | // "KeyX" 14 | // "Digit9" 15 | // "Numpad1" 16 | private codePrefix = ['Key', 'Digit', 'Numpad'] 17 | 18 | private input = '' 19 | 20 | public register(code: string, cb: Function) { 21 | this.list.push({ 22 | code, 23 | cb, 24 | }) 25 | } 26 | 27 | private bindEvents() { 28 | window.addEventListener('keydown', (ev) => { 29 | // 不保存控制按键,不保存输入状态中的按键 30 | if ( 31 | !ev.code || 32 | ev.altKey || 33 | ev.ctrlKey || 34 | ev.metaKey || 35 | ev.shiftKey || 36 | ev.isComposing 37 | ) { 38 | return 39 | } 40 | 41 | // 保存字母和数字的按键 42 | for (const prefix of this.codePrefix) { 43 | if ( 44 | ev.code.startsWith(prefix) && 45 | ev.code.length === prefix.length + 1 46 | ) { 47 | // 如果字符串长度超过限制,则移除前面的一部分字符 48 | if (this.input.length > 99) { 49 | this.input = this.input.slice(80) 50 | } 51 | const key = ev.code[ev.code.length - 1].toLowerCase() 52 | this.input += key 53 | this.check() 54 | } 55 | } 56 | }) 57 | } 58 | 59 | private check() { 60 | for (const item of this.list) { 61 | if (this.input.endsWith(item.code)) { 62 | item.cb() 63 | } 64 | } 65 | } 66 | } 67 | 68 | const secretSignal = new SecretSignal() 69 | export { secretSignal } 70 | -------------------------------------------------------------------------------- /webpack.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'development', 5 | entry: { 6 | content: './src/ts/content.ts', 7 | background: './src/ts/background.ts', 8 | }, 9 | devtool: 'source-map', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts$/, 14 | use: 'ts-loader', 15 | exclude: /node_modules/, 16 | }, 17 | ], 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.js'], 21 | }, 22 | output: { 23 | filename: '[name].js', 24 | path: path.resolve(__dirname, 'dist/js'), 25 | }, 26 | } 27 | --------------------------------------------------------------------------------