您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
美化 Jellyfin 日志并支持“等级+模块”双筛选,模块名按字母排序并附带行数
// ==UserScript== // @name Jellyfin 日志美化 + 双筛选菜单(含模块排序和行数) // @namespace https://github.com/banned2054/jellyfin-log-beautifier // @version 1.0 // @description 美化 Jellyfin 日志并支持“等级+模块”双筛选,模块名按字母排序并附带行数 // @match http://127.0.0.1:8096/System/Logs/Log?name=log_* // @icon https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRzrq2XhIw7in73q4tTa6PTaQRO6KxAJ_XLZwgrZ7i8pkYdoJBk2NMUMBuqal72A0YyAbo&usqp=CAU // @author banned // @homepageURL https://github.com/banned2054/jellyfin-log-beautifier // @supportURL https://github.com/banned2054/jellyfin-log-beautifier/issues // @grant none // ==/UserScript== (function () { 'use strict'; const logSelector = 'pre, code, .log, .terminal, body'; const container = document.querySelector(logSelector); if (!container) return; const logText = container.innerText; const lines = logText.trim().split('\n'); const table = document.createElement('table'); table.style.borderCollapse = 'collapse'; table.style.width = '100%'; table.style.fontFamily = 'monospace'; table.style.fontSize = '14px'; const thead = document.createElement('thead'); thead.innerHTML = ` <tr> <th style="border:1px solid #ccc;padding:4px;width:120px">时间</th> <th style="border:1px solid #ccc;padding:4px;width:90px">等级</th> <th style="border:1px solid #ccc;padding:4px;width:25%">模块</th> <th style="border:1px solid #ccc;padding:4px">讯息</th> </tr>`; table.appendChild(thead); const tbody = document.createElement('tbody'); table.appendChild(tbody); const levelMap = { '[INF]': {text: '信息', color: '#2c7'}, '[WRN]': {text: '警告', color: '#fa0'}, '[ERR]': {text: '错误', color: '#e44'}, '[DBG]': {text: '调试', color: '#0cf'}, '[TRC]': {text: '追踪', color: '#ccc'}, '[FTL]': {text: '严重', color: '#f55'}, 'UNKNOWN': {text: '无', color: '#999'} }; const rows = []; const levelSet = new Set(); const moduleMap = new Map(); // 模块名 -> 计数 for (const line of lines) { const match = line.match(/^\[(.*?)\] \[(\w+)\] \[\d+\] (.*)$/); if (!match) continue; const [_, datetime, levelTag, fullMsg] = match; const dateStr = datetime.slice(0, 10); const timeStr = datetime.slice(11, 19); const levelKey = `[${levelTag}]`; const levelInfo = levelMap[levelKey] || levelMap['UNKNOWN']; const levelText = levelInfo.text; levelSet.add(levelText); let module = fullMsg; let message = ''; const colonIndex = fullMsg.indexOf(':'); if (colonIndex !== -1) { module = fullMsg.slice(0, colonIndex).trim(); message = fullMsg.slice(colonIndex + 1).trim(); } moduleMap.set(module, (moduleMap.get(module) || 0) + 1); const row = document.createElement('tr'); row.dataset.level = levelText; row.dataset.module = module; row.innerHTML = ` <td style="border:1px solid #ccc;padding:4px;text-align:center;"> <div>${dateStr}</div><div>${timeStr}</div> </td> <td style="border:1px solid #ccc;padding:4px;text-align:center;color:${levelInfo.color};font-weight:bold;"> ${levelText} </td> <td style="border:1px solid #ccc;padding:4px;word-break:break-word;">${module}</td> <td style="border:1px solid #ccc;padding:4px;white-space:pre-wrap;">${message}</td> `; rows.push(row); } function createFilterPanel(title, values, dataKey) { const wrapper = document.createElement('div'); wrapper.style.position = 'relative'; wrapper.style.display = 'inline-block'; wrapper.style.marginRight = '20px'; const toggleBtn = document.createElement('button'); toggleBtn.textContent = `筛选 ${title}`; toggleBtn.style.padding = '6px 12px'; toggleBtn.style.cursor = 'pointer'; toggleBtn.style.marginBottom = '6px'; const dropdown = document.createElement('div'); dropdown.style.display = 'none'; dropdown.style.position = 'absolute'; dropdown.style.top = '100%'; dropdown.style.left = '0'; dropdown.style.backgroundColor = '#333'; dropdown.style.border = '1px solid #555'; dropdown.style.padding = '10px'; dropdown.style.zIndex = '999'; dropdown.style.color = '#fff'; dropdown.style.borderRadius = '6px'; dropdown.style.boxShadow = '0 2px 8px rgba(0,0,0,0.2)'; dropdown.style.maxHeight = '250px'; dropdown.style.overflowY = 'auto'; const checkboxes = []; let sortedList = []; if (dataKey === 'module') { sortedList = [...values.entries()] .sort((a, b) => a[0].localeCompare(b[0], 'en')) // 排序 .map(([key, count]) => ({label: key, count})); } else { sortedList = [...values].sort().map(key => ({label: key})); } // 动态测宽度 const longest = sortedList.reduce((a, b) => (a.label + (b.count ? ` (${b.count})` : '')).length > (b.label + (b.count ? ` (${b.count})` : '')).length ? a : b , {label: ''}); const tmpSpan = document.createElement('span'); tmpSpan.style.position = 'absolute'; tmpSpan.style.visibility = 'hidden'; tmpSpan.style.fontFamily = 'monospace'; tmpSpan.textContent = longest.label + ' (999)'; document.body.appendChild(tmpSpan); dropdown.style.minWidth = tmpSpan.offsetWidth + 30 + 'px'; tmpSpan.remove(); sortedList.forEach(({label, count}) => { const labelEl = document.createElement('label'); labelEl.style.display = 'block'; labelEl.style.margin = '4px 0'; labelEl.style.cursor = 'pointer'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = true; checkbox.value = label; checkbox.style.marginRight = '8px'; checkboxes.push(checkbox); const text = count ? `${label} (${count})` : label; labelEl.appendChild(checkbox); labelEl.appendChild(document.createTextNode(text)); dropdown.appendChild(labelEl); }); toggleBtn.onclick = () => { dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; }; document.addEventListener('click', (e) => { if (!wrapper.contains(e.target)) dropdown.style.display = 'none'; }); wrapper.appendChild(toggleBtn); wrapper.appendChild(dropdown); return {wrapper, checkboxes, dataKey}; } const levelPanel = createFilterPanel('等级', levelSet, 'level'); const modulePanel = createFilterPanel('模块', moduleMap, 'module'); function filterRows() { const selectedLevels = new Set(levelPanel.checkboxes.filter(c => c.checked).map(c => c.value)); const selectedModules = new Set(modulePanel.checkboxes.filter(c => c.checked).map(c => c.value)); tbody.innerHTML = ''; for (const row of rows) { if (selectedLevels.has(row.dataset.level) && selectedModules.has(row.dataset.module)) { tbody.appendChild(row); } } } levelPanel.checkboxes.forEach(cb => cb.addEventListener('change', filterRows)); modulePanel.checkboxes.forEach(cb => cb.addEventListener('change', filterRows)); const controls = document.createElement('div'); controls.style.marginBottom = '1em'; controls.appendChild(levelPanel.wrapper); controls.appendChild(modulePanel.wrapper); container.innerHTML = ''; container.appendChild(controls); container.appendChild(table); filterRows(); })();