Neu: Kemono & Coomer Bridge
This commit is contained in:
283
KemonoCoomerBridge.php
Normal file
283
KemonoCoomerBridge.php
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kemono & Coomer Bridge
|
||||||
|
* Returns posts from Kemono and Coomer archived creators across multiple services
|
||||||
|
*/
|
||||||
|
class KemonoCoomerBridge extends BridgeAbstract
|
||||||
|
{
|
||||||
|
const NAME = 'Kemono & Coomer';
|
||||||
|
const URI = 'https://kemono.cr/';
|
||||||
|
const DESCRIPTION = 'Returns posts from Kemono (Patreon, Fanbox, etc.) and Coomer (OnlyFans, Fansly, CandFans) archived creators';
|
||||||
|
const MAINTAINER = 'Akamaru';
|
||||||
|
const CACHE_TIMEOUT = 3600; // 1 hour
|
||||||
|
|
||||||
|
public function getIcon()
|
||||||
|
{
|
||||||
|
// Return appropriate icon based on selected service
|
||||||
|
$service = $this->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 .= '<p><img src="' . htmlspecialchars($imageUrl) . '" alt="' . htmlspecialchars($imageName) . '" /></p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
README.md
11
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)
|
### [Kemono Friends Music News Bridge](https://bridge.ponywave.de/#bridge-KemonoFriendsMusicNewsBridge) (Von Akamaru)
|
||||||
- **Beschreibung**: Zeigt die neuesten Nachrichten für Kemono Friends Musik
|
- **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)
|
### [Main-Post Bridge](https://bridge.ponywave.de/#bridge-MainPostBridge) (Von Akamaru)
|
||||||
- **Beschreibung**: Nachrichten und Artikel von der Main-Post
|
- **Beschreibung**: Nachrichten und Artikel von der Main-Post
|
||||||
- **Parameter**:
|
- **Parameter**:
|
||||||
|
|||||||
Reference in New Issue
Block a user