Magnet Copy & Preview

Enhanced magnet link handling with copy, preview, and image URL features//Add buttons to copy magnet links and preview on magnet.pics, works on 1cili.com and other sites

// ==UserScript==
// @name             		Magnet Copy & Preview
// @namespace        	    http://tampermonkey.net/
// @version          		2025.09.12
// @description      		Enhanced magnet link handling with copy, preview, and image URL features//Add buttons to copy magnet links and preview on magnet.pics, works on 1cili.com and other sites
// @author           		庄引
// @icon             		data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAMAAABgZ9sFAAAAwFBMVEX///8AAAD/XV2nxf//X1//YWGsy//k5ORlZWXx8fGoqKjIyMg4ODhtbW2pyP9ZWVlBQUGAgIC+vr5fX1+urq69RUUTExPq6ura2tqKioqRq910dHRPT0/4+PguLi63t7c3QVI9SFuVlZUmJiZGUmgbGxsXGyNYaIWgvfN7kbuKo9IOExxzh69jJSWenp5bIiJhc5Oz0/8tNUSVNDMlDg57REREGhrPTEw3FBTvV1ekNjbRhYX/oqN4LCyKMzPiU1PPfiQiAAACfElEQVRIiY2VC3eiMBCFcxUFxReCPKqoq7a17Wq3q6vW3W3//7/qTIKAEqpzDgcOfJnc3EwGIXRhWz66E+0nTVhAq0fXbbSDUUOIeIjRLXTAaWM5iX0D3kRfjGAKYcK5SYsp+mD+JjUzhEJMABHDugG3pQbbI1XNq0o8IeYkniIE4is0Od4ka+B6kyGuGmPBnTM/AsU4lO/i0t210BUNzi+C/sRUC6H5SnimRcLLCGY+0PPKaOVbw094hxRFZqkStNWTTZ5ztDEvk030YgGXn8LUEqVOT0/r9R/MhzkDhye+77Sj8JyuVom3zBwddoGA7sE9G5tNpWjJZ7RJ7lu80gavuEFjk/PSTuhq/S6Xm/ZW2eJK52OSJj2daeioK+xEbIPq2AXXG3rSiYdOIXeUeWJjzfXvB6LLBo9x1ykoycU5PsFj/Ts6FeNJMS0sme5oadNvZkt1aESAhVqnlgbXmzQyDl02coInVl5/1Jw0oj2uNzPbJgdLxjvwdXQ/qee0CFywls4zZlr6rP4pei8S/6mOc5Em/h7ZCek9JLhdoE9txsnV8UiJeYJ3SQ9Wqv6dfCseQtq4VJ/ydM145ZdntFjjWRo5hXlBVyrEt6PzNh/iV2LN+JImflX4KcCXu/r2G+NAvbFPdMXYFH4hkdzW+gJbagX9MPToEA0MRVf+9C5o2oaXDtO72v4g9xqr94Q+/r2EBR+mxzeiCal9DjabwbsSQoGhBqcSnkqaE1IkrFH5p+/XVBWHNOMpav9R1u/IuI80azLLKwqrzPKPsd3X0gFG5Zg2TH1EJOj4aRg1GrT/QGlTP0UwZA8Pu92W7+vvYRl25I7ht6x1WIp8AXxjKOd1DpCTAAAAAElFTkSuQmCC
// @match            		*://u3c3.com/*
// @match            		*://hjd2048.com/*
// @match            		*://*.cctv10.cc/*
// @match            		*://*.cctv12.cc/*
// @match            		*://*.1cili.com/!*
// @match            		*://sukebei.nyaa.si/*
// @match           		*://btdig.com/*
// @match            		*://whatslink.info*
// @match            		*://btsow.pics/*
// @match            		*://*.sis001.com/*
// @grant            		GM_setClipboard
// @grant           		GM_openInTab
// @grant           		GM_xmlhttpRequest
// @grant            		GM_addElement
// @grant            		GM_addStyle
// @run-at           		document-idle
// @license MIT
// ==/UserScript==

(function () {
	'use strict';

	// Debug flag to reduce noisy logs in production
	const DEBUG = false;

	// Schedule DOM writes safely to avoid ResizeObserver loops
	const schedule = (callback) => {
		return requestAnimationFrame(() => {
			try { callback(); } catch (error) { console.error(error); }
		});
	};

	/**
	* 创建并添加HTML元素到指定父节点
	* @param {string} tagName - HTML标签名称,默认为'button'
	* @param {string} innerHTML - 元素的内部HTML内容
	* @param {Object} options - 要应用到元素上的属性
	* @param {HTMLElement} parentNode - 父节点元素
	* @param {boolean} flag - 是否添加到父节点末尾,true使用append,false使用prepend
	*/
	const addElement = (tagName = `button`, innerHTML, options, parentNode, flag = true) => {
		if (!parentNode) {
			console.warn('addElement: parentNode is null or undefined');
			return;
		}

		const el = document.createElement(tagName);
		el.innerHTML = innerHTML;
		Object.assign(el, options);

		// 使用 requestAnimationFrame 来避免 ResizeObserver 循环问题
		schedule(() => {
			try {
				parentNode[flag ? 'prepend' : 'append'](el);
				if (DEBUG) console.log(parentNode);
			} catch (error) {
				console.error('addElement error:', error);
			}
		});
	};

	/**
	 * 监听并复制图片URL
	 * @param {string} parentDiv - 父元素选择器
	 * @param {string} targetNode - 图片元素选择器
	 * @param {string} attribute - 要监听的属性名
	 * @param {string} displayDiv - 显示元素选择器
	 * @returns {boolean} 是否成功设置监听
	 */
	const monitorAndCopyImageUrls = (parentDiv, targetNode, attribute, displayDiv) => {
		const searchDiv = document.querySelector(parentDiv);
		if (!searchDiv) {
			console.error(`找不到父元素: ${parentDiv}`);
			return false;
		}

		// 防抖函数,避免频繁更新
		let debounceTimer;
		const debounce = (fn, delay) => {
			return function (...args) {
				clearTimeout(debounceTimer);
				debounceTimer = setTimeout(() => fn.apply(this, args), delay);
			};
		};

		// 处理图片URL的函数
		const processImages = () => {
			try {
				const images = document.querySelectorAll(targetNode);
				if (images.length === 0) {
					return;
				}

				// 收集所有图片URL
				const urls = Array.from(images).map((img) => img[attribute]);
				const displayElement = document.querySelector(displayDiv);
				if (!displayElement) {
					return;
				}

				// 使用 DocumentFragment 来批量操作 DOM,减少重排
				const fragment = document.createDocumentFragment();

				// 创建或更新显示区域
				let section = displayElement.querySelector('section');
				if (!section) {
					section = document.createElement("section");
					fragment.appendChild(section);
				}

				// 更新URL列表
				section.innerHTML = '';
				urls.forEach((url) => {
					const p = document.createElement("p");
					p.textContent = url;
					section.appendChild(p);
				});

				// 如果section是新创建的,添加到fragment
				if (!displayElement.querySelector('section')) {
					displayElement.appendChild(fragment);
				}

				// 添加或更新复制按钮
				let copyButton = displayElement.querySelector('.copy-urls-button');
				if (!copyButton) {
					// 使用 setTimeout 来延迟按钮创建,避免在 MutationObserver 回调中直接修改 DOM
					setTimeout(() => {
						addElement(
							'button',
							'复制所有URL',
							{
								className: 'copy-urls-button',
								onclick: (e) => {
									try {
										const currentImages = document.querySelectorAll(targetNode);
										const currentUrls = Array.from(currentImages).map((img) => img[attribute]);
										GM_setClipboard(currentUrls.join("\n"), "text");
										e.target.textContent = '已复制所有URL';
										setTimeout(() => {
											e.target.textContent = '复制所有URL';
										}, 3000);
									} catch (error) {
										console.error("复制到剪贴板时出错:", error);
										e.target.textContent = '复制失败';
										setTimeout(() => {
											e.target.textContent = '复制所有URL';
										}, 3000);
									}
								}
							},
							displayElement
						);
					}, 0);
				}
			} catch (error) {
				console.error("处理图片时出错:", error);
			}
		};

		// 使用防抖处理图片更新
		const debouncedProcessImages = debounce(processImages, 1000);

		// 创建MutationObserver监听DOM变化
		const observer = new MutationObserver((mutations) => {
			const hasRelevantChanges = mutations.some((mutation) => {
				return (
					(mutation.type === "attributes" &&
						mutation.attributeName === attribute) ||
					mutation.type === "childList"
				);
			});

			if (hasRelevantChanges) {
				// 使用 requestAnimationFrame 来避免 ResizeObserver 循环问题
				schedule(() => {
					debouncedProcessImages();
				});
			}
		});

		// 配置观察选项
		const config = {
			childList: true,
			subtree: true,
			attributes: true,
			attributeFilter: [attribute],
		};

		// 开始观察
		observer.observe(searchDiv, config);

		// 初始处理
		processImages();

		return true;
	};



	// Function to extract hash from magnet URL
	function extractHashFromMagnet(magnetUrl) {
		const btihMatch = magnetUrl.match(/urn:btih:([a-zA-Z0-9]+)/i);
		if (btihMatch && btihMatch[1]) {
			return btihMatch[1].toLowerCase();
		}
		return '';
	}
	class Drawer {
		static instance = null;

		constructor(options = {}) {
			this.direction = options.direction || 'left';
			this.width = options.width || '300px';
			this.title = options.title || 'Drawer';
			this.init();
		}

		init() {
			// 如果存在之前的实例,先移除它
			if (Drawer.instance) {
				document.body.removeChild(Drawer.instance.drawer);
			}

			// Create drawer container
			this.drawer = document.createElement('div');
			this.drawer.style.cssText = `
                position: fixed;
                top: 0;
                ${this.direction}: 0;
                width: ${this.width};
                height: 100vh;
                background: white;
                box-shadow: 0 0 10px rgba(0,0,0,0.1);
                transform: translateX(${this.direction === 'left' ? '-100%' : '100%'});
                transition: transform 0.3s ease;
                z-index: 1000;
            `;

			// Create header
			const header = document.createElement('div');
			header.style.cssText = `
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 16px;
                border-bottom: 1px solid #eee;
            `;

			// Create title
			const title = document.createElement('h5');
			title.textContent = this.title;
			title.style.margin = '0';

			// Create close button
			const closeBtn = document.createElement('button');
			closeBtn.textContent = '×';
			closeBtn.style.cssText = `
                background: none;
                border: none;
                font-size: 24px;
                cursor: pointer;
                padding: 0;
                color: #666;
            `;
			closeBtn.onclick = () => this.close();

			// Create content container
			this.content = document.createElement('div');
			this.content.style.cssText = `
                padding: 16px;
                height: calc(100vh - 60px);
                overflow-y: auto;
            `;

			// Assemble drawer
			header.appendChild(title);
			header.appendChild(closeBtn);
			this.drawer.appendChild(header);
			this.drawer.appendChild(this.content);
			document.body.appendChild(this.drawer);

			// 保存当前实例
			Drawer.instance = this;
		}

		open() {
			this.drawer.style.transform = 'translateX(0)';
		}

		close() {
			this.drawer.style.transform = `translateX(${this.direction === 'left' ? '-100%' : '100%'})`;
			// Clear content after animation completes
			setTimeout(() => {
				this.content.innerHTML = '';
			}, 300); // Match the transition duration
		}

		setContent(content) {
			this.content.innerHTML = content;
		}
	}

	// Usage example
	function openDrawer(url, title, width = '40vw', direction = 'right') {
		// 如果已经存在drawer实例,先清空内容
		const existingDrawer = document.querySelector('.drawer-container');
		if (existingDrawer) {
			const content = existingDrawer.querySelector('.drawer-content');
			if (content) {
				content.innerHTML = '';
			}
		}

		const drawer = new Drawer({
			direction: direction,
			width: `${width}`,
			title: `${title} `
		});
		drawer.setContent(`<iframe width='100%;' height='100%' src=${url}></iframe>`);
		drawer.open();
	}
	// Special handling for 1cili.com
	function handle1CiliSite() {
		const magnetBoxes = document.querySelectorAll('.magnet-box');

		if (magnetBoxes.length > 0) {
			// For each magnet box
			magnetBoxes.forEach(box => {
				const inputField = box.querySelector('#input-magnet');
				if (!inputField) return;

				const magnetUrl = inputField.value;
				const hash = extractHashFromMagnet(magnetUrl);

				if (!hash) return;

				// Get title from page
				const pageTitle = document.querySelector('.magnet-title')?.textContent || 'Magnet Preview';

				// Find the input-group-btn div where the existing buttons are
				const btnGroup = box.querySelector('.input-group-btn');

				if (btnGroup) {
					// 使用 requestAnimationFrame 来避免 ResizeObserver 循环问题
					requestAnimationFrame(() => {
						try {
							// Create a new preview button
							const previewBtn = document.createElement('a');
							previewBtn.className = 'btn preview-button-1cili';
							previewBtn.innerHTML = '<svg class="svg-icon"><use xlink:href="/assets/icons.svg#icon-search"></use></svg>';
							previewBtn.title = '预览 Preview';
							previewBtn.href = 'javascript:void(0);';

							// Add click event for preview
							previewBtn.addEventListener('click', function (e) {
								e.preventDefault();
								e.stopPropagation();

								// Open preview in side panel
								openDrawer(`https://magnet.pics/m/${hash}`, pageTitle);
							});

							// Add the button to the button group
							btnGroup.appendChild(previewBtn);
						} catch (error) {
							console.error('handle1CiliSite error:', error);
						}
					});
				}
			});
		}
	}

	/**
	 * 处理常规网站的磁力链接功能
	 * @param {string} datalist - 要处理的元素列表选择器
	 * @param {string} hash - 包含磁力链接hash的元素选择器
	 * @param {string} title - 标题元素选择器
	 * @param {string} size - 文件大小元素选择器
	 * @param {string} date - 日期元素选择器
	 * @param {string} magnet - 磁力链接元素选择器
	 * @param {boolean} flag - 是否将按钮添加到元素末尾,true使用append,false使用prepend
	 */
	const handleRegularSites = (datalist, hash, title, size, date, magnet, flag) => {
		if (DEBUG) console.log(datalist, hash, title, size, date, magnet, flag);
		if (DEBUG) console.log(document.querySelectorAll(datalist));
		//在磁力链接详情页面(.fa-magnet)也添加预览按钮
		if (/[0-9a-fA-F]{40}/.test(window.location.href)) {
			// 使用 requestAnimationFrame 来避免 ResizeObserver 循环问题
			schedule(() => {
				try {
					// 构建完整的磁力链接,包含标题、大小和日期信息
					const link = `magnet:?xt=urn:btih:${window.location.href.match(/[0-9a-fA-F]{40}/)[0].toLowerCase()}&dn=${document.querySelector('tbody > tr:nth-child(5) > td:nth-child(2)').innerText}🔞Size=${document.querySelector('tbody > tr:nth-child(6) > td:nth-child(2)').innerText}🔞Date=${document.querySelector(' tbody > tr:nth-child(7) > td:nth-child(2)').innerText}`;
					document.querySelector('tbody > tr:nth-child(4) > td:nth-child(2) > div > a').textContent = link;

					// 添加复制按钮(幂等)
					const detailContainer = document.querySelector('.fa-magnet');
					if (!detailContainer) return;
					if (!detailContainer.querySelector('.magnet-btn.copy-btn')) {
						addElement('a', '📋',
							{
								className: 'magnet-btn copy-btn',
								title: 'Copy Magnet Link',
								onclick: (e) => {
									e.preventDefault();
									e.stopPropagation();
									GM_setClipboard(decodeURIComponent(link));
								},
							},
							detailContainer
						)
					}
					// 添加预览按钮(幂等)
					if (!detailContainer.querySelector('.magnet-btn.preview-btn')) {
						addElement('a', '👁️',
							{
								className: 'magnet-btn preview-btn',
								title: 'Preview on magnet.pics',
								onclick: (e) => {
									e.preventDefault();
									e.stopPropagation();
									openDrawer(`https://magnet.pics/m/${window.location.href.match(/[0-9a-fA-F]{40}/)[0].toLowerCase()}`, document.querySelector('.fa-folder-open').innerText);
								},
							},
							detailContainer
						)
					}
				} catch (error) {
					console.error('磁力链接详情页面处理错误:', error);
				}
			});
		};
		// 遍历所有匹配的元素
		document.querySelectorAll(datalist).forEach((element, index) => {
			if (DEBUG) console.log(element);

			const hashElement = element.querySelector(hash);
			if (!hashElement) return;

			// 从链接中提取40位的磁力链接hash
			const hashMatch = hashElement.href.match(/[0-9a-fA-F]{40}/);
			if (!hashMatch) return;

			// 构建完整的磁力链接,包含标题、大小和日期信息
			const link = `magnet:?xt=urn:btih:${hashMatch[0].toLowerCase()}&dn=${element.querySelector(title).innerText}🔞Size=${element.querySelector(size).innerText}🔞Date=${element.querySelector(date).innerText}`;

			// 使用 schedule 来避免 ResizeObserver 循环问题
			schedule(() => {
				try {
					const magnetContainer = element.querySelector(magnet);
					if (!magnetContainer) return;
					magnetContainer.textContent = link;

					// 添加复制按钮(幂等)
					if (!magnetContainer.querySelector('.magnet-btn.copy-btn')) {
						addElement('a', '📋',
							{
								className: 'magnet-btn copy-btn',
								title: 'Copy Magnet Link',
								onclick: (e) => {
									e.preventDefault();
									e.stopPropagation();
									GM_setClipboard(decodeURIComponent(link));
								},
							},
							magnetContainer
						)
					}

					// 添加预览按钮(幂等)
					if (!magnetContainer.querySelector('.magnet-btn.preview-btn')) {
						addElement('a', '👁️',
							{
								className: 'magnet-btn preview-btn',
								title: 'Preview on magnet.pics',
								onclick: (e) => {
									e.preventDefault();
									e.stopPropagation();
									openDrawer(`https://magnet.pics/m/${hashMatch[0].toLowerCase()}`, element.querySelector(title).innerText);
								},
							},
							magnetContainer
						)
					}
				} catch (error) {
					console.error('handleRegularSites error:', error);
				}
			});
		})
	}
	const isIncludes = (str) => window.location.hostname.includes(str);
	switch (true) {
		case isIncludes('1cili'):
			handle1CiliSite();
			break;
		case isIncludes('whatslink.info'):
			// 自定义样式和图片URL复制功能
			GM_addStyle(`.img {flex-shrink: 0;height: auto;width: auto;margin-right: 12px;}
        .banner-title,.banner,.disc,div.wrapper:nth-child(5), #app > div > div:nth-child(7),#app > div > div.footer {display:none}
        #app {max-width: 100vw !important; padding: 5px !important;}
        .content {padding: 5px !important;}
        .wrapper {margin: 0 !important;}
        .el-input-group {width: 100vw;}
        body {place-items: baseline !important;}`);
			monitorAndCopyImageUrls(
				"div.search",
				".image-list .img img",
				"src",
				"div.search"
			);
			break;
		case isIncludes('u3c3'):
		case isIncludes("sukebei.nyaa.si"):
			GM_addStyle(`/*u9a9*/
                .container .ad,.hdr-link,tr td:nth-child(1),tr td:nth-child(3),tr td:nth-child(6),tr td:nth-child(7),.text-center{display:none !important;}
                .table {width: 100%;}
                .torrent-list>tbody>tr>td { white-space: normal;}
                .torrent-list > tbody > tr > td {max-width:90vw;white-space: normal !important;}
                .data-list .row {padding: 0;}
                .navbar-form .input-group {position: fixed; left:0;width: 100vw;}`
			);
			handleRegularSites("tbody tr",
				"td:nth-child(3) a:last-child",
				"td:nth-child(2) a",
				"td:nth-child(4)",
				"td:nth-child(5)",
				"td:nth-child(2) a:last-child");
			break;
		case isIncludes('btdig.com'):
			// GM_addStyle(`center div div {width: 100vw;}`);
			handleRegularSites(
				".one_result > div",
				".torrent_name a",
				".torrent_name a",
				".torrent_size",
				".torrent_age",
				".torrent_magnet");
			break;
		case isIncludes('btsow.pics'):
			// GM_addStyle(`center div div {width: 100vw;}`);
			GM_addStyle(`/*btsow*/
                .search {position: sticky !important;top: 80px !important;}
                .form-inline .input-group {width: 100%;}
                .hidden-xs:not(.tags-box,.text-right,.search,.search-container),.data-list:not(.detail) .size,.data-list:not(.detail)  .date{ display: none !important;}`);

			// 为 btsow.pics 添加等待机制,确保内容加载完成
			const waitForBtsowContent = () => {
				const elements = document.querySelectorAll(".q-infinite-scroll .row");
				if (elements.length > 0) {
					console.log("btsow.pics 内容已加载,找到元素数量:", elements.length);
					// 使用 requestAnimationFrame 来避免 ResizeObserver 循环问题
					requestAnimationFrame(() => {
						handleRegularSites(
							".q-infinite-scroll .row",
							"a",
							"a",
							".size",
							".date",
							"a"
						);
					});
				} else {
					console.log("btsow.pics 内容未加载,等待中...");
					setTimeout(waitForBtsowContent, 2000); // 每2秒检查一次
				}
			};

			// 开始等待内容加载
			waitForBtsowContent();
			break;
		case isIncludes('hjd2048.com'):
		case isIncludes('cctv10.cc'):
		case isIncludes('cctv12.cc'):
			break;
		// case isIncludes('javdb'):
		// 	GM_addStyle(`.container:not(.is-max-desktop):not(.is-max-widescreen) {max-width: 100%;}
		//     .movie-list .item .video-title {white-space: normal;}`);
		// 	replaceImg();
		// 	break;
		case isIncludes('javbus'):
			GM_addStyle(`.masonry #waterfall {display: grid; grid-template-columns: repeat(auto-fill, minmax(465px, 1fr)); gap: 0px; padding: 0px;}
            .movie-box{width:465px !important;height:400px !important;margin:0 !important;}
            #waterfall .masonry-brick{position:relative !important; top: 0px !important;  left: 0 !important;margin:5px}
            .item-tag {display: inline-block;}.movie-box .photo-frame {height: auto !important;margin:0 !important;}.movie-box img {height: auto !important;}`);
			break;
		default:
			GM_addStyle(`* {-webkit-touch-callout: text;-webkit-user-select: text !important;-moz-user-select: text;user-select: text;}`);;
	}

})();