From 5f2f737267eb76212e85eb51441d597bec32f2c3 Mon Sep 17 00:00:00 2001 From: Akamaru Date: Fri, 5 Dec 2025 12:23:55 +0100 Subject: [PATCH] Neu: Kemono & Coomer Bridge --- KemonoCoomerBridge.php | 283 +++++++++++++++++++++++++++++++++++++++++ README.md | 11 ++ 2 files changed, 294 insertions(+) create mode 100644 KemonoCoomerBridge.php diff --git a/KemonoCoomerBridge.php b/KemonoCoomerBridge.php new file mode 100644 index 0000000..f9368cf --- /dev/null +++ b/KemonoCoomerBridge.php @@ -0,0 +1,283 @@ +getInput('service'); + if ($service && in_array($service, ['onlyfans', 'fansly', 'candfans'])) { + return 'https://www.google.com/s2/favicons?domain=coomer.st&sz=32'; + } + return 'https://www.google.com/s2/favicons?domain=kemono.cr&sz=32'; + } + + const PARAMETERS = [ + [ + 'service' => [ + 'name' => 'Service', + 'type' => 'list', + 'required' => true, + 'values' => [ + 'Patreon' => 'patreon', + 'Fanbox' => 'fanbox', + 'Fantia' => 'fantia', + 'Gumroad' => 'gumroad', + 'SubscribeStar' => 'subscribestar', + 'Discord' => 'discord', + 'DLsite' => 'dlsite', + 'Boosty' => 'boosty', + 'Afdian' => 'afdian', + 'OnlyFans' => 'onlyfans', + 'Fansly' => 'fansly', + 'CandFans' => 'candfans' + ], + 'defaultValue' => 'patreon' + ], + 'user' => [ + 'name' => 'User ID', + 'type' => 'text', + 'required' => true, + 'title' => 'The creator\'s user ID (numeric or username)', + 'exampleValue' => '6889522' + ], + 'limit' => [ + 'name' => 'Limit', + 'type' => 'number', + 'required' => false, + 'title' => 'Maximum number of posts to return (default: 25, max: 50)', + 'defaultValue' => 25 + ] + ] + ]; + + private $creatorName = null; + + /** + * Check if the selected service is a Coomer service + */ + private function isCoomer(): bool + { + $service = $this->getInput('service') ?? ''; + return in_array($service, ['onlyfans', 'fansly', 'candfans']); + } + + /** + * Get the base URI based on the service (Kemono or Coomer) + */ + private function getBaseURI(): string + { + if ($this->isCoomer()) { + return 'https://coomer.st/'; + } + return self::URI; + } + + /** + * Get the CDN domain based on the service + */ + private function getCDNDomain(): string + { + if ($this->isCoomer()) { + return 'https://n1.coomer.st/data'; + } + return 'https://n1.kemono.cr/data'; + } + + public function collectData() + { + $service = $this->getInput('service'); + $user = $this->getInput('user'); + $limit = min((int)$this->getInput('limit'), 50); + + // Fetch user profile to get creator name + $this->fetchUserProfile($service, $user); + + // Build API URL + $apiUrl = $this->getBaseURI() . "api/v1/{$service}/user/{$user}/posts"; + + // Fetch posts with required header to bypass DDoS protection + $headers = ['Accept: text/css']; + $json = getContents($apiUrl, $headers); + $posts = json_decode($json, true); + + if (!is_array($posts)) { + throw new \Exception('Invalid API response: expected array of posts'); + } + + // Process posts (limited by user preference) + $count = 0; + foreach ($posts as $post) { + if ($count >= $limit) { + break; + } + + $item = $this->parsePost($post, $service, $user); + if ($item) { + $this->items[] = $item; + $count++; + } + } + } + + /** + * Parse a post from the API response into an RSS item + */ + private function parsePost(array $post, string $service, string $user): ?array + { + $postId = $post['id'] ?? null; + if (!$postId) { + return null; + } + + // Build post URL + $postUrl = $this->getBaseURI() . "{$service}/user/{$user}/post/{$postId}"; + + // Get title (with fallback) + $title = $post['title'] ?? 'Untitled Post'; + if (empty(trim($title))) { + $title = 'Untitled Post'; + } + + // Build content with embedded images + $content = $this->buildPostContent($post); + + // Parse timestamp + $timestamp = null; + if (isset($post['published'])) { + $timestamp = strtotime($post['published']); + } + + // Build item + $item = [ + 'title' => $title, + 'uri' => $postUrl, + 'timestamp' => $timestamp, + 'content' => $content, + 'uid' => $postId, + 'enclosures' => [] + ]; + + // Add author if we have creator name + if ($this->creatorName) { + $item['author'] = $this->creatorName; + } + + // Add main file as enclosure + if (isset($post['file']['path'])) { + $fileUrl = $this->getCDNDomain() . $post['file']['path']; + $item['enclosures'][] = $fileUrl; + } + + // Add attachments as enclosures + if (isset($post['attachments']) && is_array($post['attachments'])) { + foreach ($post['attachments'] as $attachment) { + if (isset($attachment['path'])) { + $attachmentUrl = $this->getCDNDomain() . $attachment['path']; + $item['enclosures'][] = $attachmentUrl; + } + } + } + + return $item; + } + + /** + * Build HTML content for the post with embedded images + */ + private function buildPostContent(array $post): string + { + $html = ''; + + // Add main image if present + if (isset($post['file']['path'])) { + $imageUrl = $this->getCDNDomain() . $post['file']['path']; + $imageName = $post['file']['name'] ?? 'Image'; + $html .= '

' . htmlspecialchars($imageName) . '

'; + } + + // Add content text (either full content or substring) + if (isset($post['content']) && !empty(trim($post['content']))) { + $html .= $post['content']; + } elseif (isset($post['substring']) && !empty(trim($post['substring']))) { + $html .= $post['substring']; + } + + return $html; + } + + /** + * Fetch user profile to get creator name + */ + private function fetchUserProfile(string $service, string $user): void + { + try { + $profileUrl = $this->getBaseURI() . "api/v1/{$service}/user/{$user}/profile"; + $headers = ['Accept: text/css']; + $json = getContents($profileUrl, $headers); + $profile = json_decode($json, true); + + if (isset($profile['name'])) { + $this->creatorName = $profile['name']; + } + } catch (\Exception $e) { + // If profile fetch fails, continue without creator name + // This is not critical to the bridge functionality + } + } + + /** + * Override getName to include creator name in feed title + */ + public function getName() + { + if ($this->creatorName) { + return $this->creatorName . ' - ' . self::NAME; + } + return parent::getName(); + } + + /** + * Auto-detect parameters from Kemono and Coomer URLs + */ + public function detectParameters($url) + { + $pattern = '#(kemono\.cr|coomer\.st)/([^/]+)/(user|server)/([^/?\#]+)#i'; + + if (preg_match($pattern, $url, $matches)) { + return [ + 'service' => strtolower($matches[2]), + 'user' => $matches[4] + ]; + } + + return null; + } + + /** + * Override getURI to return the user's page + */ + public function getURI() + { + $service = $this->getInput('service'); + $user = $this->getInput('user'); + + if ($service && $user) { + return $this->getBaseURI() . "{$service}/user/{$user}"; + } + + return parent::getURI(); + } +} diff --git a/README.md b/README.md index 2772309..fe22d60 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,17 @@ Diese Sammlung enthält verschiedene Bridge-Implementierungen für RSS-Bridge, u ### [Kemono Friends Music News Bridge](https://bridge.ponywave.de/#bridge-KemonoFriendsMusicNewsBridge) (Von Akamaru) - **Beschreibung**: Zeigt die neuesten Nachrichten für Kemono Friends Musik +### [Kemono & Coomer Bridge](https://bridge.ponywave.de/#bridge-KemonoCoomerBridge) (Von Akamaru) +- **Beschreibung**: Zeigt Posts von Kemono (Patreon, Fanbox, etc.) und Coomer (OnlyFans, Fansly, CandFans) archivierten Creators +- **Parameter**: + - **Service**: Patreon, Fanbox, Fantia, Gumroad, SubscribeStar, Discord, DLsite, Boosty, Afdian, OnlyFans, Fansly, CandFans + - **User ID**: Creator's User-ID (numerisch oder Username) + - **Limit** (optional): Maximale Anzahl an Posts (Standard: 25, max: 50) +- **Hinweise**: + - Unterstützt automatische URL-Erkennung für kemono.cr und coomer.st URLs + - Bilder und Attachments werden als Enclosures hinzugefügt + - Creator-Name wird als Autor gesetzt + ### [Main-Post Bridge](https://bridge.ponywave.de/#bridge-MainPostBridge) (Von Akamaru) - **Beschreibung**: Nachrichten und Artikel von der Main-Post - **Parameter**: