View difference between Paste ID: NqfDWrG2 and qFZFwpfK
SHOW: | | - or go back to the newest paste.
1-
javascript:(function(){
1+
// ==UserScript==
2
3-
    function absoluteUrl(h){try{return new URL(h,window.location.origin).href}catch(e){return h||''}}
3+
// @name         TikTok Metadata Extractor
4
5-
    function getText(sel){var el=document.querySelector(sel);return el?el.textContent.trim():'N/A'}
5+
// @namespace    http://tampermonkey.net/
6
7-
    function getDescriptionBBCode(){
7+
// @version      0.4
8
9-
        var container=document.querySelector('div[data-e2e="browse-video-desc"]'); if(!container) return 'N/A';
9+
// @description  Extracts video metadata and adds a button with hashtag formatting (underlined links when available)
10
11-
        var parts=[];
11+
// @author       You
12
13-
        container.childNodes.forEach(function(node){
13+
// @match        *://www.tiktok.com/*
14
15-
            if(node.nodeType===Node.TEXT_NODE){var t=node.textContent.replace(/\s+/g,' ').trim(); if(t) parts.push(t);}
15+
// @grant        none
16
17-
            else if(node.nodeType===Node.ELEMENT_NODE){
17+
// ==/UserScript==
18
19-
                if(node.matches && node.matches('a[data-e2e="search-common-link"]')){
19+
20
(function() {
21-
                    parts.push('[u][url='+absoluteUrl(node.getAttribute('href'))+']'+node.textContent.trim()+'[/url][/u]');
21+
22
    const tiktokSelectors = {
23
24
        creatorName: ['span[data-e2e="browse-username"]'],
25-
                    var anchors = node.querySelectorAll && node.querySelectorAll('a[data-e2e="search-common-link"]');
25+
26
        // creator avatar link (profile URL)
27-
                    if(anchors && anchors.length){
27+
28
        creatorUrl: ['a[data-e2e="browse-user-avatar"]', 'a[href^="/@"]'],
29-
                        var sp=[]; node.childNodes.forEach(function(sn){
29+
30
        // engagement: try multiple attribute names used in different layouts
31-
                            if(sn.nodeType===Node.TEXT_NODE){var s=sn.textContent.replace(/\s+/g,' ').trim(); if(s) sp.push(s)}
31+
32
        likes: [
33-
                            else if(sn.nodeType===Node.ELEMENT_NODE && sn.matches && sn.matches('a[data-e2e="search-common-link"]')) sp.push('[u][url='+absoluteUrl(sn.getAttribute('href'))+']'+sn.textContent.trim()+'[/url][/u]');
33+
34
            'strong[data-e2e="like-count"]',
35-
                            else {var s=sn.textContent.replace(/\s+/g,' ').trim(); if(s) sp.push(s)}
35+
36
            'strong[data-e2e="browse-like-count"]',
37-
                        });
37+
38
            'strong[data-e2e="browse-like-count"], strong.css-vc3yj-StrongText'
39-
                        if(sp.length) parts.push(sp.join(' '));
39+
40
        ],
41-
                    } else {
41+
42
        comments: [
43-
                        var t=node.textContent.replace(/\s+/g,' ').trim(); if(t) parts.push(t);
43+
44
            'strong[data-e2e="comment-count"]',
45
46
            'strong[data-e2e="browse-comment-count"]',
47
48
            'strong[data-e2e="browse-comment-count"], strong.css-vc3yj-StrongText'
49
50
        ],
51
52
        shares: [
53-
        return parts.join(' ').replace(/\s+/g,' ').trim()||'N/A';
53+
54
            'strong[data-e2e="share-count"]',
55
56
            'strong[data-e2e="browse-share-count"]',
57-
    function copy(t){var ta=document.createElement('textarea');ta.value=t;ta.style.position='fixed';ta.style.left='-9999px';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta)}
57+
58
            'strong[data-e2e="undefined-count"]', // fallback seen in some layouts
59-
    function toast(m){var d=document.createElement('div');Object.assign(d.style,{position:'fixed',top:'10px',right:'10px',padding:'12px 24px',backgroundColor:'#00C851',color:'white',borderRadius:'4px',fontSize:'14px',zIndex:100000,boxShadow:'0 2px 5px rgba(0,0,0,0.2)'});d.textContent=m;document.body.appendChild(d);setTimeout(()=>d.remove(),3000)}
59+
60
            'strong.css-vc3yj-StrongText'
61-
    if(window.location.hostname==='www.tiktok.com'){
61+
62
        ]
63-
        var metadata={description:getDescriptionBBCode(), creator:getText('span[data-e2e="browse-username"]'), creatorUrl:absoluteUrl((document.querySelector('a[data-e2e="browse-user-avatar"]')||{}).getAttribute('href')), likes:getText('strong[data-e2e="like-count"], strong[data-e2e="browse-like-count"], strong.css-vc3yj-StrongText'), comments:getText('strong[data-e2e="comment-count"], strong[data-e2e="browse-comment-count"], strong.css-vc3yj-StrongText'), shares:getText('strong[data-e2e="share-count"], strong[data-e2e="browse-share-count"], strong[data-e2e="undefined-count"], strong.css-vc3yj-StrongText')};
63+
64
    };
65-
        var url=window.location.href;
65+
66
67-
        var bb='[b]'+metadata.description+'[/b]\n\n[url='+url+']'+url+'[/url]\n\n[b]Creator Info[/b]\n[u][url='+metadata.creatorUrl+']'+metadata.creator+'[/url][/u]\n\n[b]Engagement[/b]\nLikes: '+metadata.likes+'\nComments: '+metadata.comments+'\nShares: '+metadata.shares;
67+
    function getElement(selectors) {
68
69-
        copy(bb); toast('TikTok data copied');
69+
        if (!selectors) return null;
70
71-
    } else { alert('Please use this bookmarklet on a TikTok video page'); }
71+
        if (typeof selectors === 'string') selectors = [selectors];
72
73
        return selectors.reduce((found, selector) => found || document.querySelector(selector), null);
74
75
    }
76
77
78
    function getText(selectors) {
79
80
        const el = getElement(selectors);
81
82
        return el ? el.textContent.trim() : 'N/A';
83
84
    }
85
86
87
    function absoluteUrl(href) {
88
89
        try {
90
91
            return new URL(href, window.location.origin).href;
92
93
        } catch (e) {
94
95
            return href || '';
96
97
        }
98
99
    }
100
101
102
    // Converts the description area into BBCode.
103
104
    // - Underlines hashtag links (a[data-e2e="search-common-link"]) as [u][url=ABS]#tag[/url][/u]
105
106
    // - Preserves plain text spans
107
108
    // - Works with both the "default" and "comment/profile" type layout (iterates childNodes)
109
110
    function getDescriptionBBCode() {
111
112
        const container = document.querySelector('div[data-e2e="browse-video-desc"]');
113
114
        if (!container) return 'N/A';
115
116
117
        const parts = [];
118
119
        container.childNodes.forEach(node => {
120
121
            if (node.nodeType === Node.TEXT_NODE) {
122
123
                const txt = node.textContent.replace(/\s+/g, ' ').trim();
124
125
                if (txt) parts.push(txt);
126
127
            } else if (node.nodeType === Node.ELEMENT_NODE) {
128
129
                // hashtag links (common)
130
131
                if (node.matches && node.matches('a[data-e2e="search-common-link"]')) {
132
133
                    const href = absoluteUrl(node.getAttribute('href'));
134
135
                    const text = node.textContent.trim();
136
137
                    // if the link text contains a visible # (or the strong contains it), include as hashtag
138
139
                    parts.push(`[u][url=${href}]${text}[/url][/u]`);
140
141
                } else if (node.matches && (node.matches('span[data-e2e="new-desc-span"]') || node.matches('strong.css-1p6dp51-StrongText'))) {
142
143
                    const txt = node.textContent.replace(/\s+/g, ' ').trim();
144
145
                    if (txt) parts.push(txt);
146
147
                } else {
148
149
                    // fallback: get visible text of the node (covers nested spans/strong inside anchors)
150
151
                    const txt = node.textContent.replace(/\s+/g, ' ').trim();
152
153
                    if (txt) {
154
155
                        // If this node contains hashtag anchors, convert them:
156
157
                        const anchors = node.querySelectorAll && node.querySelectorAll('a[data-e2e="search-common-link"]');
158
159
                        if (anchors && anchors.length) {
160
161
                            // build text from child nodes, converting anchors to underlined bbcode
162
163
                            const subParts = [];
164
165
                            node.childNodes.forEach(sn => {
166
167
                                if (sn.nodeType === Node.TEXT_NODE) {
168
169
                                    const s = sn.textContent.replace(/\s+/g,' ').trim();
170
171
                                    if (s) subParts.push(s);
172
173
                                } else if (sn.nodeType === Node.ELEMENT_NODE && sn.matches && sn.matches('a[data-e2e="search-common-link"]')) {
174
175
                                    subParts.push(`[u][url=${absoluteUrl(sn.getAttribute('href'))}]${sn.textContent.trim()}[/url][/u]`);
176
177
                                } else {
178
179
                                    const s = sn.textContent.replace(/\s+/g,' ').trim();
180
181
                                    if (s) subParts.push(s);
182
183
                                }
184
185
                            });
186
187
                            if (subParts.length) parts.push(subParts.join(' '));
188
189
                        } else {
190
191
                            parts.push(txt);
192
193
                        }
194
195
                    }
196
197
                }
198
199
            }
200
201
        });
202
203
204
        return parts.join(' ').replace(/\s+/g, ' ').trim() || 'N/A';
205
206
    }
207
208
209
    function getCreatorUrl() {
210
211
        const el = getElement(tiktokSelectors.creatorUrl);
212
213
        return el ? absoluteUrl(el.getAttribute('href')) : window.location.origin + window.location.pathname;
214
215
    }
216
217
218
    function copyToClipboard(text) {
219
220
        const ta = document.createElement('textarea');
221
222
        ta.value = text;
223
224
        // make textarea invisible but present
225
226
        ta.style.position = 'fixed';
227
228
        ta.style.left = '-9999px';
229
230
        document.body.appendChild(ta);
231
232
        ta.select();
233
234
        document.execCommand('copy');
235
236
        document.body.removeChild(ta);
237
238
    }
239
240
241
    function showToast(message, isError = false) {
242
243
        const toast = document.createElement('div');
244
245
        Object.assign(toast.style, {
246
247
            position: 'fixed',
248
249
            top: '10px',
250
251
            right: '10px',
252
253
            padding: '12px 24px',
254
255
            backgroundColor: isError ? '#ff4444' : '#00C851',
256
257
            color: 'white',
258
259
            borderRadius: '4px',
260
261
            fontSize: '14px',
262
263
            zIndex: 100000,
264
265
            boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
266
267
        });
268
269
        toast.textContent = message;
270
271
        document.body.appendChild(toast);
272
273
        setTimeout(() => toast.remove(), 3000);
274
275
    }
276
277
278
    function buildBBCode(metadata) {
279
280
        const videoUrl = window.location.href;
281
282
        return `[b]${metadata.description}[/b]\n\n[url=${videoUrl}]${videoUrl}[/url]\n\n[b]Creator Info[/b]\n[u][url=${metadata.creatorUrl}]${metadata.creator}[/url][/u]\n\n[b]Engagement[/b]\nLikes: ${metadata.likes}\nComments: ${metadata.comments}\nShares: ${metadata.shares}`;
283
284
    }
285
286
287
    function processAndCopy() {
288
289
        const metadata = {
290
291
            description: getDescriptionBBCode(),
292
293
            creator: getText(tiktokSelectors.creatorName),
294
295
            creatorUrl: getCreatorUrl(),
296
297
            likes: getText(tiktokSelectors.likes),
298
299
            comments: getText(tiktokSelectors.comments),
300
301
            shares: getText(tiktokSelectors.shares)
302
303
        };
304
305
        const bb = buildBBCode(metadata);
306
307
        copyToClipboard(bb);
308
309
        showToast('Metadata copied to clipboard!');
310
311
    }
312
313
314
    function createButton() {
315
316
        const button = document.createElement('button');
317
318
        button.id = 'tiktok-metadata-button';
319
320
        button.textContent = 'Copy Metadata';
321
322
        Object.assign(button.style, {
323
324
            padding: '8px 12px',
325
326
            background: '#000',
327
328
            color: '#fff',
329
330
            border: 'none',
331
332
            borderRadius: '4px',
333
334
            cursor: 'pointer',
335
336
            fontSize: '13px',
337
338
            marginLeft: '8px'
339
340
        });
341
342
        button.addEventListener('click', processAndCopy);
343
344
        return button;
345
346
    }
347
348
349
    // Insert button next to the follow/author area whenever that area appears.
350
351
    const observer = new MutationObserver(() => {
352
353
        // Find any follow wrapper (covers both button[data-e2e="browse-follow"] and div[data-e2e="browse-follow"])
354
355
        const followWrapper = document.querySelector('[data-e2e="browse-follow"]');
356
357
        if (!followWrapper) return;
358
359
360
        // Don't add duplicate button
361
362
        if (document.getElementById('tiktok-metadata-button')) return;
363
364
365
        // Prefer attaching to the followWrapper.parentElement so the button appears "seamless" with author info.
366
367
        const attachTo = followWrapper.parentElement || followWrapper;
368
369
        if (!attachTo || !document.body.contains(attachTo)) return;
370
371
372
        const btn = createButton();
373
374
        try {
375
376
            attachTo.appendChild(btn);
377
378
        } catch (e) {
379
380
            // fallback: append to body fixed as safe fallback
381
382
            Object.assign(btn.style, { position: 'fixed', top: '10px', left: '10px', marginLeft: '0' });
383
384
            document.body.appendChild(btn);
385
386
        }
387
388
    });
389
390
391
    observer.observe(document.body, { childList: true, subtree: true });
392
393
394
    // also allow quick manual access: if user loads bookmarklet-ish page where followWrapper doesn't appear fast,
395
396
    // pressing Ctrl+Shift+Y will copy metadata (handy during testing)
397
398
    window.addEventListener('keydown', (e) => {
399
400
        if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'y') {
401
402
            processAndCopy();
403
404
        }
405
406
    });
407
408
})();