Skip to content

Instantly share code, notes, and snippets.

@vdeemann
Last active November 19, 2025 08:21
Show Gist options
  • Select an option

  • Save vdeemann/038d1fc32f0cd86c63c674771460ba22 to your computer and use it in GitHub Desktop.

Select an option

Save vdeemann/038d1fc32f0cd86c63c674771460ba22 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name SoundCloud Artist Repost Hider
// @namespace http://tampermonkey.net/
// @version 2.1
// @description Hide reposts from specific artists on SoundCloud
// @author You
// @match https://soundcloud.com/*
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// List of artist usernames whose reposts you want to hide
let blockedArtists = GM_getValue('blockedArtists', []);
// Add control panel to page - embedded in header between Library and Search
function addControlPanel() {
// Wait for header to exist
const checkHeader = setInterval(() => {
// Find the navigation list that contains Home, Feed, Library
const navMenu = document.querySelector('.header__navMenu');
if (navMenu) {
clearInterval(checkHeader);
// Detect dark mode
const isDarkMode = document.body.classList.contains('dark') ||
document.documentElement.classList.contains('dark') ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
// Set colors based on mode
const colors = isDarkMode ? {
buttonBg: '#2a2a2a',
buttonBorder: '#404040',
buttonText: '#e5e5e5',
dropdownBg: '#1a1a1a',
dropdownBorder: '#404040',
inputBg: '#2a2a2a',
inputBorder: '#404040',
inputText: '#e5e5e5',
listItemBg: '#2a2a2a',
listItemBorder: '#404040',
listText: '#e5e5e5',
mutedText: '#999'
} : {
buttonBg: '#f2f2f2',
buttonBorder: '#e5e5e5',
buttonText: '#333',
dropdownBg: 'white',
dropdownBorder: '#e5e5e5',
inputBg: 'white',
inputBorder: '#ccc',
inputText: '#333',
listItemBg: 'white',
listItemBorder: '#e5e5e5',
listText: '#333',
mutedText: '#999'
};
// Create a list item to match the navigation style
const panel = document.createElement('li');
panel.id = 'repost-filter-panel';
panel.style.cssText = 'position: relative; display: inline-flex; align-items: center; margin-top: 3px;';
panel.innerHTML = `
<button id="filter-toggle-button" style="background: ${colors.buttonBg}; border: 1px solid ${colors.buttonBorder}; border-radius: 4px; padding: 8px 10px; cursor: pointer; display: inline-flex; align-items: center; gap: 5px; font-size: 13px; color: ${colors.buttonText}; font-weight: 400; box-sizing: border-box; margin-left: 12px;">
<span style="white-space: nowrap;">🚫 Filter</span>
<div id="toggle-filter-btn" style="width: 14px; height: 14px; display: flex; align-items: center;"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M20.5303 9.53033L12 18.0607L3.46967 9.53033L4.53033 8.46967L12 15.9393L19.4697 8.46967L20.5303 9.53033Z" fill="currentColor"></path></svg></div>
</button>
<div id="filter-dropdown" style="display: none; position: absolute; top: calc(100% + 8px); left: 12px; background: ${colors.dropdownBg}; border: 1px solid ${colors.dropdownBorder}; border-radius: 4px; padding: 12px; width: 280px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 1000;">
<input type="text" id="artist-username-input" placeholder="Enter username to block" style="width: 100%; padding: 8px; margin-bottom: 8px; border-radius: 3px; border: 1px solid ${colors.inputBorder}; background: ${colors.inputBg}; color: ${colors.inputText}; box-sizing: border-box; font-size: 12px;">
<button id="add-artist-btn" style="width: 100%; padding: 8px; background: #f50; color: white; border: none; border-radius: 3px; cursor: pointer; margin-bottom: 12px; font-weight: 600; font-size: 12px;">Block Reposts</button>
<div id="blocked-list" style="max-height: 200px; overflow-y: auto; font-size: 11px;"></div>
</div>
`;
// Append as a new list item at the end of the nav menu
navMenu.appendChild(panel);
updateBlockedList();
document.getElementById('filter-toggle-button').addEventListener('click', togglePanel);
document.getElementById('add-artist-btn').addEventListener('click', addArtist);
document.getElementById('artist-username-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') addArtist();
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
const dropdown = document.getElementById('filter-dropdown');
const button = document.getElementById('filter-toggle-button');
if (dropdown && button && !button.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.style.display = 'none';
}
});
}
}, 500);
// Stop checking after 10 seconds if header not found
setTimeout(() => clearInterval(checkHeader), 10000);
}
function addArtist() {
const input = document.getElementById('artist-username-input');
const username = input.value.trim().toLowerCase().replace(/^\//, '');
if (username && !blockedArtists.includes(username)) {
blockedArtists.push(username);
GM_setValue('blockedArtists', blockedArtists);
input.value = '';
updateBlockedList();
filterReposts();
console.log('Added artist to block list:', username);
}
}
function removeArtist(username) {
blockedArtists = blockedArtists.filter(a => a !== username);
GM_setValue('blockedArtists', blockedArtists);
updateBlockedList();
filterReposts();
console.log('Removed artist from block list:', username);
}
function updateBlockedList() {
const list = document.getElementById('blocked-list');
if (!list) return;
// Detect dark mode
const isDarkMode = document.body.classList.contains('dark') ||
document.documentElement.classList.contains('dark') ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
const colors = isDarkMode ? {
mutedText: '#999',
labelText: '#b3b3b3',
itemBg: '#2a2a2a',
itemBorder: '#404040',
itemText: '#e5e5e5'
} : {
mutedText: '#999',
labelText: '#666',
itemBg: 'white',
itemBorder: '#e5e5e5',
itemText: '#333'
};
if (blockedArtists.length === 0) {
list.innerHTML = `<em style="color: ${colors.mutedText}; font-size: 11px;">No artists blocked yet</em>`;
} else {
list.innerHTML = `
<div style="color: ${colors.labelText}; font-size: 10px; margin-bottom: 5px; font-weight: 600;">
Blocked (${blockedArtists.length}):
</div>
` + blockedArtists.map(artist => `
<div style="display: flex; justify-content: space-between; align-items: center; padding: 5px 6px; background: ${colors.itemBg}; margin-bottom: 3px; border-radius: 3px; border: 1px solid ${colors.itemBorder};">
<span style="color: ${colors.itemText}; font-size: 10px; word-break: break-word; flex: 1; overflow: hidden; text-overflow: ellipsis;">${artist}</span>
<button class="remove-artist" data-artist="${artist}" style="background: #f50; color: white; border: none; padding: 2px 6px; border-radius: 2px; cursor: pointer; font-size: 9px; margin-left: 6px; flex-shrink: 0;">×</button>
</div>
`).join('');
document.querySelectorAll('.remove-artist').forEach(btn => {
btn.addEventListener('click', (e) => {
removeArtist(e.target.dataset.artist);
});
});
}
}
function togglePanel() {
const dropdown = document.getElementById('filter-dropdown');
if (dropdown.style.display === 'none') {
dropdown.style.display = 'block';
} else {
dropdown.style.display = 'none';
}
}
function extractUsername(element) {
// Try multiple methods to extract username
const methods = [
// Method 1: Look for links to user profiles
() => {
const userLink = element.querySelector('a[href^="/"]');
if (userLink) {
const href = userLink.getAttribute('href');
const match = href.match(/^\/([^\/\?]+)/);
if (match) return match[1].toLowerCase();
}
return null;
},
// Method 2: Look for data attributes or class names that might contain username
() => {
const links = element.querySelectorAll('a[href]');
for (const link of links) {
const href = link.getAttribute('href');
if (href && href.startsWith('/') && !href.includes('/tracks/') && !href.includes('/sets/')) {
const parts = href.split('/').filter(p => p);
if (parts.length > 0 && !parts[0].includes('?')) {
return parts[0].toLowerCase();
}
}
}
return null;
},
// Method 3: Look in parent elements
() => {
let parent = element.parentElement;
for (let i = 0; i < 5 && parent; i++) {
const userLink = parent.querySelector('a[href^="/"]');
if (userLink) {
const href = userLink.getAttribute('href');
const match = href.match(/^\/([^\/\?]+)/);
if (match && !match[1].includes('tracks') && !match[1].includes('sets')) {
return match[1].toLowerCase();
}
}
parent = parent.parentElement;
}
return null;
}
];
for (const method of methods) {
const username = method();
if (username) return username;
}
return null;
}
function filterReposts() {
// Only run filter on the feed page
const currentPath = window.location.pathname;
if (currentPath !== '/feed' && currentPath !== '/stream') {
return; // Exit if not on feed/stream page
}
let hiddenCount = 0;
// Look for various possible container classes that might hold stream items
const selectors = [
'li[class*="soundList"]',
'article',
'div[class*="streamItem"]',
'li',
'div[class*="userStream"]'
];
const allItems = new Set();
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(item => allItems.add(item));
});
allItems.forEach(item => {
// Skip if already processed
if (item.dataset.repostFilterProcessed) return;
// Look for repost indicators in the text content
const text = item.textContent || '';
const hasRepostIndicator =
text.includes('reposted') ||
text.includes('Reposted') ||
item.querySelector('[class*="repost"]') ||
item.querySelector('[aria-label*="repost" i]');
if (hasRepostIndicator) {
const username = extractUsername(item);
if (username) {
console.log('Found repost from:', username);
if (blockedArtists.includes(username)) {
item.style.display = 'none';
hiddenCount++;
console.log('Hiding repost from blocked artist:', username);
}
}
item.dataset.repostFilterProcessed = 'true';
}
});
if (hiddenCount > 0) {
console.log(`Hidden ${hiddenCount} reposts from blocked artists`);
}
}
// Initialize
function init() {
console.log('SoundCloud Repost Filter initialized');
console.log('Blocked artists:', blockedArtists);
addControlPanel();
// Initial filter
setTimeout(filterReposts, 1000);
// Watch for new content being added to the page
const observer = new MutationObserver(() => {
filterReposts();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Also run filter periodically in case things are missed
setInterval(filterReposts, 2000);
}
// Wait for page to load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment