// ==UserScript==
// @name Kemono Tweaks & Player
// @namespace http://tampermonkey.net/
// @version 3.0
// @description Fetches post title for accuracy, features an expandable title header for long names, and plays .wav/.mp3 files in a feature-rich audio player modal. No need to download WAVs or MP#s to play them anymore.
// @match https://kemono.cr/*
// @author medy17
// @run-at document-start
// @grant GM_xmlhttpRequest
// @license MIT
// ==/UserScript==
(function () {
"use strict";
try {
const style = document.createElement("style");
style.textContent = `
:root {
--c-primary: #3a86ff;
--c-secondary: #007bff;
--brilliant-white: #ffffff;
}
.post-card { position: relative !important; }
.post-card__header { padding: 5px !important; z-index: 1 !important; color: #fff !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: 100% !important; display: block !important; position: relative !important; }
.post-card:hover .post-card__header { white-space: normal !important; overflow: visible !important; background: #2e1905 !important; color: #fff !important; padding: 4px 6px !important; z-index: 9999 !important; position: absolute !important; width: auto !important; max-width: 300px !important; border-radius: 6px !important; }
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 10000; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.3s ease; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
.modal-overlay.show { opacity: 1; }
#audio-player-container { position: relative; z-index: 1; background-color: #1e1e1e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; border-radius: 16px; overflow: hidden; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); border: 1px solid rgba(255, 255, 255, 0.1); width: 90%; max-width: 600px; transform: scale(0.95) translateY(20px); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); padding: 25px; }
.modal-overlay.show #audio-player-container { transform: scale(1) translateY(0); }
#audio-element { display: none; }
#close-audio-btn { position: absolute; top: 1rem; right: 1rem; z-index: 20; background: rgba(24, 24, 27, 0.5); backdrop-filter: blur(4px); border: none; font-size: 1.5rem; color: var(--brilliant-white); cursor: pointer; padding: 0.5rem; border-radius: 50%; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; transition: opacity 0.3s ease, background 0.2s ease, transform 0.2s ease; }
#close-audio-btn:hover { background: linear-gradient(135deg, rgba(245, 169, 184, 0.15), rgba(91, 206, 250, 0.1)), rgba(24, 24, 27, 0.7); background-blend-mode: overlay; color: var(--c-primary); transform: rotate(90deg); }
.audio-title-container { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; margin: 0 40px 20px 0; }
.audio-title { flex: 1; min-width: 0; font-size: 1.3em; font-weight: 500; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.expand-arrow { flex-shrink: 0; font-size: 1.2em; color: #aaa; transition: transform 0.3s ease; }
.audio-title-container.expanded .audio-title { white-space: normal; overflow-wrap: break-word; }
.audio-title-container.expanded .expand-arrow { transform: rotate(180deg); }
.audio-controls-container { color: #fff; user-select: none; }
.audio-controls-container button { background: none; border: none; color: #fff; padding: 0; cursor: pointer; opacity: 0.85; transition: opacity 0.2s, transform 0.2s; }
.audio-controls-container button:hover { opacity: 1; transform: scale(1.1); }
.audio-controls-container button svg { width: 24px; height: 24px; display: block; stroke-width: 2; }
.controls { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0.2rem; }
.controls-left, .controls-right { display: flex; align-items: center; gap: 0.75rem; }
.play-pause-btn .play-icon { display: block; fill: currentColor; }
.play-pause-btn .pause-icon { display: none; fill: currentColor; }
#audio-player-container:not(.paused) .play-pause-btn .play-icon { display: none; }
#audio-player-container:not(.paused) .play-pause-btn .pause-icon { display: block; }
.volume-container { display: flex; align-items: center; }
.volume-container:hover .volume-slider { width: 80px; transform: scaleX(1); }
.volume-slider { width: 0; transform: scaleX(0); transform-origin: left; transition: width 0.2s ease-in-out, transform 0.2s ease-in-out; cursor: pointer; }
.volume-btn svg { stroke-width: 0; fill: currentColor; }
.volume-btn .low-volume-icon, .volume-btn .muted-icon { display: none; }
.time-container { font-size: 0.9rem; min-width: 95px; white-space: nowrap; text-align: center; padding: 0 0.25rem; }
.timeline-container { padding: 0.5rem 0.2rem; cursor: pointer; }
.timeline-container:hover .timeline { height: 8px; }
.timeline-container:hover .timeline .progress-bar::after { transform: translateY(-50%) scale(1); }
.timeline { height: 5px; width: 100%; background-color: rgba(255, 255, 255, 0.3); border-radius: 3px; position: relative; }
.timeline .progress-bar { height: 100%; width: 0%; background: linear-gradient(90deg, var(--c-primary), var(--c-secondary)); border-radius: 3px; position: relative; transition: width 0.1s linear; }
.timeline .progress-bar::after { content: ''; position: absolute; right: -6px; top: 50%; transform: translateY(-50%) scale(0); width: 12px; height: 12px; border-radius: 50%; background-color: var(--brilliant-white); transition: transform 0.2s; }
.timeline .buffered-bar, .timeline .hover-indicator { position: absolute; top: 0; left: 0; height: 100%; width: 0; background-color: rgba(255, 255, 255, 0.5); border-radius: 3px; }
input[type=range].volume-slider { -webkit-appearance: none; appearance: none; background: #fff0; }
input[type=range].volume-slider::-webkit-slider-runnable-track { height: 5px; background: rgba(255, 255, 255, 0.3); border-radius: 3px; }
input[type=range].volume-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -3.5px; background-color: #fff; height: 12px; width: 12px; border-radius: 50%; }
.audio-loader { margin-bottom: 5px; }
.audio-progress-bar { width: 100%; background-color: #333; border-radius: 4px; overflow: hidden; height: 8px; }
.audio-progress-fill { width: 0%; height: 100%; background-image: linear-gradient(45deg, var(--c-secondary), var(--c-primary)); transition: width 0.1s linear; }
.audio-progress-text { font-size: 0.8em; color: #aaa; text-align: center; margin-top: 8px; }
`;
(document.head || document.documentElement).appendChild(style);
} catch (e) {
}
const shim = function () {
try {
const S = String.prototype;
const origSlice = S.slice;
const origConcat = S.concat;
let lastSliceValue = null;
let lastSliceSource = null;
Object.defineProperty(S, "slice", {
configurable: true,
writable: true,
value: function (start, end) {
const src = String(this);
const out = origSlice.call(src, start, end);
if (
start === 0 &&
end === 50 &&
typeof out === "string" &&
src.length > 50
) {
lastSliceValue = out;
lastSliceSource = src;
} else {
lastSliceValue = null;
lastSliceSource = null;
}
return out;
},
});
Object.defineProperty(S, "concat", {
configurable: true,
writable: true,
value: function (...args) {
try {
if (
(this === "" || String(this) === "") &&
args.length === 2 &&
args[1] === "..." &&
typeof args[0] === "string" &&
lastSliceValue !== null &&
args[0] === lastSliceValue
) {
return lastSliceSource;
}
} catch (e) {
}
return origConcat.apply(this, args);
},
});
} catch (e) {
}
};
try {
const s = document.createElement("script");
s.textContent = `(${shim})();`;
(document.head || document.documentElement).appendChild(s);
s.remove();
} catch (e) {
}
const audioPlayer = (() => {
let isInitialized = false;
let modalOverlay,
playerContainer,
audio,
closeBtn,
titleContainer,
titleEl,
loaderContainer,
progressFill,
progressText,
controlsContainer,
playPauseBtn,
volumeBtn,
volumeSlider,
currentTimeEl,
totalTimeEl,
timelineContainer,
progressBar,
bufferedBar,
hoverIndicator,
downloadBtn;
let currentAudioUrl = null;
let currentFileName = "";
let activeRequest = null;
let lastVolume = 1;
function init() {
if (isInitialized) return;
const playerTemplate = `
<div class="modal-overlay" id="audioModal" style="display: none;">
<div id="audio-player-container">
<button id="close-audio-btn" aria-label="Close Audio Player"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/></svg></button>
<div class="audio-title-container">
<span class="expand-arrow">▼</span>
<h3 class="audio-title"></h3>
</div>
<div class="audio-loader">
<div class="audio-progress-bar"><div class="audio-progress-fill"></div></div>
<div class="audio-progress-text">Initializing...</div>
</div>
<audio id="audio-element" preload="metadata"></audio>
<div class="audio-controls-container" style="display:none;">
<div class="timeline-container">
<div class="timeline">
<div class="hover-indicator"></div><div class="buffered-bar"></div><div class="progress-bar"></div>
</div>
</div>
<div class="controls">
<div class="controls-left">
<button class="play-pause-btn" aria-label="Play/Pause">
<svg class="play-icon" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
<svg class="pause-icon" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>
</button>
<div class="volume-container">
<button class="volume-btn" aria-label="Mute/Unmute">
<svg class="high-volume-icon" viewBox="0 0 24 24"><path d="M14,3.23V5.29C16.89,6.15 19,8.83 19,12C19,15.17 16.89,17.84 14,18.7V20.77C18,19.86 21,16.28 21,12C21,7.72 18,4.14 14,3.23M16.5,12C16.5,10.23 15.5,8.71 14,7.97V16C15.5,15.29 16.5,13.76 16.5,12M3,9V15H7L12,20V4L7,9H3Z" /></svg>
<svg class="low-volume-icon" viewBox="0 0 24 24"><path d="M5,9V15H9L14,20V4L9,9M18.5,12C18.5,10.23 17.5,8.71 16,7.97V16C17.5,15.29 18.5,13.76 18.5,12Z" /></svg>
<svg class="muted-icon" viewBox="0 0 24 24"><path d="M12,4L9.91,6.09L12,8.18M4.27,3L3,4.27L7.73,9H3V15H7L12,20V13.27L16.25,17.53C15.58,18.04 14.83,18.46 14,18.7V20.77C15.38,20.45 16.63,19.82 17.68,18.96L19.73,21L21,19.73L12,10.73M19,12C19,12.94 18.8,13.82 18.46,14.64L19.97,16.15C20.62,14.91 21,13.5 21,12C21,7.72 18,4.14 14,3.23V5.29C16.89,6.15 19,8.83 19,12M16.5,12C16.5,12.22 16.47,12.43 16.43,12.64L14,10.21V7.97C15.5,8.71 16.5,10.23 16.5,12Z" /></svg>
</button>
<input class="volume-slider" type="range" min="0" max="1" step="any" value="1">
</div>
<div class="time-container"><span class="current-time">0:00</span> / <span class="total-time">0:00</span></div>
</div>
<div class="controls-right"><button class="download-btn" aria-label="Download Audio"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M3 15v4c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2v-4M17 9l-5 5-5-5M12 12.8V2.5"/></svg></button></div>
</div>
</div>
</div>
</div>
`;
const tempContainer = document.createElement("div");
tempContainer.innerHTML = playerTemplate.trim();
document.body.appendChild(tempContainer.firstChild);
modalOverlay = document.getElementById("audioModal");
playerContainer = document.getElementById("audio-player-container");
audio = document.getElementById("audio-element");
closeBtn = document.getElementById("close-audio-btn");
titleContainer = playerContainer.querySelector(".audio-title-container");
titleEl = titleContainer.querySelector(".audio-title");
loaderContainer = playerContainer.querySelector(".audio-loader");
progressFill = playerContainer.querySelector(".audio-progress-fill");
progressText = playerContainer.querySelector(".audio-progress-text");
controlsContainer = playerContainer.querySelector(
".audio-controls-container",
);
playPauseBtn = playerContainer.querySelector(".play-pause-btn");
volumeBtn = playerContainer.querySelector(".volume-btn");
volumeSlider = playerContainer.querySelector(".volume-slider");
currentTimeEl = playerContainer.querySelector(".current-time");
totalTimeEl = playerContainer.querySelector(".total-time");
timelineContainer = playerContainer.querySelector(
".timeline-container",
);
progressBar = playerContainer.querySelector(".progress-bar");
bufferedBar = playerContainer.querySelector(".buffered-bar");
hoverIndicator = playerContainer.querySelector(".hover-indicator");
downloadBtn = playerContainer.querySelector(".download-btn");
bindEvents();
isInitialized = true;
}
const formatBytes = (bytes, d = 2) =>
bytes === 0
? "0 Bytes"
: `${parseFloat((bytes / Math.pow(1024, Math.floor(Math.log(bytes) / Math.log(1024)))).toFixed(d < 0 ? 0 : d))} ${["Bytes", "KB", "MB", "GB", "TB"][Math.floor(Math.log(bytes) / Math.log(1024))]}`;
function bindEvents() {
closeBtn.addEventListener("click", close);
modalOverlay.addEventListener(
"click",
(e) => e.target === modalOverlay && close(),
);
titleContainer.addEventListener("click", () =>
titleContainer.classList.toggle("expanded"),
);
playPauseBtn.addEventListener("click", togglePlay);
audio.addEventListener("play", () =>
playerContainer.classList.remove("paused"),
);
audio.addEventListener("pause", () =>
playerContainer.classList.add("paused"),
);
audio.addEventListener("loadedmetadata", handleMetadataLoaded);
audio.addEventListener("timeupdate", handleTimeUpdate);
audio.addEventListener("progress", handleBufferUpdate);
audio.addEventListener("volumechange", updateVolumeUI);
volumeBtn.addEventListener("click", toggleMute);
volumeSlider.addEventListener(
"input",
(e) => (audio.volume = e.target.value),
);
downloadBtn.addEventListener("click", downloadAudio);
timelineContainer.addEventListener("mousemove", handleTimelineHover);
timelineContainer.addEventListener("click", handleTimelineSeek);
}
function handleMetadataLoaded() {
playerContainer.classList.add("paused");
totalTimeEl.textContent = formatTime(audio.duration);
audio.volume = volumeSlider.value;
updateVolumeUI();
audio.play().catch((e) => console.error("Autoplay prevented:", e));
}
function togglePlay() {
audio.paused ? audio.play() : audio.pause();
}
function toggleMute() {
audio.volume > 0
? ((lastVolume = audio.volume), (audio.volume = 0))
: (audio.volume = lastVolume);
}
function handleTimeUpdate() {
currentTimeEl.textContent = formatTime(audio.currentTime);
progressBar.style.width = `${(audio.currentTime / audio.duration) * 100}%`;
}
function handleBufferUpdate() {
if (audio.duration > 0)
for (let i = 0; i < audio.buffered.length; i++)
if (
audio.buffered.start(i) <= audio.currentTime &&
audio.currentTime <= audio.buffered.end(i)
) {
bufferedBar.style.width = `${(audio.buffered.end(i) / audio.duration) * 100}%`;
break;
}
}
function handleTimelineHover(e) {
const rect = timelineContainer.getBoundingClientRect();
hoverIndicator.style.width = `${(Math.min(Math.max(0, e.x - rect.x), rect.width) / rect.width) * 100}%`;
}
function handleTimelineSeek(e) {
const rect = timelineContainer.getBoundingClientRect();
audio.currentTime =
(Math.min(Math.max(0, e.x - rect.x), rect.width) / rect.width) *
audio.duration;
}
function updateVolumeUI() {
volumeSlider.value = audio.volume;
const icons = [
".high-volume-icon",
".low-volume-icon",
".muted-icon",
].map((s) => volumeBtn.querySelector(s));
icons.forEach((i) => (i.style.display = "none"));
if (audio.volume === 0 || audio.muted) icons[2].style.display = "block";
else if (audio.volume < 0.5) icons[1].style.display = "block";
else icons[0].style.display = "block";
}
function formatTime(t) {
const r = new Date(t * 1000).toISOString().substr(11, 8);
return r.startsWith("00:") ? r.substr(3) : r;
}
function downloadAudio() {
if (!audio.src) return;
const link = document.createElement("a");
link.href = audio.src;
link.download = currentFileName || "kemono-audio";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function handleKeyboardShortcuts(e) {
if (document.activeElement.tagName.toLowerCase() === "input") return;
switch (e.key.toLowerCase()) {
case "escape":
close();
break;
case " ":
if (document.activeElement.tagName.toLowerCase() !== "button") {
e.preventDefault();
togglePlay();
}
break;
case "m":
toggleMute();
break;
case "arrowright":
audio.currentTime = Math.min(
audio.duration,
audio.currentTime + 5,
);
break;
case "arrowleft":
audio.currentTime = Math.max(0, audio.currentTime - 5);
break;
}
}
function open(url, fileName) {
if (currentAudioUrl) URL.revokeObjectURL(currentAudioUrl);
currentFileName = fileName;
titleContainer.classList.remove("expanded");
titleEl.textContent = fileName;
loaderContainer.style.display = "block";
controlsContainer.style.display = "none";
progressFill.style.width = "0%";
progressText.textContent = "Initializing...";
modalOverlay.style.display = "flex";
setTimeout(() => modalOverlay.classList.add("show"), 10);
document.body.style.overflow = "hidden";
document.addEventListener("keydown", handleKeyboardShortcuts);
activeRequest = GM_xmlhttpRequest({
method: "GET",
url,
responseType: "blob",
onprogress: (p) => {
if (p.lengthComputable) {
const percent = Math.round((p.loaded / p.total) * 100);
progressFill.style.width = `${percent}%`;
progressText.textContent = `Downloading... ${percent}% (${formatBytes(p.loaded)} / ${formatBytes(p.total)})`;
}
},
onload: (res) => {
activeRequest = null;
currentAudioUrl = URL.createObjectURL(res.response);
audio.src = currentAudioUrl;
audio.load();
loaderContainer.style.display = "none";
controlsContainer.style.display = "block";
},
onerror: () => {
activeRequest = null;
progressText.textContent = "Error: Could not load audio file.";
},
onabort: () => (activeRequest = null),
});
}
function close() {
if (activeRequest) activeRequest.abort();
audio.pause();
modalOverlay.classList.remove("show");
setTimeout(() => {
modalOverlay.style.display = "none";
if (currentAudioUrl) {
URL.revokeObjectURL(currentAudioUrl);
currentAudioUrl = null;
currentFileName = "";
audio.removeAttribute("src");
audio.load();
}
}, 300);
document.body.style.overflow = "";
document.removeEventListener("keydown", handleKeyboardShortcuts);
}
return { init, open };
})();
function initializeScript() {
audioPlayer.init();
document.body.addEventListener("click", (e) => {
const link = e.target.closest(
'a[href*=".wav?f="], a[href*=".mp3?f="]',
);
if (link) {
e.preventDefault();
e.stopPropagation();
let title = "Audio Player";
const titleElement = document.querySelector("h1.post__title");
if (titleElement) {
title = titleElement.textContent.trim();
}
else {
try {
const urlParams = new URLSearchParams(link.search);
title = decodeURIComponent(urlParams.get("f")) || title;
} catch {
}
}
audioPlayer.open(link.href, title);
}
});
}
if (document.body) {
initializeScript();
} else {
new MutationObserver((mutations, observer) => {
if (document.body) {
initializeScript();
observer.disconnect();
}
}).observe(document.documentElement, { childList: true });
}
})();