Skip to content

Instantly share code, notes, and snippets.

@iqiancheng
Forked from ImN1/url_tagger.user.js
Created August 25, 2024 04:14
Show Gist options
  • Save iqiancheng/48d7a684ad2ea07583d71ee0b7040c44 to your computer and use it in GitHub Desktop.
Save iqiancheng/48d7a684ad2ea07583d71ee0b7040c44 to your computer and use it in GitHub Desktop.
URL Tagger
// ==UserScript==
// @name URL Tagger
// @namespace http://tampermonkey.net/
// @version 1.5
// @description Tag URLs based on predefined patterns and data
// @author Your Name
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect 192.168.x.x
// ==/UserScript==
// Add styles to the page
GM_addStyle(`
.custom-tooltip {
position: absolute;
background-color: darkgreen;
color: white;
padding: 5px;
border-radius: 4px;
font-size: 16px;
pointer-events: none;
z-index: 10000;
white-space: nowrap;
display: none;
}
`);
(function() {
'use strict';
const timestamp = new Date().getTime()
const patternsUrl = `http://192.168.x.x:x/url_tags_regex.csv?${timestamp}`;
const dataUrl = `http://192.168.x.x:x/url_tags.csv?${timestamp}`;
const doneTextDecoration = "solid line-through darkgreen 2px";
const highlightBackColor = '#6c6';
const highlightFontColor = '#fff';
const webdavUser = 'user';
const webdavPassword = 'pass';
const encodedCredentials = btoa(`${webdavUser}:${webdavPassword}`);
function loadCSV(url, callback) {
GM_xmlhttpRequest({
method: "GET",
url: url,
headers: {
"Authorization": `Basic ${encodedCredentials}`
},
onload: function(response) {
if (response.status===200) {
callback(response.responseText);
} else {
console.error("Failed to load CSV from", url, "Status:", response.status, response.statusText);
}
},
onerror: function() {
console.error("Network error occurred while loading CSV from", url);
}
});
}
function parseCSV(csvText) {
const lines = csvText.split('\n').filter(line => line.trim() !== '');
const headers = lines[0].split(';');
return lines.slice(1).map(line => {
const values = line.split(';');
return headers.reduce((obj, header, index) => {
obj[header.trim()] = values[index].trim();
return obj;
}, {});
});
}
function createAbsoluteUrl(href) {
const link = document.createElement('a');
link.href = href;
return link.href;
}
function checkDomainPair(A, B, ref) {
if (!ref) {
console.error("文件内容尚未加载");
return false;
}
if (A === B) {
return true;
}
for (let line of ref) {
if (line.includes(A) && line.includes(B)) {
console.log("两者匹配");
return true;
}
}
return false;
}
function fullMatch(linkurl, linkhost, data, patterns, alias, domains) {
return data.filter(item => item.url===linkurl || (domains.includes(linkhost) && patterns.some(pattern => {
const check1 = checkDomainPair(item.domain, linkhost, alias);
const check2 = checkDomainPair(pattern.domain, linkhost, alias);
return check1 && check2 && pattern.regex.exec(linkurl)?.[1]===item.submatch;
}))).sort((a, b) => b.datetime - a.datetime)[0];
}
function formatDate(datetimestr) {
let formattedDate;
if (datetimestr==='') {
formattedDate = 'History';
} else {
if (datetimestr){
if (typeof datetimestr === 'number') {
const datetime = new Date(datetimestr);
formattedDate = datetime.toISOString().slice(0, 10).replace(/-/g,'').slice(2, 10);
} else if (/^\d+$/.test(datetimestr)) {
const timestampStr = parseInt(datetimestr);
const datetime = new Date(timestampStr);
formattedDate = datetime.toISOString().slice(0, 10).replace(/-/g,'').slice(2, 10);
} else {
const datetimeStr = datetimestr.trim();
formattedDate = datetimeStr.slice(0, 10).replace(/-/g,'').slice(2, 10);
};
} else {
const datetime = new Date();
const formattedDate = `${datetime.getFullYear().toString().slice(-2)}${('0' + (datetime.getMonth() + 1)).slice(-2)}${('0' + datetime.getDate()).slice(-2)}`;
}
};
return formattedDate;
}
loadCSV(patternsUrl, patternsText => {
const patterns = parseCSV(patternsText).map(item => ({
domain: item.domain,
download: item.download,
regex: new RegExp(item.regex, 'i')
}));
const patternDomains = new Set(patterns.map(pattern => pattern.domain));
const domainsStr = Array.from(patternDomains).join(',');
const downloadSites = new Set(
patterns
.filter(item => item.download)
.map(item => item.domain)
);
loadCSV(dataUrl, dataText => {
let data = parseCSV(dataText);
data = data.map(item => {
const url = new URL(item.url);
const domain = url.hostname;
return { ...item, domain: domain };
});
const headers = dataText.split('\n')[0];
if (downloadSites.has(window.location.hostname)) {
const matchingData = fullMatch(window.location.href, window.location.hostname, data, patterns, patternDomains, domainsStr);
if (matchingData) {
alert('Already Downloaded!');
return
};
};
const links = document.querySelectorAll('a');
links.forEach(link => {
// link.dataset.withRedTag = false;
const linkText = link.textContent;
const linkUrl = new URL(createAbsoluteUrl(link.href));
const linkDomain = linkUrl.hostname;
if (!(patternDomains.has(linkDomain) || domainsStr.includes(linkDomain))) {
return;
};
const matchingData = fullMatch(linkUrl.href, linkDomain, data, patterns, patternDomains, domainsStr);
const matchedPattern = patterns.find(pattern => checkDomainPair(pattern.domain, linkDomain, patternDomains) && pattern.regex.test(linkUrl.href));
if (matchedPattern) {
const submatch = matchedPattern.regex.exec(linkUrl.href)?.[1];
if (matchingData && matchingData.tags) {
addGreenTag(link, linkUrl.href, linkText, matchingData.hostname, matchingData.datetime, matchingData.submatch, matchingData.tags)
} else {
link.addEventListener('mouseover', handleMouseOver);
link.addEventListener('mouseout', handleMouseOut);
}
}
});
function handleMouseOver(event) {
const link = event.target;
if (!link.previousElementSibling ||
(!link.previousElementSibling.classList.contains('red-tag') && !link.previousElementSibling.classList.contains('green-tag'))
) {
const linkUrl = new URL(createAbsoluteUrl(link.href));
addRedTag(link, linkUrl.href, link.textContent);
}
}
function handleMouseOut(event) {
const link = event.target;
const redTag = link.previousElementSibling;
if (redTag && redTag.classList.contains('red-tag')) {
redTag.remove();
}
}
function addGreenTag(link, url, title, hostname, datetime, submatch, tags) {
if (tags===null || typeof tags==='undefined' || tags.trim()==='') {
return;
}
let yymmdd = formatDate(datetime);
let tooltipContent = `${yymmdd}: ${tags}`;
const greenTag = createTag('\u{1F3F7}\uFE0F', 'green');
greenTag.addEventListener('mouseover', () => {
let tooltip = document.querySelector('.custom-tooltip');
if (!tooltip) {
tooltip = document.createElement('div');
tooltip.className = 'custom-tooltip';
document.body.appendChild(tooltip);
}
tooltip.textContent = tooltipContent.replace(/(,\s*)?done$/i, '');
tooltip.style.display = 'block';
const rect = greenTag.getBoundingClientRect();
tooltip.style.left = `${rect.left + window.scrollX}px`;
tooltip.style.top = `${rect.top + window.scrollY + greenTag.offsetHeight + 5}px`;
});
greenTag.addEventListener('mouseout', () => {
const tooltip = document.querySelector('.custom-tooltip');
if (tooltip) {
tooltip.style.display = 'none';
}
});
greenTag.addEventListener('click', () => {
editTags(url, title, hostname, submatch, tags, (result, datetime) => {
const tooltip = document.querySelector('.custom-tooltip');
if (result) {
if (result!==tags){
const yymmdd = formatDate(datetime);
tooltipContent = `${yymmdd}: ${result}`;
if (tooltip) {
tooltip.textContent = tooltipContent.replace(/(,\s*)?done$/i, '');
};
if (/done$/i.test(result)) {
link.style.textDecoration = doneTextDecoration;
link.style.backgroundColor = '';
link.style.color = '';
} else {
link.style.textDecoration = "none";
link.style.backgroundColor = highlightBackColor;
link.style.color = highlightFontColor;
};
};
} else {
if (tooltip) {
tooltip.style.display = 'none';
}
greenTag.remove();
link.style.textDecoration = "none";
link.style.backgroundColor = '';
link.style.color = '';
link.addEventListener('mouseover', handleMouseOver);
link.addEventListener('mouseout', handleMouseOut);
}
});
});
if (/done$/i.test(tags)) {
link.style.textDecoration = doneTextDecoration;
link.style.backgroundColor = '';
link.style.color = '';
} else {
link.style.textDecoration = "none";
link.style.backgroundColor = highlightBackColor;
link.style.color = highlightFontColor;
};
link.parentNode.insertBefore(greenTag, link);
}
function addRedTag(link, url, title) {
const redTag = createTag('\u{1F516}\uFE0F', 'red');
redTag.addEventListener('mouseover', () => {
redTag.title = 'Click to add tags';
});
redTag.addEventListener('mouseout', () => {
redTag.hideTimeout = setTimeout(() => {
redTag.style.display = 'none';
// redTag.remove();
}, 200);
});
redTag.addEventListener('click', () => {
addTags(url, title, (tags, datetime, submatch, hostname) => {
redTag.remove();
if (tags) {
addGreenTag(link, url, title, hostname, datetime, submatch, tags);
link.removeEventListener('mouseover', handleMouseOver);
link.removeEventListener('mouseout', handleMouseOut);
}
});
});
link.parentNode.insertBefore(redTag, link);
}
function createTag(text, color) {
const tag = document.createElement('span');
tag.textContent = text;
tag.classList.add(`${color}-tag`);
tag.style.color = color;
tag.style.cursor = 'pointer';
tag.style.marginLeft = '2px';
tag.style.marginRight = '2px';
return tag;
}
function createInputContainer(placeholder, value = '') {
function createInput(placeholder, value = '') {
const input = document.createElement('input');
input.type = 'text';
input.placeholder = placeholder;
input.value = value;
input.style.marginRight = '4px';
return input;
}
function createButton(text) {
const button = document.createElement('button');
button.textContent = text;
return button;
}
function createContainer(...elements) {
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.left = '50%';
container.style.top = '50%';
container.style.backgroundColor = 'white';
container.style.border = '1px solid black';
container.style.padding = '10px';
container.style.zIndex = '10000';
elements.forEach(element => container.appendChild(element));
return container;
}
const input = createInput(placeholder, value);
const okButton = createButton('OK');
const cancelButton = createButton('Cancel');
const container = createContainer(input, okButton, cancelButton);
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
okButton.click();
}
});
document.body.appendChild(container);
return { container, input, okButton, cancelButton };
}
function addTags(url, linkText, callback) {
const { container, input, okButton, cancelButton } = createInputContainer('Enter tags');
okButton.addEventListener('click', () => {
const tags = input.value.trim();
if (tags) {
const matchedPattern = patterns.find(pattern => pattern.regex.exec(url));
const submatch = matchedPattern ? matchedPattern.regex.exec(url)[1] : '';
const datetime = new Date().getTime();
const hostname = new URL(url).hostname;
const newRecord = `${hostname};${submatch};${tags};${datetime};${url};${linkText}`;
appendToCSV(newRecord, headers);
container.remove();
if (callback) callback(tags, datetime, submatch, hostname);
} else {
container.remove();
if (callback) callback(null, null, null, null);
}
});
cancelButton.addEventListener('click', () => {
container.remove();
if (callback) callback(null, null, null, null);
});
}
function editTags(url, linkText, hostname, submatch, oldTags, callback) {
const { container, input, okButton, cancelButton } = createInputContainer('', oldTags);
okButton.addEventListener('click', () => {
const tags = input.value.trim();
const datetime = new Date().getTime();
let newRecord;
if (tags) {
newRecord = `${hostname};${submatch};${tags};${datetime};${url};${linkText}`;
} else {
newRecord = `${hostname};${submatch};;${datetime};${url};${linkText}`;
}
appendToCSV(newRecord, headers);
container.remove();
if (callback) callback(tags, datetime);
});
cancelButton.addEventListener('click', () => {
container.remove();
if (callback) callback(oldTags, null);
});
}
function appendToCSV(newRecord, headers) {
loadCSV(dataUrl, oldCSVText => {
const updatedCSV = `${oldCSVText}\n${newRecord}`;
GM_xmlhttpRequest({
method: "PUT",
url: dataUrl,
data: updatedCSV,
headers: {
"Authorization": `Basic ${encodedCredentials}`,
"Content-Type": "text/csv"
},
// onload: function(response) {
// if (response.status===200) {
// alert("Record successfully appended to CSV");
// } else {
// alert("Failed to append record to CSV: " + response.statusText);
// console.error("Failed to append record to CSV:", response.statusText);
// }
// },
// onerror: function() {
// alert("Network error occurred while appending record to CSV");
// console.error("Network error occurred while appending record to CSV");
// }
});
});
}
// ============================================
});
});
})();
We can make this file beautiful and searchable if this error is corrected: No commas found in this CSV file in line 0.
hostname;submatch;tags;datetime;url;title
pan.baidu.com;jkQqkM_RUK4V3BdZJ9K2HA;12;1723084223659;https://pan.baidu.com/s/1jkQqkM_RUK4V3BdZJ9K2HA?pwd=1234;
rosefile.net;/rosefile.net/g2688x0jeh/zjp.7z.004.html;done;1723726822764;https://rosefile.net/g2688x0jeh/zjp.7z.004.html;https://rosefile.net/g2688x0jeh/zjp.7z.004.html
We can make this file beautiful and searchable if this error is corrected: It looks like row 5 should actually have 1 column, instead of 2 in line 4.
domain;regex;download
rosefile.net;/(.+\.html)$;1
pan.baidu.com;/s/1([^?/#&]+);
pan.baidu.com;surl=(.+)(?:[?&#]|$);
www.v2ex.com,hk.v2ex.com;/t/(\d+)\D;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment