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 | })(); |