Skip to content

Instantly share code, notes, and snippets.

@metacoma
Created January 2, 2026 21:15
Show Gist options
  • Select an option

  • Save metacoma/3bef1c7b98bf025088ed82fb53a99ecf to your computer and use it in GitHub Desktop.

Select an option

Save metacoma/3bef1c7b98bf025088ed82fb53a99ecf to your computer and use it in GitHub Desktop.
twitch clip collect script for violentmonkey
// ==UserScript==
// @name Twitch Clip JSON Collector (Final, URL-based streamer)
// @namespace http://your.namespace/
// @version 1.9
// @description Collect Twitch clips with streamer extracted from URL, clean metadata, numeric views and age in seconds. Ctrl+Shift+L to download JSON file + log to console.
// @author You
// @match *://*/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
function saveJSONToFile(filename, data) {
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function parseViews(viewsText) {
const match = viewsText.replace(/,/g, '').match(/(\d+)/);
return match ? parseInt(match[1], 10) : 0;
}
function parseAgeToSeconds(ageText) {
const match = ageText.match(/(\d+)\s+(second|minute|hour|day|week|month|year)s?/i);
if (!match) return null;
const num = parseInt(match[1], 10);
const unit = match[2].toLowerCase();
const secondsMap = {
second: 1,
minute: 60,
hour: 3600,
day: 86400,
week: 604800,
month: 2592000,
year: 31536000
};
return secondsMap[unit] ? num * secondsMap[unit] : null;
}
function extractStreamerFromURL(url) {
try {
const parts = new URL(url);
const path = parts.pathname.split('/');
return path[1] ? path[1].toLowerCase() : 'unknown';
} catch (e) {
return 'unknown';
}
}
function collectTwitchClipData() {
const articles = document.querySelectorAll('article');
const clips = [];
articles.forEach(article => {
const clipLink = article.querySelector('a[href*="/clip/"]');
const authorLink = article.querySelector('a[data-a-target="preview-card-clip-curator-link"]');
const title = article.querySelector('h4');
const stats = Array.from(article.querySelectorAll('.tw-media-card-stat')).map(e => e.textContent.trim());
if (clipLink) {
const href = clipLink.href.startsWith("http") ? clipLink.href : location.origin + clipLink.getAttribute('href');
const streamer = extractStreamerFromURL(href);
const author = authorLink ? authorLink.textContent.replace(/^Clipped by\s+/i, '').trim().toLowerCase() : "unknown";
const viewsText = stats.find(s => s.includes("views")) || "0";
const ageText = stats.find(s => s.includes("ago")) || "";
//const length = stats.find(s => s.includes(":")) || "";
const length = stats.find(s => /\b[0-9]:[0-9]+\b/.test(s)) || "";
const views = parseViews(viewsText);
const age_seconds = parseAgeToSeconds(ageText);
console.log(title.innerText);
clips.push({
url: href,
streamer: streamer,
title: title.innerText,
views: views,
age_seconds: age_seconds,
length: length,
author: author
});
}
});
if (clips.length === 0) {
console.log('🎬 No Twitch clips found.');
return;
}
saveJSONToFile('clips_list.json', clips);
console.log(`💾 Saved ${clips.length} Twitch clip(s) to "clips_list.json"`);
}
window.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'l') {
collectTwitchClipData();
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment