Skip to content

Instantly share code, notes, and snippets.

@stu43005
Last active November 11, 2023 02:17
Show Gist options
  • Save stu43005/1e19b6612b78a7370fd4dd51c714a4f6 to your computer and use it in GitHub Desktop.
Save stu43005/1e19b6612b78a7370fd4dd51c714a4f6 to your computer and use it in GitHub Desktop.
用DiscordChatExporter匯出頻道訊息後,從指定的時間開始回放訊息
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();
})();
<!-- 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