✨ Add comic filter for Bex and Keit
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 2 Oct 2025 13:18:11 +0000 (15:18 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Thu, 2 Oct 2025 13:18:11 +0000 (15:18 +0200)
src/main/kotlin/net/pterodactylus/rhynodge/filters/comics/BexAndKeitComicFilter.kt [new file with mode: 0644]
src/main/kotlin/net/pterodactylus/rhynodge/watchers/BexAndKeitWatcher.kt [new file with mode: 0644]
src/test/kotlin/net/pterodactylus/rhynodge/filters/comics/BexAndKeitComicFilterTest.kt [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/filters/comics/bex-and-keit.html [new file with mode: 0644]

diff --git a/src/main/kotlin/net/pterodactylus/rhynodge/filters/comics/BexAndKeitComicFilter.kt b/src/main/kotlin/net/pterodactylus/rhynodge/filters/comics/BexAndKeitComicFilter.kt
new file mode 100644 (file)
index 0000000..1d7c241
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.rhynodge.filters.comics
+
+import com.google.common.base.Optional
+import com.google.common.base.Optional.absent
+import net.pterodactylus.rhynodge.filters.ComicSiteFilter
+import org.jsoup.nodes.Document
+
+class BexAndKeitComicFilter : ComicSiteFilter() {
+
+       override fun extractTitle(document: Document): Optional<String> = absent()
+
+       override fun extractImageUrls(document: Document) =
+               document.select(".row")
+                       .takeWhile { !it.attr("class").contains("adsrow") }
+                       .flatMap { it.select("picture.comic-image source:first-child") }
+                       .map { it.attr("srcset") }
+
+       override fun extractImageComments(document: Document) = emptyList<String>()
+
+}
diff --git a/src/main/kotlin/net/pterodactylus/rhynodge/watchers/BexAndKeitWatcher.kt b/src/main/kotlin/net/pterodactylus/rhynodge/watchers/BexAndKeitWatcher.kt
new file mode 100644 (file)
index 0000000..ddcb797
--- /dev/null
@@ -0,0 +1,13 @@
+package net.pterodactylus.rhynodge.watchers
+
+import net.pterodactylus.rhynodge.filters.HtmlFilter
+import net.pterodactylus.rhynodge.filters.comics.BexAndKeitComicFilter
+import net.pterodactylus.rhynodge.mergers.ComicMerger
+import net.pterodactylus.rhynodge.queries.HttpQuery
+
+@Suppress("unused")
+class BexAndKeitWatcher : DefaultWatcher(query, filters, merger)
+
+private val query = HttpQuery("https://krooxworld.com/bex-and-keit")
+private val filters = listOf(HtmlFilter(), BexAndKeitComicFilter())
+private val merger = ComicMerger()
diff --git a/src/test/kotlin/net/pterodactylus/rhynodge/filters/comics/BexAndKeitComicFilterTest.kt b/src/test/kotlin/net/pterodactylus/rhynodge/filters/comics/BexAndKeitComicFilterTest.kt
new file mode 100644 (file)
index 0000000..db1da31
--- /dev/null
@@ -0,0 +1,17 @@
+package net.pterodactylus.rhynodge.filters.comics
+
+import net.pterodactylus.rhynodge.states.ComicState
+
+class BexAndKeitComicFilterTest : ComicSiteFilterTest() {
+
+       override val filter = BexAndKeitComicFilter()
+       override val baseUrl = "https://krooxworld.com/bex-and-keit"
+       override val resource = "bex-and-keit.html"
+       override val expectedComics = listOf(
+               ComicState.Comic("")
+                       .add(ComicState.Strip("https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=672&fit=max", ""))
+                       .add(ComicState.Strip("https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=672&fit=max", ""))
+                       .add(ComicState.Strip("https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=672&fit=max", ""))
+       )
+
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/filters/comics/bex-and-keit.html b/src/test/resources/net/pterodactylus/rhynodge/filters/comics/bex-and-keit.html
new file mode 100644 (file)
index 0000000..5d96c73
--- /dev/null
@@ -0,0 +1,422 @@
+
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    
+    
+    <!-- Google tag (gtag.js) -->
+    <script async src="https://www.googletagmanager.com/gtag/js?id=G-YXQ82S5C0P"></script>
+    <script>
+        window.dataLayer = window.dataLayer || [];
+        function gtag(){dataLayer.push(arguments);}
+        gtag('js', new Date());
+
+        gtag('config', 'G-YXQ82S5C0P');
+    </script>
+    
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
+    <title>Home page - Krooxworld</title>
+    <!-- Bootstrap 5.3.3 CSS -->
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
+    <!-- Lightbox -->
+    <script src="https://cdn.jsdelivr.net/npm/bs5-lightbox@1.8.3/dist/index.bundle.min.js"></script>
+    <link rel="stylesheet" href="/Css/Kroox.css" />
+    <!-- Cookie Banner -->
+
+    <style>
+        #cb-cookie-banner {
+            position: fixed;
+            bottom: 0;
+            left: 0;
+            width: 100%;
+            z-index: 999;
+            border-radius: 0;
+            display: none;
+        }
+    </style>
+</head>
+
+<body>
+
+<div id="contentOverlay"></div>
+
+<!-- Age Verification Modal -->
+<div class="modal fade" id="ageModal" tabindex="-1" aria-labelledby="ageModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-dialog-centered">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title fw-bold" id="ageModalLabel">Age Verification</h5>
+            </div>
+            <div class="modal-body">
+                <p>You must be 18 years or older to enter this site. Please confirm your age.</p>
+            </div>
+            <div class="modal-footer">
+                <button type="button" id="confirmAge" class="btn btn-primary">I am 18 or older</button>
+                <button type="button" id="denyAge" class="btn btn-secondary">I am under 18</button>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div id="cb-cookie-banner" class="alert alert-dark text-center mb-0" role="alert">
+    &#x1F36A; This website uses cookies to ensure you get the best experience on our website.
+    <a href="https://www.cookiesandyou.com/" target="blank">Learn more</a>
+    <button type="button" class="btn btn-primary btn-sm ms-3" onclick="window.cb_hideCookieBanner()">
+        I Got It
+    </button>
+</div>
+
+<!-- Header -->
+<header>
+    <nav class="navbar navbar-expand-lg navbar-light bg-light">
+        <div class="container">
+            <a class="navbar-brand" href="/">Krooxworld</a>
+            <button class="navbar-toggler" type="button" data-bs-toggle="collapse"
+                    data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
+                    aria-expanded="false" aria-label="Toggle navigation">
+                <span class="navbar-toggler-icon"></span>
+            </button>
+            <div class="collapse navbar-collapse" id="navbarSupportedContent">
+                <ul class="navbar-nav me-auto mb-2 mb-lg-0">
+                        <li class="nav-item">
+                            <a class="nav-link" href="https://bsky.app/profile/lekroox.bsky.social">Bluesky</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" href="https://www.patreon.com/c/creux">Patreon</a>
+                        </li>
+                        <li class="nav-item">
+                            <a class="nav-link" href="https://www.krooxworld.com/rss">RSS</a>
+                        </li>
+                </ul>
+
+                <ul class="navbar-nav">
+                    <li class="nav-item">
+                            <a class="nav-link" href="/login">Login</a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </nav>
+</header>
+
+<!-- Main Content -->
+<main>
+    <div class="container py-4">
+        
+
+
+<!-- Tabs for Comics -->
+<ul class="nav nav-tabs comics-nav-tabs">
+        <li class="nav-item">
+            <a class="nav-link active" href="/bex-and-keit">Bex &amp; Keit</a>
+        </li>
+        <li class="nav-item">
+            <a class="nav-link " href="/splatball">Splatball</a>
+        </li>
+</ul>
+
+<div class="tab-content comics-content">
+    <div id="comicContent">
+        <div class="row">
+            <div class="comic-navigation">
+
+
+                <a href="/bex-and-keit/artist" class="">
+                    <i class="fa fa-step-backward"></i>
+                </a>
+                <a href="/bex-and-keit/sentient-bikini" class="">
+                    <i class="fa fa-chevron-left"></i>
+                </a>
+                <a href="http://www.topwebcomics.com/vote/28260"
+                   title="Vote for Bex & Keit on TopWebComics!"
+                   style="display: inline-block; vertical-align: middle; margin-top: -13px;">
+                    <img src="http://www.topwebcomics.com//vote_link.php?t=cubes&s=28260" alt="vote">
+                </a>
+                <a class="nav-disabled">
+                    <i class="fa fa-chevron-right"></i>
+                </a>
+                <a href="/bex-and-keit/bonktober-1-tomboy" class="nav-disabled">
+                    <i class="fa fa-step-forward"></i>
+                </a>
+            </div>
+        </div>
+
+                <div class="row">
+                    <div class="col-sm-12">
+                        <div class="image-row">
+                            <picture class="img-fluid comic-image">
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=672&fit=max"
+                                    media="(min-width: 600px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=636&fit=max"
+                                    media="(min-width: 550px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=586&fit=max"
+                                    media="(min-width: 500px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=536&fit=max"
+                                    media="(min-width: 450px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=486&fit=max"
+                                    media="(min-width: 400px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=436&fit=max"
+                                    media="(min-width: 350px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=386&fit=max"
+                                    media="(min-width: 300px)"
+                                />
+                                <img
+                                    src="https://cdn.sanity.io/images/4h3kafsm/production/fbf3f2a13434998df21b658b536e501f86af6224-1109x1399.jpg?w=336&fit=max"
+                                    alt="Sample"
+                                    class="img-fluid comic-image"
+                                />
+                            </picture>
+                        </div>
+                    </div>
+                </div>
+                <div class="row">
+                    <div class="col-sm-12">
+                        <div class="image-row">
+                            <picture class="img-fluid comic-image">
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=672&fit=max"
+                                    media="(min-width: 600px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=636&fit=max"
+                                    media="(min-width: 550px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=586&fit=max"
+                                    media="(min-width: 500px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=536&fit=max"
+                                    media="(min-width: 450px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=486&fit=max"
+                                    media="(min-width: 400px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=436&fit=max"
+                                    media="(min-width: 350px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=386&fit=max"
+                                    media="(min-width: 300px)"
+                                />
+                                <img
+                                    src="https://cdn.sanity.io/images/4h3kafsm/production/8bb1efb285d5fa42e79c6dd6ebdb165e7a5c3229-1109x1399.jpg?w=336&fit=max"
+                                    alt="Sample"
+                                    class="img-fluid comic-image"
+                                />
+                            </picture>
+                        </div>
+                    </div>
+                </div>
+                <div class="row">
+                    <div class="col-sm-12">
+                        <div class="image-row">
+                            <picture class="img-fluid comic-image">
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=672&fit=max"
+                                    media="(min-width: 600px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=636&fit=max"
+                                    media="(min-width: 550px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=586&fit=max"
+                                    media="(min-width: 500px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=536&fit=max"
+                                    media="(min-width: 450px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=486&fit=max"
+                                    media="(min-width: 400px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=436&fit=max"
+                                    media="(min-width: 350px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=386&fit=max"
+                                    media="(min-width: 300px)"
+                                />
+                                <img
+                                    src="https://cdn.sanity.io/images/4h3kafsm/production/65a2d010ba31e5e4647c3594bfb3beb59fcf7a87-1109x1399.jpg?w=336&fit=max"
+                                    alt="Sample"
+                                    class="img-fluid comic-image"
+                                />
+                            </picture>
+                        </div>
+                    </div>
+                </div>
+                <div class="row adsrow">
+
+                        <h3>Replace these ads with a bonus panel by becoming a
+                            <a class="login-link"
+                               href="/login">Patreon!</a>
+                        </h3>
+
+                    <div class="ad-container">
+
+                        <script type="text/javascript" src="https://www.comicad.net/r/pzTuMfXSqG/"></script>
+
+
+                    </div>
+
+
+                    <p>Bonus panels are never required reading to understand the free content, in fact sometimes bonus
+                        panels ruins the joke!</p>
+                </div>
+                <div class="row">
+                    <div class="col-sm-12">
+                        <div class="image-row">
+                            <picture class="img-fluid comic-image">
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/513190da4e7c4d4546296180c99a81ef24f1411c-1109x1399.jpg?w=672&fit=max"
+                                    media="(min-width: 600px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/513190da4e7c4d4546296180c99a81ef24f1411c-1109x1399.jpg?w=636&fit=max"
+                                    media="(min-width: 550px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/513190da4e7c4d4546296180c99a81ef24f1411c-1109x1399.jpg?w=586&fit=max"
+                                    media="(min-width: 500px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/513190da4e7c4d4546296180c99a81ef24f1411c-1109x1399.jpg?w=536&fit=max"
+                                    media="(min-width: 450px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/513190da4e7c4d4546296180c99a81ef24f1411c-1109x1399.jpg?w=486&fit=max"
+                                    media="(min-width: 400px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/513190da4e7c4d4546296180c99a81ef24f1411c-1109x1399.jpg?w=436&fit=max"
+                                    media="(min-width: 350px)"
+                                />
+                                <source
+                                    srcset="https://cdn.sanity.io/images/4h3kafsm/production/513190da4e7c4d4546296180c99a81ef24f1411c-1109x1399.jpg?w=386&fit=max"
+                                    media="(min-width: 300px)"
+                                />
+                                <img
+                                    src="https://cdn.sanity.io/images/4h3kafsm/production/513190da4e7c4d4546296180c99a81ef24f1411c-1109x1399.jpg?w=336&fit=max"
+                                    alt="Sample"
+                                    class="img-fluid comic-image"
+                                />
+                            </picture>
+                        </div>
+                    </div>
+                </div>
+
+
+        <div class="row">
+            <div class="comic-caption-container">
+                <p>It&#x27;s that time of the year again. One new comic every day, all of October!&#xA;(God willing)</p>
+            </div>
+        </div>
+
+        <div class="row">
+            <div class="comic-navigation">
+
+
+                <a href="/bex-and-keit/artist" class="">
+                    <i class="fa fa-step-backward"></i>
+                </a>
+                <a href="/bex-and-keit/sentient-bikini" class="">
+                    <i class="fa fa-chevron-left"></i>
+                </a>
+                <a href="http://www.topwebcomics.com/vote/28260"
+                   title="Vote for Bex & Keit on TopWebComics!"
+                   style="display: inline-block; vertical-align: middle; margin-top: -13px;">
+                    <img src="http://www.topwebcomics.com//vote_link.php?t=cubes&s=28260" alt="vote">
+                </a>
+                <a class="nav-disabled">
+                    <i class="fa fa-chevron-right"></i>
+                </a>
+                <a href="/bex-and-keit/bonktober-1-tomboy" class="nav-disabled">
+                    <i class="fa fa-step-forward"></i>
+                </a>
+            </div>
+        </div>
+
+    </div>
+</div>
+    </div>
+</main>
+
+<!-- Bootstrap 5.3.3 JS -->
+<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/bs5-lightbox@1.8.3/dist/index.bundle.min.js"></script>
+<script src="/js/site.js?v=y_Ngd8W21UpuCUhEqGbUKDEqFnNfTjAfb24GhJJGUyM"></script>
+
+<script>
+    // Show the age verification modal on page load if not verified
+    document.addEventListener('DOMContentLoaded', function () {
+        if (!sessionStorage.getItem('ageVerified')) {
+            var ageModal = new bootstrap.Modal(document.getElementById('ageModal'), {
+                backdrop: 'static',
+                keyboard: false
+            });
+            ageModal.show();
+        } else {
+            // Hide the content overlay if age is already verified
+            document.getElementById('contentOverlay').style.display = 'none';
+        }
+
+        // Check if cookies are accepted 
+        if (localStorage.getItem('cb-enabled') === 'true') {
+            document.getElementById('cb-cookie-banner').style.display = 'none';
+        } else {
+            document.getElementById('cb-cookie-banner').style.display = 'block';
+        }
+    });
+
+    // Hide the cookie banner on click of the button
+    window.cb_hideCookieBanner = function () {
+        console.log('Hiding cookie banner');
+        localStorage.setItem('cb-enabled', 'true');
+        document.getElementById('cb-cookie-banner').style.display = 'none';
+    };
+
+    // Handle the age confirmation button
+    document.getElementById('confirmAge').addEventListener('click', function () {
+        // Store the age verification status in sessionStorage
+        sessionStorage.setItem('ageVerified', 'true');
+        // Hide the modal
+        var ageModalEl = document.getElementById('ageModal');
+        var ageModal = bootstrap.Modal.getInstance(ageModalEl);
+        ageModal.hide();
+        // Remove the content overlay
+        document.getElementById('contentOverlay').style.display = 'none';
+    });
+
+    // Handle the age denial button
+    document.getElementById('denyAge').addEventListener('click', function () {
+        // Redirect to a different site or show a message
+        window.location.href = '/age-denied';
+    });
+</script>
+
+
+</body>
+
+</html>