Last active
November 19, 2025 08:21
-
-
Save vdeemann/038d1fc32f0cd86c63c674771460ba22 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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