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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/core/pervertmonkey.core.es.d.ts +391 -0
- package/dist/core/pervertmonkey.core.es.js +8497 -0
- package/dist/core/pervertmonkey.core.es.js.map +1 -0
- package/dist/core/pervertmonkey.core.umd.js +8500 -0
- package/dist/core/pervertmonkey.core.umd.js.map +1 -0
- package/dist/userscripts/3hentai.user.js +1176 -0
- package/dist/userscripts/camgirlfinder.user.js +68 -0
- package/dist/userscripts/camwhores.user.js +1602 -0
- package/dist/userscripts/e-hentai.user.js +1212 -0
- package/dist/userscripts/ebalka.user.js +1231 -0
- package/dist/userscripts/eporner.user.js +1265 -0
- package/dist/userscripts/erome.user.js +1245 -0
- package/dist/userscripts/eroprofile.user.js +1194 -0
- package/dist/userscripts/javhdporn.user.js +1178 -0
- package/dist/userscripts/missav.user.js +1182 -0
- package/dist/userscripts/motherless.user.js +1380 -0
- package/dist/userscripts/namethatporn.user.js +1218 -0
- package/dist/userscripts/nhentai.user.js +1262 -0
- package/dist/userscripts/pornhub.user.js +1199 -0
- package/dist/userscripts/spankbang.user.js +1239 -0
- package/dist/userscripts/xhamster.user.js +1374 -0
- package/dist/userscripts/xvideos.user.js +1254 -0
- package/package.json +54 -0
- package/src/core/data-control/data-filter.ts +143 -0
- package/src/core/data-control/data-manager.ts +144 -0
- package/src/core/data-control/index.ts +2 -0
- package/src/core/infinite-scroll/index.ts +143 -0
- package/src/core/jabroni-config/default-scheme.ts +97 -0
- package/src/core/jabroni-config/default-store.ts +9 -0
- package/src/core/pagination-parsing/index.ts +55 -0
- package/src/core/pagination-parsing/pagination-strategies/PaginationStrategy.ts +44 -0
- package/src/core/pagination-parsing/pagination-strategies/PaginationStrategyDataParams.ts +66 -0
- package/src/core/pagination-parsing/pagination-strategies/PaginationStrategyPathnameParams.ts +77 -0
- package/src/core/pagination-parsing/pagination-strategies/PaginationStrategySearchParams.ts +56 -0
- package/src/core/pagination-parsing/pagination-strategies/index.ts +4 -0
- package/src/core/pagination-parsing/pagination-utils/index.ts +84 -0
- package/src/core/rules/index.ts +385 -0
- package/src/index.ts +42 -0
- package/src/types/index.ts +7 -0
- package/src/userscripts/ascii-logos.js +468 -0
- package/src/userscripts/index.ts +1 -0
- package/src/userscripts/meta.json +11 -0
- package/src/userscripts/scripts/3hentai.ts +20 -0
- package/src/userscripts/scripts/camgirlfinder.ts +68 -0
- package/src/userscripts/scripts/camwhores.ts +382 -0
- package/src/userscripts/scripts/e-hentai.ts +68 -0
- package/src/userscripts/scripts/ebalka.ts +58 -0
- package/src/userscripts/scripts/eporner.ts +90 -0
- package/src/userscripts/scripts/erome.ts +105 -0
- package/src/userscripts/scripts/eroprofile.ts +38 -0
- package/src/userscripts/scripts/javhdporn.ts +24 -0
- package/src/userscripts/scripts/missav.ts +28 -0
- package/src/userscripts/scripts/motherless.ts +222 -0
- package/src/userscripts/scripts/namethatporn.ts +68 -0
- package/src/userscripts/scripts/nhentai.ts +135 -0
- package/src/userscripts/scripts/pornhub.ts +53 -0
- package/src/userscripts/scripts/spankbang.ts +61 -0
- package/src/userscripts/scripts/thisvid.ts +716 -0
- package/src/userscripts/scripts/xhamster.ts +179 -0
- package/src/userscripts/scripts/xvideos.ts +83 -0
- package/src/utils/arrays/index.ts +15 -0
- package/src/utils/async/index.ts +3 -0
- package/src/utils/dom/dom-observers.ts +76 -0
- package/src/utils/dom/index.ts +156 -0
- package/src/utils/events/index.ts +2 -0
- package/src/utils/events/on-pointer-over-and-leave.ts +35 -0
- package/src/utils/events/tick.ts +27 -0
- package/src/utils/fetch/index.ts +37 -0
- package/src/utils/math/index.ts +3 -0
- package/src/utils/objects/index.ts +9 -0
- package/src/utils/objects/memoize.ts +25 -0
- package/src/utils/observers/index.ts +44 -0
- package/src/utils/observers/lazy-image-loader.ts +27 -0
- package/src/utils/parsers/index.ts +30 -0
- package/src/utils/parsers/time-parser.ts +28 -0
- package/src/utils/strings/index.ts +10 -0
- package/src/utils/strings/regexes.ts +35 -0
- package/src/vite-env.d.ts +4 -0
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { from } from 'ix/asynciterable';
|
|
2
|
+
import { concatMap, flatMap, map, takeWhile } from 'ix/asynciterable/operators';
|
|
3
|
+
import { LSKDB } from 'lskdb';
|
|
4
|
+
import type { MonkeyUserScript } from 'vite-plugin-monkey';
|
|
5
|
+
import { GM_addStyle, unsafeWindow } from '$';
|
|
6
|
+
import { InfiniteScroller } from '../../core/infinite-scroll';
|
|
7
|
+
import { getPaginationStrategy } from '../../core/pagination-parsing';
|
|
8
|
+
import { RulesGlobal } from '../../core/rules';
|
|
9
|
+
import { range } from '../../utils/arrays';
|
|
10
|
+
import {
|
|
11
|
+
downloader,
|
|
12
|
+
getCommonParents,
|
|
13
|
+
parseHtml,
|
|
14
|
+
querySelectorLastNumber,
|
|
15
|
+
replaceElementTag,
|
|
16
|
+
} from '../../utils/dom';
|
|
17
|
+
import { onPointerOverAndLeave, Tick } from '../../utils/events';
|
|
18
|
+
import { fetchHtml } from '../../utils/fetch';
|
|
19
|
+
import { circularShift } from '../../utils/math';
|
|
20
|
+
import { objectToFormData } from '../../utils/objects';
|
|
21
|
+
import { parseCssUrl } from '../../utils/parsers';
|
|
22
|
+
|
|
23
|
+
export const meta: MonkeyUserScript = {
|
|
24
|
+
name: 'ThisVid.com Improved',
|
|
25
|
+
version: '8.0.0',
|
|
26
|
+
description: 'Infinite scroll [optional]. Preview for private videos. Filter: title, duration, public/private. Check access to private vids. Mass friend request button. Sorts messages. Download button 📼',
|
|
27
|
+
match: ['https://*.thisvid.com/*'],
|
|
28
|
+
icon: 'https://www.google.com/s2/favicons?sz=64&domain=thisvid.com',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const $ = (unsafeWindow as any).$;
|
|
32
|
+
|
|
33
|
+
type RulesConfig = ConstructorParameters<typeof RulesGlobal>[0];
|
|
34
|
+
|
|
35
|
+
const lskdb = new LSKDB();
|
|
36
|
+
|
|
37
|
+
const IS_MEMBER_PAGE = /^\/members\/\d+\/$/.test(location.pathname);
|
|
38
|
+
const IS_MESSAGES_PAGE = /^\/my_messages\//.test(location.pathname);
|
|
39
|
+
const IS_PLAYLIST = /^\/playlist\/\d+\//.test(location.pathname);
|
|
40
|
+
const IS_VIDEO_PAGE = /^\/videos\//.test(location.pathname);
|
|
41
|
+
const IS_MY_WALL = /^\/my_wall\//.test(location.pathname);
|
|
42
|
+
const MY_ID = (document.querySelector('[target="_self"]') as HTMLAnchorElement)?.href.match(
|
|
43
|
+
/\/(\d+)\//,
|
|
44
|
+
)?.[1];
|
|
45
|
+
const LOGGED_IN = !!MY_ID;
|
|
46
|
+
const IS_MY_MEMBER_PAGE =
|
|
47
|
+
LOGGED_IN && !!document.querySelector('.my-avatar') && IS_MEMBER_PAGE;
|
|
48
|
+
const IS_OTHER_MEMBER_PAGE = !IS_MY_MEMBER_PAGE && IS_MEMBER_PAGE;
|
|
49
|
+
const IS_MEMBER_FRIEND =
|
|
50
|
+
IS_OTHER_MEMBER_PAGE &&
|
|
51
|
+
(document.querySelector('.case-left') as HTMLElement)?.innerText.includes(
|
|
52
|
+
'is in your friends',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
function fixPlaylistThumbUrl(src: string) {
|
|
56
|
+
return src.replace(/playlist\/\d+\/video/, () => 'videos');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultRulesConfig: RulesConfig = {
|
|
60
|
+
thumbsSelector:
|
|
61
|
+
'div:has(> .tumbpu[title]):not(.thumbs-photo) > .tumbpu[title], .thumb-holder',
|
|
62
|
+
getThumbImgData(thumb: HTMLElement) {
|
|
63
|
+
const img = thumb.querySelector('img') as HTMLImageElement;
|
|
64
|
+
const privateThumb = thumb.querySelector('.private') as HTMLElement;
|
|
65
|
+
|
|
66
|
+
let imgSrc = img?.getAttribute('data-original') as string;
|
|
67
|
+
|
|
68
|
+
if (privateThumb) {
|
|
69
|
+
imgSrc = parseCssUrl(privateThumb.style.background);
|
|
70
|
+
privateThumb.removeAttribute('style');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
img.removeAttribute('data-original');
|
|
74
|
+
img.removeAttribute('data-cnt');
|
|
75
|
+
img.classList.remove('lazy-load');
|
|
76
|
+
|
|
77
|
+
return { img, imgSrc };
|
|
78
|
+
},
|
|
79
|
+
containerSelectorLast: '.thumbs-items',
|
|
80
|
+
titleSelector: '.title',
|
|
81
|
+
durationSelector: '.duration',
|
|
82
|
+
customThumbDataSelectors: {
|
|
83
|
+
private: { selector: '.private', type: 'boolean' },
|
|
84
|
+
hd: { selector: '.quality', type: 'boolean' },
|
|
85
|
+
views: { selector: '.view', type: 'number' },
|
|
86
|
+
},
|
|
87
|
+
animatePreview,
|
|
88
|
+
customDataSelectorFns: [
|
|
89
|
+
'filterInclude',
|
|
90
|
+
'filterExclude',
|
|
91
|
+
'filterDuration',
|
|
92
|
+
{
|
|
93
|
+
filterPrivate: (el, state) => (state.filterPrivate && el.private) as boolean,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
filterPublic: (el, state) => (state.filterPublic && !el.private) as boolean,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
filterHD: (el, state) => (state.filterHD && !el.hd) as boolean,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
filterNonHD: (el, state) => (state.filterNonHD && el.hd) as boolean,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
schemeOptions: [
|
|
106
|
+
'Text Filter',
|
|
107
|
+
'Duration Filter',
|
|
108
|
+
'Privacy Filter',
|
|
109
|
+
{
|
|
110
|
+
title: 'HD Filter',
|
|
111
|
+
content: [
|
|
112
|
+
{ filterHD: false, label: 'hd' },
|
|
113
|
+
{ filterNonHD: false, label: 'non-hd' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
'Badge',
|
|
117
|
+
{
|
|
118
|
+
title: 'Advanced',
|
|
119
|
+
content: [{ autoRequestAccess: false, label: 'check access sends friend requests' }],
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
gropeStrategy:
|
|
123
|
+
getCommonParents([
|
|
124
|
+
...document.querySelectorAll<HTMLElement>(
|
|
125
|
+
'div:has(> .tumbpu[title]):not(.thumbs-photo) > .tumbpu[title], .thumb-holder',
|
|
126
|
+
),
|
|
127
|
+
]).length < 2
|
|
128
|
+
? 'all-in-one'
|
|
129
|
+
: 'all-in-all',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const config: RulesConfig =
|
|
133
|
+
IS_MY_MEMBER_PAGE || IS_MY_WALL ? await createPrivateFeed() : defaultRulesConfig;
|
|
134
|
+
|
|
135
|
+
const rules = new RulesGlobal(config);
|
|
136
|
+
|
|
137
|
+
GM_addStyle(`
|
|
138
|
+
.haveNoAccess { background: linear-gradient(to bottom, #b50000 0%, #2c2c2c 100%) red !important; }
|
|
139
|
+
.haveAccess { background: linear-gradient(to bottom, #4e9299 0%, #2c2c2c 100%) green !important; }
|
|
140
|
+
.success { background: linear-gradient(#2f6eb34f, #66666647) !important; }
|
|
141
|
+
.failure { background: linear-gradient(rgba(179, 47, 47, 0.31), rgba(102, 102, 102, 0.28)) !important; }
|
|
142
|
+
.friend-button { background: radial-gradient(#5ccbf4, #e1ccb1) !important; }
|
|
143
|
+
.friendProfile { background: radial-gradient(circle, rgb(28, 42, 50) 48%, rgb(0, 0, 0) 100%) !important; }
|
|
144
|
+
`);
|
|
145
|
+
|
|
146
|
+
//====================================================================================================
|
|
147
|
+
|
|
148
|
+
function friend(id: string, message = '') {
|
|
149
|
+
return fetch(
|
|
150
|
+
`https://thisvid.com/members/${id}/?action=add_to_friends_complete&function=get_block&block_id=member_profile_view_view_profile&format=json&mode=async&message=${message}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function acceptFriendship(id: string | number) {
|
|
155
|
+
const body = objectToFormData({
|
|
156
|
+
action: 'confirm_add_to_friends',
|
|
157
|
+
function: 'get_block',
|
|
158
|
+
block_id: 'member_profile_view_view_profile',
|
|
159
|
+
confirm: '',
|
|
160
|
+
format: 'json',
|
|
161
|
+
mode: 'async',
|
|
162
|
+
});
|
|
163
|
+
const url = `https://thisvid.com/members/${id}/`;
|
|
164
|
+
return fetch(url, { body, method: 'post' });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function getMemberFriends(
|
|
168
|
+
memberId: string,
|
|
169
|
+
by?: 'activity' | 'popularity',
|
|
170
|
+
): Promise<AsyncGenerator<string>> {
|
|
171
|
+
const { friendsCount } = await getMemberData(memberId);
|
|
172
|
+
const offset = Math.ceil(friendsCount / 24);
|
|
173
|
+
|
|
174
|
+
let friendsURL = `https://thisvid.com/members/${memberId}/friends/`;
|
|
175
|
+
if (by === 'activity') friendsURL = 'https://thisvid.com/my_friends_by_activity/';
|
|
176
|
+
if (by === 'popularity') friendsURL = 'https://thisvid.com/my_friends_by_popularity/';
|
|
177
|
+
|
|
178
|
+
async function* g() {
|
|
179
|
+
for (const o of range(offset)) {
|
|
180
|
+
const html = await fetchHtml(`${friendsURL}${o}/`);
|
|
181
|
+
for await (const id of getMembers(html)) {
|
|
182
|
+
yield id;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return g();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getMembers(el: HTMLElement) {
|
|
191
|
+
const friendsList = el.querySelector('#list_members_friends_items') || el;
|
|
192
|
+
return Array.from(friendsList.querySelectorAll<HTMLAnchorElement>('.tumbpu') || [])
|
|
193
|
+
.map((e) => e.href.match(/\d+/)?.[0] as string)
|
|
194
|
+
.filter((_) => _);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function friendMemberFriends(orientationFilter?: string) {
|
|
198
|
+
const memberId = window.location.pathname.match(/\d+/)?.[0] as string;
|
|
199
|
+
friend(memberId);
|
|
200
|
+
const friends = await getMemberFriends(memberId);
|
|
201
|
+
|
|
202
|
+
await from(friends)
|
|
203
|
+
.pipe(
|
|
204
|
+
flatMap(async (fid: string) => {
|
|
205
|
+
if (!orientationFilter) {
|
|
206
|
+
await friend(fid);
|
|
207
|
+
} else {
|
|
208
|
+
const { orientation, uploadedPrivate } = await getMemberData(fid);
|
|
209
|
+
if (
|
|
210
|
+
uploadedPrivate > 0 &&
|
|
211
|
+
(orientation === orientationFilter ||
|
|
212
|
+
(orientationFilter === 'Straight' && orientation === 'Lesbian'))
|
|
213
|
+
) {
|
|
214
|
+
await friend(fid);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}, 60),
|
|
218
|
+
)
|
|
219
|
+
.forEach(() => {});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function initFriendship() {
|
|
223
|
+
GM_addStyle(
|
|
224
|
+
'.buttons {display: flex; flex-wrap: wrap} .buttons button, .buttons a {align-self: center; padding: 4px; margin: 5px;}',
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const buttons = [
|
|
228
|
+
{ color: '#ff7194', orientation: undefined },
|
|
229
|
+
{ color: '#ba71ff', orientation: 'Straight' },
|
|
230
|
+
{ color: '#46baff', orientation: 'Gay' },
|
|
231
|
+
{ color: '#4ebaaf', orientation: 'Bisexual' },
|
|
232
|
+
] as const;
|
|
233
|
+
|
|
234
|
+
buttons.forEach((b) => {
|
|
235
|
+
const button = parseHtml(
|
|
236
|
+
`<button style="background: radial-gradient(red, ${b.color});">friend ${b.orientation || 'everyone'}</button>`,
|
|
237
|
+
);
|
|
238
|
+
document.querySelector('.buttons')?.append(button);
|
|
239
|
+
button.addEventListener('click', (e: PointerEvent) => handleClick(e, b.orientation), {
|
|
240
|
+
once: true,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
function handleClick(e: PointerEvent, orientationFilter?: string) {
|
|
245
|
+
const button = e.target as HTMLElement;
|
|
246
|
+
button.style.background = 'radial-gradient(#ff6114, #5babc4)';
|
|
247
|
+
button.innerText = 'processing requests';
|
|
248
|
+
friendMemberFriends(orientationFilter).then(() => {
|
|
249
|
+
button.style.background = 'radial-gradient(blue, lightgreen)';
|
|
250
|
+
button.innerText = 'friend requests sent';
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
//====================================================================================================
|
|
256
|
+
|
|
257
|
+
type MemberData = {
|
|
258
|
+
uploadedPublic: number;
|
|
259
|
+
uploadedPrivate: number;
|
|
260
|
+
name: string;
|
|
261
|
+
friendsCount: number;
|
|
262
|
+
orientation: string;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
async function getMemberData(id: string) {
|
|
266
|
+
const url = id.includes('member') ? id : `/members/${id}/`;
|
|
267
|
+
const doc = await fetchHtml(url);
|
|
268
|
+
const data: Partial<MemberData> = {};
|
|
269
|
+
|
|
270
|
+
doc.querySelectorAll<HTMLElement>('.profile span').forEach((s) => {
|
|
271
|
+
if (s.innerText.includes('Name:')) {
|
|
272
|
+
data.name = (s.firstElementChild as HTMLElement)?.innerText as string;
|
|
273
|
+
}
|
|
274
|
+
if (s.innerText.includes('Orientation:')) {
|
|
275
|
+
data.orientation = (s.firstElementChild as HTMLElement)?.innerText as string;
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
data.uploadedPublic = querySelectorLastNumber(
|
|
280
|
+
'.headline:has(+ #list_videos_public_videos_items) span',
|
|
281
|
+
doc,
|
|
282
|
+
);
|
|
283
|
+
data.uploadedPrivate = querySelectorLastNumber(
|
|
284
|
+
'.headline:has(+ #list_videos_private_videos_items) span',
|
|
285
|
+
doc,
|
|
286
|
+
);
|
|
287
|
+
data.friendsCount = querySelectorLastNumber('#list_members_friends span', doc);
|
|
288
|
+
|
|
289
|
+
return data as MemberData;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
//====================================================================================================
|
|
293
|
+
|
|
294
|
+
function requestAccessVideoPage() {
|
|
295
|
+
const holder = document.querySelector('.video-holder > p');
|
|
296
|
+
if (holder) {
|
|
297
|
+
const uploader = (document.querySelector('a.author') as HTMLAnchorElement).href
|
|
298
|
+
.match(/\d+/)
|
|
299
|
+
?.at(-1);
|
|
300
|
+
const button = parseHtml(
|
|
301
|
+
`<button onclick="requestPrivateAccess(event, ${uploader}); this.onclick=null;">Friend Request</button>`,
|
|
302
|
+
);
|
|
303
|
+
holder.parentElement?.append(button);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const requestPrivateAccess = (e: PointerEvent, memberid: string) => {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
friend(memberid, '');
|
|
310
|
+
(e.target as HTMLElement).innerText = (e.target as HTMLElement).innerText.replace(
|
|
311
|
+
'🚑',
|
|
312
|
+
'🍆',
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
async function checkPrivateVideoAccess(url: string) {
|
|
317
|
+
const html = await fetchHtml(url);
|
|
318
|
+
const holder = html.querySelector('.video-holder > p');
|
|
319
|
+
|
|
320
|
+
const access = !holder;
|
|
321
|
+
|
|
322
|
+
const uploaderEl = (
|
|
323
|
+
holder ? holder.querySelector('a') : html.querySelector('a.author')
|
|
324
|
+
) as HTMLAnchorElement;
|
|
325
|
+
const uploaderURL = uploaderEl.href.match(/\d+/)?.at(-1) as string;
|
|
326
|
+
const uploaderName = uploaderEl.innerText;
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
access,
|
|
330
|
+
uploaderURL,
|
|
331
|
+
uploaderName,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getUncheckedPrivateThumbs(html = document) {
|
|
336
|
+
return [
|
|
337
|
+
...html.querySelectorAll<HTMLAnchorElement | HTMLElement>(
|
|
338
|
+
'.tumbpu:has(.private):not(.haveNoAccess,.haveAccess), .thumb-holder:has(.private):not(.haveNoAccess,.haveAccess)',
|
|
339
|
+
),
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const uploadersChecked = new Set();
|
|
344
|
+
|
|
345
|
+
async function requestAccess() {
|
|
346
|
+
const checkAccess = async (thumb: HTMLElement | HTMLAnchorElement) => {
|
|
347
|
+
const url = (thumb.querySelector('a')?.href ||
|
|
348
|
+
(thumb as HTMLAnchorElement)?.href) as string;
|
|
349
|
+
const { access, uploaderURL } = await checkPrivateVideoAccess(url);
|
|
350
|
+
|
|
351
|
+
thumb.classList.add(access ? 'haveAccess' : 'haveNoAccess');
|
|
352
|
+
if (access) return;
|
|
353
|
+
|
|
354
|
+
if (rules.store.state.autoRequestAccess && !uploadersChecked.has(uploaderURL)) {
|
|
355
|
+
acceptFriendship(uploaderURL);
|
|
356
|
+
friend(uploaderURL);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
for (const t of getUncheckedPrivateThumbs()) {
|
|
361
|
+
await checkAccess(t);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
//====================================================================================================
|
|
366
|
+
|
|
367
|
+
const createDownloadButton = () =>
|
|
368
|
+
downloader({
|
|
369
|
+
append: '',
|
|
370
|
+
after: '.share_btn',
|
|
371
|
+
button: '<li><a href="#" style="text-decoration: none;font-size: 2rem;">📼</a></li>',
|
|
372
|
+
cbBefore: () => $('.fp-ui').click(),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
//====================================================================================================
|
|
376
|
+
|
|
377
|
+
function animatePreview(_: HTMLElement) {
|
|
378
|
+
const tick = new Tick(750);
|
|
379
|
+
$('img[alt!="Private"]').off();
|
|
380
|
+
|
|
381
|
+
function iteratePreviewFrames(img: HTMLImageElement) {
|
|
382
|
+
img.src = (img.getAttribute('src') as string).replace(
|
|
383
|
+
/(\d+)(?=\.jpg$)/,
|
|
384
|
+
(_, n) => `${circularShift(parseInt(n), 6)}`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function animate(container: HTMLElement) {
|
|
389
|
+
onPointerOverAndLeave(
|
|
390
|
+
container,
|
|
391
|
+
(target) => !!target.getAttribute('src'),
|
|
392
|
+
(target) => {
|
|
393
|
+
const e = target as HTMLImageElement;
|
|
394
|
+
const orig = target.getAttribute('src') as string;
|
|
395
|
+
tick.start(
|
|
396
|
+
() => iteratePreviewFrames(e),
|
|
397
|
+
() => {
|
|
398
|
+
e.src = orig;
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
},
|
|
402
|
+
() => tick.stop(),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
animate(document.querySelector('.content') || document.body);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
//====================================================================================================
|
|
410
|
+
|
|
411
|
+
async function getMemberVideos(id: string, type = 'private') {
|
|
412
|
+
const { uploadedPrivate, uploadedPublic, name } = await getMemberData(id);
|
|
413
|
+
const videosCount = type === 'private' ? uploadedPrivate : uploadedPublic;
|
|
414
|
+
|
|
415
|
+
const url = new URL(`https://thisvid.com/members/${id}/${type}_videos/`);
|
|
416
|
+
const doc = (await fetchHtml(url.href)) as unknown as Document;
|
|
417
|
+
|
|
418
|
+
const paginationStrategy = getPaginationStrategy({ doc, url });
|
|
419
|
+
|
|
420
|
+
const memberVideosGenerator =
|
|
421
|
+
InfiniteScroller.generatorForPaginationStrategy(paginationStrategy);
|
|
422
|
+
|
|
423
|
+
return { name, videosCount, memberVideosGenerator };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function createPrivateFeedButton() {
|
|
427
|
+
const container = document.querySelectorAll('.sidebar ul')[1];
|
|
428
|
+
|
|
429
|
+
const links = [
|
|
430
|
+
{ hov: '#private_feed', text: 'My Private Feed' },
|
|
431
|
+
{ hov: '#private_feed_popularity', text: 'My Private Feed by Popularity' },
|
|
432
|
+
{ hov: '#private_feed_activity', text: 'My Private Feed by Activity' },
|
|
433
|
+
{ hov: '#public_feed', text: 'My Public Feed' },
|
|
434
|
+
{ hov: '#public_feed_popularity', text: 'My Public Feed by Popularity' },
|
|
435
|
+
{ hov: '#public_feed_activity', text: 'My Public Feed by Activity' },
|
|
436
|
+
];
|
|
437
|
+
|
|
438
|
+
const fragment = document.createDocumentFragment();
|
|
439
|
+
links.forEach(({ hov, text }) => {
|
|
440
|
+
const button = parseHtml(
|
|
441
|
+
`<li><a style="color: lightblue;" href="https://thisvid.com/my_wall/${hov}" class="selective"><i class="ico-arrow"></i>${text}</a></li>`,
|
|
442
|
+
);
|
|
443
|
+
fragment.append(button);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
container.append(fragment);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function createPrivateFeed() {
|
|
450
|
+
createPrivateFeedButton();
|
|
451
|
+
if (!location.hash.includes('feed')) return defaultRulesConfig;
|
|
452
|
+
const isPubKey = window.location.hash.includes('public_feed') ? 'public' : 'private';
|
|
453
|
+
const sortByFeed = window.location.hash.includes('activity')
|
|
454
|
+
? 'activity'
|
|
455
|
+
: window.location.hash.includes('popularity')
|
|
456
|
+
? 'popularity'
|
|
457
|
+
: undefined;
|
|
458
|
+
|
|
459
|
+
const container = parseHtml('<div class="thumbs-items"></div>');
|
|
460
|
+
const ignored = parseHtml('<div class="ignored"><h2>IGNORED:</h2></div>');
|
|
461
|
+
|
|
462
|
+
const containerParent = document.querySelector(
|
|
463
|
+
'.main > .container > .content',
|
|
464
|
+
) as HTMLElement;
|
|
465
|
+
containerParent.innerHTML = '';
|
|
466
|
+
containerParent?.nextElementSibling?.remove();
|
|
467
|
+
containerParent.append(container);
|
|
468
|
+
container.before(ignored);
|
|
469
|
+
|
|
470
|
+
GM_addStyle(`
|
|
471
|
+
.content { width: auto; }
|
|
472
|
+
.member-videos, .ignored { background: #b3b3b324; min-height: 3rem; margin: 1rem 0px; color: #fff; font-size: 1.24rem; display: flex; flex-wrap: wrap; justify-content: center;
|
|
473
|
+
padding: 10px; width: 100%; }
|
|
474
|
+
.member-videos * { padding: 5px; margin: 4px; }
|
|
475
|
+
.member-videos h2 a { font-size: 1.24rem; margin: 0; padding: 0; display: inline; }
|
|
476
|
+
.ignored * { padding: 4px; margin: 5px; }
|
|
477
|
+
.thumbs-items { display: flex; flex-wrap: wrap; }`);
|
|
478
|
+
|
|
479
|
+
const { friendsCount } = await getMemberData(MY_ID as string);
|
|
480
|
+
|
|
481
|
+
class FeedGenerator {
|
|
482
|
+
private offset = 0;
|
|
483
|
+
private minVideoCount = 1;
|
|
484
|
+
|
|
485
|
+
public skip(n: number) {
|
|
486
|
+
this.offset += n;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
public filterMinVideoCount(n: number) {
|
|
490
|
+
this.minVideoCount = n;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
constructor(
|
|
494
|
+
private id: string,
|
|
495
|
+
private memberGeneratorCallback?: (
|
|
496
|
+
name: string,
|
|
497
|
+
videosCount: number,
|
|
498
|
+
id: string,
|
|
499
|
+
) => void,
|
|
500
|
+
private type = 'private',
|
|
501
|
+
private by: Parameters<typeof getMemberFriends>[1] = undefined,
|
|
502
|
+
) {}
|
|
503
|
+
|
|
504
|
+
public async *consume() {
|
|
505
|
+
const membersIds = await getMemberFriends(this.id, this.by);
|
|
506
|
+
|
|
507
|
+
const stream = from(membersIds).pipe(
|
|
508
|
+
concatMap(async (mid, index) => {
|
|
509
|
+
if (index < this.offset) return from([]);
|
|
510
|
+
this.offset = index;
|
|
511
|
+
|
|
512
|
+
const { memberVideosGenerator, name, videosCount } = await getMemberVideos(
|
|
513
|
+
mid,
|
|
514
|
+
this.type,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (lskdb.hasKey(mid) || videosCount < this.minVideoCount) return from([]);
|
|
518
|
+
|
|
519
|
+
this.memberGeneratorCallback?.(name, videosCount, mid);
|
|
520
|
+
|
|
521
|
+
return from(memberVideosGenerator).pipe(
|
|
522
|
+
takeWhile(() => index >= this.offset),
|
|
523
|
+
map(async (element) => ({ url: element.url, offset: index })),
|
|
524
|
+
);
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
yield* stream;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const feedGenerator = new FeedGenerator(
|
|
533
|
+
MY_ID as string,
|
|
534
|
+
(name: string, videosCount: number, id: string) => {
|
|
535
|
+
if (container.querySelector(`#mem-${id}`)) return;
|
|
536
|
+
container.append(
|
|
537
|
+
parseHtml(`
|
|
538
|
+
<div class="member-videos" id="mem-${id}">
|
|
539
|
+
<h2><a href="/members/${id}/">${name}</a> ${videosCount} videos</h2>
|
|
540
|
+
<button onClick="hideMemberVideos(event)">ignore 🗡</button>
|
|
541
|
+
<button onClick="hideMemberVideos(event, false)">skip</button>
|
|
542
|
+
</div>`),
|
|
543
|
+
);
|
|
544
|
+
},
|
|
545
|
+
isPubKey,
|
|
546
|
+
sortByFeed,
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
const ignoredMembers = lskdb.getAllKeys();
|
|
550
|
+
ignoredMembers.forEach((im) => {
|
|
551
|
+
document
|
|
552
|
+
.querySelector('.ignored')
|
|
553
|
+
?.append(
|
|
554
|
+
parseHtml(`<button id="#ir-${im}" onClick="unignore(event)">${im} 🗡</button>`),
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const skip = (n: number) => {
|
|
559
|
+
feedGenerator.skip(n);
|
|
560
|
+
(document.querySelector('.thumbs-items') as HTMLElement).innerHTML = '';
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const hideMemberVideos = (e: PointerEvent, ignore = true) => {
|
|
564
|
+
const container = (e.target as HTMLElement)?.closest('div') as HTMLElement;
|
|
565
|
+
let id = container.id;
|
|
566
|
+
|
|
567
|
+
const videosCount = querySelectorLastNumber(`#${id}`);
|
|
568
|
+
document
|
|
569
|
+
.querySelectorAll<HTMLElement>(`#${id}~a`)
|
|
570
|
+
.values()
|
|
571
|
+
.take(videosCount)
|
|
572
|
+
.forEach((e) => {
|
|
573
|
+
e.remove();
|
|
574
|
+
});
|
|
575
|
+
container.remove();
|
|
576
|
+
|
|
577
|
+
id = id.slice(4);
|
|
578
|
+
if (ignore) {
|
|
579
|
+
const btn = parseHtml(
|
|
580
|
+
`<button id="irm-${id}" onClick="unignore(event)">${id} X</button>`,
|
|
581
|
+
);
|
|
582
|
+
document.querySelector('.ignored')?.append(btn);
|
|
583
|
+
lskdb.setKey(id);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
const unignore = (e: PointerEvent) => {
|
|
588
|
+
const target = e.target as HTMLElement;
|
|
589
|
+
const id = target.id.slice(4);
|
|
590
|
+
lskdb.removeKey(id);
|
|
591
|
+
target.remove();
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const filterMinVideoCount = (n: number) => feedGenerator.filterMinVideoCount(n);
|
|
595
|
+
|
|
596
|
+
Object.assign(unsafeWindow, { unignore, hideMemberVideos });
|
|
597
|
+
|
|
598
|
+
const customGenerator = await feedGenerator.consume();
|
|
599
|
+
|
|
600
|
+
const rulesConfig: RulesConfig = Object.assign(defaultRulesConfig, {
|
|
601
|
+
containerSelector: () => container,
|
|
602
|
+
intersectionObservableSelector: '.footer',
|
|
603
|
+
customGenerator,
|
|
604
|
+
paginationStrategyOptions: {
|
|
605
|
+
getPaginationLast: () => friendsCount,
|
|
606
|
+
paginationSelector: '.footer',
|
|
607
|
+
},
|
|
608
|
+
schemeOptions: [
|
|
609
|
+
'Text Filter',
|
|
610
|
+
'Duration Filter',
|
|
611
|
+
'Privacy Filter',
|
|
612
|
+
'Badge',
|
|
613
|
+
{
|
|
614
|
+
title: 'Feed Controls',
|
|
615
|
+
content: [
|
|
616
|
+
{ 'skip 10': () => skip(10) },
|
|
617
|
+
{ 'skip 100': () => skip(100) },
|
|
618
|
+
{ 'skip 1000': () => skip(1000) },
|
|
619
|
+
{ 'filter >10': () => filterMinVideoCount(10) },
|
|
620
|
+
{ 'filter >25': () => filterMinVideoCount(25) },
|
|
621
|
+
{ 'filter >100': () => filterMinVideoCount(100) },
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
'Advanced',
|
|
625
|
+
],
|
|
626
|
+
} as RulesConfig);
|
|
627
|
+
|
|
628
|
+
return rulesConfig;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
//====================================================================================================
|
|
632
|
+
|
|
633
|
+
function deleteMsg(id: string) {
|
|
634
|
+
fetch(
|
|
635
|
+
`https://thisvid.com/my_messages/inbox/?mode=async&format=json&action=delete&function=get_block&block_id=list_messages_my_conversation_messages&delete[]=${id}`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function clearMessages() {
|
|
640
|
+
const sortMsgs = (doc: Document | HTMLElement) => {
|
|
641
|
+
doc.querySelectorAll<HTMLElement>('.entry').forEach((e) => {
|
|
642
|
+
const id = e.querySelector<HTMLInputElement>('input[name="delete[]"]')
|
|
643
|
+
?.value as string;
|
|
644
|
+
const msg = e.querySelector<HTMLElement>('.user-comment')?.innerText as string;
|
|
645
|
+
|
|
646
|
+
if (/has confirmed|declined your|has removed/g.test(msg)) deleteMsg(id);
|
|
647
|
+
});
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
await Promise.all(
|
|
651
|
+
Array.from({ length: rules.paginationStrategy.getPaginationLast() }, (_, i) =>
|
|
652
|
+
fetchHtml(`https://thisvid.com/my_messages/inbox/${i + 1}/`).then((html) =>
|
|
653
|
+
sortMsgs(html),
|
|
654
|
+
),
|
|
655
|
+
),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function clearMessagesButton() {
|
|
660
|
+
const btn = parseHtml('<button>clear messages</button>');
|
|
661
|
+
btn.addEventListener('click', clearMessages);
|
|
662
|
+
document.querySelector('.headline')?.append(btn);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function highlightMessages() {
|
|
666
|
+
document.querySelectorAll<HTMLElement>('.entry').forEach((entry) => {
|
|
667
|
+
const memberUrl = entry.querySelector<HTMLAnchorElement>('a')?.href as string;
|
|
668
|
+
getMemberData(memberUrl).then(({ uploadedPublic, uploadedPrivate }) => {
|
|
669
|
+
if (uploadedPrivate > 0) {
|
|
670
|
+
const success = !entry.innerText.includes('has declined');
|
|
671
|
+
entry.classList.add(success ? 'success' : 'failure');
|
|
672
|
+
}
|
|
673
|
+
(entry.querySelector('.user-comment p') as HTMLElement).innerText +=
|
|
674
|
+
` | videos: ${uploadedPublic} public, ${uploadedPrivate} private`;
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
//====================================================================================================
|
|
680
|
+
|
|
681
|
+
if (LOGGED_IN) {
|
|
682
|
+
rules.store.eventSubject.subscribe((x) => {
|
|
683
|
+
if (x.includes('check access')) {
|
|
684
|
+
requestAccess();
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (IS_MESSAGES_PAGE) {
|
|
690
|
+
clearMessagesButton();
|
|
691
|
+
highlightMessages();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (IS_VIDEO_PAGE) {
|
|
695
|
+
requestAccessVideoPage();
|
|
696
|
+
createDownloadButton();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (IS_OTHER_MEMBER_PAGE) {
|
|
700
|
+
initFriendship();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
Object.assign(unsafeWindow, { requestPrivateAccess });
|
|
704
|
+
|
|
705
|
+
if (IS_MEMBER_FRIEND) {
|
|
706
|
+
document.querySelector('.profile')?.classList.add('friendProfile');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (IS_PLAYLIST) {
|
|
710
|
+
const videoUrl = fixPlaylistThumbUrl(location.pathname) as string;
|
|
711
|
+
const desc = document.querySelector(
|
|
712
|
+
'.tools-left > li:nth-child(4) > .title-description',
|
|
713
|
+
) as HTMLElement;
|
|
714
|
+
const link = replaceElementTag(desc, 'a') as HTMLAnchorElement;
|
|
715
|
+
link.href = videoUrl;
|
|
716
|
+
}
|