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(); } }