Last active
November 11, 2023 02:17
-
-
Save stu43005/1e19b6612b78a7370fd4dd51c714a4f6 to your computer and use it in GitHub Desktop.
用DiscordChatExporter匯出頻道訊息後,從指定的時間開始回放訊息
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
class ChatController { | |
/** | |
* @param {YoutubeController} yt | |
*/ | |
constructor(yt) { | |
this.yt = yt; | |
this.isSticky = true; | |
} | |
get isSticky() { | |
return this._isSticky; | |
} | |
set isSticky(value) { | |
this._isSticky = value; | |
if (this.stickyButton) this.stickyButton.style.display = value ? 'none' : ''; | |
} | |
init() { | |
this.addStickyButton(); | |
const playerTime = this.yt.getRealTime(); | |
this.cursor = 0; | |
this.messages = [...document.querySelectorAll(".chatlog__message-container")].map((message) => { | |
const messageId = message.dataset.messageId; | |
const timestamp = Number((BigInt(messageId) >> 22n) + 1420070400000n); | |
if (playerTime < timestamp) { | |
message.style.display = 'none'; | |
} | |
return { | |
element: message, | |
messageId, | |
timestamp, | |
}; | |
}); | |
document.addEventListener('scroll', (event) => this.onScroll(event)); | |
setInterval(() => this.scroll(), 100); | |
setInterval(() => this.checkMessage(), 1000); | |
this.yt.addEventListener('stateChange', () => this.checkMessage()); | |
} | |
addStickyButton() { | |
const div = document.createElement('div'); | |
div.innerText = 'Move to bottom'; | |
div.style.position = 'fixed'; | |
div.style.bottom = '20px'; | |
div.style.right = '20px'; | |
div.style.cursor = 'pointer'; | |
div.style.backgroundColor = 'dodgerblue'; | |
div.style.display = 'none'; | |
div.addEventListener('click', () => { | |
this.isSticky = true; | |
this.scroll(); | |
}); | |
document.body.append(div); | |
this.stickyButton = div; | |
} | |
onScroll(event) { | |
if (this.isSticky && !this.autoScroll && this.scrollY !== window.scrollY) { | |
this.isSticky = false; | |
} | |
if (!this.isSticky) { | |
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); | |
const viewport = window.scrollY + vh; | |
const pageHeight = document.documentElement.scrollHeight; | |
const percentage = (viewport * 100) / pageHeight; | |
this.isSticky = percentage >= 100; | |
} | |
this.autoScroll = false; | |
} | |
scroll() { | |
if (this.isSticky) { | |
this.autoScroll = true; | |
window.scrollTo({ | |
left: 0, | |
top: document.documentElement.scrollHeight, | |
behavior: 'instant', | |
}); | |
this.scrollY = window.scrollY; | |
} | |
} | |
checkMessage() { | |
const playerTime = this.yt.getRealTime(); | |
while (this.messages[this.cursor] && playerTime >= this.messages[this.cursor].timestamp) { | |
this.messages[this.cursor].element.style.display = ''; | |
[...this.messages[this.cursor].element.querySelectorAll('img')].forEach((img) => { | |
img.addEventListener('load', () => { | |
this.scroll(); | |
}); | |
}) | |
this.cursor++; | |
} | |
while (this.messages[this.cursor - 1] && playerTime < this.messages[this.cursor - 1].timestamp) { | |
this.messages[this.cursor - 1].element.style.display = 'none'; | |
this.cursor--; | |
} | |
} | |
} | |
class YoutubeController extends EventTarget { | |
constructor(videoId) { | |
super(); | |
this.videoId = videoId; | |
this.isPlaying = false; | |
this.height = 360; | |
} | |
get height() { | |
return this._height; | |
} | |
set height(value) { | |
this._height = value; | |
this._width = Math.floor(value / 9 * 16); | |
} | |
get width() { | |
return this._width; | |
} | |
set width(value) { | |
this._width = value; | |
this._height = Math.floor(value / 16 * 9); | |
} | |
async loadVideoMetadata() { | |
if ('METADATA' in window && window.METADATA.items[0].id === this.videoId) { | |
this.metadata = window.METADATA.items[0]; | |
} else { | |
const res = await fetch(`https://yt.lemnoslife.com/noKey/videos?part=id,snippet,liveStreamingDetails&id=${this.videoId}`, { | |
mode: "cors", | |
}); | |
const json = await res.json(); | |
this.metadata = json.items[0]; | |
} | |
this.actualStartTime = new Date(this.metadata.liveStreamingDetails.actualStartTime); | |
} | |
async loadIframeApi() { | |
const tag = document.createElement('script'); | |
tag.src = "https://www.youtube.com/iframe_api"; | |
const firstScriptTag = document.getElementsByTagName('script')[0]; | |
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); | |
return new Promise((resolve) => { | |
window.onYouTubeIframeAPIReady = () => resolve(); | |
}); | |
} | |
addPlayerContainer() { | |
this.elementId = 'youtube-player'; | |
const div = document.createElement('div'); | |
div.id = this.elementId; | |
div.style.position = 'fixed'; | |
div.style.top = '20px'; | |
div.style.right = '20px'; | |
document.body.append(div); | |
this.element = div; | |
} | |
addResizeButton() { | |
const div = document.createElement('div'); | |
div.innerText = '╰'; | |
div.style.position = 'fixed'; | |
div.style.top = `${this.height + 20}px`; | |
div.style.right = `${this.width + 20}px`; | |
div.style.cursor = 'ne-resize'; | |
div.style.userSelect = 'none'; | |
let dragStarted = false; | |
let x = 0, oldWidth = this.width; | |
div.addEventListener('mousedown', (event) => { | |
dragStarted = true; | |
x = event.x; | |
oldWidth = this.width; | |
}); | |
document.addEventListener('mousemove', (event) => { | |
if (dragStarted) { | |
const vw = Math.min(document.documentElement.clientWidth || 0, window.innerWidth || 0); | |
const move = x - event.x; | |
this.width = Math.min(oldWidth + move, vw - 40); | |
this.player.setSize(this.width, this.height); | |
this.resizeButton.style.top = `${this.height + 20}px`; | |
this.resizeButton.style.right = `${this.width + 20}px`; | |
} | |
}); | |
document.addEventListener('mouseup', () => { | |
if (dragStarted) { | |
dragStarted = false; | |
} | |
}); | |
document.body.append(div); | |
this.resizeButton = div; | |
} | |
async init() { | |
await this.loadVideoMetadata(); | |
await this.loadIframeApi(); | |
this.addPlayerContainer(); | |
return new Promise((resolve) => { | |
this.player = new YT.Player(this.elementId, { | |
height: this.height, | |
width: this.width, | |
videoId: this.videoId, | |
playerVars: { | |
'playsinline': 1 | |
}, | |
events: { | |
'onReady': (event) => { | |
this.onReady(event); | |
resolve(event); | |
}, | |
'onStateChange': (event) => this.onPlayerStateChange(event), | |
}, | |
}); | |
}); | |
} | |
onReady(event) { | |
this.addResizeButton(); | |
} | |
onPlayerStateChange(event) { | |
this.isPlaying = event.data === YT.PlayerState.PLAYING; | |
this.dispatchEvent(new CustomEvent('stateChange')); | |
} | |
getCurrentTime() { | |
return this.player?.getCurrentTime() ?? 0; | |
} | |
getRealTime() { | |
return new Date(this.actualStartTime.getTime() + this.getCurrentTime() * 1000); | |
} | |
} | |
(async () => { | |
const yt = new YoutubeController(videoId); | |
await yt.init(); | |
const chat = new ChatController(yt); | |
chat.init(); | |
})(); |
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
<!-- Add to the HTML file exported by DiscordChatExporter. --> | |
<script>var videoId = '{insert video id here}';</script> | |
<script src="https://gistcdn.githack.com/stu43005/1e19b6612b78a7370fd4dd51c714a4f6/raw/dc_archive.js"></script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment