Guest User

Untitled

a guest
Aug 24th, 2025
12
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. // ==UserScript==
  2.  
  3. // @name         TikTok Metadata Extractor
  4.  
  5. // @namespace    http://tampermonkey.net/
  6.  
  7. // @version      0.4
  8.  
  9. // @description  Extracts video metadata and adds a button with hashtag formatting (underlined links when available)
  10.  
  11. // @author       You
  12.  
  13. // @match        *://www.tiktok.com/*
  14.  
  15. // @grant        none
  16.  
  17. // ==/UserScript==
  18.  
  19.  
  20. (function() {
  21.  
  22.     const tiktokSelectors = {
  23.  
  24.         creatorName: ['span[data-e2e="browse-username"]'],
  25.  
  26.         // creator avatar link (profile URL)
  27.  
  28.         creatorUrl: ['a[data-e2e="browse-user-avatar"]', 'a[href^="/@"]'],
  29.  
  30.         // engagement: try multiple attribute names used in different layouts
  31.  
  32.         likes: [
  33.  
  34.             'strong[data-e2e="like-count"]',
  35.  
  36.             'strong[data-e2e="browse-like-count"]',
  37.  
  38.             'strong[data-e2e="browse-like-count"], strong.css-vc3yj-StrongText'
  39.  
  40.         ],
  41.  
  42.         comments: [
  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.  
  54.             'strong[data-e2e="share-count"]',
  55.  
  56.             'strong[data-e2e="browse-share-count"]',
  57.  
  58.             'strong[data-e2e="undefined-count"]', // fallback seen in some layouts
  59.  
  60.             'strong.css-vc3yj-StrongText'
  61.  
  62.         ]
  63.  
  64.     };
  65.  
  66.  
  67.     function getElement(selectors) {
  68.  
  69.         if (!selectors) return null;
  70.  
  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. })();
Advertisement
Add Comment
Please, Sign In to add comment