better-bh

Adds useful QoL features when browsing boundhub

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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