# @akomalabs/kemono

A production-grade TypeScript API wrapper for the Kemono/Coomer platforms with advanced features like schema validation, caching, and rate limiting.

## Features

- 🚀 Full TypeScript support with comprehensive type definitions
- ✅ Zod schema validation for type safety
- 🔄 Built-in caching with in-memory and Redis support
- 🛡️ Configurable rate limiting
- 📝 Detailed logging options
- ⚡ Bun-optimized for maximum performance
- 🔒 Session management for favorites
- 🌐 Support for multiple base URLs (kemono.su and coomer.su)

## Installation

```bash
bun add @akomalabs/kemono
```

## Quick Start

```typescript
import { KemonoClient } from '@akomalabs/kemono';

// Create a client instance
const client = new KemonoClient({
  baseUrl: 'https://kemono.su/api/v1', // or 'https://coomer.su/api/v1'
  cache: {
    enabled: true,
    ttl: 300, // 5 minutes
  },
});

// Fetch a creator's profile
const profile = await client.creators.getProfile('onlyfans', 'creatorId');

// Get recent posts
const posts = await client.posts.listRecent();

// Get posts from a specific creator
const creatorPosts = await client.posts.getByCreator('onlyfans', 'creatorId');

// Get a specific post with attachments
const post = await client.posts.getPost('onlyfans', 'creatorId', 'postId');
const attachments = await client.posts.getAttachments('onlyfans', 'creatorId', 'postId');
```

## Type System

The library uses Zod for runtime type validation and TypeScript for static typing. All types are inferred from Zod schemas, ensuring runtime type safety.

### Available Types and Schemas

```typescript
import {
  // Schemas (for runtime validation)
  KemonoBaseUrlSchema,
  KemonoServiceSchema,
  KemonoConfigSchema,
  FileSchema,
  AttachmentSchema,
  PostSchema,
  CreatorSchema,
  SearchParamsSchema,
  
  // Types (for static typing)
  type KemonoBaseUrl,
  type KemonoService,
  type KemonoConfig,
  type File,
  type Attachment,
  type Post,
  type Creator,
  type SearchParams,
  
  // Error types
  KemonoApiError,
  AuthenticationError,
  NotFoundError,
  ValidationError,
  RateLimitError,
} from '@akomalabs/kemono';

// Example: Using schemas for runtime validation
const config = KemonoConfigSchema.parse({
  baseUrl: 'https://kemono.su/api/v1',
  cache: { enabled: true },
});

// Example: Using types for static typing
const post: Post = {
  id: '123',
  title: 'Example Post',
  content: 'Hello World',
  // ...other required fields
};
```

## Configuration

The client accepts a comprehensive configuration object that is validated at runtime:

```typescript
interface KemonoConfig {
  // Base URL for API requests
  baseUrl?: 'https://kemono.su/api/v1' | 'https://coomer.su/api/v1';
  
  // Session key for favorites management
  sessionKey?: string;
  
  // Caching configuration
  cache?: {
    enabled: boolean;
    ttl?: number;
    redis?: {
      host: string;
      port: number;
      password?: string;
    };
  };
  
  // Rate limiting configuration
  rateLimit?: {
    maxRequests: number;
    windowMs: number;
  };
  
  // Logging configuration
  logging?: {
    enabled: boolean;
    level?: 'error' | 'warn' | 'info' | 'debug';
  };
}
```

## API Reference

### Creators API

```typescript
// List all creators
const creators = await client.creators.listAll();

// Get a creator's profile
const profile = await client.creators.getProfile(service, creatorId);
```

### Posts API

```typescript
// List recent posts
const posts = await client.posts.listRecent({
  q?: string;     // Search query
  o?: number;     // Offset
  limit?: number; // Number of posts to return
});

// Get posts by creator
const posts = await client.posts.getByCreator(service, creatorId);

// Get a specific post
const post = await client.posts.getPost(service, creatorId, postId);

// Get post attachments
const attachments = await client.posts.getAttachments(service, creatorId, postId);
```

### Favorites API

The Favorites API requires a valid session key to be provided in the client configuration. All favorites methods will throw an `AuthenticationError` if the session key is missing or invalid.

```typescript
// Initialize client with session key
const client = new KemonoClient({
  baseUrl: 'https://kemono.su/api/v1',
  sessionKey: 'your-session-key', // Required for favorites
});

// List favorite creators with error handling
try {
  const creators = await client.favorites.listCreators();
  console.log(`Found ${creators.length} favorite creators`);
  
  // Access creator properties
  creators.forEach(creator => {
    console.log(`${creator.name} (${creator.service})`);
    console.log(`Last imported: ${creator.last_imported}`);
    console.log(`Favorite sequence: ${creator.faved_seq}`);
  });
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.error('Please provide a valid session key');
  } else {
    console.error('Failed to fetch favorite creators:', error);
  }
}

// List favorite posts with pagination example
try {
  const posts = await client.favorites.listPosts();
  console.log(`Found ${posts.length} favorite posts`);
  
  // Access post properties
  posts.forEach(post => {
    console.log(`${post.title} by ${post.user}`);
    console.log(`Published: ${post.published}`);
    console.log(`Content: ${post.content.substring(0, 100)}...`);
    
    // Handle attachments
    if (post.attachments.length > 0) {
      console.log('Attachments:', post.attachments.map(a => a.name).join(', '));
    }
  });
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.error('Please provide a valid session key');
  } else {
    console.error('Failed to fetch favorite posts:', error);
  }
}

// Managing favorites
async function manageFavorites() {
  try {
    // Add a creator to favorites
    await client.favorites.addCreator('fanbox', 'creator123');
    
    // Add a post to favorites
    await client.favorites.addPost('fanbox', 'creator123', 'post456');
    
    // Remove from favorites
    await client.favorites.removeCreator('fanbox', 'creator123');
    await client.favorites.removePost('fanbox', 'creator123', 'post456');
    
  } catch (error) {
    if (error instanceof AuthenticationError) {
      console.error('Session key required');
    } else if (error instanceof NotFoundError) {
      console.error('Creator or post not found');
    } else if (error instanceof RateLimitError) {
      console.error('Too many requests, please try again later');
    } else {
      console.error('Operation failed:', error);
    }
  }
}

// Response Types

interface FavoriteCreator {
  id: string;          // Creator's unique identifier
  name: string;        // Creator's display name
  service: KemonoService; // Platform (e.g., 'fanbox', 'patreon')
  indexed: string;     // ISO date when the creator was indexed
  updated: string;     // ISO date of last update
  faved_seq: number;   // Sequence number in favorites
  last_imported: string; // ISO date of last import
  public_id: string | null;    // Public identifier if available
  relation_id: string | null;  // Related identifier if available
}

interface FavoritePost {
  id: string;          // Post unique identifier
  user: string;        // Creator's ID
  service: KemonoService; // Platform service
  title: string;       // Post title
  content: string;     // Post content
  added: string;       // ISO date when added to favorites
  published: string;   // ISO date when published
  edited: string | null; // ISO date of last edit
  file: {             // Main file if present
    name: string;     // File name
    path: string;     // File path
  };
  attachments: any[]; // Additional files
  embed: Record<string, any>; // Embedded content
  shared_file: boolean; // Indicates if file is shared
  faved_seq: number;    // Sequence number in favorites
}
```

## Supported Services

- fanbox
- patreon
- gumroad
- subscribestar
- dlsite
- fantia
- onlyfans
- fansly

## Error Handling

The client includes custom error classes for better error handling:

```typescript
try {
  await client.creators.getProfile('onlyfans', 'nonexistent');
} catch (error) {
  if (error instanceof NotFoundError) {
    // Handle 404 error
  } else if (error instanceof AuthenticationError) {
    // Handle authentication error
  } else if (error instanceof ValidationError) {
    // Handle validation errors (e.g., invalid config)
  } else if (error instanceof RateLimitError) {
    // Handle rate limit errors
  } else if (error instanceof KemonoApiError) {
    // Handle other API errors
  }
}
```

## Development

```bash
# Install dependencies
bun install

# Run tests
bun test

# Build package
bun run build
```

## License

MIT License

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
