SoundCloud Media Feed Tracker

Track titles and artists of songs played on your soundcloud feed, and shows links as played, with exportSongs() feature.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         SoundCloud Media Feed Tracker
// @version      2.1.2
// @author       LucasTavaresA
// @license      GPL-3.0-or-later
// @namespace    https://gist.github.com/LucasTavaresA/51b9a4b36dd7070f96abddf7948dae94
// @description  Track titles and artists of songs played on your soundcloud feed, and shows links as played, with exportSongs() feature.
// @grant        unsafeWindow
// @match        https://soundcloud.com/feed
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    const STORAGE_KEY = 'soundcloud_track_history';
    const MARK_CLASS = 'sc-played-track';

    let lastTrackUrl = null;
    let trackHistory = [];
    let playedUrlsSet = new Set();

    function normalizeUrl(url) {
        try {
            const urlObj = new URL(url);
            return urlObj.origin + urlObj.pathname;
        } catch (e) {
            console.error('Error normalizing URL:', e);
            return url;
        }
    }

    function exportSongs() {
        const tracks = JSON.stringify(trackHistory, null, 2);
        const blob = new Blob([tracks], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'sc-tracks.json';

        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);

        URL.revokeObjectURL(url);
    }

    function loadHistory() {
        try {
            const saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                trackHistory = JSON.parse(saved);
                playedUrlsSet = new Set(trackHistory.map(t => normalizeUrl(t.url)));
                console.log(`📚 Loaded ${trackHistory.length} tracks from history`);
            }
        } catch (e) {
            console.error('Error loading history:', e);
            trackHistory = [];
            playedUrlsSet = new Set();
        }
    }

    function saveHistory() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(trackHistory));
        } catch (e) {
            console.error('Error saving history:', e);
        }
    }

    function markPlayedTracks() {
        const feedContainer = document.querySelector('.lazyLoadingList__list');
        if (!feedContainer) return;

        const links = feedContainer.querySelectorAll('a[href*="/"]');

        links.forEach(link => {
            const normalizedUrl = normalizeUrl(link.href);

            if (playedUrlsSet.has(normalizedUrl)) {
                link.classList.add(MARK_CLASS);
            }
        });
    }

    function getTrackInfo() {
        const titleLink = document.querySelector('.playbackSoundBadge__titleLink');
        const artistLink = document.querySelector('.playbackSoundBadge__lightLink');

        if (!titleLink) return null;

        return {
            title: titleLink.getAttribute('title') || titleLink.textContent.trim(),
            artist: artistLink ? artistLink.textContent.trim() : 'Unknown',
            url: normalizeUrl(titleLink.href),
            timestamp: new Date().toISOString()
        };
    }

    function trackChanged() {
        const info = getTrackInfo();

        if (!info) return;

        if (info.url !== lastTrackUrl) {
            lastTrackUrl = info.url;

            if (!playedUrlsSet.has(info.url)) {
                trackHistory.push(info);
                playedUrlsSet.add(info.url);
                saveHistory();
                markPlayedTracks();
            }
        }
    }

    function setupTracker() {
        const playbackBar = document.querySelector('.playControls__soundBadge');

        if (!playbackBar) {
            setTimeout(setupTracker, 1000);
            return;
        }

        loadHistory();
        markPlayedTracks();

        const observer = new MutationObserver(() => {
            trackChanged();
            markPlayedTracks();
        });

        observer.observe(playbackBar, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['href', 'title']
        });

        const feedObserver = new MutationObserver(markPlayedTracks);
        const feed = document.querySelector('.lazyLoadingList__list') || document.body;

        feedObserver.observe(feed, {
            childList: true,
            subtree: true
        });
    }

    function topArtists(n) {
        const counts = {};

        document.querySelectorAll('.soundContext__usernameLink').forEach(el => {
            const name = el.textContent.trim();
            counts[name] = (counts[name] || 0) + 1;
        });

        const topN = Object.entries(counts)
            .sort((a, b) => b[1] - a[1])
            .slice(0, n);

        console.table(topN.map(([name, count]) => ({ name, count })));
    }

    if (window.location.href === "https://soundcloud.com/feed") {
        const style = document.createElement('style');
        style.textContent = `
            a.${MARK_CLASS} {
                color: #f70 !important;
                text-decoration: underline !important;
            }
        `;
        document.head.appendChild(style);

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', setupTracker);
        } else {
            setupTracker();
        }

        unsafeWindow.exportSongs = exportSongs;
        unsafeWindow.topArtists = topArtists;
    }
})();