pervert-monkey 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/core/pervertmonkey.core.es.d.ts +391 -0
  4. package/dist/core/pervertmonkey.core.es.js +8497 -0
  5. package/dist/core/pervertmonkey.core.es.js.map +1 -0
  6. package/dist/core/pervertmonkey.core.umd.js +8500 -0
  7. package/dist/core/pervertmonkey.core.umd.js.map +1 -0
  8. package/dist/userscripts/3hentai.user.js +1176 -0
  9. package/dist/userscripts/camgirlfinder.user.js +68 -0
  10. package/dist/userscripts/camwhores.user.js +1602 -0
  11. package/dist/userscripts/e-hentai.user.js +1212 -0
  12. package/dist/userscripts/ebalka.user.js +1231 -0
  13. package/dist/userscripts/eporner.user.js +1265 -0
  14. package/dist/userscripts/erome.user.js +1245 -0
  15. package/dist/userscripts/eroprofile.user.js +1194 -0
  16. package/dist/userscripts/javhdporn.user.js +1178 -0
  17. package/dist/userscripts/missav.user.js +1182 -0
  18. package/dist/userscripts/motherless.user.js +1380 -0
  19. package/dist/userscripts/namethatporn.user.js +1218 -0
  20. package/dist/userscripts/nhentai.user.js +1262 -0
  21. package/dist/userscripts/pornhub.user.js +1199 -0
  22. package/dist/userscripts/spankbang.user.js +1239 -0
  23. package/dist/userscripts/xhamster.user.js +1374 -0
  24. package/dist/userscripts/xvideos.user.js +1254 -0
  25. package/package.json +54 -0
  26. package/src/core/data-control/data-filter.ts +143 -0
  27. package/src/core/data-control/data-manager.ts +144 -0
  28. package/src/core/data-control/index.ts +2 -0
  29. package/src/core/infinite-scroll/index.ts +143 -0
  30. package/src/core/jabroni-config/default-scheme.ts +97 -0
  31. package/src/core/jabroni-config/default-store.ts +9 -0
  32. package/src/core/pagination-parsing/index.ts +55 -0
  33. package/src/core/pagination-parsing/pagination-strategies/PaginationStrategy.ts +44 -0
  34. package/src/core/pagination-parsing/pagination-strategies/PaginationStrategyDataParams.ts +66 -0
  35. package/src/core/pagination-parsing/pagination-strategies/PaginationStrategyPathnameParams.ts +77 -0
  36. package/src/core/pagination-parsing/pagination-strategies/PaginationStrategySearchParams.ts +56 -0
  37. package/src/core/pagination-parsing/pagination-strategies/index.ts +4 -0
  38. package/src/core/pagination-parsing/pagination-utils/index.ts +84 -0
  39. package/src/core/rules/index.ts +385 -0
  40. package/src/index.ts +42 -0
  41. package/src/types/index.ts +7 -0
  42. package/src/userscripts/ascii-logos.js +468 -0
  43. package/src/userscripts/index.ts +1 -0
  44. package/src/userscripts/meta.json +11 -0
  45. package/src/userscripts/scripts/3hentai.ts +20 -0
  46. package/src/userscripts/scripts/camgirlfinder.ts +68 -0
  47. package/src/userscripts/scripts/camwhores.ts +382 -0
  48. package/src/userscripts/scripts/e-hentai.ts +68 -0
  49. package/src/userscripts/scripts/ebalka.ts +58 -0
  50. package/src/userscripts/scripts/eporner.ts +90 -0
  51. package/src/userscripts/scripts/erome.ts +105 -0
  52. package/src/userscripts/scripts/eroprofile.ts +38 -0
  53. package/src/userscripts/scripts/javhdporn.ts +24 -0
  54. package/src/userscripts/scripts/missav.ts +28 -0
  55. package/src/userscripts/scripts/motherless.ts +222 -0
  56. package/src/userscripts/scripts/namethatporn.ts +68 -0
  57. package/src/userscripts/scripts/nhentai.ts +135 -0
  58. package/src/userscripts/scripts/pornhub.ts +53 -0
  59. package/src/userscripts/scripts/spankbang.ts +61 -0
  60. package/src/userscripts/scripts/thisvid.ts +716 -0
  61. package/src/userscripts/scripts/xhamster.ts +179 -0
  62. package/src/userscripts/scripts/xvideos.ts +83 -0
  63. package/src/utils/arrays/index.ts +15 -0
  64. package/src/utils/async/index.ts +3 -0
  65. package/src/utils/dom/dom-observers.ts +76 -0
  66. package/src/utils/dom/index.ts +156 -0
  67. package/src/utils/events/index.ts +2 -0
  68. package/src/utils/events/on-pointer-over-and-leave.ts +35 -0
  69. package/src/utils/events/tick.ts +27 -0
  70. package/src/utils/fetch/index.ts +37 -0
  71. package/src/utils/math/index.ts +3 -0
  72. package/src/utils/objects/index.ts +9 -0
  73. package/src/utils/objects/memoize.ts +25 -0
  74. package/src/utils/observers/index.ts +44 -0
  75. package/src/utils/observers/lazy-image-loader.ts +27 -0
  76. package/src/utils/parsers/index.ts +30 -0
  77. package/src/utils/parsers/time-parser.ts +28 -0
  78. package/src/utils/strings/index.ts +10 -0
  79. package/src/utils/strings/regexes.ts +35 -0
  80. package/src/vite-env.d.ts +4 -0
@@ -0,0 +1,105 @@
1
+ import type { MonkeyUserScript } from 'vite-plugin-monkey';
2
+ import { GM_addStyle, unsafeWindow } from '$';
3
+ import { RulesGlobal } from '../../core/rules';
4
+
5
+ export const meta: MonkeyUserScript = {
6
+ name: 'Erome PervertMonkey',
7
+ version: '5.0.0',
8
+ description: 'Infinite scroll [optional], Filter by Title and Video/Photo albums',
9
+ match: ['*://*.erome.com/*'],
10
+ icon: 'https://www.google.com/s2/favicons?sz=64&domain=erome.com',
11
+ };
12
+
13
+ const $ = (unsafeWindow as any).$;
14
+ declare var LazyLoad: ObjectConstructor;
15
+
16
+ const rules = new RulesGlobal({
17
+ containerSelector: '#albums',
18
+ thumbsSelector: 'div[id^=album-]',
19
+ titleSelector: '.album-title',
20
+ uploaderSelector: '.album-user',
21
+ gropeStrategy: 'all-in-one',
22
+ customThumbDataSelectors: {
23
+ videoAlbum: {
24
+ type: 'boolean',
25
+ selector: '.album-videos',
26
+ },
27
+ },
28
+ storeOptions: { showPhotos: true },
29
+ customDataSelectorFns: [
30
+ 'filterInclude',
31
+ 'filterExclude',
32
+ {
33
+ filterPhotoAlbums: (el, state) =>
34
+ (state.filterPhotoAlbums && !el.videoAlbum) as boolean,
35
+ },
36
+ {
37
+ filterVideoAlbums: (el, state) =>
38
+ (state.filterVideoAlbums && el.videoAlbum) as boolean,
39
+ },
40
+ ],
41
+ schemeOptions: [
42
+ 'Text Filter',
43
+ {
44
+ title: 'Filter Albums',
45
+ content: [
46
+ {
47
+ filterVideoAlbums: true,
48
+ label: 'video albums',
49
+ },
50
+ {
51
+ filterPhotoAlbums: true,
52
+ label: 'photo albums',
53
+ },
54
+ ],
55
+ },
56
+ 'Badge',
57
+ 'Advanced',
58
+ ],
59
+ });
60
+
61
+ rules.infiniteScroller?.onScroll(() => {
62
+ setTimeout(() => new LazyLoad(), 100);
63
+ });
64
+
65
+ GM_addStyle(`
66
+ .inactive-gm { background: #a09f9d; }
67
+ .active-gm { background: #eb6395 !important; }
68
+ `);
69
+
70
+ (function disableDisclaimer() {
71
+ if (!$('#disclaimer').length) return;
72
+ $.ajax({ type: 'POST', url: '/user/disclaimer', async: true });
73
+ $('#disclaimer').remove();
74
+ $('body').css('overflow', 'visible');
75
+ })();
76
+
77
+ const IS_ALBUM_PAGE = /^\/a\//.test(window.location.pathname);
78
+
79
+ function togglePhotoElements() {
80
+ $('.media-group > div:last-child:not(.video)').toggle(rules.store.state.showPhotos);
81
+ $('#togglePhotos').toggleClass('active-gm', rules.store.state.showPhotos);
82
+ $('#togglePhotos').text(!rules.store.state.showPhotos ? 'show photos' : 'hide photos');
83
+ }
84
+
85
+ function setupAlbumPage() {
86
+ $('#user_name')
87
+ .parent()
88
+ .append(
89
+ '<button id="togglePhotos" class="btn btn-pink inactive-gm">show/hide photos</button>',
90
+ );
91
+
92
+ $('#togglePhotos').on('click', () => {
93
+ rules.store.state.showPhotos = !rules.store.state.showPhotos;
94
+ });
95
+
96
+ rules.store.stateSubject.subscribe(() => {
97
+ togglePhotoElements();
98
+ });
99
+
100
+ togglePhotoElements();
101
+ }
102
+
103
+ if (IS_ALBUM_PAGE) {
104
+ setupAlbumPage();
105
+ }
@@ -0,0 +1,38 @@
1
+ import type { MonkeyUserScript } from 'vite-plugin-monkey';
2
+ import { RulesGlobal } from '../../core/rules';
3
+
4
+ export const meta: MonkeyUserScript = {
5
+ name: 'Eroprofile PervertMonkey',
6
+ version: '2.0.0',
7
+ description: 'Infinite scroll [optional], Filter by Title and Duration',
8
+ match: ['https://*.eroprofile.com/*'],
9
+ icon: 'https://www.google.com/s2/favicons?sz=64&domain=eroprofile.com',
10
+ };
11
+
12
+ document.querySelector('.videoGrid')?.after(document.querySelector('.clB') as HTMLElement);
13
+
14
+ const rules = new RulesGlobal({
15
+ paginationStrategyOptions: {
16
+ paginationSelector: '.boxNav2',
17
+ searchParamSelector: 'pnum',
18
+ },
19
+ titleSelector: '[title]',
20
+ durationSelector: '.videoDur',
21
+ containerSelector: '.videoGrid',
22
+ thumbsSelector: '.video',
23
+ customDataSelectorFns: ['filterInclude', 'filterExclude', 'filterDuration'],
24
+ schemeOptions: [
25
+ 'Text Filter',
26
+ 'Duration Filter',
27
+ {
28
+ title: 'Sort By ',
29
+ content: [
30
+ {
31
+ 'sort by duration': () => {},
32
+ },
33
+ ],
34
+ },
35
+ 'Badge',
36
+ 'Advanced',
37
+ ],
38
+ });
@@ -0,0 +1,24 @@
1
+ import type { MonkeyUserScript } from 'vite-plugin-monkey';
2
+ import { RulesGlobal } from '../../core/rules';
3
+
4
+ export const meta: MonkeyUserScript = {
5
+ name: 'Javhdporn PervertMonkey',
6
+ version: '3.0.0',
7
+ description: 'Infinite scroll [optional], Filter by Title and Duration',
8
+ match: [
9
+ "https://*.javhdporn.net/*",
10
+ "https://*.javhdporn.*/*"
11
+ ],
12
+ icon: 'https://www.google.com/s2/favicons?sz=64&domain=javhdporn.net',
13
+ };
14
+
15
+ const rules = new RulesGlobal({
16
+ containerSelector: 'div:has(> article)',
17
+ thumbsSelector: 'article.thumb-block',
18
+ titleSelector: 'header.entry-header',
19
+ durationSelector: '.duration',
20
+ paginationStrategyOptions: {
21
+ pathnameSelector: /\/page\/(\d+)\/?$/,
22
+ },
23
+ schemeOptions: ['Text Filter', 'Badge', 'Duration Filter', 'Advanced'],
24
+ });
@@ -0,0 +1,28 @@
1
+ import type { MonkeyUserScript } from 'vite-plugin-monkey';
2
+ import { RulesGlobal } from '../../core/rules';
3
+
4
+ export const meta: MonkeyUserScript = {
5
+ name: 'Missav PervertMonkey',
6
+ version: '3.0.0',
7
+ description: 'Infinite scroll [optional], Filter by Title and Duration',
8
+ match: [
9
+ 'https://*.missav.*/*',
10
+ 'https://*.missav123.com/*',
11
+ 'https://*.missav.ws/*',
12
+ 'https://*.missav.to/*',
13
+ 'https://*.missav.live/*',
14
+ ],
15
+ icon: 'https://www.google.com/s2/favicons?sz=64&domain=missav123.com',
16
+ };
17
+
18
+ const rules = new RulesGlobal({
19
+ paginationStrategyOptions: {
20
+ paginationSelector: 'nav[x-data]',
21
+ },
22
+ containerSelector: '.grid[x-data]',
23
+ thumbsSelector: 'div:has(> .thumbnail.group)',
24
+ getThumbImgDataStrategy: 'auto',
25
+ titleSelector: 'div > div > a.text-secondary',
26
+ durationSelector: 'div > a > span.text-xs',
27
+ schemeOptions: ['Text Filter', 'Duration Filter', 'Badge', 'Advanced'],
28
+ });
@@ -0,0 +1,222 @@
1
+ import type { MonkeyUserScript } from 'vite-plugin-monkey';
2
+ import { GM_addStyle, unsafeWindow } from '$';
3
+ import { RulesGlobal } from '../../core/rules';
4
+ import { replaceElementTag } from '../../utils/dom';
5
+ import { onPointerOverAndLeave, Tick } from '../../utils/events';
6
+ import { fetchWith } from '../../utils/fetch';
7
+
8
+ export const meta: MonkeyUserScript = {
9
+ name: 'Motherless PervertMonkey',
10
+ version: '5.0.0',
11
+ description: 'Infinite scroll [optional], Filter by Title and Duration',
12
+ match: ['https://motherless.com/*'],
13
+ icon: 'https://www.google.com/s2/favicons?sz=64&domain=motherless.com',
14
+ };
15
+
16
+ (unsafeWindow as any).__is_premium = true;
17
+ const $ = (unsafeWindow as any).$;
18
+
19
+ const rules = new RulesGlobal({
20
+ containerSelectorLast: '.content-inner',
21
+ thumbsSelector: '.thumb-container, .mobile-thumb',
22
+ uploaderSelector: '.uploader',
23
+ titleSelector: '.title',
24
+ durationSelector: '.size',
25
+ getThumbImgDataStrategy: 'auto',
26
+ paginationStrategyOptions: {
27
+ paginationSelector: '.pagination_link, .ml-pagination',
28
+ },
29
+ animatePreview,
30
+ gropeStrategy: 'all-in-all',
31
+ schemeOptions: ['Text Filter', 'Duration Filter', 'Badge', 'Advanced'],
32
+ });
33
+
34
+ function animatePreview(_: HTMLElement) {
35
+ const ANIMATION_INTERVAL = 500;
36
+ const tick = new Tick(ANIMATION_INTERVAL);
37
+ let currentOverlay: HTMLElement | null = null;
38
+
39
+ function onLeave(target: HTMLElement) {
40
+ tick.stop();
41
+
42
+ const img = target.querySelector('img.static') as HTMLElement;
43
+ img.classList.remove('animating');
44
+
45
+ if (currentOverlay) {
46
+ currentOverlay.style.display = 'none';
47
+ }
48
+ }
49
+
50
+ function onOver(target: HTMLElement) {
51
+ $('.video').off();
52
+ const container = target.closest('.desktop-thumb.video') as HTMLElement;
53
+ const img = container.querySelector('img.static') as HTMLImageElement;
54
+ const stripSrc = img.getAttribute('data-strip-src');
55
+
56
+ img.classList.add('animating');
57
+
58
+ let overlay = img.nextElementSibling as HTMLElement;
59
+ if (!overlay || overlay.tagName !== 'DIV') {
60
+ overlay = document.createElement('div');
61
+ overlay.setAttribute(
62
+ 'style',
63
+ 'z-index: 8; position: absolute; top: 0; left: 0; pointer-events: none;',
64
+ );
65
+ img.parentNode?.insertBefore(overlay, img.nextSibling);
66
+ }
67
+
68
+ currentOverlay = overlay;
69
+ overlay.style.display = 'block';
70
+
71
+ let j = 0;
72
+ const containerHeight = container.offsetHeight;
73
+
74
+ tick.start(() => {
75
+ const w = img.offsetWidth;
76
+ const h = img.offsetHeight;
77
+
78
+ const widthRatio = Math.floor((1000.303 * w) / 100);
79
+ const heightRatio = Math.floor((228.6666 * h) / 100);
80
+
81
+ const verticalOffset = (containerHeight - h) / 2;
82
+
83
+ Object.assign(overlay.style, {
84
+ width: `${w}px`,
85
+ height: `${containerHeight}px`,
86
+ backgroundImage: `url('${stripSrc}')`,
87
+ backgroundSize: `${widthRatio}px ${heightRatio}px`,
88
+ backgroundPosition: `-${(j++ * w) % widthRatio}px ${verticalOffset}px`,
89
+ backgroundRepeat: 'no-repeat',
90
+ });
91
+ });
92
+
93
+ const onOverCallback = () => onLeave(container);
94
+ return { onOverCallback, leaveTarget: container };
95
+ }
96
+
97
+ onPointerOverAndLeave(
98
+ document.body,
99
+ (e) => {
100
+ const container = e.closest('.desktop-thumb.video') as HTMLElement;
101
+ if (!container) return false;
102
+
103
+ const img = container.querySelector('img.static') as HTMLImageElement;
104
+ if (!img) return false;
105
+
106
+ const stripSrc = img.getAttribute('data-strip-src');
107
+ if (!stripSrc || img.classList.contains('animating')) return false;
108
+
109
+ return true;
110
+ },
111
+ onOver,
112
+ );
113
+ }
114
+
115
+ //====================================================================================================
116
+
117
+ function fixURLs() {
118
+ document.querySelectorAll<HTMLElement>('.gallery-container').forEach((g) => {
119
+ const x = (g.innerText as string).match(/([\d|.]+)k? videos/gi)?.[0];
120
+ const hasVideos = parseInt(x as string) > 0;
121
+
122
+ const header = hasVideos ? '/GV' : '/GI';
123
+
124
+ g.querySelectorAll<HTMLAnchorElement>('a').forEach((a) => {
125
+ a.href = a.href.replace(/\/G/, () => header);
126
+ });
127
+ });
128
+
129
+ document
130
+ .querySelectorAll<HTMLAnchorElement>('a[href^="/term/"]:not([href^="/term/videos/"])')
131
+ .forEach((a) => {
132
+ a.href = a.href.replace(
133
+ /[\w|+]+$/,
134
+ (v) => `videos/${v}?term=${v}&range=0&size=0&sort=date`,
135
+ );
136
+ });
137
+
138
+ document
139
+ .querySelectorAll<HTMLAnchorElement>('#media-groups-container a[href^="/g/"]')
140
+ .forEach((a) => {
141
+ a.href = a.href.replace(/\/g\//, '/gv/');
142
+ });
143
+ }
144
+
145
+ //====================================================================================================
146
+
147
+ function mobileGalleryToDesktop(e: HTMLElement) {
148
+ e.querySelector('.clear-left')?.remove();
149
+ const container = e.firstElementChild as HTMLElement;
150
+ container.appendChild(container.nextElementSibling as HTMLElement);
151
+ e.className = 'thumb-container gallery-container';
152
+ container.className = 'desktop-thumb image medium';
153
+ (container.firstElementChild?.nextElementSibling as HTMLElement).className =
154
+ 'gallery-captions';
155
+ replaceElementTag(container.firstElementChild as HTMLElement, 'a');
156
+ return e;
157
+ }
158
+
159
+ async function desktopAddMobGalleries() {
160
+ const galleries = document.querySelector('.media-related-galleries');
161
+ if (!galleries) return;
162
+
163
+ const galleriesContainer = galleries.querySelector('.content-inner') as HTMLElement;
164
+ const galleriesCount = galleries.querySelectorAll('.gallery-container').length;
165
+ const mobDom = await fetchWith(window.location.href, { type: 'html', mobile: true });
166
+ const mobGalleries = (mobDom as HTMLElement).querySelectorAll<HTMLElement>(
167
+ '.ml-gallery-thumb',
168
+ );
169
+
170
+ for (const [i, x] of mobGalleries.entries()) {
171
+ if (i > galleriesCount - 1) {
172
+ galleriesContainer.append(mobileGalleryToDesktop(x));
173
+ }
174
+ }
175
+ }
176
+
177
+ //====================================================================================================
178
+
179
+ const overwrite1 = (x: string) => `@media only screen and (max-width: 1280px) {
180
+ #categories-page.inner ${x} }`;
181
+
182
+ rules.dataManager.dataFilter.applyCSSFilters(overwrite1);
183
+
184
+ GM_addStyle(`
185
+ .img-container, .desktop-thumb { min-height: 150px; max-height: 150px; }
186
+
187
+ .group-minibio, .gallery-container { display: block !important; }
188
+
189
+ .ml-masonry-images.masonry-columns-4 .content-inner { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
190
+ .ml-masonry-images.masonry-columns-6 .content-inner { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); }
191
+ .ml-masonry-images.masonry-columns-8 .content-inner { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); }
192
+ `);
193
+
194
+ //====================================================================================================
195
+
196
+ function applySearchFilters() {
197
+ let pathname = window.location.pathname;
198
+
199
+ const wordsToFilter =
200
+ (rules.store.state.filterExcludeWords as string)
201
+ .replace(/f:/g, '')
202
+ .match(/(?<!user:)\b\w+\b(?!\s*:)/g) || [];
203
+
204
+ wordsToFilter
205
+ .filter((w) => !pathname.includes(w))
206
+ .forEach((w) => {
207
+ pathname += `+-${w.trim()}`;
208
+ });
209
+
210
+ if (wordsToFilter.some((w) => !window.location.href.includes(w))) {
211
+ window.location.href = pathname;
212
+ }
213
+ }
214
+
215
+ //====================================================================================================
216
+
217
+ desktopAddMobGalleries().then(() => fixURLs());
218
+
219
+ const IS_SEARCH = /^\/term\//.test(location.pathname);
220
+ if (IS_SEARCH) {
221
+ applySearchFilters();
222
+ }
@@ -0,0 +1,68 @@
1
+ import type { MonkeyUserScript } from 'vite-plugin-monkey';
2
+ import { unsafeWindow } from '$';
3
+ import { RulesGlobal } from '../../core/rules';
4
+
5
+ export const meta: MonkeyUserScript = {
6
+ name: 'NameThatPorn PervertMonkey',
7
+ version: '3.0.0',
8
+ description: 'Infinite scroll [optional], Filter by Title and Un/Solved',
9
+ match: ['https://namethatporn.com/*'],
10
+ icon: 'https://www.google.com/s2/favicons?sz=64&domain=namethatporn.com',
11
+ };
12
+
13
+ const rules = new RulesGlobal({
14
+ thumbsSelector: '.item, .nsw_r_w',
15
+ containerSelector: '#items_wrapper, #nsw_r',
16
+ titleSelector: '.item_title, .nsw_r_tit',
17
+ uploaderSelector: '.item_answer b, .nsw_r_desc',
18
+ paginationStrategyOptions: {
19
+ paginationSelector: '#smi_wrp, #nsw_p',
20
+ },
21
+ customThumbDataSelectors: {
22
+ solved: {
23
+ type: 'boolean',
24
+ selector: '.item_solved, .nsw_r_slvd',
25
+ },
26
+ },
27
+ gropeStrategy: 'all-in-all',
28
+ getThumbImgDataStrategy: 'auto',
29
+ getThumbImgDataAttrSelector: (img: HTMLImageElement) =>
30
+ img.getAttribute('data-dyn')?.concat('.webp') || (img.getAttribute('src') as string),
31
+ customDataSelectorFns: [
32
+ 'filterInclude',
33
+ 'filterExclude',
34
+ {
35
+ filterSolved: (el, state) => (state.filterSolved && el.solved) as boolean,
36
+ },
37
+ {
38
+ filterUnsolved: (el, state) => (state.filterUnsolved && !el.solved) as boolean,
39
+ },
40
+ ],
41
+ schemeOptions: [
42
+ 'Text Filter',
43
+ {
44
+ title: 'Filter Status',
45
+ content: [
46
+ { filterSolved: false, label: 'solved' },
47
+ { filterUnsolved: false, label: 'unsolved' },
48
+ ],
49
+ },
50
+ 'Badge',
51
+ 'Advanced',
52
+ ],
53
+ });
54
+
55
+ // some monkeypatching here...
56
+ unsafeWindow.confirm = () => true;
57
+
58
+ function handleKeys(event: KeyboardEvent) {
59
+ if (event.key === 'c') {
60
+ const name = document.querySelector<HTMLElement>('#loggedin_box_new_username')
61
+ ?.innerText as string;
62
+ if (!document.querySelector(`.ida_confirm_usernames a[href$="${name}.html"]`)) {
63
+ document.querySelector<HTMLButtonElement>('.id_answer_buttons > .iab.iac')?.click();
64
+ }
65
+ }
66
+ }
67
+
68
+ unsafeWindow.addEventListener('keydown', handleKeys, { once: true });
@@ -0,0 +1,135 @@
1
+ import type { MonkeyUserScript } from 'vite-plugin-monkey';
2
+ import { RulesGlobal } from '../../core/rules';
3
+ import { parseHtml } from '../../utils/dom';
4
+
5
+ export const meta: MonkeyUserScript = {
6
+ name: 'NHentai PervertMonkey',
7
+ version: '4.0.0',
8
+ description: 'Infinite scroll [optional], Filter by Title',
9
+ match: ['https://*.nhentai.*/*', 'https://*.nhentai.net/*'],
10
+ icon: 'https://www.google.com/s2/favicons?sz=64&domain=nhentai.net',
11
+ };
12
+
13
+ const IS_TITLE_PAGE = /^\/g\/\d+/.test(location.pathname);
14
+ const IS_SEARCH_PAGE = /^\/search\//.test(location.pathname);
15
+
16
+ const nhentaiRules = new RulesGlobal({
17
+ getThumbImgDataAttrDelete: 'auto',
18
+ getThumbImgDataStrategy: 'auto',
19
+ thumbsSelector: '.gallery',
20
+ containerSelectorLast: '.index-container, .container',
21
+ titleSelector: '.caption',
22
+ customDataSelectorFns: ['filterInclude', 'filterExclude'],
23
+ schemeOptions: ['Text Filter', 'Badge', 'Advanced'],
24
+ gropeStrategy: 'all-in-all',
25
+ });
26
+
27
+ // 2026: now only one language works, problem in nhentai.net itself
28
+ const filterDescriptors = {
29
+ english: { query: 'english', name: '🇬🇧' },
30
+ japanese: { query: 'japanese', name: '🇯🇵' },
31
+ chinese: { query: 'chinese', name: '🇨🇳' },
32
+ gay: { query: '-gay', name: 'Exclude Gay' },
33
+ fullColor: { query: 'color', name: 'Full Color' },
34
+ } as const;
35
+
36
+ function checkURL(url_: string) {
37
+ return Object.keys(filterDescriptors).reduce((url, k) => {
38
+ const q = filterDescriptors[k as keyof typeof filterDescriptors].query;
39
+ return (nhentaiRules.store.state.custom as unknown as Record<string, boolean>)[k]
40
+ ? url.includes(q)
41
+ ? url
42
+ : `${url}+${q}`
43
+ : url.replace(`+${q}`, () => '');
44
+ }, url_);
45
+ }
46
+
47
+ function filtersUI() {
48
+ const state = nhentaiRules.store.state as any;
49
+ const btnContainer = Array.from(document.querySelectorAll('.sort-type')).pop();
50
+ const descs = Array.from(Object.keys(filterDescriptors));
51
+ [descs.slice(0, 3), [descs[3]], [descs[4]]].forEach((groupOfButtons) => {
52
+ const btns = parseHtml(`<div class="sort-type"></div>`);
53
+ groupOfButtons.forEach((k) => {
54
+ const btn = parseHtml(
55
+ `<a href="#" ${
56
+ state.custom[k] ? 'style="background: rgba(59, 49, 70, 1)"' : ''
57
+ }>${filterDescriptors[k as keyof typeof filterDescriptors].name}</a>`,
58
+ );
59
+ btn.addEventListener('click', (e) => {
60
+ e.preventDefault();
61
+ state.custom[k] = !state.custom[k];
62
+ location.href = checkURL(location.href);
63
+ });
64
+ btns.append(btn);
65
+ });
66
+ btnContainer?.after(btns);
67
+ });
68
+ const fixedURL = checkURL(location.href);
69
+ if (location.href !== fixedURL) location.href = checkURL(location.href);
70
+ }
71
+
72
+ function findSimilar() {
73
+ let tags = Array.from(
74
+ document.querySelectorAll<HTMLElement>('.tags .tag[href^="/tag/"] .name'),
75
+ )
76
+ .map((tag) => tag.innerText)
77
+ .join(' ')
78
+ .split(' ');
79
+ tags = Array.from(new Set(tags)).sort((a, b) => a.length - b.length);
80
+
81
+ const urls = {
82
+ searchSimilar: `/search/?q=${tags.slice(0, 5).join('+')}`,
83
+ searchSimilarLess: `/search/?q=${tags.reverse().slice(0, 5).join('+')}`,
84
+ };
85
+
86
+ Object.keys(urls).forEach((url) => {
87
+ urls[url as keyof typeof urls] = checkURL(urls[url as keyof typeof urls]);
88
+ });
89
+
90
+ Array.from(document.links)
91
+ .filter((l) =>
92
+ /\/(search|category|tag|character|artist|group|parody)\/\w+/.test(l.href),
93
+ )
94
+ .forEach((l) => {
95
+ l.href = checkURL(
96
+ l.href
97
+ .replace(/(search|category|tag|character|artist|group|parody)\//, 'search/?q=')
98
+ .replace(/\/$/, ''),
99
+ );
100
+ });
101
+
102
+ document
103
+ .querySelector('.buttons')
104
+ ?.append(
105
+ parseHtml(
106
+ `<a href="${urls.searchSimilar}" class="btn" style="background: rgba(59, 49, 70, 1)"><i class="fa fa-search"></i> Similar</a>`,
107
+ ),
108
+ parseHtml(
109
+ `<a href="${urls.searchSimilarLess}" class="btn" style="background: rgba(59, 49, 70, .9)"><i class="fa fa-search"></i> Less Similar</a>`,
110
+ ),
111
+ );
112
+ }
113
+
114
+ function route() {
115
+ if (!nhentaiRules.store.state.custom) {
116
+ const custom = Object.entries(filterDescriptors).reduce(
117
+ (acc, [k, _]) => {
118
+ acc[k] = false;
119
+ return acc;
120
+ },
121
+ {} as Record<string, any>,
122
+ );
123
+ Object.assign(nhentaiRules.store.state, { custom });
124
+ }
125
+
126
+ if (IS_TITLE_PAGE) {
127
+ findSimilar();
128
+ }
129
+
130
+ if (IS_SEARCH_PAGE) {
131
+ filtersUI();
132
+ }
133
+ }
134
+
135
+ route();
@@ -0,0 +1,53 @@
1
+ import type { MonkeyUserScript } from 'vite-plugin-monkey';
2
+ import { RulesGlobal } from '../../core/rules';
3
+
4
+ export const meta: MonkeyUserScript = {
5
+ name: 'PornHub PervertMonkey',
6
+ version: '4.0.0',
7
+ description:
8
+ 'Infinite scroll [optional]. Filter by Title and Duration',
9
+ match: ['https://*.pornhub.com/*'],
10
+ exclude: 'https://*.pornhub.com/embed/*',
11
+ icon: 'https://www.google.com/s2/favicons?sz=64&domain=pornhub.com',
12
+ };
13
+
14
+ const rules = new RulesGlobal({
15
+ paginationStrategyOptions: {
16
+ paginationSelector: '.paginationGated',
17
+ overwritePaginationLast: (n: number) => (n === 9 ? 999 : n),
18
+ },
19
+ containerSelector: () =>
20
+ [...document.querySelectorAll<HTMLElement>('ul:has(> li[data-video-vkey])')]
21
+ .filter((e) => e.children.length > 0 && e.checkVisibility())
22
+ .pop() as HTMLElement,
23
+
24
+ dataManagerOptions: {
25
+ parseDataParentHomogenity: { id: true, className: true },
26
+ },
27
+ thumbsSelector: 'li[data-video-vkey]',
28
+ getThumbImgDataStrategy: 'auto',
29
+ getThumbImgDataAttrSelector: ['data-mediumthumb', 'data-image'],
30
+ uploaderSelector: '.usernameWrap',
31
+ titleSelector: 'span.title',
32
+ durationSelector: '.duration',
33
+ gropeStrategy: 'all-in-all',
34
+ schemeOptions: ['Text Filter', 'Duration Filter', 'Badge', 'Advanced'],
35
+ });
36
+
37
+ function bypassAgeVerification() {
38
+ document
39
+ .querySelectorAll<HTMLButtonElement>('[data-label="over18_enter"]')
40
+ .forEach((b) => {
41
+ b.click();
42
+ });
43
+
44
+ setTimeout(() => {
45
+ cookieStore.set({
46
+ name: 'accessAgeDisclaimerPH',
47
+ value: '2',
48
+ expires: Date.now() + 90 * 24 * 60 * 60 * 1000,
49
+ });
50
+ }, 1000);
51
+ }
52
+
53
+ bypassAgeVerification();