Генерация YouTube ссылки
// ==UserScript==
// @name Boost Link Generator
// @namespace http://tampermonkey.net/
// @version 0.0.1
// @description Генерация YouTube ссылки
// @match https://www.youtube.com/watch*
// @match https://m.youtube.com/watch*
// @grant GM_setClipboard
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
"use strict";
const CONFIG = {
searchBase: "https://www.youtube.com/results?search_query=",
panelId: "tm-boost-link-panel",
styleId: "tm-boost-link-style",
pageCheckInterval: 700,
initDelay: 600
};
const CHANNEL_URL_SELECTORS = [
'ytd-video-owner-renderer a[href^="/@"]',
'ytd-video-owner-renderer a[href^="/channel/"]',
'ytd-channel-name a[href]',
'#owner #channel-name a[href]'
];
const CHANNEL_NAME_SELECTORS = [
"ytd-video-owner-renderer ytd-channel-name a",
"ytd-channel-name a",
"#owner #channel-name a"
];
const TITLE_SELECTORS = [
"h1.ytd-watch-metadata yt-formatted-string",
"h1.title yt-formatted-string",
"yt-formatted-string.style-scope.ytd-watch-metadata"
];
const PANEL_TARGET_SELECTORS = [
"#above-the-fold #top-row",
"#above-the-fold",
"ytd-watch-metadata"
];
const TEXTS = {
refresh: "Обновите страницу если ненаход / Refresh the page if not found",
copyOk: "Скопировано",
copyError: "Ошибка копирования",
dataError: "Не удалось получить данные",
unknownChannel: "Unknown Channel",
channelNotFound: "Channel not found",
previewNotFound: "Not found"
};
let currentVideoId = null;
let initTimer = null;
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, ch => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'"
})[ch]);
}
function getVideoIdFromUrl() {
const url = new URL(location.href);
return url.searchParams.get("v") || "";
}
function cleanText(text) {
return (text || "").replace(/\s+/g, " ").trim();
}
function queryFirst(selectors, mapper) {
for (const selector of selectors) {
const value = mapper(document.querySelector(selector));
if (value) return value;
}
return "";
}
function removeFreeTags(title) {
if (!title) return "";
const first19 = title.slice(0, 19).replace(/\b(FREE\s+FOR\s+PROFIT|FREE)\b/gi, "");
return first19 + title.slice(19);
}
function normalizeTitle(title) {
let result = cleanText(title);
result = removeFreeTags(result);
result = result.replace(/[^a-zA-Zа-яёА-ЯЁ0-9\s&$"'’\-]/g, "");
result = result.replace(/\s+/g, " ").trim();
result = result.replace(/\bprod.*$/i, "").trim();
return result;
}
function encodeQuery(query) {
return encodeURIComponent(query).replace(/%20/g, "+");
}
function getSuffixByAge(datePublished) {
if (!datePublished) return "CE";
const published = new Date(datePublished);
if (Number.isNaN(published.getTime())) return "CE";
const now = new Date();
const diffMs = now - published;
const diffDays = Math.floor(diffMs / 86400000);
if (diffDays > 6) return "EE";
if (diffDays > 0) return "DE";
return "CE";
}
function generateSearchUrl({ title, channelName, datePublished, noChannel, sortByDate }) {
const normalizedTitle = normalizeTitle(title);
const query = noChannel ? normalizedTitle : `${normalizedTitle} ${channelName}`.trim();
const formattedQuery = encodeQuery(query);
if (!sortByDate) {
return `${CONFIG.searchBase}${formattedQuery}`;
}
const char = "I";
const suffix = getSuffixByAge(datePublished);
return `${CONFIG.searchBase}${formattedQuery}&sp=CA${char}SBAg${suffix}AE%253D`;
}
function getMetaContent(selector) {
return document.querySelector(selector)?.content?.trim() || "";
}
function getChannelUrl() {
return queryFirst(CHANNEL_URL_SELECTORS, el => el?.href) || getMetaContent('link[itemprop="name"]');
}
function getChannelName() {
return queryFirst(CHANNEL_NAME_SELECTORS, el => cleanText(el?.textContent))
|| cleanText(getMetaContent('meta[itemprop="author"]'))
|| TEXTS.unknownChannel;
}
function getTitle() {
return queryFirst(TITLE_SELECTORS, el => cleanText(el?.textContent))
|| cleanText(getMetaContent('meta[property="og:title"]'))
|| document.title.replace(/\s*-\s*YouTube$/i, "");
}
function getThumbnailUrl() {
return getMetaContent('meta[property="og:image"]');
}
function getDatePublished() {
return getMetaContent('meta[itemprop="datePublished"]');
}
function getVideoData() {
const title = getTitle();
const channelName = getChannelName();
const channelUrl = getChannelUrl();
const thumbnailUrl = getThumbnailUrl();
const datePublished = getDatePublished();
return {
title,
channelName,
channelUrl,
thumbnailUrl,
datePublished
};
}
async function copyToClipboard(text, html = "") {
try {
if (navigator.clipboard && window.ClipboardItem && html) {
const item = new ClipboardItem({
"text/plain": new Blob([text], { type: "text/plain" }),
"text/html": new Blob([html], { type: "text/html" })
});
await navigator.clipboard.write([item]);
return true;
}
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (_) {}
try {
if (typeof GM_setClipboard === "function") {
GM_setClipboard(html || text, { type: html ? "html" : "text", mimetype: html ? "text/html" : "text/plain" });
return true;
}
} catch (_) {}
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.focus();
ta.select();
document.execCommand("copy");
ta.remove();
return true;
} catch (_) {
return false;
}
}
function showStatus(message, ok = true) {
const status = document.querySelector(`#${CONFIG.panelId} .tm-status`);
if (!status) return;
status.textContent = message;
status.style.color = ok ? "#22c55e" : "#ef4444";
clearTimeout(status._timer);
status._timer = setTimeout(() => {
status.textContent = "";
}, 2200);
}
function buildMessage(data, options) {
const searchUrl = generateSearchUrl({
title: data.title,
channelName: data.channelName,
datePublished: data.datePublished,
noChannel: options.noChannel,
sortByDate: options.sortByDate
});
const channelVideosUrl = data.channelUrl
? `${data.channelUrl.replace(/\/$/, "")}/videos`
: TEXTS.channelNotFound;
const notFoundText = `Ненаход / Not found: ${channelVideosUrl}`;
const previewText = `Превью / Preview: ${data.thumbnailUrl || TEXTS.previewNotFound}`;
const lines = [
searchUrl,
"",
TEXTS.refresh,
"",
notFoundText,
"",
previewText
];
const safeThumb = escapeHtml(data.thumbnailUrl || "");
const htmlMessage = [
`<div>`,
...lines.flatMap(line => line ? [`<div>${escapeHtml(line)}</div>`] : ["<br>"]),
safeThumb ? `<br><a href="${safeThumb}">⠀⠀⠀⠀⠀</a>` : "",
`</div>`
].join("");
return { textMessage: lines.join("\n"), htmlMessage };
}
function injectStyles() {
if (document.getElementById(CONFIG.styleId)) return;
const style = document.createElement("style");
style.id = CONFIG.styleId;
style.textContent = `
#${CONFIG.panelId} {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin: 12px 0;
padding: 12px 14px;
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 14px;
font-family: Arial, sans-serif;
}
#${CONFIG.panelId} .tm-title {
font-size: 15px;
font-weight: 700;
color: var(--yt-spec-text-primary, #fff);
margin-right: 4px;
}
#${CONFIG.panelId} .tm-option {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--yt-spec-text-primary, #fff);
font-size: 15px;
user-select: none;
cursor: pointer;
}
#${CONFIG.panelId} input[type="checkbox"] {
cursor: pointer;
}
#${CONFIG.panelId} .tm-btn {
border: 0;
border-radius: 10px;
padding: 8px 12px;
cursor: pointer;
font-size: 15px;
font-weight: 700;
transition: transform .12s ease, opacity .12s ease;
}
#${CONFIG.panelId} .tm-btn:hover {
transform: translateY(-1px);
opacity: .95;
}
#${CONFIG.panelId} .tm-btn-copy {
background: #3ea6ff;
color: #111;
}
#${CONFIG.panelId} .tm-status {
min-width: 110px;
font-size: 14px;
font-weight: 700;
}
#${CONFIG.panelId} .tm-output {
width: 100%;
margin-top: 8px;
padding: 10px 12px;
border-radius: 10px;
background: rgba(0,0,0,0.18);
color: var(--yt-spec-text-primary, #fff);
font-size: 14px;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
display: block;
}
`;
document.head.appendChild(style);
}
function createPanel() {
if (document.getElementById(CONFIG.panelId)) return;
const target = queryFirst(PANEL_TARGET_SELECTORS, el => el);
if (!target) return;
const panel = document.createElement("div");
panel.id = CONFIG.panelId;
panel.innerHTML = `
<div class="tm-title">Boost Link</div>
<label class="tm-option">
<input type="checkbox" class="tm-no-channel">
Без канала / Without a channel
</label>
<label class="tm-option">
<input type="checkbox" class="tm-sort-date" checked>
По дате / By date
</label>
<button class="tm-btn tm-btn-copy" type="button">Скопировать</button>
<div class="tm-status"></div>
<div class="tm-output"></div>
`;
target.parentNode.insertBefore(panel, target.nextSibling);
const btnCopy = panel.querySelector(".tm-btn-copy");
const cbNoChannel = panel.querySelector(".tm-no-channel");
const cbSortDate = panel.querySelector(".tm-sort-date");
const output = panel.querySelector(".tm-output");
function getMessage() {
const data = getVideoData();
if (!data.title || !data.channelName) {
output.textContent = TEXTS.dataError;
return null;
}
return buildMessage(data, {
noChannel: cbNoChannel.checked,
sortByDate: cbSortDate.checked
});
}
function updateOutput() {
const message = getMessage();
if (!message) return null;
const { textMessage } = message;
output.textContent = textMessage;
return message;
}
btnCopy.addEventListener("click", async () => {
const message = updateOutput();
if (!message) {
showStatus(TEXTS.dataError, false);
return;
}
const ok = await copyToClipboard(message.textMessage, message.htmlMessage);
showStatus(ok ? TEXTS.copyOk : TEXTS.copyError, ok);
});
cbNoChannel.addEventListener("change", updateOutput);
cbSortDate.addEventListener("change", updateOutput);
updateOutput();
}
function removeOldPanel() {
document.getElementById(CONFIG.panelId)?.remove();
}
function init() {
if (!/\/watch/.test(location.pathname)) return;
const videoId = getVideoIdFromUrl();
if (!videoId) return;
if (videoId === currentVideoId && document.getElementById(CONFIG.panelId)) return;
currentVideoId = videoId;
injectStyles();
removeOldPanel();
clearTimeout(initTimer);
initTimer = setTimeout(() => {
createPanel();
}, CONFIG.initDelay);
}
function setupObservers() {
document.addEventListener("yt-navigate-finish", init, true);
window.addEventListener("load", init, { once: true });
let lastHref = location.href;
setInterval(() => {
if (location.href !== lastHref) {
lastHref = location.href;
init();
}
}, CONFIG.pageCheckInterval);
const observer = new MutationObserver(() => {
if (!document.getElementById(CONFIG.panelId) && /\/watch/.test(location.pathname)) {
init();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
}
setupObservers();
init();
})();