Holodex for YouTube

A userscript that adds a player in your YouTube page when current video has valid timeline on Holodex.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Holodex for YouTube
// @description  A userscript that adds a player in your YouTube page when current video has valid timeline on Holodex.
// @author       Allen Wei
// @namespace    https://github.com/Alllen95Wei
// @license      MIT
// @match        https://www.youtube.com/*
// @match        https://holodex.net/*
// @version      1.0.3
// @grant        GM_getValues
// @grant        GM_setValues
// ==/UserScript==


let HOLODEX_API_KEY = GM_getValues({holodex_api_key: null}).holodex_api_key
const STYLE_TEXT = `
/*
 * Below is the stylesheet injected by "Holodex for YouTube" and is for elements created by the userscript.
 * Don't worry; other elements shouldn't be affected!
 */

table, th, td {
    border: 1px solid #000000;
    border-collapse: collapse;
}

th {
    background-color: #f06292;
}

td {
    background-color: #9cc9fd;
}

button:hover {
    cursor: pointer;
}

#song-list-btn {
    border: none;
    background-image: linear-gradient(to bottom right, #5da2f2, #f06292);
}

#song-list-btn:hover {
    background-image: linear-gradient(to bottom right, #7bb1f3, #ed82a8);
}

#song-list-div {
    font-size: small;
    white-space: pre-line;
}

#song-list-table-div {
    max-height: 75vh;
    margin: 0.5vh;
    overflow: auto;
    border-radius: 12px;
}

#autoplay-div {
    width: 100%;
    background-color: white;
}

#autoplay-switch-label {
    margin-left: 5px;
}

#song-info-div {
    width: 100%;
    overflow: auto;
    display: flex;
    align-items: center;
}

#hd-thumbnail {
    margin: 5px;
    width: 80px;
    height: 80px;
    border-radius: 10px;
    box-shadow: 0 0 5px 0 #A5A5A5;
    transition: width 0.1s linear, height 0.1s linear;
}

#hd-thumbnail:hover {
    width: 110px;
    height: 110px;
    transition: width 0.1s linear, height 0.1s linear;
}

#song-detail-div {
    display: inline-grid;
}

#song-info-name {
    font-size: large;
    display: inline-block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-right: 5px;
    margin-bottom: 0.05em;
}

#song-info-artist {
    font-size: small;
    display: inline-block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-right: 5px;
    margin-bottom: 0.1em;
}

#autoplay-div {
    position: sticky;
    top: 0;
}

#control-div {
    display: flex;
    align-items: center;
    justify-content: center;
}

#progress-div {
    width: 100%;
    display: flex;
}

#progress-bar {
    -webkit-appearance: none;
    width: 80%;
    display: flex;
    appearance: none;
    background: transparent;
    cursor: pointer;
}

#progress-bar:focus {
    outline: none;
}

/******** Chrome, Safari, Opera and Edge Chromium styles ********/
/* slider track */
#progress-bar::-webkit-slider-runnable-track {
    background-image: linear-gradient(to right, #81b7f3, #ef86aa);
    border-radius: 0.5rem;
    height: 0.25rem;
}

/* slider thumb */
#progress-bar::-webkit-slider-thumb {
    -webkit-appearance: none; /* Override default look */
    appearance: none;
    margin-top: -4px;  /* Centers thumb on the track */
    background-color: #808080;
    border-radius: 0.1rem;
    height: 1.2rem;
    width: 0.3rem;
}

#progress-bar:focus::-webkit-slider-thumb {
    outline: 3px solid #808080;
    outline-offset: 0.125rem;
}

/*********** Firefox styles ***********/
/* slider track */
#progress-bar::-moz-range-track {
    background-image: linear-gradient(to right, #81b7f3, #ef86aa);
    border-radius: 0.5rem;
    height: 0.25rem;
}

/* slider thumb */
#progress-bar::-moz-range-thumb {
    background-color: #808080;
    border: none; /*Removes extra border that FF applies*/
    border-radius: 0.1rem;
    height: 1rem;
    width: 0.2rem;
}

#progress-bar:focus::-moz-range-thumb{
    outline: 3px solid #808080;
    outline-offset: 0.125rem;
}

.song-name-div {
    height: 40px;
    max-height: 40px;
    overflow: auto;
    align-content: center;
}

.song-artist {
    font-size: 10px;
    font-style: italic;
}

.hd-button {
    border: none;
    width: 100%;
    height: auto;
    padding: 5px;
    transition-duration: 0.15s;
}

.hd-button:hover {
    background-color: #b6b6b6;
}

.play-button {
    border: none;
    width: 40px;
    height: 40px;
    background-size: cover;
    background-color: #7f7f7f;
}

.progress-span {
    width: 10%;
    display: flex;
    justify-content: center;
}

.control-buttons {
    border: none;
    margin: 2px 4px;
    width: 3.5vw;
    background-color: darkgray;
}

/* ============= Google Material Symbols Stylesheet ============= */
.material-symbols-rounded {
  font-variation-settings:
  'FILL' 1,
  'wght' 400,
  'GRAD' 200,
  'opsz' 24
}
`
const DEFAULT_ART_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAGHaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49J++7vycgaWQ9J1c1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCc/Pg0KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyI+PHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj48cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0idXVpZDpmYWY1YmRkNS1iYTNkLTExZGEtYWQzMS1kMzNkNzUxODJmMWIiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIj48dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPjwvcmRmOkRlc2NyaXB0aW9uPjwvcmRmOlJERj48L3g6eG1wbWV0YT4NCjw/eHBhY2tldCBlbmQ9J3cnPz4slJgLAAABAElEQVR4Xu3RoQHAIBDAwNLFf3TwLEDEnYzNmpn9kfHfgbcMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEkxpAYQ2IMiTEk5gAHmwNEhZDUhAAAAABJRU5ErkJggg=="
const GOOGLE_ICONS_URL = [
    // "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@40,500,1,200&icon_names=skip_previous",
    // "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@40,500,1,200&icon_names=play_arrow",
    // "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@40,500,1,200&icon_names=skip_next"
    "https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,[email protected],100..700,0..1,-50..20"
]

const escapeHTMLPolicy = trustedTypes.createPolicy("forceInner", {createHTML: (to_escape) => to_escape})
let UPDATE_TRY = 0
let LAST_SONG_ID = ""
let LAST_SONG_INDEX = 0
let IS_RANDOM = false

/*
* 0 = no repeat \
* 1 = repeat one \
* 2 = repeat all
*/
let REPEAT_MODE = 0
let QUEUE = []
let SONG_INDEX = []


// Utils

function secondsToFormattedString(seconds) {
    return `${Math.floor(seconds / 60)}:${seconds % 60 < 10 ? "0" : ""}${Math.floor(seconds % 60)}`
}

function compareSongSequence(a, b) {
    if (a["start"] > b["start"]) {
        return 1
    }
    if (a["start"] < b["start"]) {
        return -1
    }
    return 0
}

function shuffle(array) {
    for (let i = array.length - 1; i > 0; i--) {
        let j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
}

function autoplay(videoPlayer, updateNow, goNext, goPrevious) {
    UPDATE_TRY++
    // check every 4 triggers (see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/timeupdate_event)
    if (UPDATE_TRY >= 4 || updateNow) {
        UPDATE_TRY = 0
        const currentTime = videoPlayer.currentTime
        let nextSong = null
        for (const [start, end, i] of SONG_INDEX) {
            if (start <= currentTime && currentTime <= end) {  // now playing
                const nowPlaying = QUEUE[i]
                console.log(`Now playing: ${nowPlaying["name"]}, by ${nowPlaying["original_artist"]}`)
                LAST_SONG_INDEX = i
                if (goNext) {
                    nextSong = QUEUE[i + 1] || null
                } else if (goPrevious) {
                    nextSong = QUEUE[i - 1] || null
                } else {
                    nextSong = "PLAYING"
                }
                break
            } else if (!IS_RANDOM && start > currentTime) {  // finds the next song only when "shuffle" is not enabled
                // if (nextSong === songList[i]) {  // the next song is confirmed and will NOT change
                //     break
                // }
                nextSong = QUEUE[i]
                break
            } else if (IS_RANDOM) {  // the playlist is random and no song is playing
                nextSong = QUEUE[LAST_SONG_INDEX + 1] || null
                break
            }
        }
        if (nextSong !== null && nextSong !== "PLAYING") {
            console.log("Next song:", nextSong)
            console.log(`Seeking to ${nextSong["start"]}`)
            videoPlayer.currentTime = nextSong["start"]
        } else if (nextSong !== "PLAYING") {
            console.log(nextSong)
            console.log("The last song in the list has finished, pausing the player")
            videoPlayer.pause()
        }
    }
}

function getNowPlayingSong(videoPlayer) {
    const currentTime = videoPlayer.currentTime
    let i = 0
    for (const song of QUEUE) {
        if (song.start <= currentTime && currentTime <= song.end) {
            return [song, i]
        }
        i++
    }
    return [{
        "id": null,
        "name": "未在播放任何歌曲",
        "original_artist": "啟用自動播放以使用播放器功能",
        "art": DEFAULT_ART_BASE64,
        "start": 0,
        "end": 0
    }, -1]
}

function updateNowPlayingInfo(videoPlayer, forceUpdate) {
    const [nowSong, index] = getNowPlayingSong(videoPlayer)
    const currentTime = videoPlayer.currentTime
    const progressBar = document.getElementById("progress-bar")
    if (nowSong.id !== LAST_SONG_ID || forceUpdate) {
        // Update song info
        document.getElementById("hd-thumbnail").src = (nowSong.art || DEFAULT_ART_BASE64).replace("100x100", "500x500")
        document.getElementById("song-info-name").textContent = nowSong.name
        document.getElementById("song-info-name").title = nowSong.name
        document.getElementById("song-info-artist").textContent = nowSong.original_artist
        document.getElementById("song-info-artist").title = nowSong.original_artist
        // Update time length of the song
        const duration = nowSong.end - nowSong.start
        document.getElementById("progress-duration").textContent = secondsToFormattedString(duration)
        LAST_SONG_ID = nowSong.id
        const controlBtns = document.getElementsByClassName("control-buttons")
        if (nowSong.id === null) {
            for (const btn of controlBtns) {
                btn.disabled = true
            }
            progressBar.disabled = true
        } else {
            for (const btn of controlBtns) {
                btn.disabled = false
            }
            progressBar.disabled = false
            // Disable skip buttons on the first/last one
            if (index === QUEUE.length - 1) {
                document.getElementById("skip-next-btn").disabled = true
            }
            if (index === 0) {
                document.getElementById("skip-previous-btn").disabled = true
            }
        }
    }
    // Update progress bar
    if (nowSong.id === null) {
        progressBar.min = 0
        progressBar.max = 1
        progressBar.value = 0
        document.getElementById("progress-nowtime").textContent = "0:00"
        document.getElementById("progress-nowtime").title = "-0:00"
    } else {
        progressBar.min = `${nowSong.start}`
        progressBar.max = `${nowSong.end}`
        progressBar.value = currentTime
        document.getElementById("progress-nowtime").textContent = secondsToFormattedString(currentTime - nowSong.start)
        document.getElementById("progress-nowtime").title = `-${secondsToFormattedString(nowSong.end - currentTime)}`
    }
}

function renderQueue(songListTable, videoPlayer) {
    const tableLength = songListTable.rows.length
    for (let i = 0; i < (tableLength - 1); i++) {
        songListTable.deleteRow(-1)
    }
    let index = 1
    for (const song of QUEUE) {
        const row = songListTable.insertRow()
        // Song no.
        const songNoCell = row.insertCell()
        songNoCell.style.textAlign = "center"
        songNoCell.textContent = `${index}`
        // Song name
        const songNameCell = row.insertCell()
        songNameCell.style.paddingLeft = "0.3vw"
        const songNameDiv = document.createElement("div")
        songNameDiv.className = "song-name-div"
        const songName = document.createElement("p")
        songName.className = "song-name"
        songName.textContent = song["name"]
        const songArtist = document.createElement("p")
        songArtist.className = "song-artist"
        songArtist.textContent = song["original_artist"]
        songNameDiv.append(songName, songArtist)
        songNameCell.append(songNameDiv)
        // Song duration
        const songDurationCell = row.insertCell()
        songDurationCell.style.textAlign = "center"
        songDurationCell.textContent = secondsToFormattedString(song["end"] - song["start"])
        // Play button
        const playButton = document.createElement("button")
        playButton.id = `play-button-${index}`
        playButton.className = "play-button"
        playButton.textContent = "▶️"
        playButton.style.backgroundImage = `url(${song["art"]})`
        // Left click: seek to the start of the song
        playButton.addEventListener("click", () => {
            videoPlayer.currentTime = song["start"]
        })
        // Right click: open in Musicdex
        playButton.addEventListener("contextmenu", () => {
            window.open(`https://music.holodex.net/song/${song["id"]}`, "_blank")
        })
        row.insertCell().appendChild(playButton)
        index++
    }
}

function generateQueue(songListTable, videoPlayer, start, isRandom) {
    if (isRandom) {
        QUEUE = shuffle(QUEUE)
    } else {
        QUEUE = QUEUE.sort(compareSongSequence)
    }
    console.log("New queue:", QUEUE)
    renderQueue(songListTable, videoPlayer)
    let index = 0
    SONG_INDEX = []
    for (const song of QUEUE) {
        // Append to index
        SONG_INDEX.push([song["start"], song["end"], index])
        index++
    }
}


// Main function
function main() {
    // Try to remove previous ones
    LAST_SONG_ID = ""
    LAST_SONG_INDEX = 0
    IS_RANDOM = false
    QUEUE = []
    SONG_INDEX = []
    try {
        document.getElementById("song-list-div").remove()
    } catch (e) {}  // Ignore error when nothing found
    const vId = new URLSearchParams(window.location.search).get("v")
    if (vId === null) {
        console.log("No video ID found, not a video page.")
        return
    }
    let data = {}
    if (HOLODEX_API_KEY === null || HOLODEX_API_KEY === undefined) {
        console.warn("No API key found. Visit https://holodex.net/ and login to update your API key.")
        return
    } else {
        console.log(`Holodex API key found: ${HOLODEX_API_KEY.slice(0, 9)}****-****-****-************`)
    }
    fetch(`https://holodex.net/api/v2/videos/${vId}`, {headers: {"X-APIKEY": HOLODEX_API_KEY}})
        .then((response) => {
            console.log("API responded with status code: ", response.status)
            return response.json()
        })
        .then(response => {
            data = response
            if (!("songs" in data)) {
                console.log("No songs found.")
                return
            }
            // Get song list
            QUEUE = data.songs
            console.log(QUEUE)
            // Inject CSS
            const stylesheet = document.createElement("style");
            stylesheet.textContent = STYLE_TEXT;
            document.head.append(stylesheet)
            for (const url of GOOGLE_ICONS_URL) {
                const googleStylesheet = document.createElement("link")
                googleStylesheet.rel = "stylesheet"
                googleStylesheet.href = url
                document.head.append(googleStylesheet)
            }
            // Fetch YouTube elements
            const minorColumn = document.getElementById("secondary-inner")
            const videoPlayer = document.getElementsByTagName("video")[0]
            // Create song list div
            const songListDiv = document.createElement("div")
            songListDiv.id = "song-list-div"
            // Create div for table to prevent super long table
            const songListTableDiv = document.createElement("div")
            songListTableDiv.id = "song-list-table-div"
            songListTableDiv.style.height = "0px"
            songListTableDiv.style.visibility = "hidden"
            // Create "autoplay" switch
            const autoplayDiv = document.createElement("div")
            autoplayDiv.id = "autoplay-div"
            const autoplaySwitchLabel = document.createElement("label")
            autoplaySwitchLabel.id = "autoplay-switch-label"
            autoplaySwitchLabel.htmlFor = "autoplay-switch"
            autoplaySwitchLabel.textContent = "啟用自動播放"
            const autoplaySwitch = document.createElement("input")
            autoplaySwitch.id = "autoplay-switch"
            autoplaySwitch.type = "checkbox"
            autoplayDiv.append(autoplaySwitchLabel, autoplaySwitch)
            songListTableDiv.append(autoplayDiv)
            // Create player
            const playerDiv = document.createElement("div")
            playerDiv.id = "player-div"
            // Create info div
            const songInfoDiv = document.createElement("div")
            songInfoDiv.id = "song-info-div"
            const thumbnail = document.createElement("img")
            thumbnail.id = "hd-thumbnail"
            const songDetailDiv = document.createElement("div")
            songDetailDiv.id = "song-detail-div"
            const songName = document.createElement("span")
            songName.id = "song-info-name"
            const songArtist = document.createElement("span")
            songArtist.id = "song-info-artist"
            songDetailDiv.append(songName, songArtist)
            songInfoDiv.append(thumbnail, songDetailDiv)
            // Create control div
            const controlDiv = document.createElement("div")
            controlDiv.id = "control-div"
            controlDiv.style.display = "flex"
            const progressDiv = document.createElement("div")
            progressDiv.id = "progress-div"
            const nowTime = document.createElement("span")
            nowTime.id = "progress-nowtime"
            nowTime.className = "progress-span"
            nowTime.textContent = "0:00"
            const duration = document.createElement("span")
            duration.id = "progress-duration"
            duration.className = "progress-span"
            duration.textContent = "0:00"
            const progressBar = document.createElement("input")
            progressBar.id = "progress-bar"
            progressBar.type = "range"
            progressBar.addEventListener("input", () => {
                videoPlayer.currentTime = parseInt(progressBar.value)
            })
            progressDiv.append(nowTime, progressBar, duration)
            const repeatBtn = document.createElement("button")
            repeatBtn.classList.add("material-symbols-rounded", "control-buttons")
            repeatBtn.id = "repeat-btn"
            repeatBtn.textContent = "repeat"
            repeatBtn.addEventListener("click", () => {
                if (REPEAT_MODE === 2) {
                    REPEAT_MODE = 0
                } else {
                    REPEAT_MODE++
                }
                if (REPEAT_MODE === 0) {
                    repeatBtn.textContent = "repeat"
                } else if (REPEAT_MODE === 1) {
                    repeatBtn.textContent = "repeat_one_on"
                } else if (REPEAT_MODE === 2) {
                    repeatBtn.textContent = "repeat_on"
                }
            })
            const previousBtn = document.createElement("button")
            previousBtn.classList.add("material-symbols-rounded", "control-buttons")
            previousBtn.id= "skip-previous-btn"
            previousBtn.textContent = "skip_previous"
            const playBtn = document.createElement("button")
            playBtn.classList.add("material-symbols-rounded", "control-buttons")
            playBtn.id = "play-pause-btn"
            playBtn.textContent = "pause"
            playBtn.addEventListener("click", () => {
                if (videoPlayer.paused) {
                    videoPlayer.play().then()
                } else {
                    videoPlayer.pause()
                }
            })
            const nextBtn = document.createElement("button")
            nextBtn.classList.add("material-symbols-rounded", "control-buttons")
            nextBtn.id = "skip-next-btn"
            nextBtn.textContent = "skip_next"
            const shuffleBtn = document.createElement("button")
            shuffleBtn.classList.add("material-symbols-rounded", "control-buttons")
            shuffleBtn.id = "shuffle-btn"
            shuffleBtn.textContent = "shuffle"
            shuffleBtn.addEventListener("click", () => {
                let [_, nowSongIndex] = getNowPlayingSong(videoPlayer)
                if (IS_RANDOM) {
                    shuffleBtn.textContent = "shuffle"
                } else {
                    shuffleBtn.textContent = "shuffle_on"
                }
                IS_RANDOM = !IS_RANDOM
                generateQueue(songListTable, videoPlayer, nowSongIndex + 1, IS_RANDOM)
                updateNowPlayingInfo(videoPlayer, true)
            })
            controlDiv.append(repeatBtn, previousBtn, playBtn, nextBtn, shuffleBtn)
            playerDiv.append(songInfoDiv, progressDiv, controlDiv)
            autoplayDiv.append(playerDiv)
            // Create song list table
            const songListTable = document.createElement("table")
            songListTable.id = "song-list-table"
            songListTable.style.width = "100%"
            songListTable.style.backgroundColor = "#FFFFFF"
            songListTable.innerHTML = escapeHTMLPolicy.createHTML(`
            <tr style="height: fit-content">
                <th style="width: 5%">#</th>
                <th style="width: 75%">歌名</th>
                <th style="width: 10%">時長</th>
                <th style="width: 40px">播放</th>
            </tr>
            `)
            generateQueue(songListTable, videoPlayer, 0, false)
            // Set up autoplay after loop
            // Wrap autoplay as a lambda object, so that we can remove it when needed
            function apl() {
                autoplay(videoPlayer, false, false, false)
            }
            autoplaySwitch.addEventListener("change", () => {
                console.log("Autoplay enabled: ", autoplaySwitch.checked)
                if (autoplaySwitch.checked) {
                    videoPlayer.addEventListener("timeupdate", apl)
                } else {
                    videoPlayer.removeEventListener("timeupdate", apl)
                }
            })
            // Remove the old autoplay instance before switching to other videos
            document.addEventListener("yt-navigate-start", () => {
                try {
                    videoPlayer.removeEventListener("timeupdate", apl)
                } catch (e) {}
            })
            // Set up song info updater
            // Same reason as autoplay
            function unpil() {
                updateNowPlayingInfo(videoPlayer, false)
            }
            videoPlayer.addEventListener("timeupdate", unpil)
            document.addEventListener("yt-navigate-start", () => {
                try {
                    videoPlayer.removeEventListener("timeupdate", unpil)
                } catch (e) {}
            })
            videoPlayer.addEventListener("play", () => {
                playBtn.textContent = "pause"
            })
            videoPlayer.addEventListener("pause", () => {
                playBtn.textContent = "play_arrow"
            })
            // Set up control buttons
            function appl() {
                autoplay(videoPlayer, true, false, true)
            }
            previousBtn.addEventListener("click", appl)
            function apnl() {
                autoplay(videoPlayer, true, true, false)
            }
            nextBtn.addEventListener("click", apnl)
            // Append the table into its div before it appends into the main div
            songListTableDiv.append(songListTable)
            songListDiv.append(songListTableDiv)
            // Create "toggle song list" button
            const songListBtn = document.createElement("button")
            songListBtn.id = "song-list-btn"
            songListBtn.classList.add(
                "hd-button",
                "yt-spec-button-shape-next",
                "yt-spec-button-shape-next--outline",
                "yt-spec-button-shape-next--mono",
                "yt-spec-button-shape-next--size-m",
                "yt-spec-button-shape-next--enable-backdrop-filter-experiment"
            )
            songListBtn.textContent = "開啟歌曲清單"
            songListBtn.addEventListener("click", () => {
                if (songListTableDiv.style.visibility !== "visible") {
                    songListBtn.textContent = "關閉歌曲清單"
                    songListTableDiv.style.visibility = "visible"
                    songListTableDiv.style.height = ""
                } else {
                    songListBtn.textContent = "開啟歌曲清單"
                    songListTableDiv.style.visibility = "hidden"
                    songListTableDiv.style.height = "0px"
                }
            })
            songListDiv.prepend(songListBtn)
            minorColumn.prepend(songListDiv)
        })
        .catch((error) => {
            console.error(error)
        })
}

(function () {
    "use strict";

    console.log("Extension starts")
    if (window.location.hostname === "holodex.net") {
        console.log("We're in Holodex!")
        const rawData = JSON.parse(window.localStorage.getItem("holodex-v2"))
        if (rawData.userdata.user === null) {
            console.warn("Not logged in. Login to update your API key.")
            return
        }
        if (HOLODEX_API_KEY === null || HOLODEX_API_KEY === undefined) {
            console.log("API key is missing, fetching")
        } else if (HOLODEX_API_KEY !== rawData.userdata.user.api_key) {
            console.log("New API key found, updating")
        } else {
            console.log("Your API key is good to go!")
            return
        }
        HOLODEX_API_KEY = rawData.userdata.user.api_key
        GM_setValues({holodex_api_key: HOLODEX_API_KEY})
        console.log(`Your new API key: ${HOLODEX_API_KEY.slice(0, 9)}****-****-****-************`)
    }

    // 程式碼開始
    // if (new URLSearchParams(window.location.search).get("v") !== null) {
    //     document.addEventListener("load", main)
    // }
    // document.addEventListener("yt-navigate-finish", main)
    // main()

    document.addEventListener("yt-navigate-finish", () => {
        console.log("yt-navigate-finish")
        main()
    });
})()