Goondex/docs/FRONTEND_API_GUIDE.md
Stu Leak 16fb407a3c v0.1.0-dev4: Add web frontend with UI component library
- Implement full web interface with Go html/template server
- Add GX component library (buttons, dialogs, tables, forms, etc.)
- Create scene/performer/studio/movie detail and listing pages
- Add Adult Empire scraper for additional metadata sources
- Implement movie support with database schema
- Add import and sync services for data management
- Include comprehensive API and frontend documentation
- Add custom color scheme and responsive layout

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 10:47:30 -05:00

979 lines
23 KiB
Markdown

# Goondex Frontend API Guide
**For Frontend Developers**
This guide explains how to interact with the Goondex API using JavaScript. No backend knowledge required - just JavaScript, HTML, CSS, and Bootstrap skills!
## Table of Contents
1. [Getting Started](#getting-started)
2. [Base URL](#base-url)
3. [Data Models](#data-models)
4. [API Endpoints](#api-endpoints)
5. [Common Workflows](#common-workflows)
6. [Error Handling](#error-handling)
7. [Real-time Progress Updates](#real-time-progress-updates)
8. [Complete Examples](#complete-examples)
---
## Getting Started
All API endpoints return JSON data. You can use the `fetch` API to make requests from your JavaScript code.
### Basic API Response Format
All API responses follow this structure:
```javascript
{
"success": true, // boolean - whether the operation succeeded
"message": "Success text", // string - human-readable message
"data": { ... } // object - actual data (optional)
}
```
---
## Base URL
The API server runs at: `http://localhost:8080` (by default)
All endpoints are prefixed with this URL.
---
## Data Models
### Performer
```javascript
{
"id": 123,
"name": "Performer Name",
"aliases": "Alias1, Alias2",
// Physical Attributes
"gender": "female", // male/female/trans/other
"birthday": "1995-03-15", // YYYY-MM-DD
"astrology": "Pisces",
"birthplace": "Los Angeles, CA",
"ethnicity": "Caucasian",
"nationality": "US", // ISO country code (US, GB, FR, etc.)
"country": "United States",
"eye_color": "Blue",
"hair_color": "Blonde",
"height": 165, // centimeters
"weight": 55, // kilograms
"measurements": "34C-24-36",
"cup_size": "34C",
"tattoo_description": "Dragon on left shoulder",
"piercing_description": "Nose piercing",
"boob_job": "False", // "True" or "False" as string
// Career
"career": "2015-2023",
"career_start_year": 2015,
"career_end_year": 2023,
"date_of_death": "", // YYYY-MM-DD if applicable
"active": true,
// Media
"image_path": "/path/to/image.jpg",
"image_url": "https://example.com/image.jpg",
"poster_url": "https://example.com/poster.jpg",
"bio": "Biography text...",
// Metadata
"source": "tpdb",
"source_id": "abc-123-def",
"source_numeric_id": 456,
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-02T12:00:00Z"
}
```
### Studio
```javascript
{
"id": 456,
"name": "Studio Name",
"parent_id": 123, // null if no parent studio
"image_path": "/path/to/logo.jpg",
"image_url": "https://example.com/logo.jpg",
"description": "Studio description...",
"source": "tpdb",
"source_id": "xyz-789",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-02T12:00:00Z"
}
```
### Scene
```javascript
{
"id": 789,
"title": "Scene Title",
"code": "SCENE-001", // DVD code or scene identifier
"date": "2024-01-15", // Release date YYYY-MM-DD
"studio_id": 456,
"description": "Scene description...",
"image_path": "/path/to/thumbnail.jpg",
"image_url": "https://example.com/thumbnail.jpg",
"director": "Director Name",
"url": "https://example.com/scene",
"source": "tpdb",
"source_id": "scene-123",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-02T12:00:00Z",
// Relationships (when populated)
"performers": [ /* array of Performer objects */ ],
"tags": [ /* array of Tag objects */ ],
"studio": { /* Studio object */ }
}
```
### Movie
```javascript
{
"id": 321,
"title": "Movie Title",
"date": "2024-01-01", // Release date
"studio_id": 456,
"description": "Movie description...",
"director": "Director Name",
"duration": 120, // Duration in minutes
"image_path": "/path/to/cover.jpg",
"image_url": "https://example.com/cover.jpg",
"back_image_url": "https://example.com/back-cover.jpg",
"url": "https://example.com/movie",
"source": "tpdb",
"source_id": "movie-456",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-02T12:00:00Z",
// Relationships (when populated)
"scenes": [ /* array of Scene objects */ ],
"performers": [ /* array of Performer objects */ ],
"tags": [ /* array of Tag objects */ ],
"studio": { /* Studio object */ }
}
```
### Tag
```javascript
{
"id": 111,
"name": "Tag Name",
"category_id": 5,
"aliases": "Alias1, Alias2",
"description": "Tag description...",
"source": "tpdb",
"source_id": "tag-789",
"created_at": "2024-01-01T12:00:00Z",
"updated_at": "2024-01-02T12:00:00Z"
}
```
---
## API Endpoints
### Search & Import
#### 1. Import Performer by Search
**Endpoint:** `POST /api/import/performer`
**Description:** Search for a performer and import all matching results from TPDB.
**Request Body:**
```javascript
{
"query": "performer name"
}
```
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/import/performer', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: 'Jane Doe'
})
});
const result = await response.json();
// result = {
// "success": true,
// "message": "Imported 5 performer(s)",
// "data": { "imported": 5, "found": 5 }
// }
```
#### 2. Import Studio by Search
**Endpoint:** `POST /api/import/studio`
**Request Body:**
```javascript
{
"query": "studio name"
}
```
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/import/studio', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'Brazzers' })
});
const result = await response.json();
```
#### 3. Import Scene by Search
**Endpoint:** `POST /api/import/scene`
**Description:** Search for a scene and import all matching results. This also imports associated performers, studio, and tags.
**Request Body:**
```javascript
{
"query": "scene title"
}
```
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/import/scene', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'Scene Title' })
});
const result = await response.json();
```
### Bulk Import
#### 4. Bulk Import All
**Endpoint:** `POST /api/import/all`
**Description:** Import all performers, studios, and scenes from your local database. This fetches full metadata from TPDB.
**No Request Body Required**
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/import/all', {
method: 'POST'
});
const result = await response.json();
// result.data contains import statistics
```
#### 5. Bulk Import All Performers
**Endpoint:** `POST /api/import/all-performers`
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/import/all-performers', {
method: 'POST'
});
const result = await response.json();
// result = {
// "success": true,
// "message": "Imported 150/200 performers",
// "data": {
// "total": 200,
// "imported": 150,
// "skipped": 50,
// "errors": 0
// }
// }
```
#### 6. Bulk Import All Studios
**Endpoint:** `POST /api/import/all-studios`
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/import/all-studios', {
method: 'POST'
});
const result = await response.json();
```
#### 7. Bulk Import All Scenes
**Endpoint:** `POST /api/import/all-scenes`
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/import/all-scenes', {
method: 'POST'
});
const result = await response.json();
```
### Sync
#### 8. Sync All Data
**Endpoint:** `POST /api/sync`
**Description:** Synchronize all data with TPDB to get the latest updates.
**Request Body (Optional):**
```javascript
{
"force": false // Set to true to force sync even if recently synced
}
```
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force: true })
});
const result = await response.json();
```
#### 9. Get Sync Status
**Endpoint:** `GET /api/sync/status`
**Description:** Get the last sync time for all entities.
**Example:**
```javascript
const response = await fetch('http://localhost:8080/api/sync/status');
const result = await response.json();
// result.data contains sync status for each entity type
```
### Global Search
#### 10. Search Everything
**Endpoint:** `GET /api/search?q=query`
**Description:** Search across performers, studios, scenes, and tags simultaneously.
**Example:**
```javascript
const query = 'search term';
const response = await fetch(`http://localhost:8080/api/search?q=${encodeURIComponent(query)}`);
const result = await response.json();
// result.data = {
// "performers": [...],
// "studios": [...],
// "scenes": [...],
// "tags": [...],
// "total": 25
// }
```
---
## Real-time Progress Updates
For bulk imports, there are special endpoints that provide real-time progress updates using Server-Sent Events (SSE).
### Bulk Import with Progress
These endpoints stream progress updates as the import happens:
- `GET /api/import/all-performers/progress`
- `GET /api/import/all-studios/progress`
- `GET /api/import/all-scenes/progress`
**Example with EventSource:**
```javascript
// Create an EventSource to listen for progress updates
const eventSource = new EventSource('http://localhost:8080/api/import/all-performers/progress');
eventSource.onmessage = function(event) {
const update = JSON.parse(event.data);
if (update.error) {
console.error('Import error:', update.error);
eventSource.close();
return;
}
if (update.complete) {
console.log('Import complete!', update.result);
eventSource.close();
return;
}
// Progress update
console.log(`Progress: ${update.current}/${update.total}`);
console.log(`Current item: ${update.name}`);
console.log(`Status: ${update.status}`);
// Update UI
updateProgressBar(update.current, update.total);
};
eventSource.onerror = function(error) {
console.error('EventSource error:', error);
eventSource.close();
};
```
**Progress Update Format:**
```javascript
{
"current": 15, // Current item number
"total": 100, // Total items to process
"name": "Jane Doe", // Name of current item being processed
"status": "importing" // Status message
}
```
**Completion Format:**
```javascript
{
"complete": true,
"result": {
"total": 100,
"imported": 95,
"skipped": 4,
"errors": 1
}
}
```
---
## Common Workflows
### 1. Search and Display Performers
```javascript
// Search for performers
async function searchPerformers(searchQuery) {
const response = await fetch(
`http://localhost:8080/api/search?q=${encodeURIComponent(searchQuery)}`
);
const result = await response.json();
if (result.success) {
return result.data.performers;
}
throw new Error(result.message);
}
// Display in HTML
async function displayPerformers() {
const performers = await searchPerformers('jane');
const container = document.getElementById('performers-list');
container.innerHTML = performers.map(p => `
<div class="card">
<img src="${p.image_url}" class="card-img-top" alt="${p.name}">
<div class="card-body">
<h5 class="card-title">${p.name}</h5>
<p class="card-text">
${p.nationality ? getFlagEmoji(p.nationality) : ''}
${p.gender || 'Unknown'}
</p>
<a href="/performers/${p.id}" class="btn btn-primary">View Details</a>
</div>
</div>
`).join('');
}
// Helper: Convert country code to flag emoji
function getFlagEmoji(countryCode) {
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String.fromCodePoint(...codePoints);
}
```
### 2. Import Data with Progress Bar
```javascript
async function importPerformersWithProgress() {
const progressBar = document.getElementById('progress-bar');
const statusText = document.getElementById('status-text');
const eventSource = new EventSource(
'http://localhost:8080/api/import/all-performers/progress'
);
eventSource.onmessage = function(event) {
const update = JSON.parse(event.data);
if (update.error) {
statusText.textContent = `Error: ${update.error}`;
progressBar.style.width = '100%';
progressBar.classList.add('bg-danger');
eventSource.close();
return;
}
if (update.complete) {
statusText.textContent =
`Complete! Imported ${update.result.imported}/${update.result.total}`;
progressBar.style.width = '100%';
progressBar.classList.add('bg-success');
eventSource.close();
return;
}
// Update progress
const percent = (update.current / update.total) * 100;
progressBar.style.width = `${percent}%`;
statusText.textContent =
`Importing ${update.name} (${update.current}/${update.total})`;
};
eventSource.onerror = function(error) {
statusText.textContent = 'Connection error';
progressBar.classList.add('bg-danger');
eventSource.close();
};
}
```
### 3. Sync Data
```javascript
async function syncAllData(forceSync = false) {
const response = await fetch('http://localhost:8080/api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ force: forceSync })
});
const result = await response.json();
if (result.success) {
console.log('Sync completed:', result.data);
return result.data;
}
throw new Error(result.message);
}
// Check last sync status
async function checkSyncStatus() {
const response = await fetch('http://localhost:8080/api/sync/status');
const result = await response.json();
if (result.success) {
console.log('Last sync times:', result.data);
return result.data;
}
throw new Error(result.message);
}
```
### 4. Search and Import New Performer
```javascript
async function importNewPerformer(performerName) {
const response = await fetch('http://localhost:8080/api/import/performer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: performerName })
});
const result = await response.json();
if (result.success) {
alert(`Successfully imported ${result.data.imported} performer(s)`);
return result.data;
}
alert(`Failed: ${result.message}`);
throw new Error(result.message);
}
// Usage with a form
document.getElementById('import-form').addEventListener('submit', async (e) => {
e.preventDefault();
const performerName = document.getElementById('performer-name').value;
await importNewPerformer(performerName);
// Refresh the performer list
location.reload();
});
```
---
## Error Handling
### Best Practices
Always check the `success` field in the response:
```javascript
async function safeApiCall(url, options = {}) {
try {
const response = await fetch(url, options);
// Check HTTP status
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
// Check API success field
if (!result.success) {
throw new Error(result.message);
}
return result.data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
// Usage
try {
const performers = await safeApiCall('http://localhost:8080/api/search?q=jane');
console.log('Performers:', performers);
} catch (error) {
alert(`Error: ${error.message}`);
}
```
### Common Error Responses
**API Key Not Configured:**
```javascript
{
"success": false,
"message": "TPDB_API_KEY not configured"
}
```
**No Results Found:**
```javascript
{
"success": false,
"message": "No performers found"
}
```
**Search Failed:**
```javascript
{
"success": false,
"message": "Search failed: connection timeout"
}
```
---
## Complete Examples
### Example 1: Search Form with Results
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Goondex Search</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1>Search Goondex</h1>
<form id="search-form" class="mb-4">
<div class="input-group">
<input type="text" id="search-input" class="form-control" placeholder="Search...">
<button type="submit" class="btn btn-primary">Search</button>
</div>
</form>
<div id="results"></div>
</div>
<script>
const API_BASE = 'http://localhost:8080';
document.getElementById('search-form').addEventListener('submit', async (e) => {
e.preventDefault();
const query = document.getElementById('search-input').value;
const resultsDiv = document.getElementById('results');
try {
const response = await fetch(`${API_BASE}/api/search?q=${encodeURIComponent(query)}`);
const result = await response.json();
if (!result.success) {
resultsDiv.innerHTML = `<div class="alert alert-danger">${result.message}</div>`;
return;
}
const data = result.data;
resultsDiv.innerHTML = `
<h2>Results (${data.total} total)</h2>
<h3>Performers (${data.performers.length})</h3>
<div class="row">
${data.performers.map(p => `
<div class="col-md-3 mb-3">
<div class="card">
<img src="${p.image_url || '/static/placeholder.jpg'}" class="card-img-top">
<div class="card-body">
<h5 class="card-title">${p.name}</h5>
<p class="card-text">${p.nationality || ''}</p>
</div>
</div>
</div>
`).join('')}
</div>
<h3>Studios (${data.studios.length})</h3>
<ul class="list-group">
${data.studios.map(s => `
<li class="list-group-item">${s.name}</li>
`).join('')}
</ul>
`;
} catch (error) {
resultsDiv.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
}
});
</script>
</body>
</html>
```
### Example 2: Import Progress with Bootstrap
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Import Performers</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
<h1>Import All Performers</h1>
<button id="start-import" class="btn btn-primary mb-4">Start Import</button>
<div class="progress mb-2" style="display: none;">
<div id="progress-bar" class="progress-bar" role="progressbar" style="width: 0%">0%</div>
</div>
<div id="status" class="alert alert-info" style="display: none;"></div>
</div>
<script>
const API_BASE = 'http://localhost:8080';
document.getElementById('start-import').addEventListener('click', () => {
const progressDiv = document.querySelector('.progress');
const progressBar = document.getElementById('progress-bar');
const statusDiv = document.getElementById('status');
const button = document.getElementById('start-import');
button.disabled = true;
progressDiv.style.display = 'block';
statusDiv.style.display = 'block';
const eventSource = new EventSource(`${API_BASE}/api/import/all-performers/progress`);
eventSource.onmessage = function(event) {
const update = JSON.parse(event.data);
if (update.error) {
statusDiv.className = 'alert alert-danger';
statusDiv.textContent = `Error: ${update.error}`;
progressBar.className = 'progress-bar bg-danger';
progressBar.style.width = '100%';
button.disabled = false;
eventSource.close();
return;
}
if (update.complete) {
const result = update.result;
statusDiv.className = 'alert alert-success';
statusDiv.textContent =
`Complete! Imported ${result.imported}/${result.total} performers ` +
`(${result.skipped} skipped, ${result.errors} errors)`;
progressBar.className = 'progress-bar bg-success';
progressBar.style.width = '100%';
progressBar.textContent = '100%';
button.disabled = false;
eventSource.close();
return;
}
const percent = Math.round((update.current / update.total) * 100);
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
statusDiv.className = 'alert alert-info';
statusDiv.textContent =
`Importing: ${update.name} (${update.current}/${update.total})`;
};
eventSource.onerror = function() {
statusDiv.className = 'alert alert-danger';
statusDiv.textContent = 'Connection error';
progressBar.className = 'progress-bar bg-danger';
button.disabled = false;
eventSource.close();
};
});
</script>
</body>
</html>
```
---
## Tips for Frontend Developers
### 1. Always Use `encodeURIComponent` for Query Parameters
```javascript
// Good
const query = 'Jane Doe & Associates';
fetch(`/api/search?q=${encodeURIComponent(query)}`);
// Bad - will break with special characters
fetch(`/api/search?q=${query}`);
```
### 2. Handle Image Loading Errors
```javascript
<img
src="${performer.image_url}"
onerror="this.src='/static/placeholder.jpg'"
alt="${performer.name}"
>
```
### 3. Use Bootstrap Classes for Quick Styling
```html
<!-- Alert messages -->
<div class="alert alert-success">Success!</div>
<div class="alert alert-danger">Error!</div>
<div class="alert alert-warning">Warning!</div>
<!-- Loading spinner -->
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<!-- Progress bar -->
<div class="progress">
<div class="progress-bar" style="width: 75%">75%</div>
</div>
```
### 4. Debounce Search Input
```javascript
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Usage
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(performSearch, 300);
searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value));
```
### 5. Format Dates for Display
```javascript
function formatDate(dateString) {
if (!dateString) return 'Unknown';
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
// Usage
console.log(formatDate('2024-01-15')); // "January 15, 2024"
```
### 6. Calculate Age from Birthday
```javascript
function calculateAge(birthday) {
if (!birthday) return null;
const birthDate = new Date(birthday);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
}
// Usage
const age = calculateAge('1995-03-15');
console.log(`Age: ${age}`);
```
---
## Need Help?
If you have questions about the API or need clarification:
1. Check the data model structures above
2. Look at the complete examples
3. Test endpoints using browser DevTools Network tab
4. Consult your backend developer if you need custom endpoints
Happy coding!