// ==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()
});
})()