Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name TikTok Metadata Extractor
- // @namespace http://tampermonkey.net/
- // @version 0.4
- // @description Extracts video metadata and adds a button with hashtag formatting (underlined links when available)
- // @author You
- // @match *://www.tiktok.com/*
- // @grant none
- // ==/UserScript==
- (function() {
- const tiktokSelectors = {
- creatorName: ['span[data-e2e="browse-username"]'],
- // creator avatar link (profile URL)
- creatorUrl: ['a[data-e2e="browse-user-avatar"]', 'a[href^="/@"]'],
- // engagement: try multiple attribute names used in different layouts
- likes: [
- 'strong[data-e2e="like-count"]',
- 'strong[data-e2e="browse-like-count"]',
- 'strong[data-e2e="browse-like-count"], strong.css-vc3yj-StrongText'
- ],
- comments: [
- 'strong[data-e2e="comment-count"]',
- 'strong[data-e2e="browse-comment-count"]',
- 'strong[data-e2e="browse-comment-count"], strong.css-vc3yj-StrongText'
- ],
- shares: [
- 'strong[data-e2e="share-count"]',
- 'strong[data-e2e="browse-share-count"]',
- 'strong[data-e2e="undefined-count"]', // fallback seen in some layouts
- 'strong.css-vc3yj-StrongText'
- ]
- };
- function getElement(selectors) {
- if (!selectors) return null;
- if (typeof selectors === 'string') selectors = [selectors];
- return selectors.reduce((found, selector) => found || document.querySelector(selector), null);
- }
- function getText(selectors) {
- const el = getElement(selectors);
- return el ? el.textContent.trim() : 'N/A';
- }
- function absoluteUrl(href) {
- try {
- return new URL(href, window.location.origin).href;
- } catch (e) {
- return href || '';
- }
- }
- // Converts the description area into BBCode.
- // - Underlines hashtag links (a[data-e2e="search-common-link"]) as [u][url=ABS]#tag[/url][/u]
- // - Preserves plain text spans
- // - Works with both the "default" and "comment/profile" type layout (iterates childNodes)
- function getDescriptionBBCode() {
- const container = document.querySelector('div[data-e2e="browse-video-desc"]');
- if (!container) return 'N/A';
- const parts = [];
- container.childNodes.forEach(node => {
- if (node.nodeType === Node.TEXT_NODE) {
- const txt = node.textContent.replace(/\s+/g, ' ').trim();
- if (txt) parts.push(txt);
- } else if (node.nodeType === Node.ELEMENT_NODE) {
- // hashtag links (common)
- if (node.matches && node.matches('a[data-e2e="search-common-link"]')) {
- const href = absoluteUrl(node.getAttribute('href'));
- const text = node.textContent.trim();
- // if the link text contains a visible # (or the strong contains it), include as hashtag
- parts.push(`[u][url=${href}]${text}[/url][/u]`);
- } else if (node.matches && (node.matches('span[data-e2e="new-desc-span"]') || node.matches('strong.css-1p6dp51-StrongText'))) {
- const txt = node.textContent.replace(/\s+/g, ' ').trim();
- if (txt) parts.push(txt);
- } else {
- // fallback: get visible text of the node (covers nested spans/strong inside anchors)
- const txt = node.textContent.replace(/\s+/g, ' ').trim();
- if (txt) {
- // If this node contains hashtag anchors, convert them:
- const anchors = node.querySelectorAll && node.querySelectorAll('a[data-e2e="search-common-link"]');
- if (anchors && anchors.length) {
- // build text from child nodes, converting anchors to underlined bbcode
- const subParts = [];
- node.childNodes.forEach(sn => {
- if (sn.nodeType === Node.TEXT_NODE) {
- const s = sn.textContent.replace(/\s+/g,' ').trim();
- if (s) subParts.push(s);
- } else if (sn.nodeType === Node.ELEMENT_NODE && sn.matches && sn.matches('a[data-e2e="search-common-link"]')) {
- subParts.push(`[u][url=${absoluteUrl(sn.getAttribute('href'))}]${sn.textContent.trim()}[/url][/u]`);
- } else {
- const s = sn.textContent.replace(/\s+/g,' ').trim();
- if (s) subParts.push(s);
- }
- });
- if (subParts.length) parts.push(subParts.join(' '));
- } else {
- parts.push(txt);
- }
- }
- }
- }
- });
- return parts.join(' ').replace(/\s+/g, ' ').trim() || 'N/A';
- }
- function getCreatorUrl() {
- const el = getElement(tiktokSelectors.creatorUrl);
- return el ? absoluteUrl(el.getAttribute('href')) : window.location.origin + window.location.pathname;
- }
- function copyToClipboard(text) {
- const ta = document.createElement('textarea');
- ta.value = text;
- // make textarea invisible but present
- ta.style.position = 'fixed';
- ta.style.left = '-9999px';
- document.body.appendChild(ta);
- ta.select();
- document.execCommand('copy');
- document.body.removeChild(ta);
- }
- function showToast(message, isError = false) {
- const toast = document.createElement('div');
- Object.assign(toast.style, {
- position: 'fixed',
- top: '10px',
- right: '10px',
- padding: '12px 24px',
- backgroundColor: isError ? '#ff4444' : '#00C851',
- color: 'white',
- borderRadius: '4px',
- fontSize: '14px',
- zIndex: 100000,
- boxShadow: '0 2px 5px rgba(0,0,0,0.2)'
- });
- toast.textContent = message;
- document.body.appendChild(toast);
- setTimeout(() => toast.remove(), 3000);
- }
- function buildBBCode(metadata) {
- const videoUrl = window.location.href;
- 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}`;
- }
- function processAndCopy() {
- const metadata = {
- description: getDescriptionBBCode(),
- creator: getText(tiktokSelectors.creatorName),
- creatorUrl: getCreatorUrl(),
- likes: getText(tiktokSelectors.likes),
- comments: getText(tiktokSelectors.comments),
- shares: getText(tiktokSelectors.shares)
- };
- const bb = buildBBCode(metadata);
- copyToClipboard(bb);
- showToast('Metadata copied to clipboard!');
- }
- function createButton() {
- const button = document.createElement('button');
- button.id = 'tiktok-metadata-button';
- button.textContent = 'Copy Metadata';
- Object.assign(button.style, {
- padding: '8px 12px',
- background: '#000',
- color: '#fff',
- border: 'none',
- borderRadius: '4px',
- cursor: 'pointer',
- fontSize: '13px',
- marginLeft: '8px'
- });
- button.addEventListener('click', processAndCopy);
- return button;
- }
- // Insert button next to the follow/author area whenever that area appears.
- const observer = new MutationObserver(() => {
- // Find any follow wrapper (covers both button[data-e2e="browse-follow"] and div[data-e2e="browse-follow"])
- const followWrapper = document.querySelector('[data-e2e="browse-follow"]');
- if (!followWrapper) return;
- // Don't add duplicate button
- if (document.getElementById('tiktok-metadata-button')) return;
- // Prefer attaching to the followWrapper.parentElement so the button appears "seamless" with author info.
- const attachTo = followWrapper.parentElement || followWrapper;
- if (!attachTo || !document.body.contains(attachTo)) return;
- const btn = createButton();
- try {
- attachTo.appendChild(btn);
- } catch (e) {
- // fallback: append to body fixed as safe fallback
- Object.assign(btn.style, { position: 'fixed', top: '10px', left: '10px', marginLeft: '0' });
- document.body.appendChild(btn);
- }
- });
- observer.observe(document.body, { childList: true, subtree: true });
- // also allow quick manual access: if user loads bookmarklet-ish page where followWrapper doesn't appear fast,
- // pressing Ctrl+Shift+Y will copy metadata (handy during testing)
- window.addEventListener('keydown', (e) => {
- if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'y') {
- processAndCopy();
- }
- });
- })();
Advertisement
Add Comment
Please, Sign In to add comment