├── 115 ├── 115.delDir.user.js └── 115.playlist.user.js ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── .stylelintrc.json ├── LICENSE ├── README.md ├── assets ├── error.png ├── pend.png ├── success.png └── warn.png ├── examples └── customConfig.example.js ├── javdb ├── JavDB.filter.user.js ├── JavDB.lists.user.js ├── JavDB.magnet.user.js ├── JavDB.match115.user.js ├── JavDB.offline115.user.js ├── JavDB.openTab.user.js ├── JavDB.scroll.user.js ├── JavDB.search.user.js ├── JavDB.sprite.user.js ├── JavDB.style.user.js └── JavDB.trailer.user.js ├── libs ├── JavPack.Grant.lib.js ├── JavPack.Magnet.lib.js ├── JavPack.Offline.lib.js ├── JavPack.Req.lib.js ├── JavPack.Req115.lib.js ├── JavPack.ReqDB.lib.js ├── JavPack.ReqMagnet.lib.js ├── JavPack.ReqSprite.lib.js ├── JavPack.ReqTrailer.lib.js ├── JavPack.Util.lib.js └── JavPack.Verify115.lib.js ├── package.json ├── pnpm-lock.yaml └── static └── JavDB.style.user.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["alloy"], 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "greasemonkey": true 7 | }, 8 | "globals": { 9 | "GM_deleteValues": true, 10 | "Grant": true, 11 | "Magnet": true, 12 | "Offline": true, 13 | "Req": true, 14 | "Req115": true, 15 | "ReqDB": true, 16 | "ReqMagnet": true, 17 | "ReqSprite": true, 18 | "ReqTrailer": true, 19 | "Util": true, 20 | "Verify115": true 21 | }, 22 | "rules": { 23 | "no-unused-vars": ["error", { "varsIgnorePattern": "^Grant|Magnet|Offline|Req|Util|Verify" }], 24 | "no-param-reassign": "off", 25 | "no-template-curly-in-string": "off" 26 | }, 27 | "ignorePatterns": ["**/*.md"] 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "quoteProps": "consistent" 4 | } 5 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-standard", "stylelint-config-recess-order"], 3 | "rules": { 4 | "no-descending-specificity": null, 5 | "selector-class-pattern": null 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /115/115.delDir.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 115.delDir 3 | // @namespace 115.delDir@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 播放页删除 7 | // @match https://115vod.com/* 8 | // @icon https://115vod.com/m_r/favicon.ico 9 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req.lib.js 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req115.lib.js 11 | // @connect 115.com 12 | // @connect self 13 | // @run-at document-end 14 | // @grant GM_xmlhttpRequest 15 | // @grant window.close 16 | // @require https://github.com/Tampermonkey/utils/raw/d8a4543a5f828dfa8eefb0a3360859b6fe9c3c34/requires/gh_2215_make_GM_xhr_more_parallel_again.js 17 | // ==/UserScript== 18 | 19 | (function () { 20 | const CONTAINER = document.querySelector(".vt-headline"); 21 | if (!CONTAINER) return; 22 | 23 | const smartDel = async ({ target }) => { 24 | const pickcode = new URL(location).searchParams.get("pickcode"); 25 | if (!pickcode || target.style.pointerEvents === "none") return; 26 | 27 | target.style.pointerEvents = "none"; 28 | target.textContent = "请求中..."; 29 | 30 | const { parent_id, file_id } = await Req115.filesVideo(pickcode); 31 | const { data } = await Req115.filesAllVideos(parent_id); 32 | await Req115.rbDelete([data.length === 1 ? parent_id : file_id]); 33 | 34 | const listNode = document.querySelector("#js-video_list"); 35 | if (listNode.querySelectorAll("li").length === 1) return window.close(); 36 | 37 | target.textContent = "删除"; 38 | target.style.pointerEvents = "auto"; 39 | 40 | const curr = listNode.querySelector("li.hover"); 41 | const near = curr.nextElementSibling ?? curr.previousElementSibling; 42 | 43 | curr.remove(); 44 | near.querySelector("a").click(); 45 | }; 46 | 47 | const obsCont = (node) => { 48 | const observer = new MutationObserver((mutations, obs) => { 49 | if (mutations[0].type !== "childList") return; 50 | CONTAINER.querySelector(":scope > div")?.remove(); 51 | CONTAINER.appendChild(node); 52 | obs.disconnect(); 53 | }); 54 | observer.observe(CONTAINER, { childList: true, attributes: false, characterData: false }); 55 | }; 56 | 57 | const delNode = document.createElement("a"); 58 | delNode.href = "javascript:void(0);"; 59 | delNode.className = "btn-opendir"; 60 | delNode.textContent = "删除"; 61 | obsCont(delNode); 62 | 63 | delNode.addEventListener("click", smartDel); 64 | document.addEventListener("keyup", ({ code }) => code === "Delete" && delNode.click()); 65 | })(); 66 | -------------------------------------------------------------------------------- /115/115.playlist.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name 115.playlist 3 | // @namespace 115.playlist@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 播放列表 7 | // @match https://115.com/* 8 | // @match https://115vod.com/* 9 | // @icon https://115vod.com/m_r/favicon.ico 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Grant.lib.js 11 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Util.lib.js 12 | // @run-at document-end 13 | // @grant GM_deleteValues 14 | // @grant GM_deleteValue 15 | // @grant GM_listValues 16 | // @grant GM_openInTab 17 | // @grant GM_getValue 18 | // @grant GM_setValue 19 | // ==/UserScript== 20 | 21 | Util.upStore(); 22 | 23 | (function () { 24 | const FILTER = document.querySelector(".list-filter"); 25 | if (!FILTER) return; 26 | 27 | const getPlaylist = (nodeList) => { 28 | const res = []; 29 | 30 | nodeList.forEach((node) => { 31 | const pc = node.getAttribute("pick_code"); 32 | if (pc) res.push({ n: node.title, pc }); 33 | }); 34 | 35 | return res; 36 | }; 37 | 38 | const onclick = (e) => { 39 | if (!FILTER.querySelector(".selected[val='4']")) return; 40 | 41 | const target = e.target.closest(".list-contents .file-name .name"); 42 | if (!target) return; 43 | 44 | const pickcode = target.closest("li").getAttribute("pick_code"); 45 | if (!pickcode) return; 46 | 47 | e.preventDefault(); 48 | e.stopPropagation(); 49 | 50 | const cid = new URL(window.parent.location).searchParams.get("cid"); 51 | const nodeList = target.closest(".list-contents ul").querySelectorAll("li"); 52 | GM_setValue(cid, getPlaylist(nodeList)); 53 | 54 | const tab = Grant.openTab(`https://115vod.com/?pickcode=${pickcode}&cid=${cid}`); 55 | tab.onclose = () => GM_deleteValue(cid); 56 | }; 57 | 58 | document.addEventListener("click", onclick, true); 59 | })(); 60 | 61 | (function () { 62 | const CONTAINER = document.querySelector("#js-video_list"); 63 | if (!CONTAINER) return; 64 | 65 | const STAR = document.querySelector(".play-slide-opt .btn-opt[btn='fav']"); 66 | const FULL = document.querySelector(".bar-side .btn-opt[rel='fullscreen']"); 67 | 68 | const onkeyup = ({ code }) => { 69 | if (code === "KeyL") return STAR?.click(); 70 | if (code === "KeyF") return FULL?.click(); 71 | 72 | const target = CONTAINER.querySelector("li.hover"); 73 | if (code === "BracketRight") return target?.nextElementSibling?.querySelector("a").click(); 74 | if (code === "BracketLeft") return target?.previousElementSibling?.querySelector("a").click(); 75 | }; 76 | 77 | document.addEventListener("keyup", onkeyup); 78 | const { searchParams } = new URL(location); 79 | 80 | const pickcode = searchParams.get("pickcode"); 81 | const cid = searchParams.get("cid"); 82 | if (!pickcode || !cid) return; 83 | 84 | const playlist = GM_getValue(cid, []); 85 | if (!playlist.find(({ pc }) => pc === pickcode)) return; 86 | 87 | const repList = (list, curr, cid) => { 88 | CONTAINER.innerHTML = list 89 | .map(({ pc, n }) => { 90 | return ` 91 |
  • 92 | 97 | ${n} 98 | 99 |
  • 100 | `; 101 | }) 102 | .join(""); 103 | }; 104 | 105 | const obsList = (callback) => { 106 | const observer = new MutationObserver((mutations, obs) => { 107 | if (mutations[0].type !== "childList") return; 108 | obs.disconnect(); 109 | callback(); 110 | }); 111 | observer.observe(CONTAINER, { childList: true, attributes: false, characterData: false }); 112 | }; 113 | 114 | const setList = () => repList(playlist, pickcode, cid); 115 | 116 | CONTAINER.querySelector("li") ? setList() : obsList(setList); 117 | })(); 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JavPack 2 | 3 | > 一点微小的工作 4 | 5 | ## 脚本 6 | 7 | ### 115 8 | 9 | | 名称 | 描述 | 安装 | 10 | | :----------- | :--------- | :----------------------------------------------------------------------------- | 11 | | 115.delDir | 播放页删除 | [安装](https://github.com/bolin-dev/JavPack/raw/main/115/115.delDir.user.js) | 12 | | 115.playlist | 播放页列表 | [安装](https://github.com/bolin-dev/JavPack/raw/main/115/115.playlist.user.js) | 13 | 14 | ### JavDB 15 | 16 | > [!NOTE] 17 | > 样式依赖 [JavDB.style](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.style.user.js) 18 | 19 | | 名称 | 描述 | 安装 | 20 | | :------------------------------ | :----------- | :----------------------------------------------------------------------------------- | 21 | | JavDB.style | 样式调整 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.style.user.js) | 22 | | [JavDB.search](#search) | 快捷搜索 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.search.user.js) | 23 | | [JavDB.openTab](#opentab) | 标签页打开 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.openTab.user.js) | 24 | | JavDB.scroll | 滚动加载 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.scroll.user.js) | 25 | | JavDB.filter | 影片过滤 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.filter.user.js) | 26 | | [JavDB.trailer](#trailer) | 预告片 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.trailer.user.js) | 27 | | JavDB.sprite | 雪碧图 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.sprite.user.js) | 28 | | JavDB.magnet | 磁链扩展 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.magnet.user.js) | 29 | | JavDB.lists | 相关清单 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.lists.user.js) | 30 | | JavDB.match115 | 115 网盘匹配 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.match115.user.js) | 31 | | [JavDB.offline115](#offline115) | 115 网盘离线 | [安装](https://github.com/bolin-dev/JavPack/raw/main/javdb/JavDB.offline115.user.js) | 32 | 33 | ## 使用 34 | 35 | ### search 36 | 37 | - 按键 `/` 聚焦选取搜索框 38 | 39 | - 按键 `Ctrl` + `/` 快速搜索粘贴板首项 40 | 41 | ### openTab 42 | 43 | - 鼠标左键新标签页前台打开,右键后台打开 44 | 45 | ### trailer 46 | 47 | - 方向键 或 `W` `A` `S` `D` 控制播放进度及音量 48 | 49 | - 按键 `Space` 播放/暂停 50 | 51 | - 按键 `M` 切换静音 52 | 53 | ### offline115 54 | 55 | > [!WARNING] 56 | > 自行确认 115 已登录 57 | > 58 | > 及时清理失败或长期未完成离线任务记录 59 | 60 | | `config[]` | 类型 | 说明 | 默认 | 61 | | :--------------------- | :-------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------- | 62 | | `name` | `string` | 按钮名称 | 必填 | 63 | | `inMagnets` | `boolean` | 磁力列表显示 | `false` | 64 | | `color` | `string` | 按钮样式,参考 [bulma](https://bulma.io/documentation/elements/button/#colors) | `"is-info"` | 65 | | `desc` | `string` | 描述 | `离线路径` | 66 | | `type` | `"plain" \| "genres" \| "actors"` | 类型 | `"plain"` | 67 | | `match` | `string[]` | 类型匹配,非 `"plain"` 类型时可用 | `[]` | 68 | | `exclude` | `string[]` | 类型排除,同上 | `[]` | 69 | | `dir` | `string \| string[]` | 离线目录,支持 `动态参数` | `"云下载"` | 70 | | `magnetOptions.filter` | `function` | 磁链筛选,参考 [filterCallbackFn](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#callbackfn) | `magnet.size` > `300MB` | 71 | | `magnetOptions.sort` | `function` | 磁链排序,参考 [sortCompareFn](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted#comparefn) | `magnet.zh` → `magnet.crack` → `magnet.size` | 72 | | `magnetOptions.max` | `number` | 最大磁链数 | `10` | 73 | | `verifyOptions.filter` | `function` | 视频筛选,参考 [filterCallbackFn](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#callbackfn) | `video.s` > `150MB` | 74 | | `verifyOptions.clean` | `boolean` | 验证失败删除任务及文件 | `true` | 75 | | `verifyOptions.max` | `number` | 验证次数(1s / 次 | `10` | 76 | | `rename` | `string` | 重命名,支持 `动态参数` | `"${zh}${crack} ${code} ${title}"` | 77 | | `renameTxt.no` | `string` | 重命名多文件分号格式 | `".${no}"` | 78 | | `renameTxt.zh` | `string` | 重命名中字匹配格式 | `"[中字]"` | 79 | | `renameTxt.crack` | `string` | 重命名破解匹配格式 | `"[破解]"` | 80 | | `tags` | `["genres", "actors"]` | 设置标签 | `["genres", "actors"]` | 81 | | `clean` | `boolean` | 验证成功删除不相关文件 | `true` | 82 | | `cover` | `boolean` | 上传设置封面 | `true` | 83 | 84 |
    动态参数及示例 85 | 86 | ```JavaScript 87 | // code 番号 88 | // codeFirstLetter 番号首字母 89 | // prefix 前缀 90 | // title 标题 91 | // date 日期 92 | // year 年 93 | // month 月 94 | // day 日 95 | // director 导演 96 | // maker 片商 97 | // publisher 发行 98 | // series 系列 99 | // genres 类别 100 | // actors 演员 101 | // list 清单 102 | 103 | // genre genres[],仅 type = "genres" 时可用 104 | // actor actors[],仅 type = "actors" 时可用 105 | 106 | // zh 字幕资源,仅 rename 内可用 107 | // crack 破解资源,仅 rename 内可用 108 | 109 | // config 自定义配置示例: 110 | const config = [ 111 | { 112 | name: "云下载", 113 | }, 114 | { 115 | name: "${genre}", // 仅 type = "genres" / "actors" 时支持 genre / actors 动态参数 116 | color: "is-warning is-medium", 117 | desc: "可自定义描述", 118 | type: "genres", 119 | match: [], 120 | exclude: ["褲襪"], // "褲襪" 会命中 "xx褲襪xx",如 "連褲襪" 121 | magnetOptions: { 122 | filter: ({ size }) => { 123 | const magnetSize = parseFloat(size); 124 | return magnetSize > 300000000 || magnetSize < 1; 125 | }, 126 | sort: (a, b) => { 127 | if (a.zh !== b.zh) return a.zh ? -1 : 1; 128 | if (a.crack !== b.crack) return a.crack ? -1 : 1; 129 | return parseFloat(b.size) - parseFloat(a.size); 130 | }, 131 | max: 10, 132 | }, 133 | dir: ["类别", "${genre}", "${maker}${prefix}"], // 等价: "类别/${genre}/${maker}${prefix}" 134 | verifyOptions: { 135 | filter: ({ s }) => s > 314572800, 136 | clean: true, 137 | max: 10, 138 | }, 139 | rename: "${zh}${crack} ${code} ${title}", 140 | renameTxt: { 141 | no: "-${no}", 142 | zh: "[中字]", // 应匹配正则: /中文|中字|字幕|\[[a-z]?hdc[a-z]?\]|[-_\s]+(uc|c|ch|cu|zh)(?![a-z])/i 143 | crack: "[破解]", // 应匹配正则: /无码|無碼|流出|破解|解密版|uncensored|破[一-鿆]版|[-_\s]+(cu|u|uc)(?![a-z])/i 144 | }, 145 | tags: ["actors"], 146 | clean: true, 147 | cover: false, 148 | }, 149 | ]; 150 | 151 | // magnetOptions.filter, magnetOptions.sort 接收参数示例: 152 | { 153 | zh: true, 154 | url: "magnet:?xt=urn:btih:9e84de75a5e7db566aa10ab6014d076041ff2f95", 155 | meta: "4.54GB, 1個文件", 156 | name: "EBWH-021-C.torrent", 157 | size: "4540000000", 158 | crack: false, 159 | } 160 | ``` 161 | 162 |
    163 | 164 | ## 许可 165 | 166 | [The GPL-3.0 License](./LICENSE) 167 | -------------------------------------------------------------------------------- /assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolin-dev/JavPack/e814d0fdd44536a78243c06fc4d3c79c88f8e8f9/assets/error.png -------------------------------------------------------------------------------- /assets/pend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolin-dev/JavPack/e814d0fdd44536a78243c06fc4d3c79c88f8e8f9/assets/pend.png -------------------------------------------------------------------------------- /assets/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolin-dev/JavPack/e814d0fdd44536a78243c06fc4d3c79c88f8e8f9/assets/success.png -------------------------------------------------------------------------------- /assets/warn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolin-dev/JavPack/e814d0fdd44536a78243c06fc4d3c79c88f8e8f9/assets/warn.png -------------------------------------------------------------------------------- /examples/customConfig.example.js: -------------------------------------------------------------------------------- 1 | const magnetOptions = { 2 | filter: ({ size }) => { 3 | const magnetSize = parseFloat(size); 4 | return magnetSize > 314572800 || magnetSize < 1; 5 | }, 6 | sort: (a, b) => { 7 | if (a.crack !== b.crack) return a.crack ? -1 : 1; 8 | if (a.zh !== b.zh) return a.zh ? -1 : 1; 9 | 10 | const aSize = parseFloat(a.size); 11 | const bSize = parseFloat(b.size); 12 | 13 | const aDiff = Math.abs(aSize - 3221225472); 14 | const bDiff = Math.abs(bSize - 3221225472); 15 | 16 | return aDiff !== bDiff ? aDiff - bDiff : bSize - aSize; 17 | }, 18 | max: 10, 19 | }; 20 | 21 | export default (config) => { 22 | return config.map((item) => { 23 | return { ...item, magnetOptions }; 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /javdb/JavDB.filter.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.filter 3 | // @namespace JavDB.filter@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 影片过滤 7 | // @match https://javdb.com/* 8 | // @exclude https://javdb.com/v/* 9 | // @icon https://javdb.com/favicon.ico 10 | // @run-at document-end 11 | // ==/UserScript== 12 | 13 | const config = [/男の娘/]; 14 | 15 | (function () { 16 | if (!config.length) return; 17 | 18 | const movieList = document.querySelectorAll(".movie-list .item"); 19 | if (!movieList.length) return; 20 | 21 | const parse = (node) => { 22 | const titleNode = node.querySelector(".video-title"); 23 | const code = titleNode?.querySelector("strong")?.textContent.trim() ?? ""; 24 | const title = titleNode?.textContent.split(code).pop().trim() ?? ""; 25 | const score = node.querySelector(".score .value")?.textContent.replace(/^\s+/, "") ?? ""; 26 | const date = node.querySelector(".meta")?.textContent.trim() ?? ""; 27 | const tags = [...node.querySelectorAll(".tags .tag")].map((tag) => tag?.textContent.trim() ?? ""); 28 | return `[${code}][${title}][${score}][${date}][${tags}]`; 29 | }; 30 | 31 | const filter = (list) => { 32 | list.forEach((item) => { 33 | const txt = parse(item); 34 | if (config.some((reg) => reg.test(txt))) item.classList.add("is-hidden"); 35 | }); 36 | }; 37 | 38 | filter(movieList); 39 | window.addEventListener("JavDB.scroll", ({ detail }) => filter(detail)); 40 | })(); 41 | -------------------------------------------------------------------------------- /javdb/JavDB.lists.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.lists 3 | // @namespace JavDB.lists@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 相关清单 7 | // @match https://javdb.com/v/* 8 | // @icon https://javdb.com/favicon.ico 9 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req.lib.js 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.ReqDB.lib.js 11 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Util.lib.js 12 | // @connect ffaoa.com 13 | // @run-at document-end 14 | // @grant GM_xmlhttpRequest 15 | // @grant GM_deleteValues 16 | // @grant GM_listValues 17 | // @grant unsafeWindow 18 | // @grant GM_getValue 19 | // @grant GM_setValue 20 | // @require https://github.com/Tampermonkey/utils/raw/d8a4543a5f828dfa8eefb0a3360859b6fe9c3c34/requires/gh_2215_make_GM_xhr_more_parallel_again.js 21 | // ==/UserScript== 22 | 23 | Util.upStore(); 24 | 25 | (function () { 26 | const mid = unsafeWindow.appData?.split("/").at(-1); 27 | if (!mid) return; 28 | 29 | const tabsNode = document.querySelector(".tabs.no-bottom"); 30 | const magnetsNode = document.querySelector("#magnets"); 31 | const reviewsNode = document.querySelector("#reviews"); 32 | const listsNode = document.querySelector("#lists"); 33 | const loadNode = document.querySelector("#tabs-container > article"); 34 | 35 | const renderCont = (insert) => { 36 | return `
    ${insert}
    `; 37 | }; 38 | 39 | const renderList = ({ id, name, movies_count }) => { 40 | return `${name}(${movies_count})`; 41 | }; 42 | 43 | const setLists = (sources) => { 44 | let domStr = "暂无数据"; 45 | if (sources.length) domStr = `
    ${sources.map(renderList).join("")}
    `; 46 | listsNode.innerHTML = renderCont(domStr); 47 | }; 48 | 49 | const showLists = ({ dataset }) => { 50 | magnetsNode.style.display = "none"; 51 | reviewsNode.style.display = "none"; 52 | listsNode.style.display = "block"; 53 | 54 | if (dataset.loaded === "true") return; 55 | dataset.loaded = "true"; 56 | 57 | const lists = GM_getValue(mid, []); 58 | if (lists.length) return setLists(lists); 59 | 60 | listsNode.innerHTML = ""; 61 | loadNode.style.display = "block"; 62 | 63 | ReqDB.related(mid) 64 | .then(({ data }) => { 65 | const sources = data?.lists ?? []; 66 | GM_setValue(mid, sources); 67 | setLists(sources); 68 | }) 69 | .catch(() => { 70 | dataset.loaded = "false"; 71 | listsNode.innerHTML = renderCont("读取失败"); 72 | }) 73 | .finally(() => loadNode.style.setProperty("display", "none")); 74 | }; 75 | 76 | const onclick = (e) => { 77 | const target = e.target.closest("li"); 78 | if (!target) return; 79 | 80 | const { dataset, classList } = target; 81 | if (dataset.movieTabTarget !== "listTab") return; 82 | 83 | e.preventDefault(); 84 | e.stopPropagation(); 85 | if (classList.contains("is-active")) return; 86 | 87 | tabsNode.querySelector(".is-active").classList.remove("is-active"); 88 | classList.add("is-active"); 89 | showLists(target); 90 | }; 91 | 92 | tabsNode.addEventListener("click", onclick, true); 93 | })(); 94 | -------------------------------------------------------------------------------- /javdb/JavDB.magnet.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.magnet 3 | // @namespace JavDB.magnet@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 磁链扩展 7 | // @match https://javdb.com/v/* 8 | // @icon https://javdb.com/favicon.ico 9 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Magnet.lib.js 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req.lib.js 11 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.ReqMagnet.lib.js 12 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Util.lib.js 13 | // @connect btdig.com 14 | // @connect nyaa.si 15 | // @run-at document-end 16 | // @grant GM_xmlhttpRequest 17 | // @grant GM_deleteValues 18 | // @grant GM_listValues 19 | // @grant unsafeWindow 20 | // @grant GM_getValue 21 | // @grant GM_setValue 22 | // @grant GM_info 23 | // @require https://github.com/Tampermonkey/utils/raw/d8a4543a5f828dfa8eefb0a3360859b6fe9c3c34/requires/gh_2215_make_GM_xhr_more_parallel_again.js 24 | // ==/UserScript== 25 | 26 | Util.upStore(); 27 | 28 | (function () { 29 | const mid = unsafeWindow.appData?.split("/").at(-1); 30 | if (!mid) return; 31 | 32 | const transByte = Magnet.useTransByte(); 33 | const HD_SIZE = parseFloat(transByte("2GB")); 34 | const MIN_SIZE = parseFloat(transByte("250MB")); 35 | 36 | const UNC = document.querySelector(".title.is-4").textContent.includes("無碼"); 37 | const CONT = document.querySelector("#magnets-content"); 38 | 39 | const getMagnets = () => { 40 | return [...CONT.querySelectorAll(".item.columns")] 41 | .map((node) => { 42 | const meta = (node.querySelector(".meta")?.textContent.trim() ?? "").split(","); 43 | return { 44 | url: node.querySelector(".magnet-name a")?.href, 45 | name: node.querySelector(".name")?.textContent.trim() ?? "", 46 | size: meta[0].replace(/\s/g, ""), 47 | files: meta?.[1]?.replace("個文件", "").trim() ?? "", 48 | zh: !!node.querySelector(".tags .is-warning"), 49 | date: node.querySelector(".time")?.textContent.trim() ?? "", 50 | }; 51 | }) 52 | .filter(({ url }) => url); 53 | }; 54 | 55 | const renderMagnet = ({ url, name, meta, zh, crack, hd, date }, idx) => { 56 | return ` 57 | 77 | `; 78 | }; 79 | 80 | const filterMin = (item) => !item.min; 81 | 82 | const parseSize = ({ size, files, ...item }) => { 83 | const meta = []; 84 | if (size) meta.push(size); 85 | if (files) meta.push(`${files}个文件`); 86 | 87 | size = transByte(size); 88 | const magnetSize = parseFloat(size); 89 | const hd = magnetSize >= HD_SIZE; 90 | const min = hd ? false : magnetSize > 0 && magnetSize <= MIN_SIZE; 91 | return { ...item, meta: meta.join(", "), size, hd, min }; 92 | }; 93 | 94 | const mergeMagnet = (target, source) => { 95 | ["name", "size", "files", "zh", "crack", "date"].forEach((key) => { 96 | if (!target[key] && source[key]) target[key] = source[key]; 97 | }); 98 | return target; 99 | }; 100 | 101 | const reduceMagnet = (acc, cur) => { 102 | const index = acc.findIndex(({ url }) => url === cur.url); 103 | return index === -1 ? acc.concat(cur) : acc.toSpliced(index, 1, mergeMagnet(acc[index], cur)); 104 | }; 105 | 106 | const parseName = ({ url, name, zh, ...item }) => { 107 | url = url.split("&")[0].toLowerCase(); 108 | if (!zh) zh = Magnet.zhReg.test(name); 109 | const crack = UNC ? false : Magnet.crackReg.test(name); 110 | return { ...item, url, name, zh, crack }; 111 | }; 112 | 113 | const setMagnets = (details) => { 114 | CONT.innerHTML = 115 | Object.values(details) 116 | .flat() 117 | .map(parseName) 118 | .reduce(reduceMagnet, []) 119 | .map(parseSize) 120 | .filter(filterMin) 121 | .toSorted(Magnet.magnetSort) 122 | .map(renderMagnet) 123 | .join("") || "暂无数据"; 124 | 125 | Util.dispatchEvent(); 126 | }; 127 | 128 | const setHeader = (code) => { 129 | const countCls = "x-magnet"; 130 | 131 | const btdig = `https://btdig.com/search?order=0&q=${code}`; 132 | const nyaa = `https://sukebei.nyaa.si/?f=0&c=2_2&q=${code}`; 133 | const iconStr = ''; 134 | 135 | CONT.insertAdjacentHTML( 136 | "beforebegin", 137 | `
    138 | ${iconStr}BTDigg 139 | ${iconStr}Sukebei 140 | ${iconStr}筛选过滤 141 | ${iconStr}综合排序 142 | 总数  143 | ${CONT.childElementCount} 144 | 145 |
    `, 146 | ); 147 | 148 | const countNode = CONT.previousElementSibling.querySelector(`.${countCls}`); 149 | 150 | window.addEventListener(GM_info.script.name, () => { 151 | countNode.textContent = CONT.childElementCount; 152 | }); 153 | }; 154 | 155 | const code = document.querySelector(".first-block .value").textContent.trim(); 156 | const codeDetails = Util.codeParse(code); 157 | setHeader(code); 158 | 159 | const details = GM_getValue(mid, {}); 160 | if (Object.keys(details).length) setMagnets(details); 161 | 162 | const setDetails = (sources, key) => { 163 | details[key] = sources; 164 | GM_setValue(mid, details); 165 | setMagnets(details); 166 | }; 167 | 168 | if (!details.origin) setDetails(getMagnets(), "origin"); 169 | if (!details.btdig) ReqMagnet.btdig(codeDetails).then((sources) => setDetails(sources, "btdig")); 170 | if (!details.nyaa) ReqMagnet.nyaa(codeDetails).then((sources) => setDetails(sources, "nyaa")); 171 | })(); 172 | -------------------------------------------------------------------------------- /javdb/JavDB.match115.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.match115 3 | // @namespace JavDB.match115@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 115 网盘匹配 7 | // @match https://javdb.com/* 8 | // @icon https://javdb.com/favicon.ico 9 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Grant.lib.js 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Magnet.lib.js 11 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req.lib.js 12 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req115.lib.js 13 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Util.lib.js 14 | // @connect 115.com 15 | // @run-at document-end 16 | // @grant GM_xmlhttpRequest 17 | // @grant GM_deleteValues 18 | // @grant GM_listValues 19 | // @grant unsafeWindow 20 | // @grant GM_openInTab 21 | // @grant GM_getValue 22 | // @grant GM_setValue 23 | // @grant GM_info 24 | // @require https://github.com/Tampermonkey/utils/raw/d8a4543a5f828dfa8eefb0a3360859b6fe9c3c34/requires/gh_2215_make_GM_xhr_more_parallel_again.js 25 | // ==/UserScript== 26 | 27 | Util.upStore(); 28 | 29 | const TARGET_TXT = "匹配中"; 30 | const TARGET_CLASS = "x-match"; 31 | 32 | const VOID = "javascript:void(0);"; 33 | const CHANNEL = new BroadcastChannel(GM_info.script.name); 34 | const MATCH_API = "reMatch"; 35 | 36 | const listenClick = (onclose, defaultAction) => { 37 | const actions = { 38 | click: { 39 | val: "pc", 40 | url: "https://115vod.com/?pickcode=%s", 41 | }, 42 | contextmenu: { 43 | val: "cid", 44 | url: "https://115.com/?cid=%s&mode=wangpan", 45 | }, 46 | }; 47 | 48 | const timer = {}; 49 | const getHref = (node) => node.closest(`a:not(.${TARGET_CLASS})`)?.href; 50 | const getTimerKey = location.pathname.startsWith("/v/") ? () => location.href : getHref; 51 | 52 | const debounce = (target) => { 53 | const key = getTimerKey(target); 54 | if (!key) return; 55 | 56 | if (timer[key]) clearTimeout(timer[key]); 57 | 58 | timer[key] = setTimeout(() => { 59 | onclose?.(target); 60 | delete timer[key]; 61 | }, 750); 62 | }; 63 | 64 | const onclick = (e) => { 65 | const { target, type } = e; 66 | if (!target.classList.contains(TARGET_CLASS)) return; 67 | 68 | e.preventDefault(); 69 | e.stopPropagation(); 70 | 71 | const action = actions[type]; 72 | if (!action) return; 73 | 74 | const val = target.dataset[action.val]; 75 | if (!val) return defaultAction?.(e); 76 | 77 | const tab = Grant.openTab(action.url.replaceAll("%s", val)); 78 | tab.onclose = () => debounce(target); 79 | }; 80 | 81 | document.addEventListener("click", onclick); 82 | document.addEventListener("contextmenu", onclick); 83 | }; 84 | 85 | const formatBytes = (bytes, k = 1024) => { 86 | if (bytes < k) return "0KB"; 87 | const units = ["KB", "MB", "GB", "TB"]; 88 | const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)) - 1, units.length - 1); 89 | const size = (bytes / Math.pow(k, i + 1)).toFixed(2); 90 | return `${size}${units[i]}`; 91 | }; 92 | 93 | const extractData = (data, keys = ["pc", "cid", "n", "s", "t"], format = "s") => { 94 | return data.map((item) => ({ ...JSON.parse(JSON.stringify(item, keys)), [format]: formatBytes(item[format]) })); 95 | }; 96 | 97 | const formatTip = ({ n, s, t }) => `${n} - ${s} / ${t}`; 98 | 99 | (function () { 100 | const CONT = document.querySelector(".movie-panel-info"); 101 | if (!CONT) return; 102 | 103 | const render = ({ pc, cid, ...data }) => { 104 | return ` 105 | 112 | ${data.n} 113 | 114 | `; 115 | }; 116 | 117 | const matchCode = async ({ code, codes, regex }, { load, cont }) => { 118 | const UUID = crypto.randomUUID(); 119 | load.dataset.uid = UUID; 120 | 121 | try { 122 | const { data = [] } = await Req115.filesSearchAllVideos(codes.join(" ")); 123 | if (load.dataset.uid !== UUID) return; 124 | 125 | const sources = extractData(data.filter((it) => regex.test(it.n))); 126 | cont.innerHTML = sources.map(render).join("") || "暂无匹配"; 127 | GM_setValue(code, sources); 128 | } catch (err) { 129 | if (load.dataset.uid !== UUID) return; 130 | cont.innerHTML = "匹配失败"; 131 | Util.print(err?.message); 132 | } 133 | 134 | load.textContent = "115"; 135 | }; 136 | 137 | const addBlock = () => { 138 | const load = `${TARGET_CLASS}-load`; 139 | const cont = `${TARGET_CLASS}-cont`; 140 | 141 | CONT.querySelector(".review-buttons + .panel-block").insertAdjacentHTML( 142 | "afterend", 143 | `
    144 | ${TARGET_TXT}: 145 |  ... 146 |
    `, 147 | ); 148 | 149 | return { 150 | load: CONT.querySelector(`.${load}`), 151 | cont: CONT.querySelector(`.${cont}`), 152 | }; 153 | }; 154 | 155 | const code = CONT.querySelector(".first-block .value").textContent.trim(); 156 | const codeDetails = Util.codeParse(code); 157 | const block = addBlock(); 158 | const matcher = () => matchCode(codeDetails, block); 159 | 160 | matcher(); 161 | listenClick(matcher); 162 | unsafeWindow[MATCH_API] = matcher; 163 | 164 | const refresh = ({ target }) => { 165 | if (target.textContent === TARGET_TXT) return; 166 | target.textContent = TARGET_TXT; 167 | matcher(); 168 | }; 169 | 170 | block.load.addEventListener("click", refresh); 171 | window.addEventListener("beforeunload", () => CHANNEL.postMessage(code)); 172 | })(); 173 | 174 | (function () { 175 | const MOVIE_SELECTOR = ".movie-list .item"; 176 | const CODE_SELECTORS = [".video-title", "strong"]; 177 | const CODE_SELECTOR = CODE_SELECTORS.join(" "); 178 | const TARGET_HTML = `${TARGET_TXT}`; 179 | 180 | const movieList = document.querySelectorAll(MOVIE_SELECTOR); 181 | if (!movieList.length) return; 182 | 183 | const parseCodeCls = (code) => ["x", ...code.split(/\s|\.|-|_/)].filter(Boolean).join("-"); 184 | 185 | const matchAfter = ({ code, regex, target }, data) => { 186 | target.closest(MOVIE_SELECTOR).classList.add(parseCodeCls(code)); 187 | const sources = data.filter((it) => regex.test(it.n)); 188 | const len = sources.length; 189 | 190 | let pc = ""; 191 | let cid = ""; 192 | let title = "鼠标左键缓存刷新,右键接口刷新"; 193 | let className = "is-normal"; 194 | let textContent = "未匹配"; 195 | 196 | if (len) { 197 | const zhs = sources.filter((it) => Magnet.zhReg.test(it.n)); 198 | const crack = sources.find((it) => Magnet.crackReg.test(it.n)); 199 | 200 | const zh = zhs[0]; 201 | const both = zhs.find((it) => Magnet.crackReg.test(it.n)); 202 | const active = both ?? zh ?? crack ?? sources[0]; 203 | 204 | pc = active.pc; 205 | cid = active.cid; 206 | title = sources.map(formatTip).join("\n\n"); 207 | className = both ? "is-danger" : zh ? "is-warning" : crack ? "is-info" : "is-success"; 208 | textContent = "已匹配"; 209 | if (len > 1) textContent += ` ${len}`; 210 | } 211 | 212 | const node = target.querySelector(`.${TARGET_CLASS}`); 213 | node.title = title; 214 | node.className = `tag ${className} ${TARGET_CLASS}`; 215 | node.dataset.pc = pc; 216 | node.dataset.cid = cid; 217 | node.textContent = textContent; 218 | }; 219 | 220 | const matchBefore = (node) => { 221 | if (node.classList.contains("is-hidden")) return; 222 | 223 | const target = node.querySelector(CODE_SELECTORS[0]); 224 | if (!target) return; 225 | 226 | const code = target.querySelector(CODE_SELECTORS[1])?.textContent.trim(); 227 | if (!code) return; 228 | 229 | if (!target.querySelector(`.${TARGET_CLASS}`)) target.insertAdjacentHTML("afterbegin", TARGET_HTML); 230 | return { ...Util.codeParse(code), target }; 231 | }; 232 | 233 | const useMatchQueue = (before, after) => { 234 | const wait = {}; 235 | const queue = []; 236 | let loading = false; 237 | 238 | const over = (pre, data = []) => { 239 | wait[pre].forEach((it) => after?.(it, data)); 240 | delete wait[pre]; 241 | }; 242 | 243 | const match = async () => { 244 | if (loading || !queue.length) return; 245 | const prefix = queue[0]; 246 | loading = true; 247 | 248 | try { 249 | const { data = [] } = await Req115.filesSearchAllVideos(prefix); 250 | const sources = extractData(data); 251 | GM_setValue(prefix, sources); 252 | over(prefix, sources); 253 | } catch (err) { 254 | over(prefix); 255 | Util.print(err?.message); 256 | } 257 | 258 | loading = false; 259 | queue.shift(); 260 | match(); 261 | }; 262 | 263 | const dispatch = (node) => { 264 | const details = before?.(node); 265 | if (!details) return; 266 | 267 | const { code, prefix } = details; 268 | const cache = GM_getValue(code) ?? GM_getValue(prefix); 269 | if (cache) return after?.(details, cache); 270 | 271 | if (!wait[prefix]) wait[prefix] = []; 272 | wait[prefix].push(details); 273 | 274 | if (queue.includes(prefix)) return; 275 | queue.push(prefix); 276 | match(); 277 | }; 278 | 279 | const callback = (entries, obs) => { 280 | entries.forEach(({ isIntersecting, target }) => { 281 | if (isIntersecting) obs.unobserve(target) || requestAnimationFrame(() => dispatch(target)); 282 | }); 283 | }; 284 | 285 | const obs = new IntersectionObserver(callback, { threshold: 0.25 }); 286 | return (nodeList) => nodeList.forEach((node) => obs.observe(node)); 287 | }; 288 | 289 | const matchQueue = useMatchQueue(matchBefore, matchAfter); 290 | matchQueue(movieList); 291 | 292 | window.addEventListener("JavDB.scroll", ({ detail }) => matchQueue(detail)); 293 | CHANNEL.onmessage = ({ data }) => matchQueue(document.querySelectorAll(`.${parseCodeCls(data)}`)); 294 | 295 | const publish = (code) => { 296 | matchQueue(document.querySelectorAll(`.${parseCodeCls(code)}`)); 297 | CHANNEL.postMessage(code); 298 | }; 299 | 300 | const matchCode = async (node) => { 301 | const movie = node.closest(MOVIE_SELECTOR); 302 | if (!movie) return; 303 | 304 | const code = movie.querySelector(CODE_SELECTOR)?.textContent.trim(); 305 | const target = movie.querySelector(`.${TARGET_CLASS}`); 306 | if (!code || !target) return; 307 | 308 | const { codes, regex } = Util.codeParse(code); 309 | const UUID = crypto.randomUUID(); 310 | target.dataset.uid = UUID; 311 | 312 | try { 313 | const { data = [] } = await Req115.filesSearchAllVideos(codes.join(" ")); 314 | if (target.dataset.uid !== UUID) return; 315 | 316 | const sources = extractData(data.filter((it) => regex.test(it.n))); 317 | GM_setValue(code, sources); 318 | } catch (err) { 319 | if (target.dataset.uid !== UUID) return; 320 | Util.print(err?.message); 321 | } 322 | 323 | publish(code); 324 | }; 325 | 326 | const refresh = ({ type, target }) => { 327 | if (target.textContent === TARGET_TXT) return; 328 | target.textContent = TARGET_TXT; 329 | target.title = ""; 330 | 331 | if (type === "contextmenu") return matchCode(target); 332 | if (type !== "click") return; 333 | const code = target.closest(MOVIE_SELECTOR)?.querySelector(CODE_SELECTOR)?.textContent.trim(); 334 | if (code) setTimeout(() => publish(code), 750); 335 | }; 336 | 337 | unsafeWindow[MATCH_API] = matchCode; 338 | listenClick(matchCode, refresh); 339 | })(); 340 | -------------------------------------------------------------------------------- /javdb/JavDB.offline115.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.offline115 3 | // @namespace JavDB.offline115@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 115 网盘离线 7 | // @match https://javdb.com/* 8 | // @match https://captchaapi.115.com/* 9 | // @icon https://javdb.com/favicon.ico 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Grant.lib.js 11 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Magnet.lib.js 12 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Offline.lib.js 13 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req.lib.js 14 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req115.lib.js 15 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Util.lib.js 16 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Verify115.lib.js 17 | // @resource pend https://github.com/bolin-dev/JavPack/raw/main/assets/pend.png 18 | // @resource warn https://github.com/bolin-dev/JavPack/raw/main/assets/warn.png 19 | // @resource error https://github.com/bolin-dev/JavPack/raw/main/assets/error.png 20 | // @resource success https://github.com/bolin-dev/JavPack/raw/main/assets/success.png 21 | // @connect jdbstatic.com 22 | // @connect aliyuncs.com 23 | // @connect javdb.com 24 | // @connect 115.com 25 | // @run-at document-end 26 | // @grant GM_removeValueChangeListener 27 | // @grant GM_addValueChangeListener 28 | // @grant GM_getResourceURL 29 | // @grant GM_xmlhttpRequest 30 | // @grant GM_notification 31 | // @grant GM_addElement 32 | // @grant unsafeWindow 33 | // @grant GM_openInTab 34 | // @grant window.close 35 | // @grant GM_getValue 36 | // @grant GM_setValue 37 | // @grant GM_info 38 | // @noframes 39 | // @require https://github.com/Tampermonkey/utils/raw/d8a4543a5f828dfa8eefb0a3360859b6fe9c3c34/requires/gh_2215_make_GM_xhr_more_parallel_again.js 40 | // ==/UserScript== 41 | 42 | const defaultConfig = [ 43 | { 44 | name: "云下载", 45 | color: "is-primary", 46 | inMagnets: true, 47 | }, 48 | { 49 | name: "番号", 50 | dir: "番号/${prefix}", 51 | color: "is-link", 52 | inMagnets: true, 53 | }, 54 | { 55 | name: "片商", 56 | dir: "片商/${maker}", 57 | inMagnets: true, 58 | }, 59 | { 60 | name: "系列", 61 | dir: "系列/${series}", 62 | color: "is-success", 63 | inMagnets: true, 64 | }, 65 | { 66 | name: "清单", 67 | dir: "清单/${list}", 68 | color: "is-danger is-light", 69 | }, 70 | { 71 | type: "genres", 72 | name: "${genre}", 73 | dir: "类别/${genre}", 74 | match: ["屁股", "連褲襪", "巨乳", "亂倫"], 75 | color: "is-warning", 76 | }, 77 | { 78 | type: "actors", 79 | name: "${actor}", 80 | dir: "演员/${actor}", 81 | exclude: ["♂"], 82 | color: "is-danger", 83 | }, 84 | ]; 85 | 86 | const CUSTOM_CONFIG = "CUSTOM_CONFIG"; 87 | 88 | const getConfig = async () => { 89 | const customConfig = GM_getValue(CUSTOM_CONFIG); 90 | if (!customConfig) return defaultConfig; 91 | 92 | const blob = new Blob([customConfig], { type: "application/javascript" }); 93 | const moduleUrl = URL.createObjectURL(blob); 94 | 95 | try { 96 | const { default: func } = await import(moduleUrl); 97 | return func(defaultConfig); 98 | } catch (_) { 99 | return defaultConfig; 100 | } finally { 101 | URL.revokeObjectURL(moduleUrl); 102 | } 103 | }; 104 | 105 | const setConfig = async (e) => { 106 | const file = e.target.files[0]; 107 | if (!file) return; 108 | 109 | try { 110 | if (file.type !== "text/javascript") throw new Error("不支持文件类型"); 111 | if (file.size > 3145728) throw new Error("文件大小限制 3 MB"); 112 | 113 | const customConfig = await new Promise((resolve, reject) => { 114 | const fileReader = new FileReader(); 115 | fileReader.onerror = () => reject(new Error("文件读取失败")); 116 | fileReader.onload = (e) => resolve(e.target.result); 117 | fileReader.readAsText(file); 118 | }); 119 | 120 | GM_setValue(CUSTOM_CONFIG, customConfig); 121 | Grant.notify({ icon: "success", msg: "导入成功,页面刷新后生效", onclick: () => location.reload() }); 122 | } catch (err) { 123 | Grant.notify({ icon: "warn", msg: err?.message }); 124 | } 125 | }; 126 | 127 | const TARGET_CLASS = "x-offline"; 128 | const LOAD_CLASS = "is-loading"; 129 | 130 | const MATCH_API = "reMatch"; 131 | const MATCH_DELAY = 750; 132 | 133 | const { HOST, STATUS_KEY, STATUS_VAL } = Verify115; 134 | const { PENDING, VERIFIED, FAILED } = STATUS_VAL; 135 | 136 | const transToByte = Magnet.useTransByte(); 137 | 138 | const onShortcut = () => { 139 | let hovered = null; 140 | const regex = /^Digit\d$/; 141 | 142 | const onMouseover = (e) => { 143 | hovered = e.target; 144 | }; 145 | 146 | const onKeydown = (e) => { 147 | if (!e.altKey || !regex.test(e.code)) return; 148 | 149 | const digit = Number(e.code[5]); 150 | const shortcut = digit ? digit - 1 : 9; 151 | 152 | const target = hovered?.closest(`.item:has(.${TARGET_CLASS})`) ?? document.querySelector(".movie-panel-info"); 153 | if (target) target.querySelectorAll(`.${TARGET_CLASS}`)?.[shortcut]?.click(); 154 | }; 155 | 156 | document.addEventListener("mouseover", onMouseover); 157 | document.addEventListener("keydown", onKeydown); 158 | }; 159 | 160 | const getDetails = (dom = document) => { 161 | const infoNode = dom.querySelector(".movie-panel-info"); 162 | if (!infoNode) return; 163 | 164 | const info = { cover: dom.querySelector(".video-cover")?.src ?? "" }; 165 | const codeNode = infoNode.querySelector(".first-block .value"); 166 | const prefix = codeNode.querySelector("a")?.textContent.trim(); 167 | const code = codeNode.textContent.trim(); 168 | info.codeFirstLetter = code[0].toUpperCase(); 169 | if (prefix) info.prefix = prefix; 170 | 171 | const titleNode = dom.querySelector(".title.is-4"); 172 | const label = titleNode.querySelector("strong").textContent; 173 | const origin = titleNode.querySelector(".origin-title"); 174 | const current = titleNode.querySelector(".current-title"); 175 | info.title = `${label}${(origin ?? current).textContent}`.replace(code, "").trim(); 176 | 177 | infoNode.querySelectorAll(":scope > .panel-block").forEach((item) => { 178 | const label = item.querySelector("strong")?.textContent.trim(); 179 | const value = item.querySelector(".value")?.textContent.trim(); 180 | if (!label || !value || value.includes("N/A")) return; 181 | 182 | switch (label) { 183 | case "日期:": 184 | info.date = value; 185 | break; 186 | case "導演:": 187 | info.director = value; 188 | break; 189 | case "片商:": 190 | info.maker = value; 191 | break; 192 | case "發行:": 193 | info.publisher = value; 194 | break; 195 | case "系列:": 196 | info.series = value; 197 | break; 198 | case "類別:": 199 | info.genres = value 200 | .split(",") 201 | .map((item) => item.trim()) 202 | .filter(Boolean); 203 | break; 204 | case "演員:": 205 | info.actors = value 206 | .split("\n") 207 | .map((item) => item.trim()) 208 | .filter(Boolean); 209 | break; 210 | } 211 | }); 212 | 213 | if (info.date) { 214 | const [year, month, day] = info.date.split("-"); 215 | info.year = year; 216 | info.month = month; 217 | info.day = day; 218 | } 219 | 220 | return { ...Util.codeParse(code), ...info }; 221 | }; 222 | 223 | const isUncensored = (dom = document) => { 224 | return dom.querySelector(".title.is-4").textContent.includes("無碼"); 225 | }; 226 | 227 | const renderAction = ({ color, index, idx, desc, name }) => { 228 | return ` 229 | 237 | `; 238 | }; 239 | 240 | const findAction = ({ index, idx }, actions) => { 241 | return actions.find((act) => act.index === Number(index) && act.idx === Number(idx)); 242 | }; 243 | 244 | const parseMagnet = (node) => { 245 | const name = node.querySelector(".name")?.textContent.trim() ?? ""; 246 | const meta = node.querySelector(".meta")?.textContent.trim() ?? ""; 247 | return { 248 | url: node.querySelector(".magnet-name a")?.href?.split("&")[0].toLowerCase(), 249 | crack: !!node.querySelector(".tag.is-info") || Magnet.crackReg.test(name), 250 | zh: !!node.querySelector(".tag.is-warning") || Magnet.zhReg.test(name), 251 | size: transToByte(meta.split(",")[0]), 252 | meta, 253 | name, 254 | }; 255 | }; 256 | 257 | const getMagnets = (dom = document) => { 258 | return [...dom.querySelectorAll("#magnets-content > .item")].map(parseMagnet).toSorted(Magnet.magnetSort); 259 | }; 260 | 261 | const checkCrack = (magnets, uncensored) => { 262 | return uncensored ? magnets.map((item) => ({ ...item, crack: false })) : magnets; 263 | }; 264 | 265 | const offline = async ({ options, magnets, onstart, onprogress, onfinally }, currIdx = 0) => { 266 | onstart?.(); 267 | const res = await Req115.handleOffline(options, magnets.slice(currIdx)); 268 | if (res.status !== "warn") return onfinally?.(res); 269 | onprogress?.(res); 270 | 271 | if (GM_getValue(STATUS_KEY) !== PENDING) { 272 | Verify115.start(); 273 | Grant.notify(res); 274 | } 275 | 276 | const listener = GM_addValueChangeListener(STATUS_KEY, (_name, _old_value, new_value) => { 277 | if (![VERIFIED, FAILED].includes(new_value)) return; 278 | GM_removeValueChangeListener(listener); 279 | if (new_value === FAILED) return onfinally?.(); 280 | offline({ options, magnets, onstart, onprogress, onfinally }, res.currIdx); 281 | }); 282 | }; 283 | 284 | (function () { 285 | if (location.host === HOST) return Verify115.verify(); 286 | 287 | const attributes = { type: "file", accept: ".js", style: "display: none;" }; 288 | const fileInput = GM_addElement(document.body, "input", attributes); 289 | 290 | document.addEventListener("keydown", (e) => e.altKey && e.code === "KeyU" && fileInput.click()); 291 | fileInput.addEventListener("change", setConfig); 292 | 293 | onShortcut(); 294 | })(); 295 | 296 | (async function () { 297 | const details = getDetails(); 298 | if (!details) return; 299 | 300 | const CONFIG = await getConfig(); 301 | const actions = Offline.getActions(CONFIG, details); 302 | if (!actions.length) return; 303 | 304 | const UNC = isUncensored(); 305 | 306 | const insertActions = (actions) => { 307 | document.querySelector(".movie-panel-info").insertAdjacentHTML( 308 | "beforeend", 309 | `
    310 | ${actions.map(renderAction).join("")} 311 |
    `, 312 | ); 313 | 314 | const inMagnets = actions.filter((item) => Boolean(item.inMagnets)); 315 | if (!inMagnets.length) return; 316 | 317 | const inMagnetsStr = inMagnets.map(renderAction).join(""); 318 | const magnetsNode = document.querySelector("#magnets-content"); 319 | 320 | const insert = (node) => node.querySelector(".buttons.column").insertAdjacentHTML("beforeend", inMagnetsStr); 321 | const insertMagnets = () => magnetsNode.querySelectorAll(".item.columns").forEach(insert); 322 | 323 | window.addEventListener("JavDB.magnet", insertMagnets); 324 | insertMagnets(); 325 | }; 326 | 327 | const onstart = (target) => { 328 | Util.setFavicon("pend"); 329 | target.classList.add(LOAD_CLASS); 330 | document.querySelectorAll(`.${TARGET_CLASS}`).forEach((item) => item.setAttribute("disabled", "")); 331 | }; 332 | 333 | const onfinally = (target, res) => { 334 | document.querySelectorAll(`.${TARGET_CLASS}`).forEach((item) => item.removeAttribute("disabled")); 335 | target.classList.remove(LOAD_CLASS); 336 | if (!res) return; 337 | 338 | Grant.notify(res); 339 | Util.setFavicon(res); 340 | setTimeout(() => unsafeWindow[MATCH_API]?.(), MATCH_DELAY); 341 | }; 342 | 343 | const onclick = (e) => { 344 | const { target } = e; 345 | if (!target.classList.contains(TARGET_CLASS)) return; 346 | 347 | e.preventDefault(); 348 | e.stopPropagation(); 349 | 350 | const action = findAction(target.dataset, actions); 351 | if (!action) return; 352 | 353 | const inMagnets = target.closest("#magnets-content > .item"); 354 | const { magnetOptions, ...options } = Offline.getOptions(action, details); 355 | 356 | const magnets = inMagnets ? [parseMagnet(inMagnets)] : Offline.getMagnets(getMagnets(), magnetOptions); 357 | if (!magnets.length) return; 358 | 359 | offline({ 360 | options, 361 | magnets: checkCrack(magnets, UNC), 362 | onstart: () => onstart(target), 363 | onprogress: Util.setFavicon, 364 | onfinally: (res) => onfinally(target, res), 365 | }); 366 | }; 367 | 368 | insertActions(actions); 369 | document.addEventListener("click", onclick); 370 | })(); 371 | 372 | (async function () { 373 | const COVER_SELECTOR = ".cover"; 374 | const movieList = document.querySelectorAll(".movie-list .item"); 375 | if (!movieList.length) return; 376 | 377 | const getParams = () => { 378 | const sectionName = document.querySelector(".section-name")?.textContent.trim() ?? ""; 379 | const actorSectionName = document.querySelector(".actor-section-name")?.textContent.trim() ?? ""; 380 | 381 | const getLastName = (txt) => txt.split(", ").at(-1).trim(); 382 | 383 | const getOnTags = () => { 384 | const nodeList = document.querySelectorAll("#tags .tag_labels .tag.is-info"); 385 | const genres = [...nodeList].map((item) => item.textContent.trim()); 386 | return { genres }; 387 | }; 388 | 389 | const getOnActors = () => { 390 | const actor = getLastName(actorSectionName).replace("(無碼)", "").trim(); 391 | const nodeList = document.querySelectorAll(".actor-tags.tags .tag.is-medium.is-link:not(.is-outlined)"); 392 | const genres = [...nodeList].map((item) => item.textContent.trim()); 393 | return { actors: [actor], genres }; 394 | }; 395 | 396 | const getOnSeries = () => { 397 | return { series: sectionName }; 398 | }; 399 | 400 | const getOnMakers = () => { 401 | return { maker: getLastName(sectionName) }; 402 | }; 403 | 404 | const getOnDirectors = () => { 405 | return { director: getLastName(sectionName) }; 406 | }; 407 | 408 | const getOnVideoCodes = () => { 409 | return { prefix: sectionName, codeFirstLetter: sectionName[0].toUpperCase() }; 410 | }; 411 | 412 | const getOnLists = () => { 413 | return { list: actorSectionName }; 414 | }; 415 | 416 | const getOnPublishers = () => { 417 | return { publisher: getLastName(sectionName) }; 418 | }; 419 | 420 | const getOnUsersList = () => { 421 | const list = document.querySelector(".title.is-4 .is-active a")?.textContent.trim() ?? ""; 422 | return { list }; 423 | }; 424 | 425 | const { pathname: PATHNAME } = location; 426 | if (PATHNAME.startsWith("/tags")) return getOnTags(); 427 | if (PATHNAME.startsWith("/actors")) return getOnActors(); 428 | if (PATHNAME.startsWith("/series")) return getOnSeries(); 429 | if (PATHNAME.startsWith("/makers")) return getOnMakers(); 430 | if (PATHNAME.startsWith("/directors")) return getOnDirectors(); 431 | if (PATHNAME.startsWith("/video_codes")) return getOnVideoCodes(); 432 | if (PATHNAME.startsWith("/lists")) return getOnLists(); 433 | if (PATHNAME.startsWith("/publishers")) return getOnPublishers(); 434 | if (PATHNAME.startsWith("/users/list_detail")) return getOnUsersList(); 435 | return {}; 436 | }; 437 | 438 | const CONFIG = await getConfig(); 439 | const params = getParams(); 440 | const actions = Offline.getActions(CONFIG, params); 441 | if (!actions.length) return; 442 | 443 | const insertActions = (actions) => { 444 | const actionsStr = `
    ${actions.map(renderAction).join("")}
    `; 445 | 446 | const insert = (node) => node.querySelector(COVER_SELECTOR)?.insertAdjacentHTML("beforeend", actionsStr); 447 | const insertList = (nodeList) => nodeList.forEach(insert); 448 | 449 | insertList(movieList); 450 | window.addEventListener("JavDB.scroll", ({ detail }) => insertList(detail)); 451 | }; 452 | 453 | const videoFocus = (target) => target.closest(COVER_SELECTOR)?.querySelector("video")?.focus(); 454 | 455 | const onstart = (target) => { 456 | target.classList.add(LOAD_CLASS); 457 | target.parentElement.querySelectorAll(`.${TARGET_CLASS}`).forEach((item) => item.setAttribute("disabled", "")); 458 | }; 459 | 460 | const onfinally = (target, res) => { 461 | target.parentElement.querySelectorAll(`.${TARGET_CLASS}`).forEach((item) => item.removeAttribute("disabled")); 462 | target.classList.remove(LOAD_CLASS); 463 | if (res) setTimeout(() => unsafeWindow[MATCH_API]?.(target), MATCH_DELAY); 464 | }; 465 | 466 | const onclick = async (e) => { 467 | const { target } = e; 468 | if (!target.classList.contains(TARGET_CLASS)) return; 469 | 470 | e.preventDefault(); 471 | e.stopPropagation(); 472 | requestAnimationFrame(() => videoFocus(target)); 473 | 474 | const action = findAction(target.dataset, actions); 475 | if (!action) return; 476 | onstart(target); 477 | 478 | try { 479 | const dom = await Req.request(target.closest("a").href); 480 | const details = getDetails(dom); 481 | if (!details) throw new Error("Not found details"); 482 | 483 | const UNC = isUncensored(dom); 484 | const { magnetOptions, ...options } = Offline.getOptions(action, details); 485 | 486 | const magnets = Offline.getMagnets(getMagnets(dom), magnetOptions); 487 | if (!magnets.length) throw new Error("Not found magnets"); 488 | 489 | offline({ 490 | options, 491 | magnets: checkCrack(magnets, UNC), 492 | onfinally: (res) => onfinally(target, res), 493 | }); 494 | } catch (err) { 495 | onfinally(target); 496 | Util.print(err?.message); 497 | } 498 | }; 499 | 500 | insertActions(actions); 501 | document.addEventListener("click", onclick, true); 502 | })(); 503 | -------------------------------------------------------------------------------- /javdb/JavDB.openTab.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.openTab 3 | // @namespace JavDB.openTab@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 新标签页打开 7 | // @match https://javdb.com/* 8 | // @exclude https://javdb.com/v/* 9 | // @icon https://javdb.com/favicon.ico 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Grant.lib.js 11 | // @run-at document-start 12 | // @grant GM_openInTab 13 | // ==/UserScript== 14 | 15 | (function () { 16 | const SELECTOR = ":is(.actors, .movie-list, .section-container) a:not(.button)"; 17 | 18 | const click = (e) => { 19 | const target = e.target.closest(SELECTOR); 20 | if (!target) return; 21 | 22 | e.preventDefault(); 23 | e.stopPropagation(); 24 | 25 | Grant.openTab(target.href); 26 | }; 27 | 28 | const useRightClick = () => { 29 | let timer = null; 30 | let currElem = null; 31 | let prevX = null; 32 | let prevY = null; 33 | 34 | const mousedown = (e) => { 35 | const target = e.target.closest(SELECTOR); 36 | if (!target) return; 37 | 38 | e.preventDefault(); 39 | e.stopPropagation(); 40 | 41 | prevX = e.pageX; 42 | prevY = e.pageY; 43 | 44 | clearTimeout(timer); 45 | currElem = target; 46 | 47 | timer = setTimeout(() => { 48 | currElem = null; 49 | timer = null; 50 | }, 500); 51 | }; 52 | 53 | const contextmenu = (e) => { 54 | if (!currElem) return; 55 | 56 | e.preventDefault(); 57 | e.stopPropagation(); 58 | }; 59 | 60 | const mouseup = (e) => { 61 | if (!currElem) return; 62 | 63 | e.preventDefault(); 64 | e.stopPropagation(); 65 | 66 | const diffX = Math.abs(e.pageX - prevX); 67 | const diffY = Math.abs(e.pageY - prevY); 68 | 69 | if (diffX < 5 && diffY < 5) Grant.openTab(currElem.href, false); 70 | }; 71 | 72 | return (e) => { 73 | if (e.button !== 2) return; 74 | if (e.type === "mousedown") mousedown(e); 75 | if (e.type === "contextmenu") contextmenu(e); 76 | if (e.type === "mouseup") mouseup(e); 77 | }; 78 | }; 79 | 80 | const rightClick = useRightClick(); 81 | document.addEventListener("mousedown", rightClick); 82 | document.addEventListener("contextmenu", rightClick); 83 | document.addEventListener("mouseup", rightClick); 84 | document.addEventListener("click", click); 85 | })(); 86 | -------------------------------------------------------------------------------- /javdb/JavDB.scroll.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.scroll 3 | // @namespace JavDB.scroll@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 滚动加载 7 | // @match https://javdb.com/* 8 | // @exclude https://javdb.com/v/* 9 | // @icon https://javdb.com/favicon.ico 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req.lib.js 11 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Util.lib.js 12 | // @connect self 13 | // @run-at document-end 14 | // @grant GM_xmlhttpRequest 15 | // @grant GM_info 16 | // @require https://github.com/Tampermonkey/utils/raw/d8a4543a5f828dfa8eefb0a3360859b6fe9c3c34/requires/gh_2215_make_GM_xhr_more_parallel_again.js 17 | // ==/UserScript== 18 | 19 | (function () { 20 | const useEditCards = () => { 21 | const cardList = document.querySelectorAll(":is(.actors, .movie-list) > :is(div, a)"); 22 | if (!cardList.length) return; 23 | 24 | const fadeIn = (node) => { 25 | const img = node.querySelector("img"); 26 | if (!img || img.complete) return; 27 | 28 | img.style.opacity = 0; 29 | img.addEventListener("load", ({ target }) => target.style.setProperty("opacity", 1), { once: true }); 30 | }; 31 | 32 | const delTitle = (node) => node.querySelector("a:has(img)")?.removeAttribute("title"); 33 | 34 | const editCards = (nodeList) => nodeList.forEach((node) => fadeIn(node) || delTitle(node)); 35 | 36 | editCards(cardList); 37 | return editCards; 38 | }; 39 | 40 | const editCards = useEditCards(); 41 | const contSelector = ":is(.actors, .movie-list, .section-container):has(+ nav.pagination)"; 42 | const nextSelector = `${contSelector} + nav.pagination .pagination-next`; 43 | const listSelector = `${contSelector} > :is(div, a)`; 44 | 45 | const CONT = document.querySelector(contSelector); 46 | const nextUrl = document.querySelector(nextSelector)?.href; 47 | const currList = document.querySelectorAll(listSelector); 48 | if (!CONT || !nextUrl || !currList.length) return; 49 | 50 | const useLoadMore = (next, list, { nextSelector, listSelector }) => { 51 | const loadCls = "is-loading"; 52 | let _next = next; 53 | let _list = list; 54 | 55 | const getUrl = (node) => node?.href; 56 | const getLbl = getUrl(list[0]) ? getUrl : (node) => getUrl(node.querySelector("a")); 57 | 58 | const parser = (dom) => { 59 | const next = dom?.querySelector(nextSelector)?.href; 60 | const list = dom?.querySelectorAll(listSelector); 61 | return { next, list }; 62 | }; 63 | 64 | const filter = (list) => { 65 | const setList = new Set([..._list].map(getLbl)); 66 | return [...list].filter((node) => !setList.has(getLbl(node))); 67 | }; 68 | 69 | return async (entries, obs) => { 70 | const { isIntersecting = true, target } = entries[0]; 71 | if (!isIntersecting || target.classList.contains(loadCls)) return; 72 | 73 | target.classList.add(loadCls); 74 | target.setAttribute("disabled", ""); 75 | 76 | try { 77 | const { next, list } = await Req.tasks(_next, [parser]); 78 | if (!list?.length) throw new Error("Not found list"); 79 | const detail = filter(list); 80 | 81 | if (detail.length) { 82 | CONT.append(...detail); 83 | Util.dispatchEvent(detail); 84 | editCards?.(detail); 85 | } 86 | 87 | if (!next || !detail.length) { 88 | target.textContent = "暂无更多"; 89 | return obs.disconnect(); 90 | } 91 | 92 | _next = next; 93 | _list = list; 94 | } catch (err) { 95 | Util.print(err?.message); 96 | target.removeAttribute("disabled"); 97 | } finally { 98 | target.classList.remove(loadCls); 99 | } 100 | }; 101 | }; 102 | 103 | const loadMore = useLoadMore(nextUrl, currList, { nextSelector, listSelector }); 104 | const obs = new IntersectionObserver(loadMore, { rootMargin: "300px" }); 105 | 106 | const load = document.createElement("button"); 107 | load.classList.add("button", "is-rounded", "has-text-grey", "is-flex", "my-4", "mx-auto", "x-load"); 108 | load.textContent = "重新加载"; 109 | 110 | CONT.insertAdjacentElement("afterend", load); 111 | load.addEventListener("click", ({ target }) => loadMore([{ target }], obs)); 112 | obs.observe(load); 113 | })(); 114 | -------------------------------------------------------------------------------- /javdb/JavDB.search.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.search 3 | // @namespace JavDB.search@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 快捷搜索 7 | // @match https://javdb.com/* 8 | // @icon https://javdb.com/favicon.ico 9 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Grant.lib.js 10 | // @run-at document-start 11 | // @grant GM_openInTab 12 | // ==/UserScript== 13 | 14 | (function () { 15 | document.addEventListener("keydown", async (e) => { 16 | if (e.ctrlKey && e.code === "Slash") { 17 | const txt = await navigator.clipboard.readText(); 18 | if (!txt) return; 19 | 20 | const url = `${location.origin}/search?q=${txt.trim()}`; 21 | if (!location.pathname.startsWith("/search")) return Grant.openTab(url); 22 | location.href = url; 23 | } 24 | }); 25 | 26 | document.addEventListener("keyup", (e) => { 27 | if (e.code !== "Slash") return; 28 | const { nodeName } = document.activeElement; 29 | if (["INPUT", "TEXTAREA"].includes(nodeName)) return; 30 | 31 | const input = document.querySelector("#video-search"); 32 | if (!input) return; 33 | 34 | input.focus(); 35 | input.select(); 36 | }); 37 | })(); 38 | -------------------------------------------------------------------------------- /javdb/JavDB.sprite.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.sprite 3 | // @namespace JavDB.sprite@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 雪碧图 7 | // @match https://javdb.com/v/* 8 | // @icon https://javdb.com/favicon.ico 9 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req.lib.js 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.ReqSprite.lib.js 11 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Util.lib.js 12 | // @connect javstore.net 13 | // @connect pixhost.to 14 | // @connect javfree.me 15 | // @connect javbee.me 16 | // @connect * 17 | // @run-at document-end 18 | // @grant GM_xmlhttpRequest 19 | // @grant GM_deleteValues 20 | // @grant GM_listValues 21 | // @grant unsafeWindow 22 | // @grant GM_getValue 23 | // @grant GM_setValue 24 | // @grant GM_info 25 | // @require https://github.com/Tampermonkey/utils/raw/d8a4543a5f828dfa8eefb0a3360859b6fe9c3c34/requires/gh_2215_make_GM_xhr_more_parallel_again.js 26 | // ==/UserScript== 27 | 28 | Util.upStore(); 29 | 30 | (function () { 31 | const mid = unsafeWindow.appData?.split("/").at(-1); 32 | if (!mid) return; 33 | 34 | const setSprite = (source) => { 35 | const target = "x-sprite"; 36 | 37 | const imgStr = ``; 38 | document.body.insertAdjacentHTML("beforeend", ``); 39 | 40 | const itemStr = ` 41 | 48 | ${imgStr} 49 | 50 | `; 51 | 52 | const container = document.querySelector(".tile-images.preview-images"); 53 | if (!container) { 54 | return document.querySelector(".video-meta-panel").insertAdjacentHTML( 55 | "afterend", 56 | `
    57 |
    ${itemStr}
    58 |
    `, 59 | ); 60 | } 61 | 62 | const tileItem = container.querySelector(".tile-item"); 63 | if (tileItem) return tileItem.insertAdjacentHTML("beforebegin", itemStr); 64 | container.insertAdjacentHTML("beforeend", itemStr); 65 | }; 66 | 67 | const sprite = GM_getValue(mid); 68 | if (sprite) return setSprite(sprite); 69 | 70 | const code = document.querySelector(".first-block .value").textContent.trim(); 71 | const codeDetails = Util.codeParse(code); 72 | 73 | ReqSprite.getSprite(codeDetails) 74 | .then((source) => { 75 | GM_setValue(mid, source); 76 | setSprite(source); 77 | }) 78 | .catch((err) => Util.print(err?.message)); 79 | })(); 80 | -------------------------------------------------------------------------------- /javdb/JavDB.style.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.style 3 | // @namespace JavDB.style@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 样式调整 7 | // @match https://javdb.com/* 8 | // @icon https://javdb.com/favicon.ico 9 | // @resource style https://github.com/bolin-dev/JavPack/raw/main/static/JavDB.style.user.css 10 | // @run-at document-start 11 | // @grant GM_getResourceText 12 | // @grant GM_addStyle 13 | // ==/UserScript== 14 | 15 | GM_addStyle(GM_getResourceText("style")); 16 | -------------------------------------------------------------------------------- /javdb/JavDB.trailer.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name JavDB.trailer 3 | // @namespace JavDB.trailer@blc 4 | // @version 0.0.2 5 | // @author blc 6 | // @description 预告片 7 | // @match https://javdb.com/* 8 | // @icon https://javdb.com/favicon.ico 9 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Req.lib.js 10 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.ReqTrailer.lib.js 11 | // @require https://github.com/bolin-dev/JavPack/raw/main/libs/JavPack.Util.lib.js 12 | // @connect caribbeancom.com 13 | // @connect pacopacomama.com 14 | // @connect tokyo-hot.com 15 | // @connect heydouga.com 16 | // @connect 10musume.com 17 | // @connect muramura.tv 18 | // @connect dmm.co.jp 19 | // @connect heyzo.com 20 | // @connect 1pondo.tv 21 | // @connect self 22 | // @run-at document-end 23 | // @grant GM_xmlhttpRequest 24 | // @grant GM_deleteValues 25 | // @grant GM_listValues 26 | // @grant unsafeWindow 27 | // @grant GM_getValue 28 | // @grant GM_setValue 29 | // @grant GM_info 30 | // @require https://github.com/Tampermonkey/utils/raw/d8a4543a5f828dfa8eefb0a3360859b6fe9c3c34/requires/gh_2215_make_GM_xhr_more_parallel_again.js 31 | // ==/UserScript== 32 | 33 | Util.upStore(); 34 | 35 | const getDetails = (dom = document) => { 36 | const infoNode = dom?.querySelector(".movie-panel-info"); 37 | if (!infoNode) return; 38 | 39 | const code = infoNode.querySelector(".first-block .value").textContent.trim(); 40 | const isWestern = /\.\d{2}\.\d{2}\.\d{2}$/.test(code); 41 | const isFC2 = code.startsWith("FC2"); 42 | 43 | const titleNode = dom.querySelector(".title.is-4"); 44 | const isVR = titleNode.textContent.includes("【VR】"); 45 | const isUncensored = titleNode.textContent.includes("無碼"); 46 | 47 | const label = titleNode.querySelector("strong").textContent; 48 | const origin = titleNode.querySelector(".origin-title"); 49 | const current = titleNode.querySelector(".current-title"); 50 | const title = (origin ?? current).textContent.replace(label, "").trim(); 51 | const cover = dom.querySelector(".video-cover")?.src ?? ""; 52 | 53 | const studio = 54 | [...infoNode.querySelectorAll(":scope > .panel-block")] 55 | .find((node) => node.querySelector("strong")?.textContent.startsWith("片商")) 56 | ?.querySelector(".value a") 57 | ?.textContent.trim() ?? ""; 58 | 59 | const sources = [...dom.querySelectorAll("#preview-video source")] 60 | .map((node) => node.getAttribute("src")?.trim()) 61 | .filter(Boolean); 62 | 63 | return { code, isWestern, isFC2, isVR, isUncensored, title, cover, studio, sources }; 64 | }; 65 | 66 | const useVideo = () => { 67 | const toggleMute = (e) => { 68 | e.preventDefault(); 69 | e.stopPropagation(); 70 | 71 | const { target } = e; 72 | target.muted = !target.muted; 73 | }; 74 | 75 | const togglePlay = (e) => { 76 | e.preventDefault(); 77 | e.stopPropagation(); 78 | 79 | const { target } = e; 80 | target.paused ? target.play() : target.pause(); 81 | }; 82 | 83 | const changeVolume = (e, step) => { 84 | e.preventDefault(); 85 | e.stopPropagation(); 86 | 87 | const { target } = e; 88 | target.volume = target.volume + step; 89 | }; 90 | 91 | const changeTime = (e, step) => { 92 | e.preventDefault(); 93 | e.stopPropagation(); 94 | 95 | const { target } = e; 96 | target.currentTime = target.currentTime + step; 97 | }; 98 | 99 | const onKeyup = (e) => { 100 | const code = e.code; 101 | 102 | if (["KeyW"].includes(code)) return changeVolume(e, 0.2); 103 | if (["KeyD"].includes(code)) return changeTime(e, 4); 104 | if (["KeyS"].includes(code)) return changeVolume(e, -0.1); 105 | if (["KeyA"].includes(code)) return changeTime(e, -2); 106 | }; 107 | 108 | const onKeydown = (e) => { 109 | const code = e.code; 110 | 111 | if (["KeyM"].includes(code)) return toggleMute(e); 112 | if (["Space"].includes(code)) return togglePlay(e); 113 | if (["ArrowUp"].includes(code)) return changeVolume(e, 0.2); 114 | if (["ArrowRight"].includes(code)) return changeTime(e, 4); 115 | if (["ArrowDown"].includes(code)) return changeVolume(e, -0.1); 116 | if (["ArrowLeft"].includes(code)) return changeTime(e, -2); 117 | }; 118 | 119 | const onVolumechange = (e) => localStorage.setItem("volume", e.target.volume); 120 | 121 | return (sources, poster) => { 122 | const video = document.createElement("video"); 123 | 124 | video.title = ""; 125 | video.poster = poster; 126 | video.controls = true; 127 | video.preload = "metadata"; 128 | video.volume = localStorage.getItem("volume") ?? 0.1; 129 | video.innerHTML = sources.map((src) => ``).join(""); 130 | 131 | video.addEventListener("volumechange", onVolumechange); 132 | video.addEventListener("keydown", onKeydown); 133 | video.addEventListener("keyup", onKeyup); 134 | return video; 135 | }; 136 | }; 137 | 138 | (async function () { 139 | const mid = unsafeWindow.appData?.split("/").at(-1); 140 | if (!mid || !location.pathname.startsWith("/v/")) return; 141 | 142 | const onstart = ({ target }) => target.style.setProperty("z-index", 11); 143 | 144 | const onstop = ({ target }) => target.style.setProperty("z-index", "auto"); 145 | 146 | const onclick = (e, video) => { 147 | if (e.target.closest(".play-button")) return; 148 | e.preventDefault(); 149 | e.stopPropagation(); 150 | 151 | video.focus(); 152 | video.paused ? video.play() : video.pause(); 153 | }; 154 | 155 | const setTrailer = ({ sources, cover }) => { 156 | const video = useVideo()(sources, cover); 157 | video.addEventListener("play", onstart); 158 | video.addEventListener("pause", onstop); 159 | video.addEventListener("ended", onstop); 160 | 161 | const container = document.querySelector(".column-video-cover a"); 162 | container.insertAdjacentElement("beforeend", video); 163 | container.addEventListener("click", (e) => onclick(e, video)); 164 | video.focus(); 165 | }; 166 | 167 | let details = GM_getValue(mid); 168 | 169 | if (!details) { 170 | details = getDetails(); 171 | GM_setValue(mid, details); 172 | } 173 | 174 | if (details.sources.length) return setTrailer(details); 175 | 176 | try { 177 | const sources = await ReqTrailer.getTrailer(details); 178 | details.sources = sources; 179 | GM_setValue(mid, details); 180 | setTrailer(details); 181 | } catch (err) { 182 | Util.print(err?.message); 183 | } 184 | })(); 185 | 186 | (function () { 187 | const selector = ".movie-list .cover"; 188 | if (!document.querySelector(selector)) return; 189 | 190 | const HOVER = "x-hovered"; 191 | const UN_HOVER = "x-un-hover"; 192 | const SHOW = "x-show"; 193 | const HIDE = "x-hide"; 194 | 195 | const audioContext = new AudioContext(); 196 | const RUNNING = audioContext.state === "running"; 197 | audioContext.close(); 198 | 199 | const createVideo = useVideo(); 200 | 201 | const onVolumechange = (e) => localStorage.setItem("muted", e.target.muted); 202 | 203 | const handleHover = (selector, onEnter, onLeave) => { 204 | let currElem = null; 205 | let nextElem = null; 206 | 207 | let prevX = null; 208 | let prevY = null; 209 | let prevTime = null; 210 | 211 | let lastX = null; 212 | let lastY = null; 213 | let lastTime = null; 214 | 215 | let trackSpeedInterval = null; 216 | let isScrolling = false; 217 | let scrollTimer = null; 218 | 219 | const onMousemove = (e) => { 220 | lastX = e.pageX; 221 | lastY = e.pageY; 222 | lastTime = Date.now(); 223 | nextElem = e.target; 224 | }; 225 | 226 | const inViewport = (elem) => { 227 | const { top, left, bottom, right } = elem.getBoundingClientRect(); 228 | const viewportWidth = window.innerWidth || document.documentElement.clientWidth; 229 | const viewportHeight = window.innerHeight || document.documentElement.clientHeight; 230 | return top >= 0 && left >= 0 && bottom <= viewportHeight && right <= viewportWidth; 231 | }; 232 | 233 | const clearTrack = (elem) => { 234 | elem.removeEventListener("mousemove", onMousemove); 235 | if (!trackSpeedInterval) return; 236 | 237 | clearInterval(trackSpeedInterval); 238 | trackSpeedInterval = null; 239 | }; 240 | 241 | const calcSpeed = () => Math.sqrt((prevX - lastX) ** 2 + (prevY - lastY) ** 2) / (lastTime - prevTime); 242 | 243 | const trackSpeed = () => { 244 | if (!nextElem.classList.contains(UN_HOVER)) { 245 | const speed = lastTime && lastTime !== prevTime ? calcSpeed() : 0; 246 | 247 | if (speed <= 0.02 && inViewport(currElem) && !isScrolling) { 248 | clearTrack(currElem); 249 | return onEnter?.(currElem); 250 | } 251 | } 252 | 253 | prevX = lastX; 254 | prevY = lastY; 255 | prevTime = Date.now(); 256 | }; 257 | 258 | const onMouseover = (e) => { 259 | if (currElem) return; 260 | 261 | const target = e.target.closest(selector); 262 | if (!target) return; 263 | 264 | prevX = e.pageX; 265 | prevY = e.pageY; 266 | prevTime = Date.now(); 267 | currElem = target; 268 | 269 | currElem.addEventListener("mousemove", onMousemove); 270 | trackSpeedInterval = setInterval(trackSpeed, 200); 271 | }; 272 | 273 | const destroy = () => { 274 | clearTrack(currElem); 275 | onLeave?.(currElem); 276 | currElem = null; 277 | }; 278 | 279 | const onMouseout = ({ relatedTarget }) => { 280 | if (!currElem) return; 281 | 282 | let node = relatedTarget; 283 | while (node) { 284 | if (node === currElem) return; 285 | node = node.parentNode; 286 | } 287 | 288 | destroy(); 289 | }; 290 | 291 | const onVisibilitychange = () => { 292 | if (document.hidden) currElem ? destroy() : document.querySelector(`${selector} .${HIDE}`)?.remove(); 293 | }; 294 | 295 | const onBlur = () => { 296 | if (currElem) destroy(); 297 | }; 298 | 299 | const onScroll = () => { 300 | clearTimeout(scrollTimer); 301 | isScrolling = true; 302 | 303 | scrollTimer = setTimeout(() => { 304 | isScrolling = false; 305 | scrollTimer = null; 306 | }, 500); 307 | }; 308 | 309 | document.addEventListener("visibilitychange", onVisibilitychange); 310 | document.addEventListener("mouseover", onMouseover); 311 | document.addEventListener("mouseout", onMouseout); 312 | 313 | const optimizedOnScroll = () => requestAnimationFrame(onScroll); 314 | window.addEventListener("scroll", optimizedOnScroll); 315 | window.addEventListener("blur", onBlur); 316 | }; 317 | 318 | const setTrailer = (elem, { sources, cover }) => { 319 | if (!elem.classList.contains(HOVER)) return; 320 | const video = createVideo(sources.toReversed(), cover); 321 | 322 | video.loop = true; 323 | video.muted = !RUNNING || localStorage.getItem("muted") === "true"; 324 | video.autoplay = true; 325 | video.currentTime = 2; 326 | video.disablePictureInPicture = true; 327 | video.setAttribute("controlslist", "nofullscreen nodownload noremoteplayback noplaybackrate"); 328 | 329 | video.addEventListener("volumechange", onVolumechange); 330 | elem.append(video); 331 | 332 | requestAnimationFrame(() => video.classList.add(SHOW)); 333 | video.focus(); 334 | }; 335 | 336 | const getTrailer = async (elem) => { 337 | elem.classList.add(HOVER); 338 | const url = elem.closest("a").href; 339 | const mid = url.split("/").at(-1); 340 | let details = GM_getValue(mid); 341 | 342 | try { 343 | if (!details) { 344 | details = await Req.tasks(url, [getDetails]); 345 | if (!details) throw new Error("Not found details"); 346 | GM_setValue(mid, details); 347 | } 348 | 349 | if (details.sources.length) return setTrailer(elem, details); 350 | if (!elem.classList.contains(HOVER)) return; 351 | 352 | const sources = await ReqTrailer.getTrailer(details); 353 | details.sources = sources; 354 | GM_setValue(mid, details); 355 | setTrailer(elem, details); 356 | } catch (err) { 357 | elem.classList.remove(HOVER); 358 | Util.print(err?.message); 359 | } 360 | }; 361 | 362 | const delTrailer = (elem) => { 363 | elem.classList.remove(HOVER); 364 | const video = elem.querySelector("video"); 365 | if (!video) return; 366 | 367 | video.addEventListener("transitionend", () => video.remove()); 368 | video.classList.replace(SHOW, HIDE); 369 | }; 370 | 371 | handleHover(selector, getTrailer, delTrailer); 372 | })(); 373 | -------------------------------------------------------------------------------- /libs/JavPack.Grant.lib.js: -------------------------------------------------------------------------------- 1 | class Grant { 2 | /** 3 | * @grant GM_openInTab 4 | */ 5 | static openTab = (url, active = true) => GM_openInTab(url, { active, setParent: true }); 6 | 7 | /** 8 | * @grant GM_getResourceURL 9 | * @grant GM_notification 10 | * @grant GM_info 11 | */ 12 | static notify = (options) => { 13 | if (typeof options === "string") options = { text: options }; 14 | options.text = options.text ?? options.msg; 15 | if (!options.text) return; 16 | 17 | if (!options.image) { 18 | const icon = options.icon ?? options.status; 19 | options.image = icon ? GM_getResourceURL(icon) : GM_info.script.icon; 20 | } 21 | 22 | GM_notification({ 23 | tag: GM_info.script.namespace, 24 | title: GM_info.script.name, 25 | silent: true, 26 | timeout: 3000, 27 | highlight: false, 28 | ...options, 29 | }); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /libs/JavPack.Magnet.lib.js: -------------------------------------------------------------------------------- 1 | class Magnet { 2 | static zhReg = /中文|中字|字幕|\[[a-z]?hdc[a-z]?\]|[-_\s]+(uc|c|ch|cu|zh)(?![a-z])/i; 3 | static crackReg = /无码|無碼|流出|破解|解密版|uncensored|破[一-鿆]版|[-_\s]+(cu|u|uc)(?![a-z])/i; 4 | 5 | static useTransByte() { 6 | const rules = [ 7 | { unit: /byte/i, trans: (size) => size }, 8 | { unit: /kb/i, trans: (size) => size * 1000 }, 9 | { unit: /mb/i, trans: (size) => size * 1000 ** 2 }, 10 | { unit: /gb/i, trans: (size) => size * 1000 ** 3 }, 11 | { unit: /tb/i, trans: (size) => size * 1000 ** 4 }, 12 | { unit: /pb/i, trans: (size) => size * 1000 ** 5 }, 13 | { unit: /eb/i, trans: (size) => size * 1000 ** 6 }, 14 | { unit: /zb/i, trans: (size) => size * 1000 ** 7 }, 15 | { unit: /yb/i, trans: (size) => size * 1000 ** 8 }, 16 | { unit: /kib/i, trans: (size) => size * 1024 }, 17 | { unit: /mib/i, trans: (size) => size * 1024 ** 2 }, 18 | { unit: /gib/i, trans: (size) => size * 1024 ** 3 }, 19 | { unit: /tib/i, trans: (size) => size * 1024 ** 4 }, 20 | { unit: /pib/i, trans: (size) => size * 1024 ** 5 }, 21 | { unit: /eib/i, trans: (size) => size * 1024 ** 6 }, 22 | { unit: /zib/i, trans: (size) => size * 1024 ** 7 }, 23 | { unit: /yib/i, trans: (size) => size * 1024 ** 8 }, 24 | ]; 25 | return (str) => { 26 | const num = str.match(/\d+\.\d+|\d+/)?.[0] ?? 0; 27 | if (num <= 0) return 0; 28 | const rule = rules.find(({ unit }) => unit.test(str)); 29 | return rule ? rule.trans(num).toFixed(2) : 0; 30 | }; 31 | } 32 | 33 | static magnetSort = (a, b) => { 34 | if (a.zh !== b.zh) return a.zh ? -1 : 1; 35 | if (a.crack !== b.crack) return a.crack ? -1 : 1; 36 | return parseFloat(b.size) - parseFloat(a.size); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /libs/JavPack.Offline.lib.js: -------------------------------------------------------------------------------- 1 | class Offline { 2 | static defaultDir = "云下载"; 3 | 4 | static defaultType = "plain"; 5 | 6 | static defaultColor = "is-info"; 7 | 8 | static defaultRename = "${zh}${crack} ${code} ${title}"; 9 | 10 | static defaultOptions = { 11 | tags: ["genres", "actors"], 12 | clean: true, 13 | cover: true, 14 | }; 15 | 16 | static defaultMagnetOptions = { 17 | filter: ({ size }) => { 18 | const magnetSize = parseFloat(size); 19 | return magnetSize > 314572800 || magnetSize < 1; 20 | }, 21 | max: 10, 22 | }; 23 | 24 | static defaultVerifyOptions = { 25 | filter: ({ s }) => s > 157286400, 26 | clean: true, 27 | max: 10, 28 | }; 29 | 30 | static defaultRenameTxt = { 31 | no: ".${no}", 32 | zh: "[中字]", 33 | crack: "[破解]", 34 | }; 35 | 36 | static parseVar(txt, params, rep = "") { 37 | const reg = /\$\{(\w+)\}/g; 38 | return txt.replace(reg, (_, key) => (params.hasOwnProperty(key) ? params[key].toString() : rep)).trim(); 39 | } 40 | 41 | static parseDir(dir, params) { 42 | const rep = "$0"; 43 | return (typeof dir === "string" ? dir.split("/") : dir).map((item) => { 44 | const txt = this.parseVar(item, params, rep); 45 | return txt.includes(rep) ? null : txt; 46 | }); 47 | } 48 | 49 | static getActions(config, params) { 50 | return config 51 | .flatMap(({ type = this.defaultType, match = [], exclude = [], ...item }, index) => { 52 | let { name, dir = this.defaultDir, rename = this.defaultRename } = item; 53 | if (!name) return null; 54 | 55 | rename = rename?.toString().trim(); 56 | if (rename) { 57 | rename = rename.replaceAll("${zh}", "$zh"); 58 | rename = rename.replaceAll("${crack}", "$crack"); 59 | if (!rename.includes("${code}")) rename = "${code} " + rename; 60 | } 61 | 62 | if (type === "plain") return { ...item, dir: this.parseDir(dir, params), rename, idx: 0, index }; 63 | 64 | let classes = params[type]; 65 | if (!Array.isArray(classes) || !classes.length) return null; 66 | 67 | if (match.length) classes = classes.filter((item) => match.some((key) => item.includes(key))); 68 | if (exclude.length) classes = classes.filter((item) => !exclude.some((key) => item.includes(key))); 69 | if (!classes.length) return null; 70 | 71 | const typeItemKey = type.replace(/s$/, ""); 72 | const typeItemTxt = "${" + typeItemKey + "}"; 73 | 74 | return classes.map((cls, idx) => { 75 | const val = cls.replace(/♀|♂/, "").trim(); 76 | return { 77 | ...item, 78 | dir: this.parseDir(dir, { ...params, [typeItemKey]: val }), 79 | rename: rename.replaceAll(typeItemTxt, val), 80 | name: name.replaceAll(typeItemTxt, val), 81 | index, 82 | idx, 83 | }; 84 | }); 85 | }) 86 | .filter((item) => Boolean(item) && item.dir.every(Boolean)) 87 | .map(({ color = this.defaultColor, desc, ...options }) => { 88 | return { ...options, color, desc: desc ? desc.toString() : options.dir.join("/") }; 89 | }); 90 | } 91 | 92 | static getOptions( 93 | { magnetOptions = {}, verifyOptions = {}, renameTxt = {}, rename, ...options }, 94 | { codes, regex, cover, ...details }, 95 | ) { 96 | options = { ...this.defaultOptions, ...options }; 97 | magnetOptions = { ...this.defaultMagnetOptions, ...magnetOptions }; 98 | verifyOptions = { ...this.defaultVerifyOptions, ...verifyOptions }; 99 | renameTxt = { ...this.defaultRenameTxt, ...renameTxt }; 100 | 101 | return { 102 | ...options, 103 | magnetOptions, 104 | verifyOptions, 105 | renameTxt, 106 | codes, 107 | regex, 108 | code: details.code, 109 | cover: options.cover ? cover : "", 110 | rename: this.parseVar(rename, details), 111 | tags: options.tags.flatMap((key) => details[key]).filter(Boolean), 112 | }; 113 | } 114 | 115 | static getMagnets(magnets, { filter, sort, max }) { 116 | if (!magnets?.length) return []; 117 | if (filter) magnets = magnets.filter(filter); 118 | if (sort) magnets = magnets.toSorted(sort); 119 | if (max) magnets = magnets.slice(0, max); 120 | return magnets; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /libs/JavPack.Req.lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @grant GM_xmlhttpRequest 3 | */ 4 | class Req { 5 | static defaultGetResponseType = "document"; 6 | static defaultPostResponseType = "json"; 7 | static defaultTimeout = 30000; 8 | static defaultMethod = "GET"; 9 | 10 | static isPlainObj = (obj) => Object.prototype.toString.call(obj) === "[object Object]"; 11 | 12 | static request(details) { 13 | if (typeof details === "string") details = { url: details }; 14 | if (!details?.url) throw new Error("URL is required"); 15 | 16 | details = { method: this.defaultMethod, timeout: this.defaultTimeout, ...details }; 17 | const { params, method, data } = details; 18 | 19 | if (params) { 20 | const urlObj = new URL(details.url); 21 | const searchParams = new URLSearchParams(params); 22 | 23 | searchParams.forEach((val, key) => urlObj.searchParams.set(key, val)); 24 | details.url = urlObj.toString(); 25 | delete details.params; 26 | } 27 | 28 | if (method === "POST") { 29 | details.responseType ??= this.defaultPostResponseType; 30 | 31 | if (this.isPlainObj(data)) { 32 | const formData = new FormData(); 33 | 34 | for (const [key, val] of Object.entries(data)) { 35 | if (!Array.isArray(val) && !this.isPlainObj(val)) { 36 | if (val !== undefined) formData.append(key, val); 37 | continue; 38 | } 39 | 40 | for (const k in val) { 41 | if (Object.hasOwnProperty.call(val, k)) formData.append(`${key}[${k}]`, val[k]); 42 | } 43 | } 44 | 45 | details.data = formData; 46 | } 47 | } else if (method === "GET") details.responseType ??= this.defaultGetResponseType; 48 | 49 | return new Promise((resolve, reject) => { 50 | GM_xmlhttpRequest({ 51 | ontimeout: () => reject(new Error(`Request timed out for ${details.url}`)), 52 | onerror: () => reject(new Error(`Request error for ${details.url}`)), 53 | onload: ({ status, finalUrl, response }) => { 54 | if (status >= 400) reject(new Error(`Request failed with status ${status} for ${details.url}`)); 55 | if (method === "HEAD") resolve(finalUrl); 56 | resolve(response); 57 | }, 58 | ...details, 59 | }); 60 | }); 61 | } 62 | 63 | static async tasks(res, steps) { 64 | for (let index = 0, { length } = steps; index < length; index++) { 65 | res = await this.request(res); 66 | res = steps[index](res); 67 | } 68 | return res; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /libs/JavPack.Req115.lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @require JavPack.Req.lib.js 3 | * 4 | * @connect 115.com 5 | */ 6 | class Drive115 extends Req { 7 | static defaultGetResponseType = "json"; 8 | 9 | static files(cid = "0", params = {}) { 10 | return this.request({ 11 | url: "https://webapi.115.com/files", 12 | params: { cid, ...params }, 13 | }); 14 | } 15 | 16 | static filesSearch(search_value, params = {}) { 17 | return this.request({ 18 | url: "https://webapi.115.com/files/search", 19 | params: { search_value, ...params }, 20 | }); 21 | } 22 | 23 | static lixianTaskLists(page = 1) { 24 | return this.request({ 25 | url: "https://115.com/web/lixian/", 26 | params: { ct: "lixian", ac: "task_lists", page }, 27 | }); 28 | } 29 | 30 | static labelList() { 31 | return this.request({ 32 | url: "https://webapi.115.com/label/list", 33 | params: { keyword: "", limit: 11150 }, 34 | }); 35 | } 36 | 37 | /** 38 | * @connect 115vod.com 39 | */ 40 | static filesVideo(pickcode) { 41 | return this.request({ 42 | url: "https://115vod.com/webapi/files/video", 43 | params: { pickcode, local: 1 }, 44 | }); 45 | } 46 | 47 | static post(details) { 48 | return this.request({ method: "POST", ...details }); 49 | } 50 | 51 | static filesAdd(cname, pid) { 52 | return this.post({ 53 | url: "https://webapi.115.com/files/add", 54 | data: { cname, pid }, 55 | }); 56 | } 57 | 58 | static lixianAddTaskUrl(url, wp_path_id) { 59 | return this.post({ 60 | url: "https://115.com/web/lixian/", 61 | params: { ct: "lixian", ac: "add_task_url" }, 62 | data: { url, wp_path_id }, 63 | }); 64 | } 65 | 66 | /** 67 | * Bulk delete offline tasks and source files 68 | * @param {string[]} hash Array of info_hashes 69 | */ 70 | static lixianTaskDel(hash) { 71 | return this.post({ 72 | url: "https://115.com/web/lixian/", 73 | params: { ct: "lixian", ac: "task_del" }, 74 | data: { hash }, 75 | }); 76 | } 77 | 78 | /** 79 | * Bulk delete files 80 | * @param {string[]} fid Array of file IDs 81 | * @param {string} pid Parent folder ID 82 | */ 83 | static rbDelete(fid, pid = "") { 84 | return this.post({ 85 | url: "https://webapi.115.com/rb/delete", 86 | data: { fid, pid, ignore_warn: 1 }, 87 | }); 88 | } 89 | 90 | /** 91 | * Batch move files 92 | * @param {string[]} fid Array of file IDs 93 | * @param {string} pid Destination folder ID 94 | */ 95 | static filesMove(fid, pid) { 96 | return this.post({ 97 | url: "https://webapi.115.com/files/move", 98 | data: { fid, pid, move_proid: "" }, 99 | }); 100 | } 101 | 102 | /** 103 | * Bulk label files 104 | * @param {string} file_ids fid1,fid2,fid3... 105 | * @param {string} file_label label_id1,label_id2,label_id3... 106 | * @returns 107 | */ 108 | static filesBatchLabel(file_ids, file_label, action = "add") { 109 | return this.post({ 110 | url: "https://webapi.115.com/files/batch_label", 111 | data: { file_ids, file_label, action }, 112 | }); 113 | } 114 | 115 | /** 116 | * Bulk rename files 117 | * @param {object} files_new_name { [fid]: rename } 118 | */ 119 | static filesBatchRename(files_new_name) { 120 | return this.post({ 121 | url: "https://webapi.115.com/files/batch_rename", 122 | data: { files_new_name }, 123 | }); 124 | } 125 | 126 | static sampleInitUpload({ filename, filesize, cid }) { 127 | return this.post({ 128 | url: "https://uplb.115.com/3.0/sampleinitupload.php", 129 | data: { filename, filesize, target: `U_1_${cid}` }, 130 | }); 131 | } 132 | 133 | /** 134 | * @connect aliyuncs.com 135 | */ 136 | static upload({ 137 | host: url, 138 | filename: name, 139 | object: key, 140 | policy, 141 | accessid: OSSAccessKeyId, 142 | callback, 143 | signature, 144 | file, 145 | }) { 146 | return this.post({ 147 | url, 148 | data: { 149 | name, 150 | key, 151 | policy, 152 | OSSAccessKeyId, 153 | success_action_status: "200", 154 | callback, 155 | signature, 156 | file, 157 | }, 158 | }); 159 | } 160 | 161 | static filesEdit(fid, fid_cover) { 162 | return this.post({ 163 | url: "https://webapi.115.com/files/edit", 164 | data: { fid, fid_cover }, 165 | }); 166 | } 167 | } 168 | 169 | class Req115 extends Drive115 { 170 | static async filesAll(cid, params = {}) { 171 | const res = await this.files(cid, params); 172 | const { count, page_size, data } = res; 173 | return count > page_size && data.length ? this.files(cid, { ...params, limit: count }) : res; 174 | } 175 | 176 | static filesAllVideos(cid, params = {}) { 177 | return this.filesAll(cid, { ...params, type: 4 }); 178 | } 179 | 180 | static filesAllSRTs(cid, params = {}) { 181 | return this.filesAll(cid, { ...params, suffix: "srt" }); 182 | } 183 | 184 | static async filesSearchAll(search_value, params = {}) { 185 | const res = await this.filesSearch(search_value, params); 186 | const { count, page_size, data } = res; 187 | return count > page_size && data.length ? this.filesSearch(search_value, { ...params, limit: count }) : res; 188 | } 189 | 190 | static filesSearchAllVideos(search_value, params = {}) { 191 | return this.filesSearchAll(search_value, { ...params, type: 4 }); 192 | } 193 | 194 | static filesSearchAllFolders(search_value, params = {}) { 195 | return this.filesSearchAll(search_value, { ...params, fc: 1 }); 196 | } 197 | 198 | static async handleDir(routes) { 199 | if (routes.length === 1 && /^\d{5,}$/.test(routes[0])) return routes[0]; 200 | 201 | let cid; 202 | const routesStr = routes.join("/"); 203 | const cachedCid = localStorage.getItem(routesStr); 204 | 205 | if (cachedCid) { 206 | const res = await this.files(cachedCid); 207 | if (res?.path?.length) { 208 | const path = res.path.slice(1).map((p) => p.name); 209 | if (path.join("/") === routesStr) cid = cachedCid; 210 | } 211 | } 212 | 213 | if (!cid) { 214 | cid = "0"; 215 | 216 | for (const route of routes) { 217 | const { data } = await this.filesSearchAllFolders(route, { cid }); 218 | let folder = data.find((folder) => folder.n === route); 219 | if (!folder) folder = await this.filesAdd(route, cid); 220 | cid = folder?.cid; 221 | if (!cid) break; 222 | } 223 | } 224 | 225 | const month = new Date().getMonth().toString(); 226 | if (localStorage.getItem("115_CD") !== month) { 227 | localStorage.clear(); 228 | localStorage.setItem("115_CD", month); 229 | } 230 | 231 | if (cid) localStorage.setItem(routesStr, cid); 232 | return cid; 233 | } 234 | 235 | static async handleVerify(info_hash, { regex, codes }, { max, filter }) { 236 | const sleep = () => { 237 | return new Promise((r) => { 238 | setTimeout(r, 1000); 239 | }); 240 | }; 241 | 242 | let file_id = ""; 243 | let videos = []; 244 | 245 | for (let index = 0; index < max; index++) { 246 | if (index) await sleep(); 247 | const { tasks } = await this.lixianTaskLists(); 248 | 249 | const task = tasks.find((task) => task.info_hash === info_hash); 250 | if (!task || task.status === -1) break; 251 | 252 | file_id = task.file_id; 253 | if (file_id) break; 254 | } 255 | 256 | if (!file_id) return { file_id, videos }; 257 | 258 | for (let index = 0; index < max; index++) { 259 | if (index) await sleep(); 260 | const { data } = await this.filesAllVideos(file_id); 261 | 262 | videos = data.filter((item) => regex.test(item.n)); 263 | if (videos.length) break; 264 | } 265 | 266 | if (!videos.length) { 267 | const { tasks } = await this.lixianTaskLists(); 268 | const task = tasks.find((task) => task.info_hash === info_hash); 269 | 270 | if (task.status === 2) { 271 | const { data } = await this.filesAllVideos(file_id); 272 | codes = codes.map((code) => code.toUpperCase()); 273 | 274 | videos = data.filter((item) => { 275 | const name = item.n.toUpperCase(); 276 | return codes.some((code) => name.includes(code)); 277 | }); 278 | } 279 | } 280 | 281 | return { videos: videos.filter(filter), file_id }; 282 | } 283 | 284 | static async handleClean(keepFiles, cid) { 285 | const needMove = keepFiles.filter((file) => file.cid !== cid).map((file) => file.fid); 286 | if (needMove.length) await this.filesMove(needMove, cid); 287 | 288 | const { data } = await this.filesAll(cid); 289 | 290 | const needRemove = data 291 | .filter((item) => !keepFiles.some((file) => file.fid === item.fid)) 292 | .map((item) => item.fid ?? item.cid); 293 | 294 | if (needRemove.length) return this.rbDelete(needRemove, cid); 295 | } 296 | 297 | static async handleTags(files, tags) { 298 | const { data } = await this.labelList(); 299 | if (!data?.list?.length) return; 300 | 301 | const { list } = data; 302 | const labels = []; 303 | 304 | tags.forEach((tag) => { 305 | const item = list.find((item) => item.name === tag); 306 | if (item) labels.push(item.id); 307 | }); 308 | 309 | if (labels.length) return this.filesBatchLabel(files.map((it) => it.fid).toString(), labels.toString()); 310 | } 311 | 312 | static handleRename(files, cid, { rename, renameTxt, zh, crack }) { 313 | rename = rename.replaceAll("$zh", zh ? renameTxt.zh : ""); 314 | rename = rename.replaceAll("$crack", crack ? renameTxt.crack : ""); 315 | rename = rename.trim(); 316 | 317 | const renameObj = { [cid]: rename }; 318 | 319 | if (files.length === 1) { 320 | const { fid, ico } = files[0]; 321 | renameObj[fid] = `${rename}.${ico}`; 322 | return this.filesBatchRename(renameObj); 323 | } 324 | 325 | const icoMap = files.reduce((acc, { ico, ...item }) => { 326 | acc[ico] ??= []; 327 | acc[ico].push(item); 328 | return acc; 329 | }, {}); 330 | 331 | const noTxt = renameTxt.no; 332 | for (const [ico, items] of Object.entries(icoMap)) { 333 | if (items.length === 1) { 334 | renameObj[items[0].fid] = `${rename}.${ico}`; 335 | continue; 336 | } 337 | 338 | items 339 | .toSorted((a, b) => a.n.localeCompare(b.n)) 340 | .forEach(({ fid }, idx) => { 341 | const no = noTxt.replaceAll("${no}", `${idx + 1}`.padStart(2, "0")); 342 | renameObj[fid] = `${rename}${no}.${ico}`; 343 | }); 344 | } 345 | 346 | return this.filesBatchRename(renameObj); 347 | } 348 | 349 | static async handleCover(url, cid, filename) { 350 | const file = await this.request({ url, timeout: 60000, responseType: "blob" }); 351 | if (!file) return; 352 | 353 | const res = await this.sampleInitUpload({ cid, filename, filesize: file.size }); 354 | if (res?.host) return this.upload({ ...res, filename, file }); 355 | } 356 | 357 | static async handleOffline( 358 | { dir, regex, codes, verifyOptions, code, rename, renameTxt, tags, clean, cover }, 359 | magnets, 360 | ) { 361 | const res = { status: "error", msg: `获取目录失败: ${dir.join("/")}` }; 362 | const cid = await this.handleDir(dir); 363 | if (!cid) return res; 364 | 365 | for (let index = 0, { length } = magnets; index < length; index++) { 366 | const { url, zh, crack } = magnets[index]; 367 | const { state, error_msg, errcode, info_hash } = await this.lixianAddTaskUrl(url, cid); 368 | 369 | if (!state) { 370 | res.msg = error_msg; 371 | res.status = "error"; 372 | res.currIdx = index; 373 | if (errcode === 10008) continue; 374 | if (errcode === 911) res.status = "warn"; 375 | break; 376 | } 377 | 378 | const { videos, file_id } = await this.handleVerify(info_hash, { regex, codes }, verifyOptions); 379 | 380 | if (!videos.length) { 381 | if (verifyOptions.clean) this.lixianTaskDel([info_hash]); 382 | if (file_id && clean) this.rbDelete([file_id], cid); 383 | 384 | res.msg = `${code} 离线验证失败`; 385 | res.status = "error"; 386 | continue; 387 | } 388 | 389 | const { data: srts = [] } = await this.filesAllSRTs(file_id); 390 | const files = [...videos, ...srts]; 391 | 392 | if (clean) await this.handleClean(files, file_id); 393 | 394 | if (tags.length) this.handleTags(videos, tags); 395 | 396 | if (rename) this.handleRename(files, file_id, { rename, renameTxt, zh: zh || srts.length, crack }); 397 | 398 | if (cover) { 399 | try { 400 | const { data } = await this.handleCover(cover, file_id, `${code}.cover.jpg`); 401 | if (data?.file_id) this.filesEdit(file_id, data.file_id); 402 | } catch (err) { 403 | console.warn("[Req115.handleCover]", err?.message); 404 | } 405 | } 406 | 407 | res.msg = `${code} 离线任务成功`; 408 | res.status = "success"; 409 | break; 410 | } 411 | 412 | return res; 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /libs/JavPack.ReqDB.lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @require JavPack.Req.lib.js 3 | */ 4 | class ReqDB extends Req { 5 | static signature() { 6 | const TS_KEY = "TS"; 7 | const SIGN_KEY = "SIGN"; 8 | 9 | const curr = Math.floor(Date.now() / 1000); 10 | const ts = localStorage.getItem(TS_KEY) || 0; 11 | if (curr - ts <= 20) return localStorage.getItem(SIGN_KEY); 12 | 13 | const secret = md5( 14 | `${curr}71cf27bb3c0bcdf207b64abecddc970098c7421ee7203b9cdae54478478a199e7d5a6e1a57691123c1a931c057842fb73ba3b3c83bcd69c17ccf174081e3d8aa`, 15 | ); 16 | const sign = `${curr}.lpw6vgqzsp.${secret}`; 17 | 18 | localStorage.setItem(TS_KEY, curr); 19 | localStorage.setItem(SIGN_KEY, sign); 20 | return sign; 21 | } 22 | 23 | /** 24 | * @connect ffaoa.com 25 | */ 26 | static related(movie_id) { 27 | return this.request({ 28 | url: "https://api.ffaoa.com/api/v1/lists/related", 29 | params: { movie_id, page: 1, limit: 24 }, 30 | headers: { jdSignature: this.signature() }, 31 | responseType: "json", 32 | }); 33 | } 34 | } 35 | 36 | function md5(string, bit = 32) { 37 | function md5_RotateLeft(lValue, iShiftBits) { 38 | return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); 39 | } 40 | function md5_AddUnsigned(lX, lY) { 41 | let lX4; 42 | let lY4; 43 | let lX8; 44 | let lY8; 45 | let lResult; 46 | lX8 = lX & 0x80000000; 47 | lY8 = lY & 0x80000000; 48 | lX4 = lX & 0x40000000; 49 | lY4 = lY & 0x40000000; 50 | lResult = (lX & 0x3fffffff) + (lY & 0x3fffffff); 51 | if (lX4 & lY4) { 52 | return lResult ^ 0x80000000 ^ lX8 ^ lY8; 53 | } 54 | if (lX4 | lY4) { 55 | if (lResult & 0x40000000) { 56 | return lResult ^ 0xc0000000 ^ lX8 ^ lY8; 57 | } else { 58 | return lResult ^ 0x40000000 ^ lX8 ^ lY8; 59 | } 60 | } else { 61 | return lResult ^ lX8 ^ lY8; 62 | } 63 | } 64 | function md5_F(x, y, z) { 65 | return (x & y) | (~x & z); 66 | } 67 | function md5_G(x, y, z) { 68 | return (x & z) | (y & ~z); 69 | } 70 | function md5_H(x, y, z) { 71 | return x ^ y ^ z; 72 | } 73 | function md5_I(x, y, z) { 74 | return y ^ (x | ~z); 75 | } 76 | // eslint-disable-next-line max-params 77 | function md5_FF(a, b, c, d, x, s, ac) { 78 | a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_F(b, c, d), x), ac)); 79 | return md5_AddUnsigned(md5_RotateLeft(a, s), b); 80 | } 81 | // eslint-disable-next-line max-params 82 | function md5_GG(a, b, c, d, x, s, ac) { 83 | a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_G(b, c, d), x), ac)); 84 | return md5_AddUnsigned(md5_RotateLeft(a, s), b); 85 | } 86 | // eslint-disable-next-line max-params 87 | function md5_HH(a, b, c, d, x, s, ac) { 88 | a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_H(b, c, d), x), ac)); 89 | return md5_AddUnsigned(md5_RotateLeft(a, s), b); 90 | } 91 | // eslint-disable-next-line max-params 92 | function md5_II(a, b, c, d, x, s, ac) { 93 | a = md5_AddUnsigned(a, md5_AddUnsigned(md5_AddUnsigned(md5_I(b, c, d), x), ac)); 94 | return md5_AddUnsigned(md5_RotateLeft(a, s), b); 95 | } 96 | function md5_ConvertToWordArray(string) { 97 | let lWordCount; 98 | let lMessageLength = string.length; 99 | let lNumberOfWords_temp1 = lMessageLength + 8; 100 | let lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; 101 | let lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; 102 | let lWordArray = Array(lNumberOfWords - 1); 103 | let lBytePosition = 0; 104 | let lByteCount = 0; 105 | while (lByteCount < lMessageLength) { 106 | lWordCount = (lByteCount - (lByteCount % 4)) / 4; 107 | lBytePosition = (lByteCount % 4) * 8; 108 | lWordArray[lWordCount] = lWordArray[lWordCount] | (string.charCodeAt(lByteCount) << lBytePosition); 109 | lByteCount++; 110 | } 111 | lWordCount = (lByteCount - (lByteCount % 4)) / 4; 112 | lBytePosition = (lByteCount % 4) * 8; 113 | lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); 114 | lWordArray[lNumberOfWords - 2] = lMessageLength << 3; 115 | lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; 116 | return lWordArray; 117 | } 118 | function md5_WordToHex(lValue) { 119 | let WordToHexValue = ""; 120 | let WordToHexValue_temp = ""; 121 | let lByte; 122 | let lCount; 123 | for (lCount = 0; lCount <= 3; lCount++) { 124 | lByte = (lValue >>> (lCount * 8)) & 255; 125 | WordToHexValue_temp = "0" + lByte.toString(16); 126 | WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length - 2, 2); 127 | } 128 | return WordToHexValue; 129 | } 130 | function md5_Utf8Encode(string) { 131 | string = string.replace(/\r\n/g, "\n"); 132 | let utfText = ""; 133 | for (let n = 0; n < string.length; n++) { 134 | let c = string.charCodeAt(n); 135 | if (c < 128) { 136 | utfText += String.fromCharCode(c); 137 | } else if (c > 127 && c < 2048) { 138 | utfText += String.fromCharCode((c >> 6) | 192); 139 | utfText += String.fromCharCode((c & 63) | 128); 140 | } else { 141 | utfText += String.fromCharCode((c >> 12) | 224); 142 | utfText += String.fromCharCode(((c >> 6) & 63) | 128); 143 | utfText += String.fromCharCode((c & 63) | 128); 144 | } 145 | } 146 | return utfText; 147 | } 148 | let x = []; 149 | let k; 150 | let AA; 151 | let BB; 152 | let CC; 153 | let DD; 154 | let a; 155 | let b; 156 | let c; 157 | let d; 158 | let S11 = 7; 159 | let S12 = 12; 160 | let S13 = 17; 161 | let S14 = 22; 162 | let S21 = 5; 163 | let S22 = 9; 164 | let S23 = 14; 165 | let S24 = 20; 166 | let S31 = 4; 167 | let S32 = 11; 168 | let S33 = 16; 169 | let S34 = 23; 170 | let S41 = 6; 171 | let S42 = 10; 172 | let S43 = 15; 173 | let S44 = 21; 174 | string = md5_Utf8Encode(string); 175 | x = md5_ConvertToWordArray(string); 176 | a = 0x67452301; 177 | b = 0xefcdab89; 178 | c = 0x98badcfe; 179 | d = 0x10325476; 180 | for (k = 0; k < x.length; k += 16) { 181 | AA = a; 182 | BB = b; 183 | CC = c; 184 | DD = d; 185 | a = md5_FF(a, b, c, d, x[k + 0], S11, 0xd76aa478); 186 | d = md5_FF(d, a, b, c, x[k + 1], S12, 0xe8c7b756); 187 | c = md5_FF(c, d, a, b, x[k + 2], S13, 0x242070db); 188 | b = md5_FF(b, c, d, a, x[k + 3], S14, 0xc1bdceee); 189 | a = md5_FF(a, b, c, d, x[k + 4], S11, 0xf57c0faf); 190 | d = md5_FF(d, a, b, c, x[k + 5], S12, 0x4787c62a); 191 | c = md5_FF(c, d, a, b, x[k + 6], S13, 0xa8304613); 192 | b = md5_FF(b, c, d, a, x[k + 7], S14, 0xfd469501); 193 | a = md5_FF(a, b, c, d, x[k + 8], S11, 0x698098d8); 194 | d = md5_FF(d, a, b, c, x[k + 9], S12, 0x8b44f7af); 195 | c = md5_FF(c, d, a, b, x[k + 10], S13, 0xffff5bb1); 196 | b = md5_FF(b, c, d, a, x[k + 11], S14, 0x895cd7be); 197 | a = md5_FF(a, b, c, d, x[k + 12], S11, 0x6b901122); 198 | d = md5_FF(d, a, b, c, x[k + 13], S12, 0xfd987193); 199 | c = md5_FF(c, d, a, b, x[k + 14], S13, 0xa679438e); 200 | b = md5_FF(b, c, d, a, x[k + 15], S14, 0x49b40821); 201 | a = md5_GG(a, b, c, d, x[k + 1], S21, 0xf61e2562); 202 | d = md5_GG(d, a, b, c, x[k + 6], S22, 0xc040b340); 203 | c = md5_GG(c, d, a, b, x[k + 11], S23, 0x265e5a51); 204 | b = md5_GG(b, c, d, a, x[k + 0], S24, 0xe9b6c7aa); 205 | a = md5_GG(a, b, c, d, x[k + 5], S21, 0xd62f105d); 206 | d = md5_GG(d, a, b, c, x[k + 10], S22, 0x2441453); 207 | c = md5_GG(c, d, a, b, x[k + 15], S23, 0xd8a1e681); 208 | b = md5_GG(b, c, d, a, x[k + 4], S24, 0xe7d3fbc8); 209 | a = md5_GG(a, b, c, d, x[k + 9], S21, 0x21e1cde6); 210 | d = md5_GG(d, a, b, c, x[k + 14], S22, 0xc33707d6); 211 | c = md5_GG(c, d, a, b, x[k + 3], S23, 0xf4d50d87); 212 | b = md5_GG(b, c, d, a, x[k + 8], S24, 0x455a14ed); 213 | a = md5_GG(a, b, c, d, x[k + 13], S21, 0xa9e3e905); 214 | d = md5_GG(d, a, b, c, x[k + 2], S22, 0xfcefa3f8); 215 | c = md5_GG(c, d, a, b, x[k + 7], S23, 0x676f02d9); 216 | b = md5_GG(b, c, d, a, x[k + 12], S24, 0x8d2a4c8a); 217 | a = md5_HH(a, b, c, d, x[k + 5], S31, 0xfffa3942); 218 | d = md5_HH(d, a, b, c, x[k + 8], S32, 0x8771f681); 219 | c = md5_HH(c, d, a, b, x[k + 11], S33, 0x6d9d6122); 220 | b = md5_HH(b, c, d, a, x[k + 14], S34, 0xfde5380c); 221 | a = md5_HH(a, b, c, d, x[k + 1], S31, 0xa4beea44); 222 | d = md5_HH(d, a, b, c, x[k + 4], S32, 0x4bdecfa9); 223 | c = md5_HH(c, d, a, b, x[k + 7], S33, 0xf6bb4b60); 224 | b = md5_HH(b, c, d, a, x[k + 10], S34, 0xbebfbc70); 225 | a = md5_HH(a, b, c, d, x[k + 13], S31, 0x289b7ec6); 226 | d = md5_HH(d, a, b, c, x[k + 0], S32, 0xeaa127fa); 227 | c = md5_HH(c, d, a, b, x[k + 3], S33, 0xd4ef3085); 228 | b = md5_HH(b, c, d, a, x[k + 6], S34, 0x4881d05); 229 | a = md5_HH(a, b, c, d, x[k + 9], S31, 0xd9d4d039); 230 | d = md5_HH(d, a, b, c, x[k + 12], S32, 0xe6db99e5); 231 | c = md5_HH(c, d, a, b, x[k + 15], S33, 0x1fa27cf8); 232 | b = md5_HH(b, c, d, a, x[k + 2], S34, 0xc4ac5665); 233 | a = md5_II(a, b, c, d, x[k + 0], S41, 0xf4292244); 234 | d = md5_II(d, a, b, c, x[k + 7], S42, 0x432aff97); 235 | c = md5_II(c, d, a, b, x[k + 14], S43, 0xab9423a7); 236 | b = md5_II(b, c, d, a, x[k + 5], S44, 0xfc93a039); 237 | a = md5_II(a, b, c, d, x[k + 12], S41, 0x655b59c3); 238 | d = md5_II(d, a, b, c, x[k + 3], S42, 0x8f0ccc92); 239 | c = md5_II(c, d, a, b, x[k + 10], S43, 0xffeff47d); 240 | b = md5_II(b, c, d, a, x[k + 1], S44, 0x85845dd1); 241 | a = md5_II(a, b, c, d, x[k + 8], S41, 0x6fa87e4f); 242 | d = md5_II(d, a, b, c, x[k + 15], S42, 0xfe2ce6e0); 243 | c = md5_II(c, d, a, b, x[k + 6], S43, 0xa3014314); 244 | b = md5_II(b, c, d, a, x[k + 13], S44, 0x4e0811a1); 245 | a = md5_II(a, b, c, d, x[k + 4], S41, 0xf7537e82); 246 | d = md5_II(d, a, b, c, x[k + 11], S42, 0xbd3af235); 247 | c = md5_II(c, d, a, b, x[k + 2], S43, 0x2ad7d2bb); 248 | b = md5_II(b, c, d, a, x[k + 9], S44, 0xeb86d391); 249 | a = md5_AddUnsigned(a, AA); 250 | b = md5_AddUnsigned(b, BB); 251 | c = md5_AddUnsigned(c, CC); 252 | d = md5_AddUnsigned(d, DD); 253 | } 254 | if (bit === 32) { 255 | return (md5_WordToHex(a) + md5_WordToHex(b) + md5_WordToHex(c) + md5_WordToHex(d)).toLowerCase(); 256 | } 257 | return (md5_WordToHex(b) + md5_WordToHex(c)).toLowerCase(); 258 | } 259 | -------------------------------------------------------------------------------- /libs/JavPack.ReqMagnet.lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @require JavPack.Req.lib.js 3 | */ 4 | class ReqMagnet extends Req { 5 | /** 6 | * @connect btdig.com 7 | */ 8 | static btdig({ code, regex }) { 9 | return this.tasks(`https://btdig.com/search?order=0&q=${code}`, [ 10 | (dom) => { 11 | return [...dom.querySelectorAll(".one_result")] 12 | .map((node) => { 13 | return { 14 | url: node.querySelector(".torrent_magnet a")?.href, 15 | name: node.querySelector(".torrent_name")?.textContent.trim() ?? "", 16 | size: node.querySelector(".torrent_size")?.textContent.replace(/\s/g, "") ?? "", 17 | files: node.querySelector(".torrent_files")?.textContent.trim() ?? "", 18 | date: (node.querySelector(".torrent_age")?.textContent ?? "").replace("found", "").trim(), 19 | }; 20 | }) 21 | .filter(({ url, name }) => url && regex.test(name)); 22 | }, 23 | ]); 24 | } 25 | 26 | /** 27 | * @connect nyaa.si 28 | */ 29 | static nyaa({ code, regex }) { 30 | return this.tasks(`https://sukebei.nyaa.si/?f=0&c=2_2&q=${code}`, [ 31 | (dom) => { 32 | return [...dom.querySelectorAll(".torrent-list tbody > tr")] 33 | .map((node) => { 34 | const [, name, link, size, date] = [...node.querySelectorAll("td")]; 35 | return { 36 | url: link.querySelectorAll("a")?.[1]?.href, 37 | name: [...name.querySelectorAll("a")].at(-1)?.textContent.trim() ?? "", 38 | size: size?.textContent.replace(/\s/g, "") ?? "", 39 | date: (date?.textContent ?? "").split(" ")[0].trim(), 40 | }; 41 | }) 42 | .filter(({ url, name }) => url && regex.test(name)); 43 | }, 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /libs/JavPack.ReqSprite.lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @require JavPack.Req.lib.js 3 | */ 4 | class ReqSprite extends Req { 5 | /** 6 | * @connect javbee.me 7 | * @connect * 8 | */ 9 | static async javbee(code, regex) { 10 | const res = await this.request(`https://javbee.me/search?keyword=${code}`); 11 | 12 | const list = res?.querySelectorAll(".content-main .card-content:has(.images-description a)"); 13 | if (!list?.length) throw new Error("Not found list"); 14 | 15 | const match = [...list].filter((node) => regex.test(node.querySelector(".title a")?.textContent ?? "")); 16 | if (!match.length) throw new Error("Not found match"); 17 | 18 | const urls = match.map((node) => node.querySelector(".images-description a").textContent?.trim()).filter(Boolean); 19 | if (!urls.length) throw new Error("Not found target"); 20 | 21 | const target = await this.request(urls[0]); 22 | const sprite = target?.querySelector(".fileviewer-file img")?.src; 23 | if (!sprite) throw new Error("Not found sprite"); 24 | 25 | return sprite; 26 | } 27 | 28 | /** 29 | * @connect javfree.me 30 | */ 31 | static async javfree(code, regex) { 32 | const res = await this.request(`https://javfree.me/?s=${code}`); 33 | 34 | const list = res?.querySelectorAll("#main .content-loop.post-loop .entry-title a"); 35 | if (!list?.length) throw new Error("Not found list"); 36 | 37 | const url = [...list].find((node) => regex.test(node.textContent || ""))?.href; 38 | if (!url) throw new Error("Not found target"); 39 | 40 | const target = await this.request(url); 41 | const images = target?.querySelectorAll("#main > article > .entry-content > p img"); 42 | if (!images?.length) throw new Error("Not found images"); 43 | 44 | const sprites = [...images].map((img) => img.src).filter((src) => /[a-z]+\.(jpe?g|png)$/i.test(src)); 45 | if (!sprites.length) throw new Error("Not found sprite"); 46 | 47 | return sprites.at(-1); 48 | } 49 | 50 | /** 51 | * @connect javstore.net 52 | * @connect pixhost.to 53 | */ 54 | static async javstore(code, regex) { 55 | const res = await this.request(`https://javstore.net/search/${code}.html`); 56 | 57 | const list = res?.querySelectorAll("#content_news li > a"); 58 | if (!list?.length) throw new Error("Not found list"); 59 | 60 | const url = [...list].find((node) => regex.test(node.title ?? ""))?.href; 61 | if (!url) throw new Error("Not found target"); 62 | 63 | const target = await this.request(url); 64 | const link = target?.querySelector(".news > a"); 65 | if (!link) throw new Error("Not found link"); 66 | 67 | const source = link.querySelector("img")?.src ?? link.href ?? ""; 68 | if (!/\.(jpe?g|png)$/i.test(source)) throw new Error("Not found source"); 69 | 70 | const sprite = source.replace("//pixhost.to/show/", "//img89.pixhost.to/images/").replace(".th.", "."); 71 | const finalUrl = await this.request({ url: sprite, method: "HEAD" }); 72 | if (!finalUrl || finalUrl.includes("removed")) throw new Error("Sprite removed"); 73 | 74 | return finalUrl; 75 | } 76 | 77 | static getSprite({ code, regex }) { 78 | return Promise.any([this.javbee(code, regex), this.javfree(code, regex), this.javstore(code, regex)]); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /libs/JavPack.ReqTrailer.lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @require JavPack.Req.lib.js 3 | */ 4 | class ReqTrailer extends Req { 5 | /** 6 | * @connect dmm.co.jp 7 | */ 8 | static useDMM() { 9 | const origin = "https://www.dmm.co.jp"; 10 | const options = { cookie: "age_check_done=1", headers: { "accept-language": "ja-JP,ja;q=0.9" } }; 11 | 12 | const rules = [ 13 | { 14 | urlSep: "service/digitalapi/-/html5_player", 15 | selector: "#dmmplayer + script", 16 | parse: (text) => { 17 | const match = text.match(/const args = (.*);/); 18 | if (!match) throw new Error("Not found match"); 19 | 20 | const { src, bitrates } = JSON.parse(match[1]); 21 | if (!src && !bitrates?.length) throw new Error("Not found res"); 22 | 23 | const samples = [src, ...bitrates.map((it) => it.src)].filter((item) => item.endsWith(".mp4")); 24 | if (!samples.length) throw new Error("Not found mp4"); 25 | 26 | return [...new Set(samples)]; 27 | }, 28 | }, 29 | { 30 | urlSep: "digital/-/vr-sample-player", 31 | selector: "#player + script + script", 32 | parse: (text) => { 33 | const match = text.match(/var sampleUrl = "(.*)";/); 34 | if (!match) throw new Error("Not found match"); 35 | 36 | const sample = match[1]; 37 | if (!sample) throw new Error("Not found sample"); 38 | if (!sample.endsWith(".mp4")) throw new Error("Not found mp4"); 39 | 40 | return [sample]; 41 | }, 42 | }, 43 | ]; 44 | 45 | const parseCid = (url) => { 46 | const { hostname, pathname, searchParams } = new URL(url); 47 | let cid = ""; 48 | 49 | switch (hostname) { 50 | case "tv.dmm.co.jp": 51 | cid = searchParams.get("content"); 52 | break; 53 | case "www.dmm.co.jp": 54 | cid = pathname 55 | .split("/") 56 | .find((item) => item.startsWith("cid=")) 57 | ?.replace("cid=", ""); 58 | break; 59 | } 60 | 61 | return cid; 62 | }; 63 | 64 | const getCid = async (searchstr) => { 65 | const urlSep = "/search/?redirect=1&enc=UTF-8&category=&searchstr="; 66 | const res = await this.request({ ...options, url: `${origin}${urlSep}${searchstr}` }); 67 | 68 | const target = res?.querySelector("#list .tmb a")?.href; 69 | if (!target) throw new Error("Not found target"); 70 | 71 | const cid = parseCid(target); 72 | if (!cid) throw new Error("Not found cid"); 73 | 74 | return cid; 75 | }; 76 | 77 | const getSamples = async (cid, { urlSep, selector, parse }) => { 78 | const res = await this.request({ ...options, url: `${origin}/${urlSep}/=/cid=${cid}` }); 79 | 80 | const target = res?.querySelector(selector)?.textContent; 81 | if (!target) throw new Error("Not found target"); 82 | 83 | return parse(target); 84 | }; 85 | 86 | return async (searchstr, isVR) => { 87 | const cid = await getCid(searchstr); 88 | return isVR ? Promise.any(rules.map((item) => getSamples(cid, item))) : getSamples(cid, rules[0]); 89 | }; 90 | } 91 | 92 | /** 93 | * @connect caribbeancom.com 94 | * @connect pacopacomama.com 95 | * @connect tokyo-hot.com 96 | * @connect heydouga.com 97 | * @connect 10musume.com 98 | * @connect muramura.tv 99 | * @connect heyzo.com 100 | * @connect 1pondo.tv 101 | */ 102 | static useStudio() { 103 | const sampleUrl = "https://smovie.$host/sample/movies/$code/%s.mp4"; 104 | const resolutions = ["1080p", "720p", "480p", "360p", "240p"]; 105 | 106 | const getSamples = (code, host) => { 107 | const sample = sampleUrl.replace("$host", host).replace("$code", code); 108 | return resolutions.map((r) => sample.replace("%s", r)); 109 | }; 110 | 111 | const rules = [ 112 | { 113 | studios: ["Tokyo-Hot", "東京熱"], 114 | samples: (code) => [`https://my.cdn.tokyo-hot.com/media/samples/${code}.mp4`], 115 | }, 116 | { 117 | studios: ["Heydouga"], 118 | samples: (code) => { 119 | code = code.toLowerCase().replace("heydouga-", "").replaceAll("-", "/"); 120 | const url = "https://sample.heydouga.com/contents"; 121 | 122 | return [`${url}/${code}/sample.mp4`, `${url}/${code}/sample_thumb.mp4`]; 123 | }, 124 | }, 125 | { 126 | studios: ["HEYZO"], 127 | samples: (code) => { 128 | code = code.toUpperCase().replace("HEYZO-", ""); 129 | const url = "https://sample.heyzo.com/contents/3000"; 130 | 131 | return [ 132 | `${url}/${code}/heyzo_hd_${code}_sample.mp4`, 133 | `${url}/${code}/sample.mp4`, 134 | `${url}/${code}/sample_low.mp4`, 135 | ]; 136 | }, 137 | }, 138 | { 139 | studios: ["一本道"], 140 | samples: (code) => getSamples(code, "1pondo.tv"), 141 | }, 142 | { 143 | studios: ["pacopacomama,パコパコママ"], 144 | samples: (code) => getSamples(code, "pacopacomama.com"), 145 | }, 146 | { 147 | studios: ["muramura"], 148 | samples: (code) => getSamples(code, "muramura.tv"), 149 | }, 150 | { 151 | studios: ["10musume", "天然むすめ"], 152 | samples: (code) => getSamples(code, "10musume.com"), 153 | }, 154 | { 155 | studios: ["Caribbeancom", "加勒比", "カリビアンコム"], 156 | samples: (code) => getSamples(code, "caribbeancom.com"), 157 | }, 158 | ]; 159 | 160 | return async (code, studio) => { 161 | if (!studio) throw new Error("Studio is required"); 162 | studio = studio.toUpperCase(); 163 | 164 | const samples = rules.find(({ studios }) => studios.some((st) => st.toUpperCase() === studio))?.samples(code); 165 | if (!samples?.length) throw new Error("Not found samples"); 166 | 167 | const results = await Promise.allSettled(samples.map((url) => this.request({ method: "HEAD", url }))); 168 | const sources = results.filter(({ status }) => status === "fulfilled").map(({ value }) => value); 169 | if (!sources.length) throw new Error("Not found sources"); 170 | 171 | return sources; 172 | }; 173 | } 174 | 175 | static getTrailer({ isVR, isFC2, isWestern, isUncensored, code, title, studio }) { 176 | if (isFC2) { 177 | throw new Error("Not Supported FC2"); 178 | } else if (isWestern) { 179 | throw new Error("Not Supported Western"); 180 | } else if (isUncensored) { 181 | const guessStudio = this.useStudio(); 182 | return guessStudio(code, studio); 183 | } else { 184 | const getDMM = this.useDMM(); 185 | return isVR ? getDMM(title, isVR) : getDMM(code, isVR).catch(() => getDMM(title, isVR)); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /libs/JavPack.Util.lib.js: -------------------------------------------------------------------------------- 1 | class Util { 2 | /** 3 | * @grant GM_deleteValues 4 | * @grant GM_listValues 5 | * @grant GM_getValue 6 | * @grant GM_setValue 7 | */ 8 | static upStore() { 9 | const KEY = "CD"; 10 | const DATE = new Date().getDate(); 11 | if (GM_getValue(KEY) === DATE) return; 12 | 13 | GM_deleteValues(GM_listValues()); 14 | GM_setValue(KEY, DATE); 15 | } 16 | 17 | static codeParse(code) { 18 | const codes = code.split(/-|_/); 19 | const sep = "\\s?(0|-|_){0,2}\\s?"; 20 | 21 | let pattern = codes.join(sep); 22 | if (/^fc2/i.test(code)) pattern = `${codes[0]}${sep}(ppv)?${sep}${codes.at(-1)}`; 23 | 24 | return { 25 | code, 26 | codes, 27 | prefix: codes[0], 28 | regex: new RegExp(`(? item.setAttribute("href", favicon)); 38 | } 39 | 40 | /** 41 | * @grant GM_info 42 | */ 43 | static dispatchEvent(detail = null) { 44 | return window.dispatchEvent(new CustomEvent(GM_info.script.name, { detail })); 45 | } 46 | 47 | /** 48 | * @grant GM_info 49 | */ 50 | static print(data) { 51 | console.warn(`[${GM_info.script.name}]`, data); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /libs/JavPack.Verify115.lib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @match https://captchaapi.115.com/* 3 | * 4 | * @grant unsafeWindow 5 | * @grant GM_openInTab 6 | * @grant window.close 7 | * @grant GM_getValue 8 | * @grant GM_setValue 9 | * @grant GM_info 10 | */ 11 | class Verify115 { 12 | static TAG = GM_info.script.name; 13 | 14 | static HOST = "captchaapi.115.com"; 15 | 16 | static STATUS_KEY = "VERIFY"; 17 | 18 | static STATUS_VAL = { 19 | PENDING: "PENDING", 20 | VERIFIED: "VERIFIED", 21 | FAILED: "FAILED", 22 | }; 23 | 24 | static get url() { 25 | return `https://${this.HOST}/?ac=security_code&type=web&cb=Close911_${new Date().getTime()}&tag=${this.TAG}`; 26 | } 27 | 28 | static start() { 29 | GM_setValue(this.STATUS_KEY, this.STATUS_VAL.PENDING); 30 | const tab = GM_openInTab(this.url, { active: true, setParent: true }); 31 | tab.onclose = () => this.finally(); 32 | } 33 | 34 | static verify() { 35 | const params = new URLSearchParams(location.search); 36 | const cb = params.get("cb"); 37 | const tag = params.get("tag"); 38 | if (!cb || tag !== this.TAG) return; 39 | 40 | const key = "setting_win"; 41 | unsafeWindow[key] ??= {}; 42 | unsafeWindow[key][cb] = () => GM_setValue(this.STATUS_KEY, this.STATUS_VAL.VERIFIED) || window.close(); 43 | } 44 | 45 | static finally() { 46 | if (GM_getValue(this.STATUS_KEY) === this.STATUS_VAL.VERIFIED) return; 47 | GM_setValue(this.STATUS_KEY, this.STATUS_VAL.FAILED); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javpack", 3 | "version": "0.0.1", 4 | "description": "一点微小的工作", 5 | "author": "blc", 6 | "license": "GPL-3.0-only", 7 | "devDependencies": { 8 | "@babel/core": "^7.23.5", 9 | "@babel/eslint-parser": "^7.23.3", 10 | "eslint": "^8.55.0", 11 | "eslint-config-alloy": "^5.1.2", 12 | "prettier": "^3.1.0", 13 | "stylelint": "^15.11.0", 14 | "stylelint-config-recess-order": "^4.4.0", 15 | "stylelint-config-standard": "^34.0.0" 16 | }, 17 | "packageManager": "pnpm@8.15.5+sha1.a58c038faac410c947dbdb93eb30994037d0fce2" 18 | } 19 | -------------------------------------------------------------------------------- /static/JavDB.style.user.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --x-gap: 1rem; 3 | --x-radius: 2px; 4 | --x-size-sm: 0.75rem; 5 | --x-size-md: 0.875rem; 6 | --x-trans: all 0.3s cubic-bezier(0, 0, 0.5, 1); 7 | --x-shadow: 2px 4px 12px #00000014; 8 | --x-success: rgb(25 135 84); 9 | --x-info: rgb(13 110 253); 10 | --x-warning: rgb(255 193 7); 11 | --x-danger: rgb(220 53 69); 12 | --x-success-bd: rgb(163 207 187); 13 | --x-info-bd: rgb(158 197 254); 14 | --x-warning-bd: rgb(255 230 156); 15 | --x-danger-bd: rgb(241 174 181); 16 | 17 | color-scheme: light; 18 | 19 | &[data-theme="dark"] { 20 | --x-success-bd: rgb(15 81 50); 21 | --x-info-bd: rgb(8 66 152); 22 | --x-warning-bd: rgb(153 116 4); 23 | --x-danger-bd: rgb(132 32 41); 24 | 25 | background-color: transparent; 26 | color-scheme: dark; 27 | 28 | .navbar { 29 | --x-rgb: 181 181 181; 30 | --x-bg-rgb: 10 10 10; 31 | } 32 | 33 | .navbar-dropdown:hover { 34 | background-color: #0a0a0a !important; 35 | } 36 | 37 | #tags dt:last-child { 38 | border-bottom: none !important; 39 | } 40 | 41 | .box:is(:focus, :hover, :active) { 42 | box-shadow: none !important; 43 | } 44 | 45 | nav.pagination { 46 | border-top: none !important; 47 | } 48 | 49 | .preview-images a, 50 | .column-video-cover a, 51 | .tile-small .tile-item img { 52 | background-color: #444; 53 | } 54 | } 55 | } 56 | 57 | html { 58 | overflow-y: overlay; 59 | scroll-behavior: smooth; 60 | 61 | &.has-navbar-fixed-bottom { 62 | padding-bottom: 0; 63 | } 64 | } 65 | 66 | body { 67 | min-height: 100%; 68 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, 69 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 70 | } 71 | 72 | .compensate-for-scrollbar { 73 | margin-right: 0 !important; 74 | } 75 | 76 | video:focus { 77 | outline: none; 78 | } 79 | 80 | .actors + br, 81 | nav.sub-header, 82 | .actor-filter hr, 83 | #magnets .top-meta, 84 | .app-desktop-banner, 85 | .x-load + nav.pagination, 86 | body:has(.x-load) #footer, 87 | #magnets-content .magnet-name br, 88 | #navbar-menu-hero .navbar-start > a.navbar-item, 89 | .section:has(.video-detail) #search-bar-container, 90 | .movie-panel-info .review-buttons .panel-block:nth-child(2) { 91 | display: none !important; 92 | } 93 | 94 | .navbar { 95 | --x-rgb: 74 74 74; 96 | --x-bg-rgb: 238 238 235; 97 | 98 | padding: 0 max(calc((100% - 1344px) / 2), 1rem); 99 | 100 | --x-cl: rgb(var(--x-rgb)); 101 | --x-bd: rgb(var(--x-rgb) / 5%); 102 | --x-bg: rgb(var(--x-bg-rgb) / 80%); 103 | --x-hv: rgb(var(--x-bg-rgb) / 50%); 104 | 105 | &.is-black { 106 | background-color: var(--x-bg) !important; 107 | backdrop-filter: saturate(1.8) blur(20px); 108 | border-bottom: 1px solid var(--x-bd); 109 | 110 | .navbar-brand > a.navbar-item:is(:focus, :hover, :focus-within) { 111 | background-color: var(--x-hv) !important; 112 | } 113 | } 114 | 115 | .navbar-brand > .navbar-item svg { 116 | width: auto; 117 | height: 2rem; 118 | 119 | path { 120 | fill: rgb(var(--x-bg-rgb)); 121 | 122 | &:last-child { 123 | fill: #2f80ed; 124 | } 125 | } 126 | } 127 | 128 | .navbar-link { 129 | font-size: var(--x-size-md); 130 | 131 | &:not(.is-arrowless)::after { 132 | margin-top: -0.42em; 133 | } 134 | } 135 | } 136 | 137 | @media screen and (width >= 1024px) { 138 | .navbar.is-black :is(.navbar-start, .navbar-end) :is(> .navbar-item, .navbar-link) { 139 | color: var(--x-cl) !important; 140 | } 141 | 142 | .navbar.is-black :is(.navbar-start, .navbar-end) .navbar-link::after { 143 | border-color: var(--x-cl) !important; 144 | } 145 | 146 | .navbar.is-black .navbar-item.has-dropdown:is(:focus, :hover, :focus-within) .navbar-link { 147 | background-color: var(--x-hv) !important; 148 | } 149 | 150 | .navbar-dropdown:is(.is-boxed, .is-right) { 151 | top: 100%; 152 | border-radius: var(--x-radius); 153 | } 154 | 155 | .navbar-dropdown.is-right { 156 | display: block; 157 | pointer-events: none; 158 | border-top: none; 159 | box-shadow: 160 | 0 8px 8px rgb(10 10 10 / 10%), 161 | 0 0 0 1px rgb(10 10 10 / 10%); 162 | opacity: 0; 163 | transition-duration: 86ms; 164 | transition-property: opacity, transform; 165 | transform: translateY(-5px); 166 | } 167 | 168 | .navbar-item.is-hoverable:is(:focus, :hover, :focus-within) .navbar-dropdown.is-right { 169 | pointer-events: auto; 170 | opacity: 1; 171 | transform: translateY(0); 172 | } 173 | } 174 | 175 | .search-bar-container { 176 | margin-bottom: 0.25rem !important; 177 | } 178 | 179 | .columns:not(:last-child) { 180 | margin-bottom: 0.25rem; 181 | } 182 | 183 | .search-bar-wrap { 184 | padding: 0.75rem; 185 | border-radius: 1px; 186 | 187 | .search-panel .control:first-child { 188 | overflow: hidden; 189 | border-top-left-radius: 1px; 190 | border-bottom-left-radius: 1px; 191 | } 192 | 193 | .search-panel .control:last-child { 194 | overflow: hidden; 195 | border-top-right-radius: 1px; 196 | border-bottom-right-radius: 1px; 197 | } 198 | 199 | .search-recent-keywords { 200 | margin: 0 -0.375rem; 201 | font-size: var(--x-size-sm); 202 | 203 | li { 204 | padding: 0 0.375rem; 205 | } 206 | } 207 | } 208 | 209 | #search-type, 210 | #video-search, 211 | #search-submit, 212 | #button-search-image, 213 | #button-advanced-search { 214 | border: none; 215 | 216 | &:is(:focus, :hover, :active) { 217 | z-index: auto !important; 218 | } 219 | } 220 | 221 | .main-tabs { 222 | margin-bottom: 1rem !important; 223 | } 224 | 225 | .tabs { 226 | &:not(:last-child) { 227 | margin-bottom: 1rem; 228 | } 229 | 230 | ul { 231 | border-bottom: none; 232 | } 233 | 234 | &.is-boxed a { 235 | border: none; 236 | } 237 | } 238 | 239 | .toolbar { 240 | padding: 0 0 0.5rem; 241 | font-size: 0; 242 | 243 | .button-group { 244 | margin-right: 0.5rem; 245 | margin-bottom: 0.5rem; 246 | vertical-align: middle; 247 | } 248 | } 249 | 250 | .movie-list { 251 | grid-template-columns: repeat(3, minmax(0, 1fr)); 252 | gap: var(--x-gap); 253 | align-items: start; 254 | padding-bottom: 0; 255 | 256 | &.v { 257 | grid-template-columns: repeat(6, minmax(0, 1fr)); 258 | 259 | .item .cover { 260 | aspect-ratio: 147 / 200; 261 | padding-top: 0; 262 | } 263 | } 264 | 265 | .box { 266 | padding: 0 0 0.5rem; 267 | overflow: hidden; 268 | border-radius: var(--x-radius); 269 | transition: var(--x-trans); 270 | 271 | &:is(:focus, :hover, :active) { 272 | box-shadow: var(--x-shadow); 273 | } 274 | 275 | .video-title, 276 | .score, 277 | .meta, 278 | .tags { 279 | padding-left: 0.5rem; 280 | } 281 | 282 | .meta-buttons { 283 | padding: calc(0.5rem - 5px) 0.5rem 0; 284 | 285 | .button { 286 | border-radius: 1px; 287 | } 288 | } 289 | } 290 | 291 | .item { 292 | position: relative; 293 | transition: border 0.2s; 294 | 295 | &:has(.x-match:not(.is-normal)) { 296 | border-style: solid; 297 | border-width: 0.375rem; 298 | border-radius: calc(0.375rem - 1px); 299 | } 300 | 301 | &:has(.x-match.is-success) { 302 | background-color: var(--x-success-bd); 303 | border-color: var(--x-success-bd); 304 | } 305 | 306 | &:has(.x-match.is-info) { 307 | background-color: var(--x-info-bd); 308 | border-color: var(--x-info-bd); 309 | } 310 | 311 | &:has(.x-match.is-warning) { 312 | background-color: var(--x-warning-bd); 313 | border-color: var(--x-warning-bd); 314 | } 315 | 316 | &:has(.x-match.is-danger) { 317 | background-color: var(--x-danger-bd); 318 | border-color: var(--x-danger-bd); 319 | } 320 | 321 | .cover { 322 | padding-top: 67.25%; 323 | overflow: hidden; 324 | 325 | img { 326 | width: 100%; 327 | object-fit: contain; 328 | transition: var(--x-trans); 329 | } 330 | 331 | video { 332 | position: absolute; 333 | inset: 0; 334 | z-index: 1; 335 | width: 100%; 336 | height: 100%; 337 | background-color: #000; 338 | object-fit: contain; 339 | opacity: 0; 340 | transition: var(--x-trans); 341 | 342 | &.x-show { 343 | opacity: 1; 344 | } 345 | 346 | &::-webkit-media-controls-play-button, 347 | &::-webkit-media-controls-fullscreen-button, 348 | &::-webkit-media-controls-timeline { 349 | display: none; 350 | } 351 | } 352 | 353 | .buttons:has(.x-offline) { 354 | position: absolute; 355 | top: 0; 356 | z-index: 2; 357 | } 358 | 359 | .x-offline { 360 | min-width: 54px; 361 | height: 2em; 362 | padding: 0 0.75em; 363 | border: none; 364 | } 365 | 366 | &:hover img { 367 | z-index: 0; 368 | transform: none; 369 | } 370 | 371 | &.x-hovered:not(:has(video)):hover { 372 | cursor: progress; 373 | } 374 | 375 | &.x-hovered:hover img { 376 | filter: blur(2px); 377 | } 378 | } 379 | 380 | .video-title { 381 | display: -webkit-box; 382 | height: calc(2em * 1.5 + 0.5rem); 383 | padding-top: 0.5rem; 384 | padding-right: 0.5rem; 385 | padding-bottom: 0; 386 | font-size: var(--x-size-md); 387 | white-space: normal; 388 | -webkit-line-clamp: 2; 389 | -webkit-box-orient: vertical; 390 | 391 | .x-match { 392 | margin-right: calc(0.375rem - 1px); 393 | border-radius: 99px; 394 | transform: translate(-1px, -1px); 395 | 396 | &.is-success { 397 | background-color: var(--x-success); 398 | } 399 | 400 | &.is-info { 401 | background-color: var(--x-info); 402 | } 403 | 404 | &.is-warning { 405 | background-color: var(--x-warning); 406 | } 407 | 408 | &.is-danger { 409 | background-color: var(--x-danger); 410 | } 411 | } 412 | } 413 | 414 | .score { 415 | padding-top: 0.75rem; 416 | padding-bottom: 0; 417 | font-size: var(--x-size-sm); 418 | } 419 | 420 | .meta { 421 | padding-bottom: 0.5rem; 422 | font-size: var(--x-size-sm); 423 | } 424 | 425 | .tags.has-addons { 426 | min-height: 2em; 427 | 428 | .tag:first-child { 429 | border-top-left-radius: 1px; 430 | border-bottom-left-radius: 1px; 431 | } 432 | 433 | .tag:last-child { 434 | border-top-right-radius: 1px; 435 | border-bottom-right-radius: 1px; 436 | } 437 | } 438 | 439 | &:has(.x-match, .x-offline) .tags.has-addons .tag { 440 | opacity: 0.75; 441 | } 442 | } 443 | 444 | .column:only-child { 445 | padding: 0; 446 | } 447 | } 448 | 449 | .x-offline { 450 | min-width: 62px; 451 | border-radius: 1px !important; 452 | } 453 | 454 | .x-load { 455 | font-size: var(--x-size-md); 456 | border-radius: 99px !important; 457 | 458 | &[disabled] { 459 | background-color: transparent !important; 460 | border-color: transparent !important; 461 | } 462 | 463 | &.is-loading::after { 464 | border-color: transparent transparent #7a7a7a #7a7a7a; 465 | } 466 | } 467 | 468 | nav.pagination { 469 | padding-top: 1rem; 470 | margin: 0 -0.25rem !important; 471 | border-top: none; 472 | 473 | .pagination-previous, 474 | .pagination-next, 475 | .pagination-link { 476 | border-radius: var(--x-radius); 477 | } 478 | } 479 | 480 | #footer { 481 | background-color: transparent; 482 | border-top: 1px solid var(--x-bd); 483 | 484 | .navbar-item { 485 | font-size: var(--x-size-md); 486 | color: var(--x-cl) !important; 487 | } 488 | } 489 | 490 | .title { 491 | font-size: 1.5rem; 492 | 493 | &:not(:last-child) { 494 | margin-bottom: 1rem; 495 | } 496 | } 497 | 498 | .main-title { 499 | padding-top: 0; 500 | font-size: 1.5rem; 501 | } 502 | 503 | #tags { 504 | margin-top: -0.5rem; 505 | margin-bottom: 0.5rem; 506 | 507 | dt { 508 | --x-height: 40px; 509 | 510 | display: flex; 511 | gap: 0.5rem; 512 | align-items: baseline; 513 | justify-content: start; 514 | padding: 0.5rem 0 0; 515 | line-height: normal; 516 | 517 | &.collapse { 518 | display: flex; 519 | height: calc(var(--x-height) + 1px); 520 | 521 | &:last-child { 522 | height: var(--x-height); 523 | } 524 | } 525 | 526 | &:last-child { 527 | border-bottom: none; 528 | } 529 | 530 | a.tag-expand { 531 | order: 3; 532 | float: none; 533 | margin-top: 0; 534 | transform: translateY(-1px); 535 | } 536 | 537 | strong { 538 | margin-right: -0.5rem; 539 | font-size: var(--x-size-md); 540 | } 541 | 542 | .tag_labels { 543 | display: flex; 544 | flex: 1; 545 | flex-wrap: wrap; 546 | gap: 0.5rem; 547 | align-items: start; 548 | justify-content: start; 549 | padding-bottom: 0.5rem; 550 | } 551 | } 552 | } 553 | 554 | .actors { 555 | grid-template-columns: repeat(6, minmax(0, 1fr)); 556 | gap: var(--x-gap); 557 | align-items: start; 558 | 559 | .box { 560 | margin-bottom: 0; 561 | overflow: hidden; 562 | font-size: var(--x-size-md); 563 | border-radius: var(--x-radius); 564 | transition: var(--x-trans); 565 | 566 | &:is(:focus, :hover, :active) { 567 | box-shadow: var(--x-shadow); 568 | } 569 | 570 | .button { 571 | margin-top: 0 !important; 572 | border-radius: 1px; 573 | } 574 | } 575 | } 576 | 577 | .actor-box a { 578 | figure { 579 | aspect-ratio: 1; 580 | overflow: hidden; 581 | 582 | span.info { 583 | height: auto; 584 | padding: 0.25rem; 585 | font-size: var(--x-size-sm); 586 | } 587 | } 588 | 589 | strong { 590 | padding: 0.75rem; 591 | line-height: normal; 592 | 593 | &:nth-child(3) { 594 | padding-top: 0; 595 | } 596 | } 597 | } 598 | 599 | .section:has(> .awards) { 600 | padding: 0; 601 | 602 | .divider-title { 603 | padding-bottom: 0; 604 | border-bottom: none; 605 | } 606 | 607 | .awards { 608 | &:last-child { 609 | padding-bottom: 0; 610 | } 611 | 612 | .mb-6 { 613 | margin-bottom: 0 !important; 614 | } 615 | } 616 | } 617 | 618 | br + .title { 619 | margin-top: 1rem; 620 | } 621 | 622 | .actor-filter-toolbar { 623 | padding-bottom: 1rem; 624 | } 625 | 626 | .actor-filter { 627 | padding: 1rem; 628 | 629 | &:not(:last-child) { 630 | margin-bottom: 1rem; 631 | } 632 | } 633 | 634 | .filter-slider .slider-title { 635 | font-size: 1rem; 636 | } 637 | 638 | .actor-filter-actions { 639 | font-size: 0; 640 | 641 | a.button { 642 | margin-right: 0.75rem; 643 | } 644 | } 645 | 646 | .section-container { 647 | grid-template-columns: repeat(3, minmax(0, 1fr)); 648 | gap: var(--x-gap); 649 | align-items: start; 650 | 651 | .box { 652 | padding: 1rem; 653 | overflow: hidden; 654 | font-size: var(--x-size-md); 655 | border-radius: var(--x-radius); 656 | transition: var(--x-trans); 657 | 658 | &:is(:focus, :hover, :active) { 659 | box-shadow: var(--x-shadow); 660 | } 661 | 662 | span { 663 | font-size: var(--x-size-sm); 664 | } 665 | 666 | .button { 667 | border-radius: 1px; 668 | } 669 | } 670 | 671 | a { 672 | display: flex; 673 | gap: 0.75rem; 674 | align-items: baseline; 675 | 676 | strong { 677 | flex: 1; 678 | overflow: hidden; 679 | text-overflow: ellipsis; 680 | white-space: nowrap; 681 | } 682 | } 683 | 684 | .column:only-child { 685 | padding: 0; 686 | } 687 | } 688 | 689 | .section-columns { 690 | padding-top: 0; 691 | } 692 | 693 | .actor-tags { 694 | padding-bottom: 0; 695 | border-bottom: none; 696 | 697 | .content { 698 | font-size: 0; 699 | 700 | &:not(.collapse) { 701 | margin-bottom: -0.5rem; 702 | } 703 | } 704 | } 705 | 706 | .user-container { 707 | margin: -1.75rem !important; 708 | } 709 | 710 | #user-menu .menu-label { 711 | padding-inline: 0.75rem; 712 | } 713 | 714 | .message-container, 715 | .message:not(:last-child) { 716 | margin-bottom: 1rem; 717 | } 718 | 719 | .message-header, 720 | .message-body { 721 | padding: 0.75rem; 722 | } 723 | 724 | .common-list { 725 | margin-top: -1rem; 726 | 727 | .list-item { 728 | margin: 0; 729 | font-size: 1rem; 730 | 731 | .column { 732 | padding-inline: 0; 733 | } 734 | 735 | .meta { 736 | font-size: var(--x-size-md); 737 | } 738 | } 739 | 740 | .field.has-addons { 741 | align-items: center; 742 | justify-content: end; 743 | height: 100%; 744 | } 745 | } 746 | 747 | .box:has(> form) { 748 | padding: 1rem; 749 | margin-bottom: 1rem !important; 750 | } 751 | 752 | .section:has(.video-detail) { 753 | padding-bottom: 0; 754 | } 755 | 756 | .video-meta-panel { 757 | padding: 0.75rem; 758 | margin-bottom: 1rem; 759 | } 760 | 761 | .column-video-cover { 762 | a { 763 | position: relative; 764 | display: block; 765 | aspect-ratio: 400 / 269; 766 | background-color: #aa9084; 767 | 768 | & > :is(img, video) { 769 | position: absolute; 770 | inset: 0; 771 | width: 100%; 772 | height: 100%; 773 | object-fit: contain; 774 | max-height: none; 775 | } 776 | 777 | video { 778 | background-color: #000; 779 | } 780 | } 781 | 782 | .cover-container { 783 | &::after { 784 | height: 100%; 785 | } 786 | 787 | .play-button { 788 | font-size: 1.5rem; 789 | } 790 | } 791 | } 792 | 793 | .movie-panel-info { 794 | font-size: var(--x-size-md); 795 | 796 | div.panel-block { 797 | align-items: normal; 798 | padding: 0.5rem 0; 799 | 800 | .copy-to-clipboard { 801 | width: 1.25rem; 802 | height: 1.25rem; 803 | padding: 0; 804 | } 805 | 806 | .x-match { 807 | display: -webkit-box; 808 | overflow: hidden; 809 | text-overflow: ellipsis; 810 | word-break: break-all; 811 | white-space: unset; 812 | -webkit-line-clamp: 1; 813 | -webkit-box-orient: vertical; 814 | } 815 | } 816 | 817 | & > div.panel-block:first-child { 818 | padding-top: 0; 819 | } 820 | 821 | & > div.panel-block:last-child { 822 | padding-bottom: 0; 823 | border-bottom: none !important; 824 | } 825 | 826 | .review-buttons .button { 827 | border-radius: 1px !important; 828 | } 829 | } 830 | 831 | .fancybox-slide { 832 | overflow-y: overlay; 833 | overscroll-behavior: contain; 834 | } 835 | 836 | .video-panel { 837 | .tile-images { 838 | gap: 0.75rem; 839 | font-size: 0; 840 | 841 | .tile-item :is(.video-number, .video-title) { 842 | font-size: var(--x-size-md); 843 | } 844 | } 845 | 846 | .magnet-links { 847 | counter-reset: magnet-counter; 848 | 849 | .item { 850 | align-items: center; 851 | margin: 0; 852 | 853 | .magnet-name { 854 | width: auto; 855 | font-size: var(--x-size-md); 856 | 857 | a { 858 | display: flex; 859 | gap: 0.5rem; 860 | align-items: center; 861 | } 862 | 863 | .name { 864 | display: -webkit-box; 865 | flex: 1; 866 | overflow: hidden; 867 | text-overflow: ellipsis; 868 | word-break: break-all; 869 | -webkit-line-clamp: 1; 870 | -webkit-box-orient: vertical; 871 | 872 | &::before { 873 | font-weight: normal; 874 | content: counter(magnet-counter, decimal-leading-zero) ". "; 875 | counter-increment: magnet-counter; 876 | } 877 | } 878 | 879 | .meta { 880 | flex-shrink: 0; 881 | width: 150px; 882 | font-size: var(--x-size-md); 883 | } 884 | 885 | .tags { 886 | flex-shrink: 0; 887 | width: 150px; 888 | padding-top: 0; 889 | } 890 | } 891 | 892 | .date { 893 | flex-shrink: 0; 894 | order: 2; 895 | width: 160px; 896 | font-size: var(--x-size-md); 897 | text-align: left; 898 | } 899 | 900 | .buttons { 901 | order: 3; 902 | margin-bottom: 0; 903 | font-size: 0; 904 | 905 | .button { 906 | margin-bottom: 0; 907 | border-radius: 1px; 908 | } 909 | 910 | &:has(.x-offline) a.button { 911 | display: none; 912 | } 913 | } 914 | } 915 | } 916 | } 917 | 918 | .preview-images a { 919 | aspect-ratio: 4 / 3; 920 | background-color: #aa9084; 921 | 922 | img { 923 | width: 100% !important; 924 | height: 100% !important; 925 | object-fit: contain; 926 | } 927 | } 928 | 929 | .preview-video-container::after { 930 | height: 100%; 931 | } 932 | 933 | #tabs-container { 934 | margin-top: -1rem; 935 | } 936 | 937 | #magnets .message { 938 | margin-bottom: 0; 939 | } 940 | 941 | .tags:has(.x-magnet) { 942 | .tag { 943 | background-color: transparent; 944 | } 945 | 946 | & + #magnets-content { 947 | max-height: calc(54px * 9); 948 | overflow-y: overlay; 949 | } 950 | } 951 | 952 | .review-items .review-item { 953 | padding: 0.75rem 0; 954 | 955 | &:first-child { 956 | padding-top: 0; 957 | } 958 | 959 | &:last-child { 960 | padding-bottom: 0; 961 | } 962 | 963 | .review-title { 964 | font-size: var(--x-size-md); 965 | 966 | & > div:first-child { 967 | margin-left: 0; 968 | } 969 | } 970 | } 971 | 972 | .plain-grid-list { 973 | gap: 0.75rem; 974 | align-items: start; 975 | 976 | a.box { 977 | display: flex; 978 | gap: 0.75rem; 979 | align-items: baseline; 980 | padding: 1rem; 981 | margin-bottom: 0; 982 | overflow: hidden; 983 | font-size: var(--x-size-md); 984 | border-radius: var(--x-radius); 985 | transition: var(--x-trans); 986 | 987 | &:is(:focus, :hover, :active) { 988 | box-shadow: var(--x-shadow); 989 | } 990 | 991 | strong { 992 | flex: 1; 993 | overflow: hidden; 994 | text-overflow: ellipsis; 995 | white-space: nowrap; 996 | } 997 | 998 | span { 999 | font-size: var(--x-size-sm); 1000 | } 1001 | } 1002 | } 1003 | 1004 | .tile-small .tile-item { 1005 | display: flex; 1006 | flex-direction: column; 1007 | 1008 | img { 1009 | flex: 1; 1010 | width: 100%; 1011 | margin-bottom: 0.375rem; 1012 | background-color: #aa9084; 1013 | object-fit: contain; 1014 | } 1015 | } 1016 | --------------------------------------------------------------------------------