better-bh

Adds useful QoL features when browsing boundhub

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         better-bh
// @namespace    ocalectu/better-bh
// @version      0.1.2
// @description  Adds useful QoL features when browsing boundhub
// @license      GNU GPLv3
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAnFBMVEUaGhoaHB0bGxsaGxwbFRMcDwoQEhWghlsUFRf/2o6tkGFMQzMNW3oZHiAMDxMMYYIIeKQFh7phUz2lj2HGpG3SrnOXf1cABA1qWkEyLSY8Niu7nGmzlWUbFxX1yoX804r/5pV2ZEcNbpMRSF4jIh//3ZBeTDQAjMsHg7QAfrvEo2zjvn0SWXQeAwAIdJ7qx4J8aUmHck8DkccBmdXThxiHAAAAt0lEQVQYlT2O2xaCIBREj3JAVAJNydTK7Iqplfn//xZWth/3mjUzsPQsSw5/Ej9Ntb9i7iyCW8azdZXz2QSpFXlVfAUhEOgiDMsNw601LiJ4/m5X7Wt5OKJDUCibOOV5fb5cFXUcaoztkACZDhpBFwsaRdOKlLL1mq6ntL8bSB7PYUj9IjZCKfGKYNC6bcuQxWNsGe/AOWNMMnyJHnHqcL9Y8Sv9MQk7+xcEx8+PyMyJrToiIdiJN5hDDp5C9Lj9AAAAAElFTkSuQmCC
// @match        https://www.boundhub.com/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @connect      *
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function (JSZip) {
	'use strict';
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);

var _GM_addStyle = (() => typeof GM_addStyle != "undefined" ? GM_addStyle : void 0)();
	var _GM_download = (() => typeof GM_download != "undefined" ? GM_download : void 0)();
	var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
	var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
	var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)();

var require_dataTypes = __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.stringOrInt = stringOrInt;
		exports.processRelativeDate = processRelativeDate;
		function stringOrInt(input, force = false) {
			try {
				if (force) {
					const result = parseInt(input);
					if (isNaN(result)) return input;
					return result;
				} else {
					if (input.length > 0 && input.endsWith("%")) {
						const result = parseInt(input.substring(0, input.length - 1));
						if (isNaN(result)) return input;
						return result / 100;
					}
					const result = parseInt(input);
					if (isNaN(result)) return input;
					return result;
				}
			} catch {
				return input;
			}
		}
		function processRelativeDate(input, replaceExtra, now = new Date()) {
			if (input === void 0) return { filtered: input };
			const exclude = [
				"\n",
				"	",
				...replaceExtra ?? []
			];
			let filteredInput = input;
			for (const item of exclude) filteredInput = filteredInput.replaceAll(item, "");
			const match = filteredInput.trim().toLowerCase().match(/^(\d+)\s+(minute|hour|day|week|month|year)s?\s*(?:ago)?$/);
			if (!match) {
				console.warn(`Invalid relative date: "${filteredInput}"`);
				return { filtered: filteredInput };
			}
			const amount = Number(match[1]);
			const unit = match[2];
			const date = new Date(now);
			switch (unit) {
				case "minute":
					date.setUTCMinutes(date.getUTCMinutes() - amount);
					break;
				case "hour":
					date.setUTCHours(date.getUTCHours() - amount);
					break;
				case "day":
					date.setUTCDate(date.getUTCDate() - amount);
					break;
				case "week":
					date.setUTCDate(date.getUTCDate() - amount * 7);
					break;
				case "month":
					date.setUTCMonth(date.getUTCMonth() - amount);
					break;
				case "year": date.setUTCFullYear(date.getUTCFullYear() - amount);
			}
			return {
				filtered: filteredInput,
				date,
				accuracy: unit
			};
		}
	}));

var require_functions = __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.delay = void 0;
		var delay = (ms) => new Promise((res) => setTimeout(res, ms));
		exports.delay = delay;
	}));

var require_utils = __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.delay = exports.processRelativeDate = exports.stringOrInt = void 0;
		var dataTypes_1 = require_dataTypes();
		Object.defineProperty(exports, "stringOrInt", {
			enumerable: true,
			get: function() {
				return dataTypes_1.stringOrInt;
			}
		});
		Object.defineProperty(exports, "processRelativeDate", {
			enumerable: true,
			get: function() {
				return dataTypes_1.processRelativeDate;
			}
		});
		var functions_1 = require_functions();
		Object.defineProperty(exports, "delay", {
			enumerable: true,
			get: function() {
				return functions_1.delay;
			}
		});
	}));

var require_logger = __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.log = log;
		exports.trace = trace;
		exports.setDebug = setDebug;
		var config = {
			debug: {}.COMMON_DEBUG === "true",
			trace: {}.COMMON_TRACE === "true"
		};
function log(...args) {
			if (config.debug) console.log("[debug]", ...args);
		}
function trace(...args) {
			if (config.trace) console.log("[trace]", ...args);
		}
function setDebug(value) {
			config.debug = value;
		}
	}));

var require_categories = __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.extractCategories = extractCategories;
		var utils_1 = require_utils();
		var logger_1 = require_logger();
		function extractCategories(rootNode) {
			let result = [];
			rootNode.find(".item").each((_, item) => {
				const extracted = extractCategoryItem(item);
				if (extracted !== void 0) result.push(extracted);
			});
			(0, logger_1.trace)("extractCategories", result);
			return result;
		}
		function extractCategoryItem(item) {
			const url = item.attr("href");
			if (url === void 0) return;
			const id = url.replace(/\/$/, "").split("/").pop();
			if (id === void 0) return;
			const name = item.attr("title");
			if (name === void 0) return;
			const thumbnailUrl = item.find(".thumb").attr("src");
			if (thumbnailUrl === void 0) return;
			const split = item.find(".videos").text().trim().split(" ");
			if (split.length === 0) return;
			const videos = (0, utils_1.stringOrInt)(split[0]);
			const ratingPercentString = item.find(".rating").text().trim();
			try {
				return {
					url,
					id,
					name,
					thumbnailUrl,
					videos,
					ratingPercentString,
					rating: parseInt(ratingPercentString.substring(0, ratingPercentString.length - 1)) / 100
				};
			} catch (e) {
				console.error(e);
				return;
			}
		}
	}));

var require_bh = __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.albumContainerIds = exports.conversationsContainerIds = exports.messagesContainerIds = exports.categoryContainerIds = exports.albumCommentId = exports.videoCommentId = exports.siteContentId = exports.singleAlbumId = exports.singleVideoId = exports.videoContainerIds = exports.BLANK_IMAGE = void 0;
		exports.BLANK_IMAGE = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
		exports.videoContainerIds = {
			list: "list_videos_most_recent_videos",
			popular: "list_videos_videos_watched_right_now_items",
			latest: "list_videos_latest_videos_list",
			common: "list_videos_common_videos_list",
			favorite: "list_videos_favourite_videos",
			uploaded: "list_videos_uploaded_videos",
			related: "list_videos_related_videos",
			private: "list_videos_private_videos"
		};
		exports.singleVideoId = "tab_video_info";
		exports.singleAlbumId = "tab_album_info";
		exports.siteContentId = "siteContent";
		exports.videoCommentId = "video_comments_video_comments_items";
		exports.albumCommentId = "album_comments_album_comments_items";
		exports.categoryContainerIds = { list: "list_categories_categories_list_items" };
		exports.messagesContainerIds = { messages: "list_messages_my_conversation_messages_items" };
		exports.conversationsContainerIds = { conversations: "list_members_my_conversations" };
		exports.albumContainerIds = {
			common: "list_albums_common_albums_list",
			related: "list_albums_related_albums",
			private: "list_albums_private_albums",
			favorite: "list_albums_my_favourite_albums",
			uploaded: "list_albums_created_albums"
		};
	}));

var import_categories = require_categories();
	var import_bh = require_bh();
	var ContainerType = function(ContainerType) {
		ContainerType["ALBUM"] = "album";
		ContainerType["VIDEO"] = "video";
		ContainerType["CATEGORY"] = "category";
		ContainerType["MESSAGES"] = "messages";
		ContainerType["CONVERSATIONS"] = "conversations";
		return ContainerType;
	}({});
	var SettingsKey = function(SettingsKey) {
		SettingsKey["HOSTNAME"] = "hostname";
		SettingsKey["HIDE_ADS"] = "hide-ads";
		SettingsKey["SCRAPE"] = "scrape";
		SettingsKey["UNHIDE_PRIVATE"] = "unhide-private";
		SettingsKey["DEBUG"] = "debug";
		SettingsKey["HIDE_SHARE"] = "hide-share";
		return SettingsKey;
	}({});

var HOSTNAME = "http://localhost:3000";
	var DELAY = 1e3;
	var settingValues = [
		{
			key: SettingsKey.SCRAPE,
			title: "Scrape on browse",
			type: "boolean",
			default: false
		},
		{
			key: SettingsKey.HOSTNAME,
			title: "Scrape Destination Hostname",
			type: "string",
			default: HOSTNAME
		},
		{
			key: SettingsKey.HIDE_ADS,
			title: "Hide Ads (doesn't affect video player)",
			type: "boolean",
			default: false
		},
		{
			key: SettingsKey.HIDE_SHARE,
			title: "Hide Share button on video and album pages",
			type: "boolean",
			default: true
		},
		{
			key: SettingsKey.UNHIDE_PRIVATE,
			title: "Make private videos easier to see",
			type: "boolean",
			default: true
		},
		{
			key: SettingsKey.DEBUG,
			title: "Enable debug logs in console",
			type: "boolean",
			default: false
		}
	];
	var settingDefaults = settingValues.reduce((prev, item) => ({
		...prev,
		[item.key]: item.default
	}), {});
	var PREFIX = "better-bh";
	var pageDefinitions = {
		videos: {
			segmentMatch: ["/videos", ""],
			containerIds: [
				{
					id: import_bh.videoContainerIds.list,
					type: ContainerType.VIDEO
				},
				{
					id: import_bh.videoContainerIds.popular,
					type: ContainerType.VIDEO
				},
				{
					id: import_bh.videoContainerIds.related,
					type: ContainerType.VIDEO
				}
			]
		},
		albums: {
			segmentMatch: ["/albums"],
			containerIds: [{
				id: import_bh.albumContainerIds.common,
				type: ContainerType.ALBUM
			}, {
				id: import_bh.albumContainerIds.related,
				type: ContainerType.ALBUM
			}]
		},
		latest: {
			segmentMatch: ["/latest-updates"],
			containerIds: [{
				id: import_bh.videoContainerIds.latest,
				type: ContainerType.VIDEO
			}]
		},
		categories: {
			segmentMatch: ["/categories"],
			containerIds: [{
				id: import_bh.categoryContainerIds.list,
				type: ContainerType.CATEGORY
			}, {
				id: import_bh.videoContainerIds.common,
				type: ContainerType.VIDEO
			}]
		},
		members: {
			segmentMatch: ["/members"],
			containerIds: [
				{
					id: import_bh.videoContainerIds.private,
					type: ContainerType.VIDEO
				},
				{
					id: import_bh.videoContainerIds.favorite,
					type: ContainerType.VIDEO
				},
				{
					id: import_bh.videoContainerIds.uploaded,
					type: ContainerType.VIDEO
				},
				{
					id: import_bh.albumContainerIds.private,
					type: ContainerType.ALBUM
				},
				{
					id: import_bh.albumContainerIds.favorite,
					type: ContainerType.ALBUM
				},
				{
					id: import_bh.albumContainerIds.uploaded,
					type: ContainerType.ALBUM
				}
			]
		},
		messages: {
			segmentMatch: ["/messages"],
			containerIds: [{
				id: import_bh.conversationsContainerIds.conversations,
				type: ContainerType.CONVERSATIONS
			}, {
				id: import_bh.messagesContainerIds.messages,
				type: ContainerType.MESSAGES
			}]
		},
		common: {
			segmentMatch: [
				"/top-rated",
				"/most-popular",
				"/sites",
				"/models",
				"/channels"
			],
			containerIds: [{
				id: import_bh.videoContainerIds.common,
				type: ContainerType.VIDEO
			}]
		}
	};

async function GM_xmlhttpRequestPromise(details) {
		return new Promise((resolve, reject) => {
			_GM_xmlhttpRequest({
				...details,
				onload: (response) => resolve(response),
				onerror: (error) => reject(error),
				ontimeout: () => reject()
			});
		});
	}

var import_logger = require_logger();
	async function processCategories(rootNode) {
		let result = (0, import_categories.extractCategories)(rootNode);
		(0, import_logger.log)("Pushing categories...", result);
		const webResult = await GM_xmlhttpRequestPromise({
			url: `${await _GM_getValue("hostname", HOSTNAME)}/api/bh/categories`,
			method: "POST",
			data: JSON.stringify({ items: result })
		});
		(0, import_logger.log)("Categories result: ", webResult);
	}

var require_common = __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.getPathnameVariables = getPathnameVariables;
		exports.getCommonDetails = getCommonDetails;
		exports.getCommonItemDetails = getCommonItemDetails;
		exports.getDurationOrImageCountText = getDurationOrImageCountText;
		exports.getScreenshots = getScreenshots;
		exports.getRelatedIds = getRelatedIds;
		var utils_1 = require_utils();
		var logger_1 = require_logger();
		function getPathnameVariables(pathname) {
			let slashSplit = pathname.replace(/\/$/, "").split("/");
			return {
				stringId: slashSplit.pop(),
				id: slashSplit.pop()
			};
		}
		function getCommonDetails(rootNode, commentId, relatedId, pathname) {
			(0, logger_1.log)("getCommonDetails");
			const pathnameVars = getPathnameVariables(pathname);
			if (pathnameVars.id === void 0) return;
			const title = rootNode.find(".headline h2").text().trim();
			const detailsBlock = rootNode.find(".block-details").first();
			const infoBlock = detailsBlock.find(".info").first();
			const firstInfoItemBlock = infoBlock.find(".item").first();
			const viewsText = firstInfoItemBlock.find("span").eq(1).find("em").first().text();
			const submittedText = firstInfoItemBlock.find("span").eq(2).find("em").first().text();
			const publishDateObj = (0, utils_1.processRelativeDate)(submittedText);
			const description = infoBlock.find(".item").eq(1).find("em").text();
			const categories = infoBlock.find(".item").eq(2).find("a").map(function(_, adapter) {
				return adapter.text().replace(/\s+/g, " ").trim();
			});
			const tags = infoBlock.find(".item").eq(3).find("a").map(function(_, adapter) {
				return adapter.text().replace(/\s+/g, " ").trim();
			});
			const userBlock = detailsBlock.find(".block-user").first();
			const username = userBlock.find(".username").first().find("a").first().text().trim();
			const userUrl = userBlock.find(".username").first().find("a").first().attr("href");
			const userId = userUrl?.replace(/\/$/, "").split("/").pop();
			const avatarUrl = userBlock.find(".avatar").first().find("img").first().attr("src");
			const related = getRelatedIds(rootNode, relatedId);
			const comments = rootNode.find(`#${commentId}`).find(".item").map(function(_, adapter) {
				const userA = adapter.find(".image").first().find("a").first();
				(0, logger_1.trace)(userA.length);
				let userInfo = {
					name: adapter.find(".username").first().text().trim(),
					url: void 0,
					id: adapter.attr("data-comment-id"),
					avatarUrl: void 0
				};
				if (userA.length > 0) userInfo = {
					name: userA.attr("title") || adapter.find(".username").first().text().trim(),
					url: userA.attr("href"),
					id: userA.attr("href")?.replace(/\/$/, "").split("/").pop(),
					avatarUrl: userA.find("img").first().attr("src")
				};
				const commentInfo = adapter.find(".comment-info").first().text().trim();
				const relativeDateText = commentInfo.match(/\d+\s+(?:minute|hour|day|week|month|year)s?\s+ago/i)?.[0] ?? commentInfo;
				const relativeDate = (0, utils_1.processRelativeDate)(relativeDateText);
				return {
					user: userInfo,
					relativeDateString: relativeDate.filtered,
					dateExtracted: new Date(),
					rating: (0, utils_1.stringOrInt)(adapter.find(".comment-rating").first().text()),
					content: adapter.find(".original-text").first().text()
				};
			});
			const isPrivate = rootNode.find(".no-player").length > 0 || rootNode.find(".block-album .images .item.private").length > 0;
			const voteParts = rootNode.find(".rating-container").find(".voters").first().text().trim().split("(");
			if (voteParts.length < 2) return;
			const votePercentageText = voteParts[0].trim();
			const voteAmountText = voteParts[1].split("votes")[0].trim();
			let viewCount = viewsText ? parseInt(viewsText.split(" ").join("")) : void 0;
			let ratingPercent = parseInt(votePercentageText.substring(0, votePercentageText.length - 1)) / 100;
			let voteAmount = (0, utils_1.stringOrInt)(voteAmountText);
			(0, logger_1.log)("Basic extracting done");
			const fullItem = {
				title,
				id: pathnameVars.id,
				stringId: pathnameVars.stringId,
				viewCount,
				description,
				relativeDateString: submittedText,
				publishDate: publishDateObj?.date,
				publishDateAccuracy: publishDateObj?.accuracy,
				tags,
				categories,
				related,
				user: {
					name: username,
					url: userUrl,
					id: userId,
					avatarUrl
				},
				comments,
				isPrivate: isPrivate ? isPrivate : void 0,
				ratingPercent,
				voteAmount,
				ratingPercentString: votePercentageText
			};
			(0, logger_1.trace)("Full Item", fullItem);
			return fullItem;
		}
		function getCommonItemDetails(itemNode) {
			const aNode = itemNode.find("a").first();
			const url = aNode.attr("href");
			if (url === void 0) return;
			const pathnameVars = getPathnameVariables(url);
			if (pathnameVars.id === void 0) return;
			const isPrivate = aNode.find(".line-private").length > 0;
			const viewsText = aNode.find(".views").text().trim().split(" views")[0].split(" ").join("");
			const ratingPercentString = aNode.find(".rating").text().trim();
			const title = aNode.attr("title") ?? aNode.find(".title").text().trim() ?? "";
			const imgNode = aNode.find("img").first();
			let thumbnailUrl = imgNode.attr("src");
			if (thumbnailUrl === void 0 || thumbnailUrl.startsWith("data:image")) thumbnailUrl = imgNode.attr("data-original");
			const thumbnailCount = imgNode.attr("data-cnt");
			try {
				let viewCount = (0, utils_1.stringOrInt)(viewsText);
				let ratingPercent = parseInt(ratingPercentString.substring(0, ratingPercentString.length - 1)) / 100;
				return {
					id: pathnameVars.id,
					stringId: pathnameVars.stringId,
					title,
					url,
					viewCount,
					thumbnailUrl,
					thumbnailCount: thumbnailCount === void 0 ? thumbnailCount : (0, utils_1.stringOrInt)(thumbnailCount),
					ratingPercent,
					ratingPercentString,
					isPrivate
				};
			} catch (e) {
				console.error(e);
				return;
			}
		}
		function getDurationOrImageCountText(rootNode, node) {
			if (node === void 0) return rootNode.find(".block-details").first().find(".info").first().find(".item").first().find("span").first().find("em").first().text().trim();
			else if (node.find(".photos").length > 0) return node.find(".photos").first().text().trim();
			else return node.find(".duration").first().text().trim();
		}
		function getScreenshots(rootNode, selector) {
			return rootNode.find(selector).find(".item").map(function(_, adapter) {
				return {
					url: adapter.attr("href"),
					contentUrl: (() => {
						const image = adapter.find("img").first();
						const src = image.attr("src");
						return src === void 0 || src.startsWith("data:image") ? image.attr("data-original") : src;
					})()
				};
			});
		}
		function getRelatedIds(rootNode, containerId) {
			return rootNode.find(`#${containerId}`).find(".item").map(function(_, adapter) {
				const extracted = getCommonItemDetails(adapter);
				if (extracted !== void 0) return extracted.id;
			}).filter((id) => id !== void 0);
		}
	}));

var import_videos = ( __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.extractVideoList = extractVideoList;
		exports.extractSingleVideo = extractSingleVideo;
		var bh_1 = require_bh();
		var common_1 = require_common();
		var logger_1 = require_logger();
		function extractVideoList(rootNode) {
			let result = [];
			rootNode.find(".item").each((index, item) => {
				(0, logger_1.trace)("extractVideoList item", index);
				const extracted = extractVideoItem(item);
				if (extracted !== void 0) result.push(extracted);
			});
			(0, logger_1.trace)("extractVideoList", result);
			if (result.length === 0) return [];
			return result;
		}
		function extractSingleVideo(rootNode, pathname) {
			(0, logger_1.log)("extractSingleVideo");
			const common = (0, common_1.getCommonDetails)(rootNode, bh_1.videoCommentId, bh_1.videoContainerIds.related, pathname);
			if (common === void 0) return;
			const durationSplit = (0, common_1.getDurationOrImageCountText)(rootNode).split(" ");
			const screenshots = (0, common_1.getScreenshots)(rootNode, ".block-screenshots");
			(0, logger_1.log)("Basic extracting done");
			try {
				let secondsString = durationSplit.pop();
				let minutesString = durationSplit.pop();
				if (secondsString === void 0 || minutesString === void 0) {
					(0, logger_1.trace)("secondsString, minutesString undefined");
					return;
				}
				secondsString = secondsString.substring(0, secondsString.length - 3);
				minutesString = minutesString.substring(0, minutesString.length - 3);
				let length = parseInt(minutesString) * 60 + parseInt(secondsString);
				const fullItem = {
					...common,
					length,
					screenshots
				};
				(0, logger_1.trace)("fullItem", fullItem);
				return fullItem;
			} catch (e) {
				console.error(e);
				return;
			}
		}
		function extractVideoItem(itemNode) {
			const common = (0, common_1.getCommonItemDetails)(itemNode);
			(0, logger_1.trace)("common", common);
			if (common === void 0) return;
			const durationText = (0, common_1.getDurationOrImageCountText)(itemNode, itemNode);
			const durationSplit = durationText.split(":");
			try {
				let secondsString = durationSplit.pop();
				let minutesString = durationSplit.pop();
				(0, logger_1.trace)("durationText", durationText, "durationSplit", durationSplit);
				(0, logger_1.trace)("secondsString", secondsString, "minutesString", minutesString);
				if (secondsString === void 0 || minutesString === void 0) return;
				secondsString = secondsString.substring(0, secondsString.length - 1);
				minutesString = minutesString.substring(0, minutesString.length - 1);
				let length = parseInt(minutesString) * 60 + parseInt(secondsString);
				return {
					...common,
					length
				};
			} catch (e) {
				console.error(e);
				return;
			}
		}
	})))();
	async function processVideoList(rootNode) {
		let result = (0, import_videos.extractVideoList)(rootNode);
		if (result.length === 0) return;
		(0, import_logger.log)("Pushing video list...", result);
		const webResult = await GM_xmlhttpRequestPromise({
			url: `${await _GM_getValue("hostname", HOSTNAME)}/api/bh/video_items`,
			method: "POST",
			data: JSON.stringify({ items: result })
		});
		(0, import_logger.log)("Video list result: ", webResult);
	}
	async function processSingleVideo(rootNode) {
		let result = (0, import_videos.extractSingleVideo)(rootNode, window.location.pathname);
		(0, import_logger.log)("Pushing single video...", result);
		const webResult = await GM_xmlhttpRequestPromise({
			url: `${await _GM_getValue("hostname", HOSTNAME)}/api/bh/video_info`,
			method: "POST",
			data: JSON.stringify(result)
		});
		(0, import_logger.log)("Single video result: ", webResult);
	}

function waitForVideo() {
		return new Promise((resolve, reject) => {
			const timeout = setTimeout(() => {
				reject( new Error("Video did not load within 10 seconds"));
			}, 1e4);
			const checkVideo = () => {
				const video = document.querySelector("video");
				if (video && video.src) {
					clearTimeout(timeout);
					console.log("Video Downloader: Video loaded successfully");
					resolve();
				} else setTimeout(checkVideo, 500);
			};
			checkVideo();
		});
	}
	async function loadVideo() {
		const fpUiElement = document.querySelector(".fp-ui");
		if (fpUiElement) {
			fpUiElement.click();
			console.log("Video Downloader: Clicked fp-ui element");
			await waitForVideo();
		} else {
			if (document.querySelector("video")) return;
			throw new Error("Video player not found. Make sure you are on a video page.");
		}
	}
	async function handleVideoDownload() {
		try {
			console.log("newnew");
			await loadVideo();
		} catch {
			let slashSplit = window.location.pathname.replace(/\/$/, "").split("/");
			slashSplit.pop();
			const id = slashSplit.pop();
			if (id === void 0) throw new Error("Unable to get ID from title, make sure the page is correct.");
			let contentUrl = (await _GM_xmlhttpRequest({
				url: `${await _GM_getValue("hostname", HOSTNAME)}/api/bh/get_download`,
				responseType: "json",
				method: "POST",
				data: JSON.stringify({ id })
			})).response;
			if (contentUrl === void 0 || !contentUrl.success) throw new Error("Unable to get contentUrl, make sure server is correct.");
			var vLink = document.createElement("a");
			vLink.setAttribute("href", contentUrl.link);
			vLink.setAttribute("download", `${id}.mp4`);
			vLink.click();
			return;
		}
		let video = document.querySelector("video");
		if (!video || !video.src) {
			video = document.querySelector("source");
			if (!video || !video.src) throw new Error("Video source not found. Make sure the video is loaded first.");
		}
		const filename = getFilenameFromUrl(document.URL);
		await downloadVideo(video.src, filename);
	}
	function getFilenameFromUrl(url) {
		try {
			let split = url.split("/");
			console.log(split[split.length - 3]);
			return `${split[split.length - 3]}.mp4`;
		} catch {
			try {
				const filename = new URL(url).pathname.split("/").pop();
				if (!filename || filename === "" || !filename.includes(".")) return `video-${( new Date()).toISOString().replace(/[:.]/g, "-")}.mp4`;
				return filename;
			} catch (error) {
				console.error("Video Downloader: Error parsing URL:", error);
				return `video-${( new Date()).toISOString().replace(/[:.]/g, "-")}.mp4`;
			}
		}
	}
	async function downloadVideo(url, filename) {
		try {
			console.log("Video Downloader: Opening video URL...");
			console.log("Video Downloader: URL:", url);
			console.log("Video Downloader: Filename:", filename);
			if (!url || url === "") throw new Error("Video URL is empty or invalid");
			if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("blob:")) throw new Error("Video URL is not a valid HTTP/HTTPS/blob URL: " + url);
			console.log("Video Downloader: Opening video URL in new tab...");
			try {
				console.log("Video Downloader: Auto-downloading MP4...");
				console.log("Video Downloader: URL:", url);
				console.log("Video Downloader: Filename:", filename);
				_GM_download({
					url,
					name: filename,
					onerror: (e) => console.error("download error", e),
					ontimeout: () => console.error("download timeout"),
					onload: () => console.error("download load")
				});
				console.log("Video Downloader: Auto-download started for", filename);
			} catch (error) {
				console.error("Video Downloader: Auto-download failed:", error);
			}
			console.log("Video Downloader: Video URL opened successfully");
		} catch (error) {
			console.error("Video Downloader: Error opening video URL:", {
				error,
				url,
				filename
			});
			throw error;
		}
	}

async function handleAlbumDownload() {
		const titleElement = document.querySelector(".headline");
		const cleanTitle = (titleElement ? titleElement.innerText.trim() : "album").replace(/[<>:"/\\|?*]/g, "_").substring(0, 100);
		const imagesContainer = document.querySelector(".images");
		if (!imagesContainer) throw new Error("Images container not found");
		const imageUrls = [...imagesContainer.children].map((child) => child.href).filter((href) => href);
		if (imageUrls.length === 0) throw new Error("No images found in album");
		console.log(`Video Downloader: Found ${imageUrls.length} images in album`);
		await downloadAlbumAsZip(imageUrls, cleanTitle);
	}
	async function downloadAlbumAsZip(imageUrls, albumTitle) {
		try {
			console.log("Video Downloader: Starting album download...");
			console.log("Video Downloader: Album title:", albumTitle);
			console.log("Video Downloader: Image count:", imageUrls.length);
			if (typeof JSZip === "undefined") throw new Error("JSZip library is not available. Please reload the extension.");
			console.log("Video Downloader: JSZip library ready, creating ZIP...");
			const zip = new JSZip();
			let downloadedCount = 0;
			const progressDiv = document.createElement("div");
			progressDiv.id = "download-progress";
			progressDiv.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: #333;
            color: white;
            padding: 15px;
            border-radius: 5px;
            z-index: 10000;
            font-family: Arial, sans-serif;
        `;
			progressDiv.innerHTML = `Downloading album... 0/${imageUrls.length}`;
			document.body.appendChild(progressDiv);
			for (let i = 0; i < imageUrls.length; i++) try {
				const imageUrl = imageUrls[i];
				console.log(`Video Downloader: Downloading image ${i + 1}/${imageUrls.length}: ${imageUrl}`);
				const response = await fetch(imageUrl, {
					method: "GET",
					mode: "cors",
					credentials: "omit",
					headers: { "Accept": "image/*" }
				});
				if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
				const blob = await response.blob();
				console.log(`Video Downloader: Image ${i + 1} blob size: ${blob.size} bytes, type: ${blob.type}`);
				let extension = ".jpg";
				if (imageUrl.includes(".png")) extension = ".png";
				else if (imageUrl.includes(".gif")) extension = ".gif";
				else if (imageUrl.includes(".webp")) extension = ".webp";
				else if (blob.type.includes("png")) extension = ".png";
				else if (blob.type.includes("gif")) extension = ".gif";
				else if (blob.type.includes("webp")) extension = ".webp";
				const filename = `image_${String(i + 1).padStart(3, "0")}${extension}`;
				const arrayBuffer = await blob.arrayBuffer();
				zip.file(filename, arrayBuffer);
				downloadedCount++;
				progressDiv.innerHTML = `Downloading album... ${downloadedCount}/${imageUrls.length}`;
				console.log(`Video Downloader: Added ${filename} to ZIP`);
			} catch (error) {
				console.warn(`Failed to download image ${i + 1}:`, error);
			}
			progressDiv.innerHTML = "Creating ZIP file...";
			const zipBlob = await zip.generateAsync({ type: "blob" });
			const downloadUrl = URL.createObjectURL(zipBlob);
			const a = document.createElement("a");
			a.href = downloadUrl;
			a.download = `${albumTitle}.zip`;
			a.style.display = "none";
			document.body.appendChild(a);
			a.click();
			document.body.removeChild(a);
			URL.revokeObjectURL(downloadUrl);
			document.body.removeChild(progressDiv);
			console.log("Video Downloader: Album download completed");
		} catch (error) {
			console.error("Video Downloader: Album download failed:", error);
			throw new Error("Failed to download album, see console for details");
		}
	}

var import_messages = ( __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.extractConversations = extractConversations;
		exports.extractMessages = extractMessages;
		var bh_1 = require_bh();
		var utils_1 = require_utils();
		var logger_1 = require_logger();
		function extractUsers(rootNode) {
			const userId = window.location.pathname?.replace(/\/$/, "").split("/").pop();
			if (userId === void 0) return;
			const username = rootNode.find(".main-container-user").first().find(".headline").first().find("a").eq(1).text().trim();
			const messages = rootNode.find(".item:not(.me):not(.grouped)");
			let avatarUrl = void 0;
			messages.each((_, item) => {
				if (avatarUrl !== void 0) return;
				let foundUrl = item.find(".image").first().find("img").first().attr("src");
				if (foundUrl !== void 0) {
					avatarUrl = foundUrl;
					return;
				}
			});
			const memberMenu = rootNode.find(".member-menu").first();
			const meAvatarUrl = memberMenu.find("img").first().attr("src");
			if (meAvatarUrl === void 0) return;
			const meUsername = memberMenu.find("img").first().attr("alt");
			if (meUsername === void 0) return;
			const dotSplit = meAvatarUrl.split(".");
			dotSplit.pop();
			const meId = dotSplit.pop()?.replace(/\/$/, "").split("/").pop();
			if (meId === void 0) return;
			const result = {
				user: {
					name: username,
					url: `https://www.boundhub.com/members/${userId}`,
					id: userId,
					avatarUrl: avatarUrl === bh_1.BLANK_IMAGE ? void 0 : avatarUrl
				},
				me: {
					name: meUsername,
					url: `https://www.boundhub.com/members/${meId}`,
					id: meId,
					avatarUrl: meAvatarUrl === bh_1.BLANK_IMAGE ? void 0 : meAvatarUrl
				}
			};
			(0, logger_1.trace)("extractUsers", result);
			return result;
		}
		async function extractConversations(rootNode) {
			let result = [];
			for (const item of rootNode.find(".item").toArray()) {
				const extracted = await extractConversationItem(item);
				if (extracted !== void 0) result.push(extracted);
			}
			(0, logger_1.trace)("extractConversations", result);
			return result;
		}
		async function extractConversationItem(item) {
			const a = item.find("a").first();
			const messageUrl = a.attr("href");
			const userId = messageUrl?.replace(/\/$/, "").split("/").pop();
			const id = userId;
			if (id === void 0 || messageUrl === void 0) return;
			const userUrl = `https://www.boundhub.com/members/${userId}`;
			const username = a.attr("title") || a.find(".title").first().text().trim();
			const avatar = a.find("img").first();
			let avatarUrl = void 0;
			if (avatar !== void 0) {
				if (avatar.complete) avatarUrl = $(avatar).attr("src");
				else await new Promise((resolve, reject) => {
					const timer = setTimeout(() => {
						reject( new Error(`Failed to wait for pic loading`));
					}, 3e4);
					avatar.addEventListener("load", function() {
						clearTimeout(timer);
						return resolve();
					});
					if (avatar.complete) {
						clearTimeout(timer);
						return resolve();
					}
				}).catch(() => null);
			}
			const relativeDateString = (0, utils_1.processRelativeDate)(a.find(".added").first().text().trim()).filtered;
			const messageCountString = a.find(".views").first().text().trim();
			return {
				id,
				url: messageUrl,
				user: {
					name: username,
					url: userUrl,
					id: userId,
					avatarUrl: avatarUrl === bh_1.BLANK_IMAGE ? void 0 : avatarUrl
				},
				messageCount: (0, utils_1.stringOrInt)(messageCountString),
				relativeDateString,
				dateExtracted: new Date()
			};
		}
		function extractMessages(rootNode) {
			const mainNode = rootNode.find(".main-content").first();
			if (mainNode === void 0) {
				console.warn("Failed to find main-content");
				return [];
			}
			const users = extractUsers(mainNode);
			if (users === void 0) {
				console.warn("Failed to find users");
				return [];
			}
			let result = [];
			rootNode.find(".item").each((_, item) => {
				const extracted = extractMessageItem(item, users.user, users.me);
				if (extracted !== void 0) result.push(extracted);
			});
			(0, logger_1.trace)("extractMessages", result);
			return result;
		}
		function extractMessageItem(item, user, me) {
			let id = item.attr("data-message-id");
			if (id === void 0) {
				id = item.find(".added").first().attr("data-message-id");
				if (id === void 0) return;
			}
			let originalText = item.find(".message-text");
			let content = "";
			if (originalText.find(".original-text").length > 0) {
				originalText.find(".original-text").first().find("img").each((_, item) => {
					item.replaceWith(`${item.attr("alt")}`);
				});
				content = originalText.find(".original-text").first().text().trim();
			} else content = originalText.text().trim();
			let relativeDateString = (0, utils_1.processRelativeDate)($(item).find(".added").first().text().trim(), ["(unread)"]).filtered;
			let isMe = $(item).hasClass("me");
			return {
				id,
				from: isMe ? me : user,
				to: isMe ? user : me,
				relativeDateString,
				dateExtracted: new Date(),
				content
			};
		}
	})))();
	async function processConversations(rootNode) {
		let result = await (0, import_messages.extractConversations)(rootNode);
		(0, import_logger.log)("Pushing conversations...", result);
		const webResult = await GM_xmlhttpRequestPromise({
			url: `${await _GM_getValue("hostname", HOSTNAME)}/api/bh/conversations`,
			method: "POST",
			data: JSON.stringify({ items: result })
		});
		(0, import_logger.log)("Conversation result: ", webResult);
	}
	async function processMessages(rootNode) {
		let result = await (0, import_messages.extractMessages)(rootNode);
		(0, import_logger.log)("Pushing messages...", result);
		const webResult = await GM_xmlhttpRequestPromise({
			url: `${await _GM_getValue("hostname", HOSTNAME)}/api/bh/messages`,
			method: "POST",
			data: JSON.stringify({ items: result })
		});
		(0, import_logger.log)("Conversation result: ", webResult);
	}

var import_utils = require_utils();
	function getSettingInput(value) {
		const id = `${PREFIX}-${value.key}`;
		return `
    <div class="row">
        <label for="${id}" class="field-label">${value.title}</label>
        ${value.type === "string" ? `<input type="text" name="${id}" id="${id}" class="textfield" value="${_GM_getValue(value.key, value.default)}" maxlength="253" placeholder="${value.title}">` : `<input type="checkbox" class="checkbox" id="${id}" name="${id}" ${_GM_getValue(value.key, value.default) ? "checked" : ""}>`}
    </div>
    `;
	}
	function initSettings() {
		$("body").append(`
        <dialog class="${PREFIX}-settings-dialog">
            <div class="center-content">
                <strong class="popup-title">Better BH Settings</strong>
                <div class="dialog-content">
                    <form id="${PREFIX}-settings-form" method="dialog">
                        ${settingValues.map((value) => getSettingInput(value))}
                    </form>
                </div>
                <a title="Close" class="${PREFIX}-close-settings fancybox-item fancybox-close" href="javascript:;"></a>
            </div>
        </dialog>
    `);
		setDialogOpen(false);
		$(`.${PREFIX}-close-settings`).on("click", () => {
			settingValues.forEach((settingValue) => {
				const id = `${PREFIX}-${settingValue.key}`;
				if (settingValue.type === "string") {
					const value = $(`#${id}`).val();
					console.log(settingValue.key, value);
					_GM_setValue(settingValue.key, value);
				} else {
					const value = $(`#${id}`).is(":checked");
					console.log(settingValue.key, value);
					_GM_setValue(settingValue.key, value);
				}
				syncSettings();
			});
			setDialogOpen(false);
		});
		syncSettings();
	}
	function syncSettings() {
		(0, import_logger.setDebug)(getBooleanSetting(SettingsKey.DEBUG));
	}
	function setDialogOpen(open) {
		if (open) $(`.${PREFIX}-settings-dialog`).show();
		else $(`.${PREFIX}-settings-dialog`).hide();
	}
	function getBooleanSetting(key) {
		return Boolean(_GM_getValue(key, settingDefaults[key]));
	}

var import_albums = ( __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.extractAlbumList = extractAlbumList;
		exports.extractSingleAlbum = extractSingleAlbum;
		var bh_1 = require_bh();
		var utils_1 = require_utils();
		var common_1 = require_common();
		var logger_1 = require_logger();
		function extractAlbumList(rootNode) {
			let result = [];
			rootNode.find(".item").each((_, item) => {
				const extracted = extractAlbumItem(item);
				if (extracted !== void 0) result.push(extracted);
			});
			(0, logger_1.trace)("extractAlbumList", result);
			if (result.length === 0) return [];
			return result;
		}
		function extractSingleAlbum(rootNode, pathname) {
			(0, logger_1.trace)("extractSingleAlbum");
			const common = (0, common_1.getCommonDetails)(rootNode, bh_1.albumCommentId, bh_1.albumContainerIds.related, pathname);
			if (common === void 0) return;
			const imageCountText = (0, common_1.getDurationOrImageCountText)(rootNode);
			const screenshots = (0, common_1.getScreenshots)(rootNode, ".images");
			(0, logger_1.log)("Basic extracting done");
			try {
				let imageCount = (0, utils_1.stringOrInt)(imageCountText);
				const fullItem = {
					...common,
					imageCount,
					images: screenshots
				};
				(0, logger_1.trace)("fullItem", fullItem);
				return fullItem;
			} catch (e) {
				console.error(e);
				return;
			}
		}
		function extractAlbumItem(itemNode) {
			const common = (0, common_1.getCommonItemDetails)(itemNode);
			if (common === void 0) return;
			const imageCountSplit = (0, common_1.getDurationOrImageCountText)(itemNode, itemNode).split("photo");
			try {
				let imageCount = (0, utils_1.stringOrInt)(imageCountSplit[0]);
				return {
					...common,
					imageCount
				};
			} catch (e) {
				console.error(e);
				return;
			}
		}
	})))();
	async function processAlbumList(rootNode) {
		let result = (0, import_albums.extractAlbumList)(rootNode);
		if (result.length === 0) return;
		(0, import_logger.log)("Pushing album list...", result);
		const webResult = await GM_xmlhttpRequestPromise({
			url: `${await _GM_getValue("hostname", HOSTNAME)}/api/bh/album_items`,
			method: "POST",
			data: JSON.stringify({ items: result })
		});
		(0, import_logger.log)("Album list result: ", webResult);
	}
	async function processSingleAlbum(rootNode) {
		let result = (0, import_albums.extractSingleAlbum)(rootNode, window.location.pathname);
		(0, import_logger.log)("Pushing single album...", result);
		const webResult = await GM_xmlhttpRequestPromise({
			url: `${await _GM_getValue("hostname", HOSTNAME)}/api/bh/album_info`,
			method: "POST",
			data: JSON.stringify(result)
		});
		(0, import_logger.log)("Single album result: ", webResult);
	}

var import_jquery = ( __commonJSMin(((exports) => {
		Object.defineProperty(exports, "__esModule", { value: true });
		exports.JQueryAdapter = void 0;
		exports.JQueryAdapter = class JQueryAdapter {
			$el;
			constructor($el) {
				this.$el = $el;
			}
			find(selector) {
				return new JQueryAdapter(this.$el.find(selector));
			}
			first() {
				return new JQueryAdapter(this.$el.first());
			}
			replaceWith(content) {
				return new JQueryAdapter(this.$el.replaceWith(content));
			}
			text() {
				return this.$el.text();
			}
			html() {
				return this.$el.html() || "";
			}
			attr(name) {
				return this.$el.attr(name);
			}
			toArray() {
				return this.$el.map((_, el) => new JQueryAdapter($(el))).get();
			}
			map(callback) {
				const results = [];
				this.$el.each((i, el) => {
					results.push(callback(i, new JQueryAdapter($(el))));
				});
				return results;
			}
			each(callback) {
				this.$el.each((i, el) => callback(i, new JQueryAdapter($(el))));
				return this;
			}
			get(index) {
				if (index === void 0) return this.$el.get(0);
				return this.$el.get(index);
			}
			eq(index) {
				if (index === void 0) return new JQueryAdapter(this.$el.eq(0));
				return new JQueryAdapter(this.$el.eq(index));
			}
			get complete() {
				return this.$el.get(0)?.complete || false;
			}
			get length() {
				return this.$el.length;
			}
			addEventListener(event, handler) {
				this.$el.each((_i, el) => {
					el.addEventListener(event, handler);
				});
			}
		};
	})))();
	var import_common = require_common();
	function segmentMatches(pathname) {
		let keys = [];
		for (const key of Object.keys(pageDefinitions)) {
			const pageDefinition = pageDefinitions[key];
			let matches = false;
			for (const matcher of pageDefinition.segmentMatch) if (typeof matcher === "string") {
				if (matcher.length <= 1) {
					if (pathname === (matcher.length === 0 ? "/" : matcher)) {
						matches = true;
						break;
					}
				} else if (pathname.includes(matcher)) {
					matches = true;
					break;
				}
			} else if (pathname.match(matcher)) {
				matches = true;
				break;
			}
			if (matches) keys.push(key);
		}
		return keys;
	}
	function isSupportedPage(pathname) {
		for (const pageDefinition of Object.values(pageDefinitions)) {
			let matches = false;
			for (const matcher of pageDefinition.segmentMatch) if (typeof matcher === "string") {
				if (matcher.length <= 1) {
					if (pathname === (matcher.length === 0 ? "/" : matcher)) {
						matches = true;
						break;
					}
				} else if (pathname.includes(matcher)) {
					matches = true;
					break;
				}
			} else if (pathname.match(matcher)) {
				matches = true;
				break;
			}
			if (matches) return matches;
		}
		return false;
	}
	function isCorrectDomain() {
		const hostname = window.location.hostname;
		return hostname.includes("boundhub.com") || hostname.includes("www.boundhub.com");
	}
	function isSingleVideo() {
		const singleVideo = document.getElementById(import_bh.singleVideoId);
		return singleVideo !== void 0 && singleVideo !== null;
	}
	function isSingleAlbum() {
		const singleAlbum = document.getElementById(import_bh.singleAlbumId);
		return singleAlbum !== void 0 && singleAlbum !== null;
	}
	async function processContainer(container) {
		await (0, import_utils.delay)(DELAY);
		const rawNode = document.getElementById(container.id);
		if (rawNode === void 0 || rawNode === null) {
			console.log("Container ID isnt on page:", container.id);
			return;
		}
		const node = $(rawNode);
		if (container.type === ContainerType.ALBUM) {
			console.log("Processing ALBUM:", container.id);
			processAlbumList(new import_jquery.JQueryAdapter(node));
			const singleAlbum = document.getElementById(import_bh.singleAlbumId);
			if (singleAlbum !== void 0 && singleAlbum !== null) {
				const rawContentNode = document.getElementById(import_bh.siteContentId);
				if (rawContentNode === void 0 || rawContentNode === null) {
					console.log("Container ID isnt on page:", import_bh.siteContentId);
					return;
				}
				processSingleAlbum(new import_jquery.JQueryAdapter($(rawContentNode)));
			}
		} else if (container.type === ContainerType.VIDEO) {
			console.log("Processing VIDEO:", container.id);
			processVideoList(new import_jquery.JQueryAdapter(node));
			const singleVideo = document.getElementById(import_bh.singleVideoId);
			if (singleVideo !== void 0 && singleVideo !== null) {
				const rawContentNode = document.getElementById(import_bh.siteContentId);
				if (rawContentNode === void 0 || rawContentNode === null) {
					console.log("Container ID isnt on page:", import_bh.siteContentId);
					return;
				}
				processSingleVideo(new import_jquery.JQueryAdapter($(rawContentNode)));
			}
		} else if (container.type === ContainerType.CATEGORY) {
			console.log("Processing CATEGORY:", container.id);
			processCategories(new import_jquery.JQueryAdapter(node));
		} else if (container.type === ContainerType.MESSAGES) {
			console.log("Processing MESSAGES:", container.id);
			processMessages(new import_jquery.JQueryAdapter(node));
		} else if (container.type === ContainerType.CONVERSATIONS) {
			console.log("Processing CONVERSATIONS:", container.id);
			processConversations(new import_jquery.JQueryAdapter(node));
		} else console.log("Unknown container id type");
	}
	function createDownloadButton(buttonText = "Download") {
		createSinglePageButton(buttonText, "video-downloader-btn", handleDownloadClick, ["download-button"]);
	}
	function createCopyIdButton() {
		createSinglePageButton("Copy ID", "copy-id-btn", async (event) => {
			const id = (0, import_common.getPathnameVariables)(window.location.pathname).id;
			if (id === void 0) {
				console.warn("Unable to determine id from pathname", window.location.pathname);
				return;
			}
			await navigator.clipboard.writeText(id);
			const element = event.target;
			if (element !== void 0) {
				$(element).text("Copied");
				$(element).addClass("active");
				setTimeout(() => {
					$(element).text("Copy ID");
					$(element).removeClass("active");
				}, 1e3);
			}
		});
	}
	function createSinglePageButton(buttonText, buttonId, onClick, classes) {
		if (document.getElementById(buttonId)) return;
		const tabsMenu = document.querySelector(".tabs-menu ul");
		if (!tabsMenu) {
			console.log("Better BH: Tabs menu not found, retrying...", buttonId);
			setTimeout(() => createSinglePageButton(buttonText, buttonId, onClick, classes), 1e3);
			return;
		}
		const newBtn = document.createElement("li");
		newBtn.id = buttonId;
		newBtn.innerHTML = `<a href="#" class="toggle-button ${classes === void 0 ? "" : classes.join(" ")}">${buttonText}</a>`;
		tabsMenu.insertBefore(newBtn, tabsMenu.firstChild);
		const anchor = newBtn.querySelector("a");
		if (anchor === null) {
			console.warn("Failed to create download button");
			return;
		}
		anchor.addEventListener("click", onClick);
		console.log(`Better BH: ${buttonText} button added`);
	}
	async function handleDownloadClick(event) {
		event.preventDefault();
		$(".download-button").addClass("disable-click");
		try {
			if (isSingleVideo()) await handleVideoDownload();
			else if (isSingleAlbum()) await handleAlbumDownload();
			$(".download-button").removeClass("disable-click");
		} catch (error) {
			console.error("Video Downloader Error:", error);
			alert("Download failed, see console for details");
		}
	}
	function init() {
		console.log("Scraper: init");
		if (isCorrectDomain() && isSupportedPage(window.location.pathname)) {
			const definitions = segmentMatches(window.location.pathname);
			for (const key of definitions) {
				const definition = pageDefinitions[key];
				for (const container of definition.containerIds) processContainer(container);
			}
			if (isSingleVideo() || isSingleAlbum()) {
				createDownloadButton("Download");
				createCopyIdButton();
			}
		} else if (!isCorrectDomain()) console.log("Scraper: Not on correct domain, skipping");
		else console.log("Scraper: Not a supported page type, skipping");
		const network = $(".top-links").first().find(".network").first();
		$(network).find("ul li").each((index, item) => {
			if (index === 0) {
				let a = $(item).find("a");
				$(a).attr("target", "").attr("href", "javascript:void(0)").text("Better BH Settings").attr("id", "better-bh-settings-open").on("click", () => {
					setDialogOpen(true);
				});
			} else $(item).html("");
		});
		initSettings();
	}
	(async () => {
		if (document.readyState === "loading") {
			console.log("Scraper: DOMContentLoaded");
			document.addEventListener("DOMContentLoaded", () => {
				init();
			});
		} else init();
		new MutationObserver((mutations) => {
			mutations.forEach(async (mutation) => {
				if (mutation.type === "childList") for (const entry of mutation.addedNodes) {
					await (0, import_utils.delay)(DELAY);
					const id = $(entry).attr("id");
					if (id === void 0) return;
					const rawNode = document.getElementById(id);
					if (rawNode === void 0 || rawNode === null) return;
					const node = $(rawNode);
					if (Object.values(import_bh.videoContainerIds).includes(id)) {
						console.log("Scraper: video found", id);
						processVideoList(new import_jquery.JQueryAdapter(node));
					} else if (Object.values(import_bh.categoryContainerIds).includes(id)) {
						console.log("Scraper: category found", id);
						processCategories(new import_jquery.JQueryAdapter(node));
					} else if (Object.values(import_bh.messagesContainerIds).includes(id)) {
						console.log("Scraper: messages found", id);
						processMessages(new import_jquery.JQueryAdapter(node));
					} else if (Object.values(import_bh.conversationsContainerIds).includes(id)) {
						console.log("Scraper: conversations found", id);
						processConversations(new import_jquery.JQueryAdapter(node));
					} else if (Object.values(import_bh.albumContainerIds).includes(id)) {
						console.log("Scraper: album found", id);
						processAlbumList(new import_jquery.JQueryAdapter(node));
					}
				}
			});
		}).observe(document.body, {
			childList: true,
			subtree: true
		});
		_GM_addStyle(`
        ${getBooleanSetting(SettingsKey.HIDE_ADS) ? `
        .ta4ble, .spo8nsor, .to3op, .p8lace {
            display: none;
        }
        ` : ""}
        ${getBooleanSetting(SettingsKey.HIDE_SHARE) ? `
        .tabs-menu ul li:nth-last-child(2) {
            display: none;
        }
        ` : ""}
        ${getBooleanSetting(SettingsKey.UNHIDE_PRIVATE) ? `
        .item.private .thumb {
            opacity: 1 !important;
        }
        ` : ""}
        .disable-click {
            pointer-events: none;
        }
        .${PREFIX}-settings-dialog {
            z-index: 100000;
            position: absolute;
            top: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            width: 100%;
            background: none;
            background-color: rgba(17, 13, 13, 0.65);

            .center-content {
                position: relative;
                max-width: 600px;
                box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
                border-radius: 4px;

                .popup-title {
                    width: 100%;
                }
            }

            .dialog-content {
                position: relative;
                background-color: #2c2c2c;
                padding: 20px;
                width: 560px;
                border-radius: 4px;
                color: #444;
            }
        }
    `);
	})();
})(JSZip);