- 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>
23 KiB
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
- Getting Started
- Base URL
- Data Models
- API Endpoints
- Common Workflows
- Error Handling
- Real-time Progress Updates
- 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:
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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:
{
"query": "performer name"
}
Example:
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:
{
"query": "studio name"
}
Example:
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:
{
"query": "scene title"
}
Example:
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:
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:
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:
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:
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):
{
"force": false // Set to true to force sync even if recently synced
}
Example:
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:
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:
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/progressGET /api/import/all-studios/progressGET /api/import/all-scenes/progress
Example with EventSource:
// 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:
{
"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:
{
"complete": true,
"result": {
"total": 100,
"imported": 95,
"skipped": 4,
"errors": 1
}
}
Common Workflows
1. Search and Display Performers
// 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
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
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
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:
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:
{
"success": false,
"message": "TPDB_API_KEY not configured"
}
No Results Found:
{
"success": false,
"message": "No performers found"
}
Search Failed:
{
"success": false,
"message": "Search failed: connection timeout"
}
Complete Examples
Example 1: Search Form with Results
<!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
<!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
// 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
<img
src="${performer.image_url}"
onerror="this.src='/static/placeholder.jpg'"
alt="${performer.name}"
>
3. Use Bootstrap Classes for Quick Styling
<!-- 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
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
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
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:
- Check the data model structures above
- Look at the complete examples
- Test endpoints using browser DevTools Network tab
- Consult your backend developer if you need custom endpoints
Happy coding!