AniCHAT - Discuss Anime Episodes

Get discussions from popular sites like MAL and Reddit for the anime you are watching right below your episode

Skript installieren?
Vom Ersteller vorgeschlagenes Skript

Ihnen könnte auch AniHIDE - Hide Unrelated Episodes gefallen.

Skript installieren
// ==UserScript==
// @name        AniCHAT - Discuss Anime Episodes
// @namespace   https://greasyfork.org/en/users/781076-jery-js
// @version     2.2.0
// @description Get discussions from popular sites like MAL and Reddit for the anime you are watching right below your episode
// @icon        https://image.myanimelist.net/ui/OK6W_koKDTOqqqLDbIoPAiC8a86sHufn_jOI-JGtoCQ
// @author      Jery
// @license     MIT
// @match       https://yugenanime.*/*
// @match       https://yugenanime.tv/*
// @match       https://yugenanime.sx/*
// @match       https://animepahe.*/*
// @match       https://animepahe.com/*/
// @match       https://anitaku.*/*
// @match       https://anitaku.so/*
// @match       https://gogoanime.*/*
// @match       https://gogoanime.to/*
// @match       https://gogoanime3.*/*
// @match       https://gogoanime3.co/*
// @match       https://aniwave.*/watch/*
// @match       https://aniwave.to/watch/*
// @match       https://aniwave.vc/watch/*
// @match       https://aniwave.ti/watch/*
// @match       https://aniwatchtv.*/watch/*
// @match       https://aniwatchtv.to/watch/*
// @match       https://hianime.*/watch/*
// @match       https://hianime.to/watch/*
// @match       https://kayoanime.*/*
// @match       https://kayoanime.com/*
// @match       https://kaas.*/*/*
// @match       https://kaas.to/*/*
// @match       https://kickassanimes.*/*/*
// @match       https://kickassanimes.io/*/*
// @match       https://*.kickassanime.*/*/*
// @match       https://*.kickassanime.mx/*/*
// @match       https://anix.*/*/*/*
// @match       https://anix.to/*/*/*
// @match       https://anix.ac/*/*/*
// @match       https://anix.vc/*/*/*
// @match       https://animeflix.*/watch/*
// @match       https://animeflix.live/watch/*
// @match       https://animehub.*/watch/*
// @match       https://animehub.ac/watch/*
// @match       https://animesuge.*/anime/*
// @match       https://animesuge.to/anime/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_notification
// @require     https://unpkg.com/axios/dist/axios.min.js
// ==/UserScript==

/**************************
 * CONSTANTS
 ***************************/
// seconds to wait before loading the discussions (to avoid spamming the service)
const TIMEOUT = 30000; // in milliseconds
// proxy to bypass the cors restriction on services like MAL
const PROXYURL = "https://proxy.cors.sh/"; //"https://test.cors.workers.dev/?"; //'https://corsproxy.io/?';

/***************************************************************
 * ANIME SITES & SERVICES
 ***************************************************************/
const animeSites = [
	{
		name: "yugenanime",
		url: ["yugenanime.tv", "yugenanime.sx"],
		chatArea: ".box.m-10-t.m-25-b.p-15",
		getAnimeTitle: () => document.querySelector(".ani-info-ep a > h1").textContent,
		getEpTitle: () => document.querySelector("h1.text-semi-bold.m-5-b").textContent,
		getEpNum: () => window.location.href.split("/")[6],
		styles: null,
	},
	{
		name: "animepahe",
		url: ["animepahe.ru", "animepahe.com"],
		chatArea: ".theatre",
		getAnimeTitle: () => document.querySelector(".theatre-info > h1 > a").textContent.split(' - ')[0],
		getEpTitle: () => document.querySelector(".theatre-info > h1 > a").textContent.split(' - ')[0],
		getEpNum: () =>  document.querySelector(".dropup.episode-menu > button").innerText.split("Episode ")[1],
		styles: '.discussion-area { max-width:1100px; margin:15px auto 0; }',
	},
	{
		name: "gogoanime",
		url: ['gogoanime3', 'gogoanimehd', 'gogoanime', 'anitaku'],
		chatArea: ".anime_video_body_comment_center",
		getAnimeTitle: () => document.querySelector(".anime-info > a").textContent,
		getEpTitle: () => document.querySelector(".anime-info > a").textContent,
		getEpNum: () => window.location.href.split("-episode-")[1],
		styles: `.chat-msg { color: white; font-size: 14px; } .discussion-title > a { font-size: 24px; color: goldenrod; }`
	},
	{
		name: "aniwave",
		url: ['aniwave', 'lite.aniwave'],
		chatArea: "#comments",
		getAnimeTitle: () => document.querySelector(".name .title").textContent,
		getEpTitle: () => document.querySelector(".name .title").textContent,
		getEpNum: () => window.location.href.split("/ep-")[1],
	},
	{
		name: "hianime",
		url: ["aniwatchtv", "hianime.to", "hianime.nz", "hianime.mm", "hianime.sx", "hianime"],
		chatArea: ".show-comments",
		getAnimeTitle: () => document.querySelector("h2.film-name > a").textContent,
		getEpTitle: () => document.querySelector("div.ssli-detail > .ep-name").textContent,
		getEpNum: () => waitForElm(".ssl-item.ep-item.active > .ssli-order").then(elm => elm.textContent),
		styles: `.chat-row .user-avatar { width: auto; overflow: visible; }`
	},
	{
		name: "kayoanime",
		url: ["kayoanime.com"],
		chatArea: "#the-post",
		getAnimeTitle: () => document.querySelector("h1.entry-title").textContent.split(/Episode \d+ English.+/)[0].trim(),
		getEpTitle: () => document.querySelector(".toggle-head").textContent.trim(),
		getEpNum: () => document.querySelector("h1.entry-title").textContent.split(/Episode (\d+) English.+/)[1],
	},
	{
		name: "kickassanime",
		url: ["kaas", "kickassanimes", "kickassanime"],
		chatArea: () => document.querySelector("#disqus_thread").parentElement,
		getAnimeTitle: () => document.querySelector(".text-h6").textContent,
		getEpTitle: () => document.querySelector(".text-h6").textContent,
		getEpNum: () => document.querySelector(".d-block .text-overline").textContent.split("Episode")[1].trim(),
	},
	{
		name: "anix",
		url: ["anix"],
		chatArea: () => document.querySelector("#disqus_thread").parentElement,
		getAnimeTitle: () => document.querySelector(".ani-name").textContent,
		getEpTitle: () => document.querySelector(".ani-name").textContent,
		getEpNum: () => window.location.href.split("/ep-")[1],
	},
	{
		name: "animeflix",
		url: ["animeflix"],
		chatArea: 'main',
		getAnimeTitle: () => document.querySelector(".details .title").textContent,
		getEpTitle: () => document.querySelector(".details .title").textContent,
		getEpNum: () => window.location.href.split("-episode-")[1],
	},
	{
		name: "animehub",
		url: ["animehub"],
		chatArea: 'mawdawin',
		getAnimeTitle: () => document.querySelector(".dc-title").textContent,
		getEpTitle: () => document.querySelector(".dc-title").textContent,
		getEpNum: () => document.querySelector("#current_episode_name").textContent.split("Episode")[1].trim(),
	},
	{
		name: "animesuge",
		url: ["animesuge.to", "animesuge"],
        chatArea: '#comment',
        getAnimeTitle: () => document.querySelector("#media-info .maindata > h1").textContent,
        getEpTitle: () => document.querySelector("#media-info .maindata > h1").textContent,
        getEpNum: () => window.location.href.split("/ep-")[1],
	}
];

const services = [
	{
		name: "MyAnimeList",
		icon: "https://image.myanimelist.net/ui/OK6W_koKDTOqqqLDbIoPAiC8a86sHufn_jOI-JGtoCQ",
		url: "https://myanimelist.net/",
		_clientId: "dbe5cec5a2f33fdda148a6014384b984",
		_proxyKey: "temp_2ed7d641dd52613591687200e7f7958b",
		async getDiscussion(animeTitle, epNum) {
			let animeId, topic, url, response, data;
			let headers = {headers: {"X-MAL-CLIENT-ID": this._clientId, "x-cors-api-key": this._proxyKey, 'x-requested-with': 'XMLHttpRequest', 'origin': window.location.origin}};
			// get the anime's MAL id using MAL API (or use Jikan API if title is too long)
			try {
				if (animeTitle.length > 500) {
					url = PROXYURL + `https://api.myanimelist.net/v2/anime?q=${animeTitle}&limit=1`;
					response = await axios.get(url, headers);
					animeId = response.data.data[0].node.id;
				} else {
					url = PROXYURL + `https://api.jikan.moe/v4/anime?q=${animeTitle}&limit=1`;
					animeId = GM_getValue('cachedId_'+url, null);
					if (!animeId) {
						response = await axios.get(url, headers);
						animeId = response.data.data[0].mal_id;
						GM_setValue('cachedId_'+url, animeId);
					}
				}
				console.log(`animeId: ${animeId}`);
			} catch (e) {
				throw new Error(`Couldn't find the anime id. Retry after a while or switch to another service.\n${e.code} : ${e}`);
			}
			// get the discussion url from the anime
			try {
				url = PROXYURL + `https://api.jikan.moe/v4/anime/${animeId}/forum`;
				response = await axios.get(url, headers);
				topic = response.data.data.find(it => it.title.includes(`Episode ${epNum} Discussion`));
				console.log(`topic: ${topic}`);
			} catch (e) {
				throw new Error(`No discussion found. Retry after a while or switch to another service.\n${e.code} : ${e}`);
			}
			// get the forum page
			try {
				url = PROXYURL + `https://api.myanimelist.net/v2/forum/topic/${topic.mal_id}?limit=100`;
				response = await axios.get(url, headers);
				data = response.data.data;
				console.log(`data: ${data}`);
			} catch (e) {
				throw new Error(`Error getting the discusssion (${topic}). Retry after a while or switch to another service.\n${e.code} : ${e}`);
			}

			let chats = [];
			data.posts.forEach((post) => {
				const user = post.created_by.name;
				const userLink = "https://myanimelist.net/profile/" + user;
				const avatar = post.created_by.forum_avator;
				const msg = this._parseBBCode(post.body);
				const timestamp = new Date(post.created_at).getTime();
				chats.push(new Chat(user, userLink, avatar, msg, timestamp, null));
			});

			const discussion = new Discussion(topic.title, topic.url, chats);
			return discussion;
		},
		_parseBBCode(bbcode) {
			const mappings = [
				{ bbcode: /\[b\](.*?)\[\/b\]/g, html: "<strong>$1</strong>" },
				{ bbcode: /\[i\](.*?)\[\/i\]/g, html: "<em>$1</em>" },
				{ bbcode: /\[u\](.*?)\[\/u\]/g, html: "<u>$1</u>" },
				{ bbcode: /\[s\](.*?)\[\/s\]/g, html: "<s>$1</s>" },
				{ bbcode: /\[url=(.*?)\](.*?)\[\/url\]/g, html: '<a href="$1">$2</a>' },
				{ bbcode: /\[img.*?\](.*?)\[\/img\]/g, html: '<img src="$1" alt="">' },
				{ bbcode: /\[code\]([\s\S]*?)\[\/code\]/g, html: "<code>$1</code>" },
				{ bbcode: /\[quote\]/g, html: '<blockquote class="quote" style="font-size: 90%; border: 1px solid; padding: 5px;">' },
				{ bbcode: /\[quote=(.*?)\s*(message=\d+)?\]/g, html: '<blockquote class="quote" style="font-size: 90%; border: 1px solid; padding: 5px;"><h4>$1 Said:</h4>' },
				{ bbcode: /\[\/quote\]/g, html: '</blockquote>' },
				{ bbcode: /\[color=(.*?)\](.*?)\[\/color\]/g, html: '<span style="color: $1;">$2</span>' },
				{ bbcode: /\[size=(.*?)\](.*?)\[\/size\]/g, html: '<span style="font-size: $1;">$2</span>' },
				{ bbcode: /\[center\](.*?)\[\/center\]/g, html: '<div style="text-align: center;">$1</div>' },
				{ bbcode: /\[list\](.*?)\[\/list\]/g, html: "<ul>$1</ul>" },
				{ bbcode: /\[list=(.*?)\](.*?)\[\/list\]/g, html: '<ol start="$1">$2</ol>' },
				{ bbcode: /\[\*\](.*?)\[\/\*\]/g, html: "<li>$1</li>" },
				{ bbcode: /\[spoiler\]([\s\S]*?)\[\/spoiler\]/g, html: '<div class="spoiler"><input type="button" onclick="this.nextSibling.style.display=\'inline-block\';this.style.display=\'none\';" value="Show spoiler" style="display: inline-block;"><span class="spoiler_content" style="display: none;"><input type="button" onclick="this.parentNode.style.display=\'none\';this.parentNode.parentNode.childNodes[0].style.display=\'inline-block\';" value="Hide spoiler">$1</span></div>' },
				{ bbcode: /\[spoiler=(.*?)\]([\s\S]*?)\[\/spoiler\]/g, html: '<div class="spoiler"><input type="button" onclick="this.nextSibling.style.display=\'inline-block\';this.style.display=\'none\';" value="Show $1" style="display: inline-block;"><span class="spoiler_content" style="display: none;"><input type="button" onclick="this.parentNode.style.display=\'none\';this.parentNode.parentNode.childNodes[0].style.display=\'inline-block\';" value="Hide $1">$2</span></div>' },
				{ bbcode: /@(\S+)/g, html: '<a href="https://myanimelist.net/profile/$1" target="_blank">@$1</a>' },
			];
			let html = bbcode;
			for (const mapping of mappings) { html = html.replace(mapping.bbcode, mapping.html); }
			return html;
		}
	},
	{
		name: "Reddit",
		icon: "https://www.redditstatic.com/desktop2x/img/favicon/apple-icon-57x57.png",
		url: "https://www.reddit.com/",
		_clientId: "dbe5cec5a2f33fdda148a6014384b984",
		_proxyKey: "temp_2ed7d641dd52613591687200e7f7958b",
		async getDiscussion(animeTitle, epNum) {
			let animeId, topic, url, response, posts;
			let headers = {headers: {"x-cors-api-key": this._proxyKey, 'x-requested-with': 'XMLHttpRequest', 'origin': window.location.origin}};
			// get the anime's MAL id
			try {
				url = PROXYURL + `https://api.jikan.moe/v4/anime?q=${animeTitle}&limit=1`;
				animeId = GM_getValue('cachedId_'+url, '');
				if (animeId == '') {
					response = await axios.get(url, headers);
					if (response.data.data.length>0) {
						animeId = response.data.data[0].mal_id;
						GM_setValue('cachedId_'+url, animeId);
					}
				}
			} catch (e) {
				throw new Error(`Couldn't find the anime id. Retry after a while or switch to another service.\n${e.code} : ${e.message}`);
			}
			// Get the discussion
			try {
				url = `https://api.reddit.com/r/anime/search.json?q=${animeTitle}+-+Episode+${epNum}+discussion+author:AutoLovepon&restrict_sr=on&include_over_18=on&sort=relevance&limit=50`;
				response = await axios.get(url);
				topic = response.data.data.children.find(it => it.data.title.includes(` - Episode ${epNum} discussion`) && it.data.selftext.includes(`[MyAnimeList](https://myanimelist.net/anime/${animeId}`))?.data;
			} catch (e) {
				throw new Error(`No discussion found. Retry after a while or switch to another service.\n${e.code} : ${e.message}`);
			}
			// get the comments in the discussion
			try {
				url = topic.url.replace('www.reddit.com', 'api.reddit.com');
				response = await axios.get(url);
				posts = response.data[1].data.children;
				if (posts[0].data.author == "AutoModerator") posts.shift();	// skip the first bot post
			} catch (e) {
				throw new Error(`Error getting the discusssion. Retry after a while or switch to another service.\n${e.code} : ${e.message}`);
			}

			let chats = [];
			for (let post of posts) chats.push(this._processPost(post));

			const discussion = new Discussion(topic.title, topic.url, chats);
			return discussion;
		},
		_processPost(post) {
			const user = post.data.author;
			const userLink = "https://www.reddit.com/user/" + user;
			// const about = axios.get(`https://api.reddit.com/user/${user}/about`);
			const avatar = axios.get(`https://api.reddit.com/user/${user}/about`).then(r=>r.data.data.icon_img.split('?')[0]);
			const msg = ((el) => { el.innerHTML = post.data.body_html; return el.value; })(document.createElement('textarea'));
			const timestamp = post.data.created_utc * 1000;
			let replies = [];
			if (post.data.replies && post.data.replies.data)
				for (let reply of post.data.replies.data.children)
					replies.push(this._processPost(reply));
			return new Chat(user, userLink, avatar, msg, timestamp, replies);
		}
	},
];

/***************************************************************
 * Classes for handling various data like settings & discussions
 ***************************************************************/
// User settings
class UserSettings {
	constructor(usernames = {}) {
		this.usernames = usernames;
	}
	static load() {
		return GM_getValue("userSettings", new UserSettings());
	}
}

// Class to hold each row of a discussion
class Chat {
	constructor(user, userLink, avatar, msg, timestamp, replies) {
		this.user = user;
		this.userLink = userLink;
		this.avatar = avatar;
		this.msg = msg;
		this.timestamp = timestamp;
		this.replies = replies;
	}

	getRelativeTime() {
		const now = new Date().getTime();
		const diff = now - this.timestamp;

		const seconds = Math.floor(diff / 1000);
		const minutes = Math.floor(seconds / 60);
		const hours = Math.floor(minutes / 60);
		const days = Math.floor(hours / 24);
		const weeks = Math.floor(days / 7);
		const months = Math.floor(days / 30);
		const years = Math.floor(days / 365);

		if (years > 0) {
			return `${years} ${years === 1 ? "year" : "years"} ago`;
		} else if (months > 0) {
			return `${months} ${months === 1 ? "month" : "months"} ago`;
		} else if (weeks > 0) {
			return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`;
		} else if (days > 0) {
			return `${days} ${days === 1 ? "day" : "days"} ago`;
		} else if (hours > 0) {
			return `${hours} ${hours === 1 ? "hour" : "hours"} ago`;
		} else if (minutes > 0) {
			return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`;
		} else {
			return `${seconds} ${seconds === 1 ? "second" : "seconds"} ago`;
		}
	}
}

// Class to hold the complete discussions
class Discussion {
	constructor(title, link, chats) {
		this.title = title;
		this.link = link;
		this.chats = chats;
	}
}

/***************************************************************
 * The UI elements
 ***************************************************************/
// generate the discussion area
async function generateDiscussionArea() {
	const discussionArea = document.createElement("div");
	discussionArea.className = "discussion-area";

	const discussionTitle = document.createElement("h3");
	discussionTitle.className = "discussion-title";

	const discussionTitleText = document.createElement("a");
	discussionTitleText.textContent = `${await site.getAnimeTitle()} Episode ${await site.getEpNum()} Discussion`;
	discussionTitleText.title = "Click to view the original discussion";
	discussionTitleText.target = "_blank";
	discussionTitle.appendChild(discussionTitleText);

	const serviceSwitcher = buildServiceSwitcher();
	discussionTitle.appendChild(serviceSwitcher);

	const discussionList = document.createElement("ul");
	discussionList.className = "discussion-list";

	discussionArea.appendChild(discussionTitle);
	discussionArea.appendChild(discussionList);

	return discussionArea;
}

function buildServiceSwitcher() {
	const servicesArea = document.createElement('div');
	servicesArea.id = 'service-switcher';
	servicesArea.innerHTML = `<img class="service-icon selected" title="Powered by ${service.name}" src="${service.icon}"><a style="padding-right:5px">▶</a>`;
	services.forEach(it => {
		servicesArea.innerHTML += `<img class="service-icon other" data-opt="${services.indexOf(it)}" title="Switch to ${it.name}" src="${it.icon}" style="cursor:pointer;">`;
	});
	servicesArea.querySelectorAll('.other').forEach(it => {
		it.addEventListener('click', () =>{
			const serviceOpt = parseInt(it.getAttribute('data-opt'));
			console.log(serviceOpt);
			GM_setValue("service", serviceOpt);
			service = services[serviceOpt];
			document.querySelector('.discussion-area').remove();
			run(0);
		});
	});
	return servicesArea;
}

// build a row for a single chat in the discussion
async function buildChatRow(chat) {
	const chatRow = document.createElement("li");
	chatRow.className = "chat-row";

	const chatContent = document.createElement("div");
	chatContent.className = "chat-content";

	const userAvatar = document.createElement("div");
	userAvatar.className = "user-avatar";
	userAvatar.innerHTML = `<img src="${service.icon}" alt="${chat.user}">`;
	if (chat.avatar instanceof Promise) chat.avatar.then(avatarUrl => userAvatar.firstChild.src = avatarUrl);
    else userAvatar.firstChild.src = chat.avatar;

	const userMsg = document.createElement("div");
	userMsg.className = "user-msg";

	const name = document.createElement("span");
	name.className = "chat-name";
	name.textContent = chat.user;

	const time = document.createElement("span");
	time.className = "chat-time";
	time.textContent = chat.getRelativeTime();
	time.title = new Date(chat.timestamp).toLocaleString(undefined, {
		weekday: "long",
		year: "numeric",
		month: "long",
		day: "numeric",
		hour: "numeric",
		minute: "numeric",
		hour12: true,
	});

	const msg = document.createElement("span");
	msg.className = "chat-msg";
	msg.innerHTML = chat.msg;

	userMsg.appendChild(name);
	userMsg.appendChild(time);
	userMsg.appendChild(msg);
	chatContent.appendChild(userAvatar);
	chatContent.appendChild(userMsg);
	chatRow.appendChild(chatContent);

	if (chat.replies && chat.replies.length > 0) {
		const repliesDiv = document.createElement("div");
		repliesDiv.className = "reply";
		for (let reply of chat.replies) {
			const replyRow = await buildChatRow(reply);
			repliesDiv.appendChild(replyRow);
		}
		chatRow.appendChild(repliesDiv);
	}

	return chatRow;
}

// Countdown to show before load the discussions
function setLoadingTimeout(timeout) {
	let countdown = timeout;

	const loadingArea = document.createElement("div");
	loadingArea.className = "loading-discussion";

	const loadingElement = document.createElement("div");
	loadingElement.innerHTML = `<img src="https://flyclipart.com/thumb2/explosion-gif-transparent-transparent-gif-sticker-741584.png" style="width: 150px; margin-right: 10px;">`;
	loadingElement.style.cssText = `display: flex; align-items: center;`;

	const progressBar = document.createElement("div");
	progressBar.className = "progress-bar";
	progressBar.style.cssText = `width: "100%"; height: 10px; background-color: #ccc; position: relative;`;

	const progressFill = document.createElement("div");
	progressFill.className = "progress-fill";
	progressFill.style.cssText = `width: 0%; height: 100%; background-color: #4CAF50; position: absolute; top: 0; left: 0;`;

	const message = document.createElement("span");
	message.textContent = `This ${
		timeout / 1000
	} secs timeout is set to reduce the load on the service and you can configure the TIMEOUT by editing the script (line 21)`;
	message.style.cssText = "font-size: 14px; color: darkgrey;";

	progressBar.appendChild(progressFill);
	loadingElement.appendChild(message);
	loadingArea.appendChild(loadingElement);
	loadingArea.appendChild(progressBar);

	console.log("Countdown started: " + countdown + "ms");

	const countdownInterval = setInterval(() => {
		countdown -= 100;
		const progressWidth = 100 - (countdown / timeout) * 100;
		progressFill.style.width = `${progressWidth}%`;
		if (countdown <= 0) {
			message.remove();
			loadingElement.remove();
			clearInterval(countdownInterval);
		}
	}, 100);

	return loadingArea;
}

// Add CSS styles to the page
const styles = `
	.discussion-area {
		border-radius: 10px;
		padding: 10px;
	}

	.discussion-title {
		display: flex;
		justify-content: space-between;
		margin-bottom: 20px;
	}

	.discussion-title > a {
		margin-right: 20px;
	}

	.service-icon {
		height: 25px;
		padding-right: 10px;
	}

	#service-switcher {
		width: 7%;
		transition: width 0.3s ease-in-out;
		overflow: hidden;
		display: flex;
	}
	#service-switcher:hover {
		width: ${8+5*services.length}%;
	}

	ul.discussion-list {
		overflow: auto;
		max-height: 90vh;
	}

	.chat-row {
		display: flex;
		flex-direction: column;
		padding: 10px 0;
		border-top: 1px solid #eee;
	}

	.chat-content {
		display: flex;
		flex-direction: row;
	}

	.chat-row > .reply {
		display: flex;
		flex-direction: column;
		padding-left: 55px;
		border-left: 0.7px solid #eee;
	}

	.user-avatar {
		width: 55px;
		height: 55px;
		margin-right: 10px;
	}

	.user-avatar > img {
		width: 55px;
		height: 55px;
		object-fit: cover;
		border-radius: 15px;
	}

	.user-msg {
		display: flex;
    	flex-direction: column;
	}

	.chat-name {
		font-weight: bold;
		font-size: 15px;
	}

	.chat-time {
		font-size: 12px;
		font-weight: bold;
		padding-top: 5px;
		color: darkgrey;
	}

	.chat-msg {
		padding: 10px 0;
	}

	.chat-msg img {
		max-width: 100%;
	}

	.error-message {
		color: red;
		white-space: pre-wrap;
	}
`;

/***************************************************************
 * Initialize all data and setup menu commands
 ***************************************************************/
// User settings
let userSettings = UserSettings.load();

// Site instance
let site = getCurrentSite();

// Service instance
let service = services[GM_getValue("service", 0)];

/***************************************************************
 * Functions for working of the script
 ***************************************************************/
// Returns a promise of the given element. Resolves when the element is found in the DOM.
function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
			let elm = document.querySelector(selector);
			// console.log(`Element Found!!: ${elm.textContent}`);
            return resolve(elm);
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
				let elm = document.querySelector(selector);
              	// console.log(`Element Detected!: ${elm.textContent}`);
                resolve(elm);
                observer.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

// Get the current website based on the URL
function getCurrentSite() {
	const currentUrl = window.location.href.toLowerCase();
	return animeSites.find((website) => website.url.some((site) => currentUrl.includes(site)));
}

// Run the script
async function run(timeout=TIMEOUT) {
	// initialize the discussionArea
	const discussionArea = await generateDiscussionArea();

	// Fallback techniques to use when chatArea cant be detected
	const selectors = [
		{ selector: () => site.chatArea && typeof site.chatArea === "string" ? document.querySelector(site.chatArea) : site.chatArea(), prepend: false },
		{ selector: () => document.querySelector('#main > .container'), prepend: false },
		{ selector: () => document.querySelector('#footer'), prepend: true },
		{ selector: () => document.querySelector('footer'), prepend: true },
		{ selector: () => document.body, prepend: false },
	];
	for (let i = 0; i < selectors.length; i++) {
		try {
			const element = selectors[i].selector();
			if (selectors[i].prepend) {
				element.prepend(discussionArea);
			} else {
				element.appendChild(discussionArea);
			}
			break;
		} catch (error) {
			continue;
		}
	}

	// Add custom css styles to the page
	const styleElement = document.createElement("style");
	styleElement.textContent = styles + (site.styles || '');
	discussionArea.append(styleElement);

	// Attach the loading element to the page
	discussionArea.appendChild(setLoadingTimeout(timeout));

	// Load the discussion after a set timeout
	setTimeout(async () => {
		try {
			const discussion = await service.getDiscussion(await site.getAnimeTitle(), await site.getEpNum());
			console.log(discussion);
			discussion.chats.forEach(async (chat) => {
				discussionArea.querySelector("ul").appendChild(await buildChatRow(chat));
			});

			discussionArea.querySelector(".discussion-title a").href = discussion.link;
			discussionArea.querySelector(".discussion-title a").textContent = discussion.title;
		} catch (error) {
			console.error(`${error.message}\n\n${error.stack}`);
			const errorElement = document.createElement("span");
			errorElement.className = "error-message";
			errorElement.textContent = `AniCHAT:\n${error.stack}\n\nCheck the console logs for more detail.`;
			discussionArea.appendChild(errorElement);
		}
	}, timeout);
}

try {
	run();
} catch (e) {
	console.error(`${e.message}\n\n${e.stack}`);
}