Greasy Fork is available in English.
Enhances pre-formatted HTTP directory listings (Nginx, lighttpd, seedbox indexes, etc.) with sortable columns (name, date, size), file type icons, date grouping, and per-site preferences. Configure which sites to activate on via URL patterns.
// ==UserScript==
// @name HTTP Index Sorter
// @namespace https://greasyfork.org/en/users/1574063-primetime43
// @version 1.0
// @description Enhances pre-formatted HTTP directory listings (Nginx, lighttpd, seedbox indexes, etc.) with sortable columns (name, date, size), file type icons, date grouping, and per-site preferences. Configure which sites to activate on via URL patterns.
// @author primetime43
// @match *://*/*
// @homepageURL https://greasyfork.org/en/users/1574063-primetime43
// @supportURL https://greasyfork.org/en/users/1574063-primetime43
// @license MIT
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
'use strict';
// --- File Type Icons ---
const FILE_ICONS = {
// Video
video: { icon: '\uD83C\uDFA5', extensions: ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v', 'mpg', 'mpeg', 'ts'] },
// Audio
audio: { icon: '\uD83C\uDFB5', extensions: ['mp3', 'flac', 'wav', 'aac', 'ogg', 'wma', 'm4a', 'opus'] },
// Images
image: { icon: '\uD83D\uDDBC\uFE0F', extensions: ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp', 'ico', 'tiff', 'tif'] },
// Archives
archive: { icon: '\uD83D\uDCE6', extensions: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'zst', 'tgz', 'tar.gz'] },
// Documents
document: { icon: '\uD83D\uDCC4', extensions: ['pdf', 'doc', 'docx', 'odt', 'rtf', 'txt', 'epub', 'mobi'] },
// Code
code: { icon: '\uD83D\uDCDD', extensions: ['js', 'ts', 'py', 'java', 'c', 'cpp', 'h', 'cs', 'go', 'rs', 'rb', 'php', 'html', 'css', 'json', 'xml', 'yaml', 'yml', 'sh', 'bat'] },
// Disk images
disk: { icon: '\uD83D\uDCBF', extensions: ['iso', 'img', 'dmg', 'bin', 'cue', 'nrg'] },
// Executables
exe: { icon: '\u2699\uFE0F', extensions: ['exe', 'msi', 'apk', 'deb', 'rpm', 'appimage'] },
// Subtitles
subtitle: { icon: '\uD83D\uDCAC', extensions: ['srt', 'sub', 'ass', 'ssa', 'vtt'] },
// Torrent
torrent: { icon: '\uD83E\uDDF2', extensions: ['torrent'] },
// NFO
nfo: { icon: '\u2139\uFE0F', extensions: ['nfo', 'nzb'] },
};
const FOLDER_ICON = '\uD83D\uDCC1';
const DEFAULT_FILE_ICON = '\uD83D\uDCC3';
function getFileIcon(name) {
if (name.endsWith('/')) return FOLDER_ICON;
const lower = name.toLowerCase();
for (const category of Object.values(FILE_ICONS)) {
if (category.extensions.some(ext => lower.endsWith('.' + ext))) {
return category.icon;
}
}
return DEFAULT_FILE_ICON;
}
// --- URL Pattern Management ---
function loadUrls() {
const stored = GM_getValue('urls', null);
if (stored === null) {
GM_setValue('urls', JSON.stringify([]));
return [];
}
try { return JSON.parse(stored); } catch { return []; }
}
function saveUrls(urls) {
GM_setValue('urls', JSON.stringify(urls));
}
function globToRegex(pattern) {
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
const withWildcards = escaped.replace(/\*/g, '.*');
return new RegExp('^' + withWildcards + '$');
}
function urlMatches(url, patterns) {
return patterns.some(p => globToRegex(p).test(url));
}
// --- Per-Site Sort Preferences ---
function getSortKey() {
const patterns = loadUrls();
for (const p of patterns) {
if (globToRegex(p).test(location.href)) {
return 'sort_' + p;
}
}
return 'sort_global';
}
function loadSort() {
const key = getSortKey();
try { return JSON.parse(GM_getValue(key, 'null')); } catch { return null; }
}
function saveSort(column, ascending) {
const key = getSortKey();
GM_setValue(key, JSON.stringify({ column, ascending }));
}
function loadGroupByDate() {
const key = getSortKey() + '_group';
return GM_getValue(key, false);
}
function saveGroupByDate(enabled) {
const key = getSortKey() + '_group';
GM_setValue(key, enabled);
}
// --- Directory Listing Detection ---
function isDirectoryListing() {
const pre = document.querySelector('pre');
if (!pre) return false;
const links = pre.querySelectorAll('a[href]');
if (links.length < 1) return false;
const text = pre.textContent || '';
return /\d{2}-[A-Za-z]{3}-\d{4}/.test(text) || /\d{4}-\d{2}-\d{2}/.test(text);
}
// --- Parsing ---
function parseEntries() {
const pre = document.querySelector('pre');
const children = Array.from(pre.childNodes);
const entries = [];
let parentEntry = null;
for (let i = 0; i < children.length; i++) {
const node = children[i];
if (node.nodeName !== 'A') continue;
const href = node.getAttribute('href');
const textNode = node.nextSibling;
const meta = (textNode && textNode.nodeType === 3) ? textNode.textContent : '';
if (href === '../' || href === '/') {
parentEntry = { element: node, textNode: textNode, name: '../', date: null, size: -1, isParent: true };
continue;
}
const name = node.textContent.trim();
const date = parseDate(meta);
const size = parseSize(meta);
entries.push({ element: node, textNode: textNode, name, date, size, isParent: false });
}
return { entries, parentEntry, pre };
}
function parseDate(text) {
// Format: 02-Jan-2024 12:34
let m = text.match(/(\d{2})-([A-Za-z]{3})-(\d{4})\s+(\d{2}):(\d{2})/);
if (m) {
const months = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
return new Date(+m[3], months[m[2]] ?? 0, +m[1], +m[4], +m[5]);
}
// Format: 2024-01-02 12:34
m = text.match(/(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2})/);
if (m) return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]);
// Date only
m = text.match(/(\d{2})-([A-Za-z]{3})-(\d{4})/);
if (m) {
const months = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };
return new Date(+m[3], months[m[2]] ?? 0, +m[1]);
}
return null;
}
function parseSize(text) {
const m = text.match(/([\d.]+)\s*([BKMGT]i?B?|[BKMGT])\b/i);
if (!m) return -1;
const val = parseFloat(m[1]);
const unit = m[2].charAt(0).toUpperCase();
const multipliers = { B: 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 };
return val * (multipliers[unit] || 1);
}
// --- Sorting ---
function sortEntries(entries, column, ascending) {
const dir = ascending ? 1 : -1;
entries.sort((a, b) => {
switch (column) {
case 'name':
return dir * a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
case 'date': {
const da = a.date ? a.date.getTime() : 0;
const db = b.date ? b.date.getTime() : 0;
return dir * (da - db);
}
case 'size':
return dir * (a.size - b.size);
default:
return 0;
}
});
}
function applySort(column, ascending) {
const { entries, parentEntry, pre } = parseEntries();
removeDateHeaders(pre);
sortEntries(entries, column, ascending);
// Rebuild content
while (pre.firstChild) pre.removeChild(pre.firstChild);
if (parentEntry) {
pre.appendChild(parentEntry.element);
if (parentEntry.textNode) pre.appendChild(parentEntry.textNode);
}
let lastDateLabel = null;
for (const entry of entries) {
if (groupByDate) {
const label = getDateLabel(entry.date);
if (label !== lastDateLabel) {
pre.appendChild(createDateHeader(label));
pre.appendChild(document.createTextNode('\n'));
lastDateLabel = label;
}
}
pre.appendChild(entry.element);
if (entry.textNode) pre.appendChild(entry.textNode);
}
// Save per-site preference
saveSort(column, ascending);
updateToolbarArrows(column, ascending);
updateItemCount();
}
// --- Date Grouping ---
let groupByDate = false;
function getDateLabel(date) {
if (!date) return 'Unknown Date';
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today.getTime() - 86400000);
const entryDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
if (entryDay.getTime() === today.getTime()) return 'Today';
if (entryDay.getTime() === yesterday.getTime()) return 'Yesterday';
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return months[date.getMonth()] + ' ' + date.getDate() + ', ' + date.getFullYear();
}
function createDateHeader(label) {
const header = document.createElement('div');
header.className = 'http-index-sorter-date-header';
header.textContent = '\u2500\u2500 ' + label + ' \u2500\u2500';
header.style.cssText = `
font-family: monospace;
font-size: 12px;
color: #888;
padding: 6px 0 2px 0;
font-weight: bold;
`;
return header;
}
function removeDateHeaders(pre) {
const headers = pre.querySelectorAll('.http-index-sorter-date-header');
for (const h of headers) h.remove();
}
// --- UI: Toolbar ---
let toolbarButtons = {};
let itemCountSpan = null;
let groupBtn = null;
function updateItemCount() {
if (!itemCountSpan) return;
const pre = document.querySelector('pre');
const allLinks = pre.querySelectorAll('a[href]');
let total = 0;
for (const link of allLinks) {
const href = link.getAttribute('href');
if (href === '../' || href === '/') continue;
total++;
}
itemCountSpan.textContent = total + ' items';
}
function updateToolbarArrows(activeColumn, ascending) {
for (const [col, btn] of Object.entries(toolbarButtons)) {
const arrow = btn.querySelector('.sort-arrow');
if (col === activeColumn) {
arrow.textContent = ascending ? ' \u25B2' : ' \u25BC';
} else {
arrow.textContent = '';
}
}
}
function addFileIcons() {
const pre = document.querySelector('pre');
const links = pre.querySelectorAll('a[href]');
for (const link of links) {
const href = link.getAttribute('href');
if (href === '../' || href === '/') continue;
if (link.dataset.iconAdded) continue;
const icon = getFileIcon(href);
const iconSpan = document.createElement('span');
iconSpan.textContent = icon + ' ';
iconSpan.style.cssText = 'font-style: normal; margin-right: 2px;';
link.insertBefore(iconSpan, link.firstChild);
link.dataset.iconAdded = 'true';
}
}
function createToolbar() {
const pre = document.querySelector('pre');
const toolbar = document.createElement('div');
toolbar.id = 'http-index-sorter-toolbar';
toolbar.style.cssText = `
font-family: monospace;
font-size: 13px;
padding: 6px 8px;
margin-bottom: 4px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 3px;
display: flex;
align-items: center;
gap: 4px;
color: #333;
`;
const label = document.createElement('span');
label.textContent = 'Sort:';
label.style.cssText = 'margin-right: 4px; color: #666;';
toolbar.appendChild(label);
let currentSort = loadSort();
const columns = [
{ key: 'name', label: 'Name' },
{ key: 'date', label: 'Date' },
{ key: 'size', label: 'Size' },
];
for (const col of columns) {
const btn = document.createElement('button');
btn.style.cssText = `
font-family: monospace;
font-size: 13px;
padding: 2px 8px;
border: 1px solid #ccc;
border-radius: 3px;
background: #fff;
cursor: pointer;
color: #333;
`;
btn.onmouseenter = () => { btn.style.background = '#e8e8e8'; };
btn.onmouseleave = () => { btn.style.background = '#fff'; };
const textSpan = document.createElement('span');
textSpan.textContent = col.label;
btn.appendChild(textSpan);
const arrow = document.createElement('span');
arrow.className = 'sort-arrow';
arrow.textContent = '';
btn.appendChild(arrow);
let ascending = true;
if (currentSort && currentSort.column === col.key) {
ascending = currentSort.ascending;
}
btn.addEventListener('click', () => {
const parsed = loadSort();
if (parsed && parsed.column === col.key) {
ascending = !parsed.ascending;
} else {
ascending = true;
}
applySort(col.key, ascending);
});
toolbarButtons[col.key] = btn;
toolbar.appendChild(btn);
}
// Separator
const sep = document.createElement('span');
sep.textContent = '|';
sep.style.cssText = 'color: #ccc; margin: 0 2px;';
toolbar.appendChild(sep);
// Group by Date toggle
groupBtn = document.createElement('button');
groupBtn.title = 'Group entries by date';
groupBtn.style.cssText = `
font-family: monospace;
font-size: 13px;
padding: 2px 8px;
border: 1px solid #ccc;
border-radius: 3px;
background: #fff;
cursor: pointer;
color: #333;
`;
function updateGroupBtnLabel() {
groupBtn.textContent = groupByDate ? 'Group: ON' : 'Group: OFF';
groupBtn.style.background = groupByDate ? '#e0ecff' : '#fff';
groupBtn.style.borderColor = groupByDate ? '#8ab4f8' : '#ccc';
}
updateGroupBtnLabel();
groupBtn.onmouseenter = () => { if (!groupByDate) groupBtn.style.background = '#e8e8e8'; };
groupBtn.onmouseleave = () => { groupBtn.style.background = groupByDate ? '#e0ecff' : '#fff'; };
groupBtn.addEventListener('click', () => {
groupByDate = !groupByDate;
saveGroupByDate(groupByDate);
updateGroupBtnLabel();
// When enabling grouping, force sort by date descending
if (groupByDate) {
applySort('date', false);
} else {
const parsed = loadSort();
if (parsed && parsed.column) {
applySort(parsed.column, parsed.ascending);
} else {
applySort('name', true);
}
}
});
toolbar.appendChild(groupBtn);
// Spacer
const spacer = document.createElement('span');
spacer.style.flex = '1';
toolbar.appendChild(spacer);
// Item count
itemCountSpan = document.createElement('span');
itemCountSpan.style.cssText = 'color: #888; font-size: 12px; margin-right: 8px;';
toolbar.appendChild(itemCountSpan);
// Settings gear
const gear = document.createElement('button');
gear.textContent = '\u2699';
gear.title = 'URL pattern settings';
gear.style.cssText = `
font-size: 16px;
padding: 2px 6px;
border: 1px solid #ccc;
border-radius: 3px;
background: #fff;
cursor: pointer;
color: #666;
`;
gear.onmouseenter = () => { gear.style.background = '#e8e8e8'; };
gear.onmouseleave = () => { gear.style.background = '#fff'; };
gear.addEventListener('click', openSettings);
toolbar.appendChild(gear);
pre.parentNode.insertBefore(toolbar, pre);
return currentSort;
}
// --- UI: Settings Panel ---
function openSettings() {
if (document.getElementById('http-index-sorter-settings')) return;
const overlay = document.createElement('div');
overlay.id = 'http-index-sorter-settings';
overlay.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.4); z-index: 10000;
display: flex; align-items: center; justify-content: center;
`;
const panel = document.createElement('div');
panel.style.cssText = `
background: #fff; border-radius: 6px; padding: 20px;
min-width: 420px; max-width: 600px; max-height: 80vh;
font-family: monospace; font-size: 13px; color: #333;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
display: flex; flex-direction: column; gap: 12px;
`;
const title = document.createElement('div');
title.textContent = 'HTTP Index Sorter \u2014 URL Patterns';
title.style.cssText = 'font-size: 15px; font-weight: bold; margin-bottom: 4px;';
panel.appendChild(title);
const desc = document.createElement('div');
desc.textContent = 'The script activates on pages matching these patterns. Use * as a wildcard.';
desc.style.cssText = 'color: #666; margin-bottom: 8px;';
panel.appendChild(desc);
const urls = loadUrls();
const list = document.createElement('div');
list.style.cssText = 'display: flex; flex-direction: column; gap: 6px; max-height: 300px; overflow-y: auto;';
function renderList() {
list.innerHTML = '';
const currentUrls = loadUrls();
for (let i = 0; i < currentUrls.length; i++) {
const row = document.createElement('div');
row.style.cssText = 'display: flex; align-items: center; gap: 6px;';
const input = document.createElement('input');
input.type = 'text';
input.value = currentUrls[i];
input.style.cssText = `
flex: 1; font-family: monospace; font-size: 12px;
padding: 4px 6px; border: 1px solid #ccc; border-radius: 3px;
`;
input.readOnly = true;
row.appendChild(input);
const removeBtn = document.createElement('button');
removeBtn.textContent = '\u2715';
removeBtn.title = 'Remove';
removeBtn.style.cssText = `
padding: 4px 8px; border: 1px solid #ccc; border-radius: 3px;
background: #fff; cursor: pointer; color: #c00; font-weight: bold;
`;
const idx = i;
removeBtn.addEventListener('click', () => {
const u = loadUrls();
u.splice(idx, 1);
saveUrls(u);
renderList();
});
row.appendChild(removeBtn);
list.appendChild(row);
}
}
renderList();
panel.appendChild(list);
// Add current URL button
const addCurrentRow = document.createElement('div');
addCurrentRow.style.cssText = 'display: flex; gap: 6px;';
const addCurrentBtn = document.createElement('button');
addCurrentBtn.textContent = '+ Add Current Page URL';
addCurrentBtn.title = 'Add a pattern matching the current page';
addCurrentBtn.style.cssText = `
flex: 1; padding: 6px 12px; border: 1px solid #5a9e5a; border-radius: 3px;
background: #5a9e5a; color: #fff; cursor: pointer; font-family: monospace;
font-size: 12px;
`;
addCurrentBtn.onmouseenter = () => { addCurrentBtn.style.background = '#4a8e4a'; };
addCurrentBtn.onmouseleave = () => { addCurrentBtn.style.background = '#5a9e5a'; };
addCurrentBtn.addEventListener('click', () => {
// Generate a pattern from the current URL: replace the last path segment with *
const url = location.href.replace(/\/[^/]*$/, '/*');
const u = loadUrls();
if (!u.includes(url)) {
u.push(url);
saveUrls(u);
location.reload();
}
});
addCurrentRow.appendChild(addCurrentBtn);
panel.appendChild(addCurrentRow);
// Add new URL row
const addRow = document.createElement('div');
addRow.style.cssText = 'display: flex; gap: 6px;';
const addInput = document.createElement('input');
addInput.type = 'text';
addInput.placeholder = 'https://example.com/files/*';
addInput.style.cssText = `
flex: 1; font-family: monospace; font-size: 12px;
padding: 4px 6px; border: 1px solid #ccc; border-radius: 3px;
`;
addRow.appendChild(addInput);
const addBtn = document.createElement('button');
addBtn.textContent = 'Add';
addBtn.style.cssText = `
padding: 4px 12px; border: 1px solid #4a90d9; border-radius: 3px;
background: #4a90d9; color: #fff; cursor: pointer; font-family: monospace;
`;
addBtn.addEventListener('click', () => {
const val = addInput.value.trim();
if (!val) return;
const u = loadUrls();
if (!u.includes(val)) {
u.push(val);
saveUrls(u);
}
addInput.value = '';
renderList();
});
addRow.appendChild(addBtn);
panel.appendChild(addRow);
// Close button
const closeRow = document.createElement('div');
closeRow.style.cssText = 'text-align: right; margin-top: 4px;';
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.style.cssText = `
padding: 4px 16px; border: 1px solid #ccc; border-radius: 3px;
background: #f5f5f5; cursor: pointer; font-family: monospace;
`;
closeBtn.addEventListener('click', () => overlay.remove());
closeRow.appendChild(closeBtn);
panel.appendChild(closeRow);
overlay.appendChild(panel);
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
document.body.appendChild(overlay);
}
// --- Menu Command ---
GM_registerMenuCommand('HTTP Index Sorter Settings', openSettings);
// --- Init ---
const urls = loadUrls();
if (!urlMatches(location.href, urls)) return;
if (!isDirectoryListing()) return;
addFileIcons();
groupByDate = loadGroupByDate();
const lastSort = createToolbar();
updateItemCount();
if (groupByDate) {
applySort('date', false);
} else if (lastSort && lastSort.column) {
applySort(lastSort.column, lastSort.ascending);
}
})();