Created
August 21, 2024 08:24
-
-
Save ImN1/6e7ddb1c0c93d4ddecd1325b094c3cee to your computer and use it in GitHub Desktop.
URL Tagger
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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"); | |
// } | |
}); | |
}); | |
} | |
// ============================================ | |
}); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment