Qobuz Linkifier

make templates copy links to clipboard

目前為 2025-05-03 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Qobuz Linkifier
// @version      0.1.1
// @description  make templates copy links to clipboard
// @author       You
// @match        https://www.qobuz.com/*/shop
// @match        https://www.qobuz.com/*/shop/*/*
// @match        https://www.qobuz.com/*/search*
// @match        https://www.qobuz.com/*/label/*/*/*
// @match        https://www.qobuz.com/*/interpreter/*/*
// @match        https://www.qobuz.com/*/album/*/*
// @match        https://www.qobuz.com/*/playlists/*/*
// @match        https://www.qobuz.com/*/genre/*/*
// @icon         https://www.qobuz.com/favicon.ico
// @grant        none
// @namespace https://greasyfork.org/users/1465219
// ==/UserScript==

(function () {
    'use strict';

    const BASE_URL = 'https://play.qobuz.com/',
        LOAD_MORE_TRACKS_DELAY = 500,
        COPY_MESSAGE_DISPLAY_DURATION = 1500,
        DEFAULT_LOC = { lang: 'en', country: '_' };

    addCustomStyle(`
        .store-wallpaper,
        .album-addtocart,
        .player__ad,
        .store-cart,
        .shop-cart,
        .price-box .price,
        .on-sale {
            display: none !important;
        }

        #store-search {
            padding-right: 0 !important;
            border-right: none !important
        }

        .shop-search {
            margin-right: 16px !important;
        }

        .track__item--button span.pct {
            position: absolute !important;
            top: 8px !important;
            left: 5px !important;
        }

        .catalog-heading__button span.pct {
            margin-right: 18px;
            font-size: 12px;
            font-weight: 700;
        }

        .product__button span.pct {
            left: 11px;
            position: absolute;
        }

        .player__webplayer span.pct {
            margin-right: 8px;
        }

        .player__webplayer span:last-child {
            padding-top: 2px;
        }

        .no-wrap {
            white-space: nowrap !important;
        }

        .color-white {
            color: #FFF !important;
        }
    `);

    const templates = infuseTemplates(window.location.pathname.split('/')[1].split('-').reverse(), {
        en: {
            _: {
                label: 'label',
                artist: 'artist',
                album: 'album',
                playlist: 'playlist',
                track: 'track',
                copyLink: function (type) {
                    return type ? `Copy ${this[type]} link` : 'Copy link'
                },
                copyLinkWithHighlight: function (type) {
                    return `<span class="color-white">Copy</span> ${this[type]} link`
                },
                linkCopied: function (type) {
                    return type ? `${this[type].toTitleCase()} link copied!` : 'Link copied!'
                },
                linkCopiedWithHighlight: function (type) {
                    return `<span class="color-white">${this[type].toTitleCase()} link</span> copied!`
                },
            },
            get uk() { return this._ },
            get ie() { return this._ },
            get us() { return this._ },
            get au() { return this._ },
            get ca() { return this._ },
            get nz() { return this._ },
            get dk() { return this._ },
            get fi() { return this._ },
            get se() { return this._ },
            get no() { return this._ }
        },
        de: {
            _: {
                label: 'Verlag',
                artist: 'Interpret',
                album: 'Album',
                playlist: 'Wiedergabeliste',
                track: 'Titel',
                copyLink: function (type) { return type ? `${this[type]}-Link kopieren` : 'Link kopieren' },
                copyLinkWithHighlight: function (type) {
                    return `<span class="color-white">${this[type]}-Link</span> kopieren`
                },
                linkCopied: function (type) {
                    return type ? `${this[type]}-Link kopiert!` : 'Link kopiert!'
                },
                linkCopiedWithHighlight: function (type) {
                    return `<span class="color-white">${this[type]}-Link</span> kopiert!`
                },
            },
            get de() { return this._ },
            get at() { return this._ },
            get ch() { return this._ },
            get lu() { return this._ }
        },
        /*         es: {
                    _: {
        
                    },
                    get es() { return this._ },
                    get mx() { return this._ },
                    get ar() { return this._ },
                    get cl() { return this._ },
                    get co() { return this._ }
                },
                pt: {
                    get pt() { return this._ },
                    get br() { return this._ }
                },
                nl: {
                    get nl() { return this._ },
                    get be() { return this._ }
                },
                fr: {
                    get fr() { return this._ },
                    get ch() { return this._ },
                    get lu() { return this._ },
                    get be() { return this._ },
                    get ca() { return this._ }
                },
                it: {
                    get it() { return this._ }
                } */
    }, (data) => ({
        track: {
            content: () => `
                <span class="pct pct pct-edit"></span>
                <span class="no-wrap">${data.copyLink()}</span>
            `,
            message: () => `
                <span class="pct pct pct-checkbox"></span>
                <span class="no-wrap">${data.linkCopied()}</span>
            `
        },
        albumGrid: {
            content: () => `
                <span class="pct pct pct-edit"></span>
                <span class="product__button--highlight">
                    ${data.copyLinkWithHighlight('album')}
                </span>
            `,
            message: () => `
                <span class="pct pct pct-checkbox"></span>
                <span class="product__button--highlight">
                    ${data.linkCopiedWithHighlight('album')}</span>
                </span>
            `
        },
        search: {
            content: () => `
                <span class="no-wrap">${data.copyLink('album')}</span>
            `,
            message: () => `
                <span class="no-wrap">${data.linkCopied('album')}</span>
            `
        },
        main: {
            content: (type) => `
                <span class="pct pct pct-edit"></span>
                <span class="no-wrap">${data.copyLink(type)}</span>
            `,
            message: (type) => `
                <span class="pct pct pct-checkbox"></span>
                <span class="no-wrap">${data.linkCopied(type)}</span>
            `
        }
    }));

    templates["main"].content("album")

    const selectors = {
        main: '.catalog-heading__button, .player__webplayer',
        search: '.btn__qobuz.btn__qobuz--see-album',
        album: '.product__button.add_to_cart',
        track: '.track__item.track__item--button',
        loadMore: '.player-more'
    }

    const mainButton = document.querySelector(selectors.main);

    if (mainButton) {
        const [type, id] = mainButton.attributes.href.value.split('/').slice(-2);

        replaceButton(mainButton, id, type, 'main');

        document.querySelectorAll(selectors.track).forEach((el, i) => {
            if (el.classList.contains('track__unavailable')) return;

            const trackButton = replaceButton(
                el,
                el.dataset.url.split('/').slice(-1)[0],
                'track'
            );

            trackButton.addEventListener('dblclick', ev => { ev.preventDefault(); ev.stopPropagation(); })
        });
    }

    document.querySelectorAll(selectors.album).forEach((el, i) => {
        replaceButton(
            el,
            el.dataset.url.split('/').slice(-1)[0],
            'album',
            'albumGrid'
        );
    });

    document.querySelectorAll(selectors.search).forEach((el, i) => {
        replaceButton(
            el,
            el.attributes.href.value.split('/').slice(-1)[0],
            'album',
            'search'
        );
    });

    const loadMore = document.querySelector(selectors.loadMore)

    if (loadMore) {
        loadMore.addEventListener('click', ev => {
            setTimeout(() => {
                document.querySelectorAll(selectors.track).forEach((el, i) => {
                    if (el.classList.contains('track__unavailable')) return;

                    const trackButton = replaceButton(
                        el,
                        el.dataset.url.split('/').slice(-1)[0],
                        'track'
                    );

                    trackButton.addEventListener('dblclick', ev => { ev.preventDefault(); ev.stopPropagation(); })
                });
            }, LOAD_MORE_TRACKS_DELAY);
        })
    }

    function replaceButton(button, id, type, contentType = type) {
        let timeout;

        const content = templates[contentType].content(type),
            url = BASE_URL + `${type}/${id}`,
            newButton = button.cloneNode(true);

        newButton.setAttribute('title', url);
        newButton.setAttribute('href', url);
        newButton.innerHTML = content;

        button.replaceWith(newButton);
        button.remove();

        let wasCopied = false;

        newButton.addEventListener('click', ev => {
            ev.preventDefault();

            navigator.clipboard.writeText(url);

            if (wasCopied) {
                clearTimeout(timeout);
            }
            else {
                newButton.innerHTML = templates[contentType].message(type);
                wasCopied = true;
            }

            timeout = window.setTimeout(
                () => {
                    newButton.innerHTML = content;
                    wasCopied = false;
                },
                COPY_MESSAGE_DISPLAY_DURATION
            );

            ev.stopPropagation();

            return false;
        });

        return newButton;
    }

    function addCustomStyle(style) {
        document.body.append(
            document.createElement('style')
                .appendChild(
                    document.createTextNode(style)
                )
                .parentNode
        );
    }

    function infuseTemplates([country, lang], strings, templates) {
        return templates(strings[country]?.[lang] || strings[DEFAULT_LOC.lang][DEFAULT_LOC.country]);
    }

    Object.defineProperty(String.prototype, "toTitleCase", {
        value: function () {
            return this[0].toUpperCase() + this.slice(1);
        },
        writable: true,
        configurable: true,
    });
})();