Various improvements
This commit is contained in:
122
README.md
122
README.md
@@ -161,43 +161,75 @@ cd backend
|
|||||||
|
|
||||||
## 📖 Documentation
|
## 📖 Documentation
|
||||||
|
|
||||||
- **[API Documentation](docs/API.md)**: Complete REST API reference with examples
|
- **[Technical Specification](storycove-spec.md)**: Complete technical specification with API documentation, data models, and all feature specifications
|
||||||
- **[Data Model](docs/DATA_MODEL.md)**: Detailed database schema and relationships
|
- **[Web Scraper Specification](storycove-scraper-spec.md)**: URL content grabbing functionality
|
||||||
- **[Technical Specification](storycove-spec.md)**: Comprehensive technical specification
|
|
||||||
- **Environment Configuration**: Multi-environment deployment setup (see above)
|
- **Environment Configuration**: Multi-environment deployment setup (see above)
|
||||||
- **Development Setup**: Local development environment setup (see below)
|
- **Development Setup**: Local development environment setup (see below)
|
||||||
|
|
||||||
|
> **Note**: All feature specifications (Collections, Tag Enhancements, EPUB Import/Export) have been consolidated into the main technical specification for easier maintenance and reference.
|
||||||
|
|
||||||
## 🗄️ Data Model
|
## 🗄️ Data Model
|
||||||
|
|
||||||
StoryCove uses a PostgreSQL database with the following core entities:
|
StoryCove uses a PostgreSQL database with the following core entities:
|
||||||
|
|
||||||
### **Stories**
|
### **Stories**
|
||||||
- **Primary Key**: UUID
|
- **Primary Key**: UUID
|
||||||
- **Fields**: title, summary, description, content_html, content_plain, source_url, word_count, rating, volume, cover_path, reading_position, last_read_at
|
- **Fields**: title, summary, description, content_html, content_plain, source_url, word_count, rating, volume, cover_path, is_read, reading_position, last_read_at, created_at, updated_at
|
||||||
- **Relationships**: Many-to-One with Author, Many-to-One with Series, Many-to-Many with Tags
|
- **Relationships**: Many-to-One with Author, Many-to-One with Series, Many-to-Many with Tags, One-to-Many with ReadingPositions
|
||||||
- **Features**: Automatic word count calculation, HTML sanitization, plain text extraction, reading progress tracking
|
- **Features**: Automatic word count calculation, HTML sanitization, plain text extraction, reading progress tracking, duplicate detection
|
||||||
|
|
||||||
### **Authors**
|
### **Authors**
|
||||||
- **Primary Key**: UUID
|
- **Primary Key**: UUID
|
||||||
- **Fields**: name, notes, author_rating, avatar_image_path
|
- **Fields**: name, notes, author_rating, avatar_image_path, created_at, updated_at
|
||||||
- **Relationships**: One-to-Many with Stories, One-to-Many with Author URLs
|
- **Relationships**: One-to-Many with Stories, One-to-Many with Author URLs (via @ElementCollection)
|
||||||
- **Features**: URL collection storage, rating system, statistics calculation
|
- **Features**: URL collection storage, rating system, statistics calculation, average story rating calculation
|
||||||
|
|
||||||
|
### **Collections**
|
||||||
|
- **Primary Key**: UUID
|
||||||
|
- **Fields**: name, description, rating, cover_image_path, is_archived, created_at, updated_at
|
||||||
|
- **Relationships**: Many-to-Many with Tags, One-to-Many with CollectionStories
|
||||||
|
- **Features**: Story ordering with gap-based positioning, statistics calculation, EPUB export, Typesense search
|
||||||
|
|
||||||
|
### **CollectionStories** (Junction Table)
|
||||||
|
- **Composite Key**: collection_id, story_id
|
||||||
|
- **Fields**: position, added_at
|
||||||
|
- **Relationships**: Links Collections to Stories with ordering
|
||||||
|
- **Features**: Gap-based positioning for efficient reordering
|
||||||
|
|
||||||
### **Series**
|
### **Series**
|
||||||
- **Primary Key**: UUID
|
- **Primary Key**: UUID
|
||||||
- **Fields**: name, description
|
- **Fields**: name, description, created_at
|
||||||
- **Relationships**: One-to-Many with Stories (ordered by volume)
|
- **Relationships**: One-to-Many with Stories (ordered by volume)
|
||||||
- **Features**: Volume-based story ordering, navigation methods
|
- **Features**: Volume-based story ordering, navigation methods (next/previous story)
|
||||||
|
|
||||||
### **Tags**
|
### **Tags**
|
||||||
- **Primary Key**: UUID
|
- **Primary Key**: UUID
|
||||||
- **Fields**: name (unique)
|
- **Fields**: name (unique), color (hex), description, created_at
|
||||||
- **Relationships**: Many-to-Many with Stories
|
- **Relationships**: Many-to-Many with Stories, Many-to-Many with Collections, One-to-Many with TagAliases
|
||||||
- **Features**: Autocomplete support, usage statistics
|
- **Features**: Color coding, alias system, autocomplete support, usage statistics, AI-powered suggestions
|
||||||
|
|
||||||
### **Join Tables**
|
### **TagAliases**
|
||||||
- **story_tags**: Links stories to tags
|
- **Primary Key**: UUID
|
||||||
- **author_urls**: Stores multiple URLs per author
|
- **Fields**: alias_name (unique), canonical_tag_id, created_from_merge, created_at
|
||||||
|
- **Relationships**: Many-to-One with Tag (canonical)
|
||||||
|
- **Features**: Transparent alias resolution, merge tracking, autocomplete integration
|
||||||
|
|
||||||
|
### **ReadingPositions**
|
||||||
|
- **Primary Key**: UUID
|
||||||
|
- **Fields**: story_id, chapter_index, chapter_title, word_position, character_position, percentage_complete, epub_cfi, context_before, context_after, created_at, updated_at
|
||||||
|
- **Relationships**: Many-to-One with Story
|
||||||
|
- **Features**: Advanced reading position tracking, EPUB CFI support, context preservation, percentage calculation
|
||||||
|
|
||||||
|
### **Libraries**
|
||||||
|
- **Primary Key**: UUID
|
||||||
|
- **Fields**: name, description, is_default, created_at, updated_at
|
||||||
|
- **Features**: Multi-library support, library switching functionality
|
||||||
|
|
||||||
|
### **Core Join Tables**
|
||||||
|
- **story_tags**: Links stories to tags (Many-to-Many)
|
||||||
|
- **collection_tags**: Links collections to tags (Many-to-Many)
|
||||||
|
- **collection_stories**: Links collections to stories with ordering
|
||||||
|
- **author_urls**: Stores multiple URLs per author (@ElementCollection)
|
||||||
|
|
||||||
## 🔌 REST API Reference
|
## 🔌 REST API Reference
|
||||||
|
|
||||||
@@ -209,6 +241,7 @@ StoryCove uses a PostgreSQL database with the following core entities:
|
|||||||
### **Stories** (`/api/stories`)
|
### **Stories** (`/api/stories`)
|
||||||
- `GET /` - List stories (paginated)
|
- `GET /` - List stories (paginated)
|
||||||
- `GET /{id}` - Get specific story
|
- `GET /{id}` - Get specific story
|
||||||
|
- `GET /{id}/read` - Get story for reading interface
|
||||||
- `POST /` - Create new story
|
- `POST /` - Create new story
|
||||||
- `PUT /{id}` - Update story
|
- `PUT /{id}` - Update story
|
||||||
- `DELETE /{id}` - Delete story
|
- `DELETE /{id}` - Delete story
|
||||||
@@ -218,6 +251,10 @@ StoryCove uses a PostgreSQL database with the following core entities:
|
|||||||
- `POST /{id}/tags/{tagId}` - Add tag to story
|
- `POST /{id}/tags/{tagId}` - Add tag to story
|
||||||
- `DELETE /{id}/tags/{tagId}` - Remove tag from story
|
- `DELETE /{id}/tags/{tagId}` - Remove tag from story
|
||||||
- `POST /{id}/reading-progress` - Update reading position
|
- `POST /{id}/reading-progress` - Update reading position
|
||||||
|
- `POST /{id}/reading-status` - Mark story as read/unread
|
||||||
|
- `GET /{id}/collections` - Get collections containing story
|
||||||
|
- `GET /random` - Get random story with optional filters
|
||||||
|
- `GET /check-duplicate` - Check for duplicate stories
|
||||||
- `GET /search` - Search stories (Typesense with faceting)
|
- `GET /search` - Search stories (Typesense with faceting)
|
||||||
- `GET /search/suggestions` - Get search suggestions
|
- `GET /search/suggestions` - Get search suggestions
|
||||||
- `GET /author/{authorId}` - Stories by author
|
- `GET /author/{authorId}` - Stories by author
|
||||||
@@ -225,6 +262,16 @@ StoryCove uses a PostgreSQL database with the following core entities:
|
|||||||
- `GET /tags/{tagName}` - Stories with tag
|
- `GET /tags/{tagName}` - Stories with tag
|
||||||
- `GET /recent` - Recent stories
|
- `GET /recent` - Recent stories
|
||||||
- `GET /top-rated` - Top-rated stories
|
- `GET /top-rated` - Top-rated stories
|
||||||
|
- `POST /batch/add-to-collection` - Add multiple stories to collection
|
||||||
|
- `POST /reindex` - Manual Typesense reindex
|
||||||
|
- `POST /reindex-typesense` - Reindex stories in Typesense
|
||||||
|
- `POST /recreate-typesense-collection` - Recreate Typesense collection
|
||||||
|
|
||||||
|
#### **EPUB Import/Export** (`/api/stories/epub`)
|
||||||
|
- `POST /import` - Import story from EPUB file
|
||||||
|
- `POST /export` - Export story as EPUB with options
|
||||||
|
- `GET /{id}/epub` - Export story as EPUB (simple)
|
||||||
|
- `POST /validate` - Validate EPUB file structure
|
||||||
|
|
||||||
### **Authors** (`/api/authors`)
|
### **Authors** (`/api/authors`)
|
||||||
- `GET /` - List authors (paginated)
|
- `GET /` - List authors (paginated)
|
||||||
@@ -244,14 +291,49 @@ StoryCove uses a PostgreSQL database with the following core entities:
|
|||||||
### **Tags** (`/api/tags`)
|
### **Tags** (`/api/tags`)
|
||||||
- `GET /` - List tags (paginated)
|
- `GET /` - List tags (paginated)
|
||||||
- `GET /{id}` - Get specific tag
|
- `GET /{id}` - Get specific tag
|
||||||
- `POST /` - Create new tag
|
- `POST /` - Create new tag (with color and description)
|
||||||
- `PUT /{id}` - Update tag
|
- `PUT /{id}` - Update tag (name, color, description)
|
||||||
- `DELETE /{id}` - Delete tag
|
- `DELETE /{id}` - Delete tag
|
||||||
- `GET /search` - Search tags
|
- `GET /search` - Search tags
|
||||||
- `GET /autocomplete` - Tag autocomplete
|
- `GET /autocomplete` - Tag autocomplete with alias resolution
|
||||||
- `GET /popular` - Most used tags
|
- `GET /popular` - Most used tags
|
||||||
- `GET /unused` - Unused tags
|
- `GET /unused` - Unused tags
|
||||||
- `GET /stats` - Tag statistics
|
- `GET /stats` - Tag statistics
|
||||||
|
- `GET /collections` - Tags used by collections
|
||||||
|
- `GET /resolve/{name}` - Resolve tag name (handles aliases)
|
||||||
|
|
||||||
|
#### **Tag Aliases** (`/api/tags/{tagId}/aliases`)
|
||||||
|
- `POST /` - Add alias to tag
|
||||||
|
- `DELETE /{aliasId}` - Remove alias from tag
|
||||||
|
|
||||||
|
#### **Tag Management**
|
||||||
|
- `POST /merge` - Merge multiple tags into one
|
||||||
|
- `POST /merge/preview` - Preview tag merge operation
|
||||||
|
- `POST /suggest` - AI-powered tag suggestions for content
|
||||||
|
|
||||||
|
### **Collections** (`/api/collections`)
|
||||||
|
- `GET /` - Search and list collections (Typesense)
|
||||||
|
- `GET /{id}` - Get collection details
|
||||||
|
- `POST /` - Create new collection (JSON or multipart)
|
||||||
|
- `PUT /{id}` - Update collection metadata
|
||||||
|
- `DELETE /{id}` - Delete collection
|
||||||
|
- `PUT /{id}/archive` - Archive/unarchive collection
|
||||||
|
- `POST /{id}/cover` - Upload collection cover image
|
||||||
|
- `DELETE /{id}/cover` - Remove collection cover image
|
||||||
|
- `GET /{id}/stats` - Get collection statistics
|
||||||
|
|
||||||
|
#### **Collection Story Management**
|
||||||
|
- `POST /{id}/stories` - Add stories to collection
|
||||||
|
- `DELETE /{id}/stories/{storyId}` - Remove story from collection
|
||||||
|
- `PUT /{id}/stories/order` - Reorder stories in collection
|
||||||
|
- `GET /{id}/read/{storyId}` - Get story with collection context
|
||||||
|
|
||||||
|
#### **Collection EPUB Export**
|
||||||
|
- `GET /{id}/epub` - Export collection as EPUB
|
||||||
|
- `POST /{id}/epub` - Export collection as EPUB with options
|
||||||
|
|
||||||
|
#### **Collection Management**
|
||||||
|
- `POST /reindex-typesense` - Reindex collections in Typesense
|
||||||
|
|
||||||
### **Series** (`/api/series`)
|
### **Series** (`/api/series`)
|
||||||
- `GET /` - List series (paginated)
|
- `GET /` - List series (paginated)
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
# Tag Enhancement Specification
|
# Tag Enhancement Specification
|
||||||
|
|
||||||
|
> **✅ Implementation Status: COMPLETED**
|
||||||
|
> This feature has been fully implemented and is available in the system.
|
||||||
|
> All tag enhancements including colors, aliases, merging, and AI suggestions are working.
|
||||||
|
> Last updated: January 2025
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document outlines the comprehensive enhancement of the tagging functionality in StoryCove, including color tags, tag deletion, merging, and aliases. These features will be accessible through a new "Tag Maintenance" page linked from the Settings page.
|
This document outlines the comprehensive enhancement of the tagging functionality in StoryCove, including color tags, tag deletion, merging, and aliases. These features will be accessible through a new "Tag Maintenance" page linked from the Settings page.
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ public class FileController {
|
|||||||
|
|
||||||
private String getCurrentLibraryId() {
|
private String getCurrentLibraryId() {
|
||||||
String libraryId = libraryService.getCurrentLibraryId();
|
String libraryId = libraryService.getCurrentLibraryId();
|
||||||
|
System.out.println("FileController - Current Library ID: " + libraryId);
|
||||||
return libraryId != null ? libraryId : "default";
|
return libraryId != null ? libraryId : "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +45,10 @@ public class FileController {
|
|||||||
response.put("message", "Cover uploaded successfully");
|
response.put("message", "Cover uploaded successfully");
|
||||||
response.put("path", imagePath);
|
response.put("path", imagePath);
|
||||||
String currentLibraryId = getCurrentLibraryId();
|
String currentLibraryId = getCurrentLibraryId();
|
||||||
response.put("url", "/api/files/images/" + currentLibraryId + "/" + imagePath);
|
String imageUrl = "/api/files/images/" + currentLibraryId + "/" + imagePath;
|
||||||
|
response.put("url", imageUrl);
|
||||||
|
|
||||||
|
System.out.println("Upload response - path: " + imagePath + ", url: " + imageUrl);
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { searchApi, tagApi } from '../../lib/api';
|
import { searchApi, tagApi, getImageUrl } from '../../lib/api';
|
||||||
import { Story, Tag } from '../../types/api';
|
import { Story, Tag } from '../../types/api';
|
||||||
import { Input } from '../ui/Input';
|
import { Input } from '../ui/Input';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
@@ -239,7 +239,7 @@ export default function CollectionForm({
|
|||||||
{(coverImagePreview || initialData?.coverImagePath) && (
|
{(coverImagePreview || initialData?.coverImagePath) && (
|
||||||
<div className="w-20 h-24 rounded overflow-hidden bg-gray-100">
|
<div className="w-20 h-24 rounded overflow-hidden bg-gray-100">
|
||||||
<img
|
<img
|
||||||
src={coverImagePreview || (initialData?.coverImagePath ? `/images/${initialData.coverImagePath}` : '')}
|
src={coverImagePreview || (initialData?.coverImagePath ? getImageUrl(initialData.coverImagePath) : '')}
|
||||||
alt="Cover preview"
|
alt="Cover preview"
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { StoryWithCollectionContext } from '../../types/api';
|
import { StoryWithCollectionContext } from '../../types/api';
|
||||||
import { storyApi } from '../../lib/api';
|
import { storyApi, getImageUrl } from '../../lib/api';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import TagDisplay from '../tags/TagDisplay';
|
import TagDisplay from '../tags/TagDisplay';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -212,7 +212,7 @@ export default function CollectionReadingView({
|
|||||||
{story.coverPath && (
|
{story.coverPath && (
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
src={`/images/${story.coverPath}`}
|
src={getImageUrl(story.coverPath)}
|
||||||
alt={`${story.title} cover`}
|
alt={`${story.title} cover`}
|
||||||
className="w-32 h-40 object-cover rounded-lg mx-auto md:mx-0"
|
className="w-32 h-40 object-cover rounded-lg mx-auto md:mx-0"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Button from '../ui/Button';
|
|||||||
import { Input } from '../ui/Input';
|
import { Input } from '../ui/Input';
|
||||||
import LibrarySwitchLoader from '../ui/LibrarySwitchLoader';
|
import LibrarySwitchLoader from '../ui/LibrarySwitchLoader';
|
||||||
import { useLibrarySwitch } from '../../hooks/useLibrarySwitch';
|
import { useLibrarySwitch } from '../../hooks/useLibrarySwitch';
|
||||||
|
import { setCurrentLibraryId, clearLibraryCache } from '../../lib/api';
|
||||||
|
|
||||||
interface Library {
|
interface Library {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,6 +66,8 @@ export default function LibrarySettings() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setCurrentLibrary(data);
|
setCurrentLibrary(data);
|
||||||
|
// Set the library ID for image URL generation
|
||||||
|
setCurrentLibraryId(data.id);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load current library:', error);
|
console.error('Failed to load current library:', error);
|
||||||
@@ -87,6 +90,8 @@ export default function LibrarySettings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSwitchComplete = () => {
|
const handleSwitchComplete = () => {
|
||||||
|
// Clear the library cache so images use the new library
|
||||||
|
clearLibraryCache();
|
||||||
// Refresh the page to reload with new library context
|
// Refresh the page to reload with new library context
|
||||||
router.refresh();
|
router.refresh();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { createContext, useContext, useEffect, useState } from 'react';
|
import { createContext, useContext, useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { authApi, setGlobalAuthFailureHandler } from '../lib/api';
|
import { authApi, setGlobalAuthFailureHandler, setCurrentLibraryId } from '../lib/api';
|
||||||
import { preloadSanitizationConfig } from '../lib/sanitization';
|
import { preloadSanitizationConfig } from '../lib/sanitization';
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
@@ -34,6 +34,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
const authenticated = authApi.isAuthenticated();
|
const authenticated = authApi.isAuthenticated();
|
||||||
setIsAuthenticated(authenticated);
|
setIsAuthenticated(authenticated);
|
||||||
|
|
||||||
|
// If authenticated, also load current library for image URLs
|
||||||
|
if (authenticated) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/libraries/current');
|
||||||
|
if (response.ok) {
|
||||||
|
const library = await response.json();
|
||||||
|
setCurrentLibraryId(library.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load current library:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth check failed:', error);
|
console.error('Auth check failed:', error);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
@@ -59,6 +72,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
try {
|
try {
|
||||||
await authApi.login(password);
|
await authApi.login(password);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
|
|
||||||
|
// Load current library after successful login
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/libraries/current');
|
||||||
|
if (response.ok) {
|
||||||
|
const library = await response.json();
|
||||||
|
setCurrentLibraryId(library.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load current library after login:', error);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed:', error);
|
console.error('Login failed:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -678,9 +678,34 @@ export const databaseApi = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Image utility
|
// Library context for images - will be set by a React context provider
|
||||||
|
let currentLibraryId: string | null = null;
|
||||||
|
|
||||||
|
// Set the current library ID (called by library context or components)
|
||||||
|
export const setCurrentLibraryId = (libraryId: string | null): void => {
|
||||||
|
currentLibraryId = libraryId;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get current library ID synchronously (fallback to 'default')
|
||||||
|
export const getCurrentLibraryId = (): string => {
|
||||||
|
return currentLibraryId || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear library cache when switching libraries
|
||||||
|
export const clearLibraryCache = (): void => {
|
||||||
|
currentLibraryId = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Image utility - now library-aware
|
||||||
export const getImageUrl = (path: string): string => {
|
export const getImageUrl = (path: string): string => {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
// Images are served directly by nginx at /images/
|
|
||||||
return `/images/${path}`;
|
// For compatibility during transition, handle both patterns
|
||||||
|
if (path.startsWith('http')) {
|
||||||
|
return path; // External URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use library-aware API endpoint
|
||||||
|
const libraryId = getCurrentLibraryId();
|
||||||
|
return `/api/files/images/${libraryId}/${path}`;
|
||||||
};
|
};
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,9 @@
|
|||||||
# StoryCove - Story Collections Feature Specification
|
# StoryCove - Story Collections Feature Specification
|
||||||
|
|
||||||
|
> **✅ Implementation Status: COMPLETED**
|
||||||
|
> This feature has been fully implemented and is available in the system.
|
||||||
|
> Last updated: January 2025
|
||||||
|
|
||||||
## 1. Feature Overview
|
## 1. Feature Overview
|
||||||
|
|
||||||
Story Collections allow users to organize stories into ordered lists for better content management and reading workflows. Collections support custom ordering, metadata, and provide an enhanced reading experience for grouped content.
|
Story Collections allow users to organize stories into ordered lists for better content management and reading workflows. Collections support custom ordering, metadata, and provide an enhanced reading experience for grouped content.
|
||||||
@@ -234,7 +238,55 @@ Response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.3 Batch Operations
|
### 3.3 Collection EPUB Export
|
||||||
|
|
||||||
|
#### GET /api/collections/{id}/epub
|
||||||
|
Export collection as EPUB file with default settings
|
||||||
|
- Includes all stories in collection order
|
||||||
|
- Includes collection metadata as book metadata
|
||||||
|
- Includes cover image if available
|
||||||
|
- Generates table of contents
|
||||||
|
|
||||||
|
#### POST /api/collections/{id}/epub
|
||||||
|
Export collection as EPUB with custom options
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"includeCoverImage": true,
|
||||||
|
"includeMetadata": true,
|
||||||
|
"includeTableOfContents": true
|
||||||
|
}
|
||||||
|
|
||||||
|
Response: EPUB file download with filename format:
|
||||||
|
{collection-name}-{export-date}.epub
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Collection Statistics
|
||||||
|
|
||||||
|
#### GET /api/collections/{id}/stats
|
||||||
|
Get detailed collection statistics
|
||||||
|
```json
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"totalStories": 15,
|
||||||
|
"totalWordCount": 125000,
|
||||||
|
"estimatedReadingTime": 625,
|
||||||
|
"averageStoryRating": 4.2,
|
||||||
|
"tagFrequency": {
|
||||||
|
"fantasy": 12,
|
||||||
|
"adventure": 8
|
||||||
|
},
|
||||||
|
"authorDistribution": [
|
||||||
|
{"authorName": "string", "storyCount": 5}
|
||||||
|
],
|
||||||
|
"readingProgress": {
|
||||||
|
"storiesRead": 8,
|
||||||
|
"percentComplete": 53.3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Batch Operations
|
||||||
|
|
||||||
#### POST /api/stories/batch/add-to-collection
|
#### POST /api/stories/batch/add-to-collection
|
||||||
Add multiple stories to a collection
|
Add multiple stories to a collection
|
||||||
|
|||||||
@@ -1,17 +1,35 @@
|
|||||||
# StoryCove - Software Requirements Specification
|
# StoryCove - Software Requirements Specification
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
1. [Executive Summary](#1-executive-summary)
|
||||||
|
2. [System Architecture](#2-system-architecture)
|
||||||
|
3. [Data Models](#3-data-models)
|
||||||
|
4. [API Specification](#4-api-specification)
|
||||||
|
5. [UI/UX Specifications](#5-uiux-specifications)
|
||||||
|
6. [Security Requirements](#6-security-requirements)
|
||||||
|
7. [Deployment](#7-deployment)
|
||||||
|
8. [Testing Strategy](#8-testing-strategy)
|
||||||
|
9. [Advanced Features (✅ IMPLEMENTED)](#9-advanced-features--implemented)
|
||||||
|
- [9.1 Collections Feature](#91-collections-feature--completed)
|
||||||
|
- [9.2 Enhanced Tag System](#92-enhanced-tag-system--completed)
|
||||||
|
- [9.3 EPUB Import/Export System](#93-epub-importexport-system--completed)
|
||||||
|
10. [Future Roadmap](#10-future-roadmap)
|
||||||
|
|
||||||
## 1. Executive Summary
|
## 1. Executive Summary
|
||||||
|
|
||||||
StoryCove is a self-hosted web application designed to store, organize, and read short stories collected from various internet sources. The application provides a clean, responsive interface for managing a personal library of stories with advanced search capabilities, author management, and rating systems.
|
StoryCove is a self-hosted web application designed to store, organize, and read short stories collected from various internet sources. The application provides a clean, responsive interface for managing a personal library of stories with advanced search capabilities, author management, and rating systems.
|
||||||
|
|
||||||
### 1.1 Key Features (MVP)
|
### 1.1 Key Features (✅ IMPLEMENTED)
|
||||||
- Story management with HTML content support
|
- **Story Management**: Complete CRUD operations with HTML content support
|
||||||
- Author profiles with ratings and metadata
|
- **Author Profiles**: Full author management with ratings, metadata, and URL collections
|
||||||
- Tag-based categorization
|
- **Enhanced Tag System**: Color tags, aliases, merging, and AI-powered suggestions
|
||||||
- Full-text search capabilities
|
- **Collections**: Organized story lists with reading flow and EPUB export
|
||||||
- Responsive reading interface
|
- **EPUB Import/Export**: Full EPUB support with metadata and reading position preservation
|
||||||
- JWT-based authentication
|
- **Advanced Search**: Typesense-powered full-text search with faceting
|
||||||
- Docker-based deployment
|
- **Reading Position Tracking**: Character-level position tracking with EPUB CFI support
|
||||||
|
- **Responsive Reading Interface**: Distraction-free reading with progress tracking
|
||||||
|
- **JWT Authentication**: Secure single-password authentication system
|
||||||
|
- **Docker Deployment**: Complete containerized deployment with Docker Compose
|
||||||
|
|
||||||
### 1.2 Technology Stack
|
### 1.2 Technology Stack
|
||||||
- **Frontend**: Next.js
|
- **Frontend**: Next.js
|
||||||
@@ -58,21 +76,23 @@ StoryCove is a self-hosted web application designed to store, organize, and read
|
|||||||
```sql
|
```sql
|
||||||
CREATE TABLE stories (
|
CREATE TABLE stories (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
title VARCHAR(500) NOT NULL,
|
title VARCHAR(255) NOT NULL,
|
||||||
summary TEXT,
|
summary TEXT,
|
||||||
|
description VARCHAR(1000),
|
||||||
author_id UUID NOT NULL,
|
author_id UUID NOT NULL,
|
||||||
content_html TEXT NOT NULL,
|
content_html TEXT NOT NULL,
|
||||||
content_plain TEXT NOT NULL,
|
content_plain TEXT NOT NULL,
|
||||||
source_url VARCHAR(1000),
|
source_url VARCHAR(1000),
|
||||||
word_count INTEGER NOT NULL,
|
word_count INTEGER DEFAULT 0,
|
||||||
series_id UUID,
|
series_id UUID,
|
||||||
volume INTEGER,
|
volume INTEGER,
|
||||||
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
|
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
|
||||||
cover_image_path VARCHAR(500), -- Phase 2: Consider storing base filename without size suffix
|
cover_path VARCHAR(500), -- Updated field name
|
||||||
|
is_read BOOLEAN DEFAULT FALSE, -- Reading status
|
||||||
reading_position INTEGER DEFAULT 0, -- Character position for reading progress
|
reading_position INTEGER DEFAULT 0, -- Character position for reading progress
|
||||||
last_read_at TIMESTAMP, -- Last time story was accessed
|
last_read_at TIMESTAMP, -- Last time story was accessed
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
FOREIGN KEY (author_id) REFERENCES authors(id),
|
FOREIGN KEY (author_id) REFERENCES authors(id),
|
||||||
FOREIGN KEY (series_id) REFERENCES series(id)
|
FOREIGN KEY (series_id) REFERENCES series(id)
|
||||||
);
|
);
|
||||||
@@ -91,13 +111,13 @@ CREATE TABLE authors (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Author URLs Table
|
#### Author URLs Collection Table
|
||||||
```sql
|
```sql
|
||||||
|
-- Note: In the actual implementation, this is managed as an @ElementCollection
|
||||||
|
-- This table is created automatically by Hibernate
|
||||||
CREATE TABLE author_urls (
|
CREATE TABLE author_urls (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
author_id UUID NOT NULL,
|
author_id UUID NOT NULL,
|
||||||
url VARCHAR(1000) NOT NULL,
|
url VARCHAR(1000) NOT NULL,
|
||||||
description VARCHAR(255),
|
|
||||||
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
|
FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
@@ -116,7 +136,98 @@ CREATE TABLE series (
|
|||||||
CREATE TABLE tags (
|
CREATE TABLE tags (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
name VARCHAR(100) NOT NULL UNIQUE,
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
color VARCHAR(7), -- Hex color code like #3B82F6
|
||||||
|
description VARCHAR(500), -- Tag description
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Collections Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE collections (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(500) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
rating INTEGER CHECK (rating >= 1 AND rating <= 5),
|
||||||
|
cover_image_path VARCHAR(500),
|
||||||
|
is_archived BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_collections_archived ON collections(is_archived);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Collection Stories Junction Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE collection_stories (
|
||||||
|
collection_id UUID NOT NULL,
|
||||||
|
story_id UUID NOT NULL,
|
||||||
|
position INTEGER NOT NULL,
|
||||||
|
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (collection_id, story_id),
|
||||||
|
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(collection_id, position)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_collection_stories_position ON collection_stories(collection_id, position);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Collection Tags Junction Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE collection_tags (
|
||||||
|
collection_id UUID NOT NULL,
|
||||||
|
tag_id UUID NOT NULL,
|
||||||
|
PRIMARY KEY (collection_id, tag_id),
|
||||||
|
FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Tag Aliases Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE tag_aliases (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
alias_name VARCHAR(100) UNIQUE NOT NULL,
|
||||||
|
canonical_tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
||||||
|
created_from_merge BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_tag_aliases_name ON tag_aliases(alias_name);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Reading Positions Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE reading_positions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
story_id UUID NOT NULL,
|
||||||
|
chapter_index INTEGER,
|
||||||
|
chapter_title VARCHAR(255),
|
||||||
|
word_position INTEGER,
|
||||||
|
character_position INTEGER,
|
||||||
|
percentage_complete DECIMAL(5,2),
|
||||||
|
epub_cfi TEXT,
|
||||||
|
context_before VARCHAR(500),
|
||||||
|
context_after VARCHAR(500),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
FOREIGN KEY (story_id) REFERENCES stories(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_reading_position_story ON reading_positions(story_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Libraries Table
|
||||||
|
```sql
|
||||||
|
CREATE TABLE libraries (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
is_default BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -241,6 +352,108 @@ Response:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### POST /api/stories/{id}/reading-status
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"isRead": true
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"isRead": true,
|
||||||
|
"readingPosition": 0,
|
||||||
|
"lastReadAt": "2024-01-01T12:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/stories/{id}/read
|
||||||
|
Returns story content optimized for reading interface
|
||||||
|
|
||||||
|
#### GET /api/stories/random
|
||||||
|
Returns a random story with optional filtering
|
||||||
|
Query parameters:
|
||||||
|
- `searchQuery` (string): Filter search
|
||||||
|
- `tags` (string[]): Filter by tags
|
||||||
|
|
||||||
|
#### GET /api/stories/check-duplicate
|
||||||
|
Check for potential duplicate stories
|
||||||
|
Query parameters:
|
||||||
|
- `title` (string): Story title to check
|
||||||
|
- `authorName` (string): Author name to check
|
||||||
|
|
||||||
|
#### GET /api/stories/{id}/collections
|
||||||
|
Get all collections containing this story
|
||||||
|
|
||||||
|
### 4.2.1 EPUB Import/Export Endpoints
|
||||||
|
|
||||||
|
#### POST /api/stories/epub/import
|
||||||
|
Import story from EPUB file
|
||||||
|
```json
|
||||||
|
Request (multipart/form-data):
|
||||||
|
{
|
||||||
|
"file": "epub file",
|
||||||
|
"authorId": "uuid (optional)",
|
||||||
|
"authorName": "string (optional)",
|
||||||
|
"seriesId": "uuid (optional)",
|
||||||
|
"seriesName": "string (optional)",
|
||||||
|
"seriesVolume": "integer (optional)",
|
||||||
|
"tags": ["string"],
|
||||||
|
"preserveReadingPosition": "boolean",
|
||||||
|
"overwriteExisting": "boolean",
|
||||||
|
"createMissingAuthor": "boolean",
|
||||||
|
"createMissingSeries": "boolean"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"storyId": "uuid",
|
||||||
|
"storyTitle": "string",
|
||||||
|
"message": "EPUB imported successfully",
|
||||||
|
"extractedData": {
|
||||||
|
"chapterCount": 12,
|
||||||
|
"wordCount": 45000,
|
||||||
|
"hasCovers": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/stories/{id}/epub
|
||||||
|
Export story as EPUB (simple download)
|
||||||
|
|
||||||
|
#### POST /api/stories/epub/export
|
||||||
|
Export story as EPUB with custom options
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"storyId": "uuid",
|
||||||
|
"includeReadingPosition": true,
|
||||||
|
"includeCoverImage": true,
|
||||||
|
"includeMetadata": true
|
||||||
|
}
|
||||||
|
|
||||||
|
Response: EPUB file download
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/stories/epub/validate
|
||||||
|
Validate EPUB file before import
|
||||||
|
```json
|
||||||
|
Request (multipart/form-data):
|
||||||
|
{
|
||||||
|
"file": "epub file"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"errors": [],
|
||||||
|
"filename": "story.epub",
|
||||||
|
"size": 1024000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### 4.3 Author Endpoints
|
### 4.3 Author Endpoints
|
||||||
|
|
||||||
#### GET /api/authors
|
#### GET /api/authors
|
||||||
@@ -298,12 +511,261 @@ Remove avatar from author
|
|||||||
### 4.4 Tag Endpoints
|
### 4.4 Tag Endpoints
|
||||||
|
|
||||||
#### GET /api/tags
|
#### GET /api/tags
|
||||||
Returns tag cloud data with usage counts
|
Returns paginated list of tags with enhanced features
|
||||||
|
Query parameters:
|
||||||
|
- `page` (integer): Page number
|
||||||
|
- `size` (integer): Items per page
|
||||||
|
- `sortBy` (string): Sort field (name, usage)
|
||||||
|
- `sortDir` (string): Sort direction (asc, desc)
|
||||||
|
|
||||||
#### GET /api/tags/autocomplete?q={query}
|
#### GET /api/tags/{id}
|
||||||
Autocomplete for tag input
|
Get specific tag with full details including aliases
|
||||||
|
|
||||||
### 4.5 Series Endpoints
|
#### POST /api/tags
|
||||||
|
Create new tag with color and description
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"name": "string",
|
||||||
|
"color": "#3B82F6",
|
||||||
|
"description": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PUT /api/tags/{id}
|
||||||
|
Update tag properties
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"name": "string",
|
||||||
|
"color": "#3B82F6",
|
||||||
|
"description": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DELETE /api/tags/{id}
|
||||||
|
Delete tag with safety checks
|
||||||
|
|
||||||
|
#### GET /api/tags/autocomplete?query={query}
|
||||||
|
Enhanced autocomplete with alias resolution
|
||||||
|
|
||||||
|
#### GET /api/tags/popular
|
||||||
|
Most used tags
|
||||||
|
|
||||||
|
#### GET /api/tags/unused
|
||||||
|
Tags not assigned to any stories
|
||||||
|
|
||||||
|
#### GET /api/tags/stats
|
||||||
|
Tag usage statistics
|
||||||
|
|
||||||
|
#### GET /api/tags/collections
|
||||||
|
Tags used by collections
|
||||||
|
|
||||||
|
#### GET /api/tags/resolve/{name}
|
||||||
|
Resolve tag name through alias system
|
||||||
|
|
||||||
|
### 4.4.1 Tag Alias Management
|
||||||
|
|
||||||
|
#### POST /api/tags/{tagId}/aliases
|
||||||
|
Add alias to tag
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"aliasName": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DELETE /api/tags/{tagId}/aliases/{aliasId}
|
||||||
|
Remove alias from tag
|
||||||
|
|
||||||
|
### 4.4.2 Tag Management Operations
|
||||||
|
|
||||||
|
#### POST /api/tags/merge
|
||||||
|
Merge multiple tags into one
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"sourceTagIds": ["uuid1", "uuid2"],
|
||||||
|
"targetTagId": "uuid3"
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"resultTag": "TagDto",
|
||||||
|
"aliasesCreated": ["alias1", "alias2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/tags/merge/preview
|
||||||
|
Preview tag merge operation without executing
|
||||||
|
|
||||||
|
#### POST /api/tags/suggest
|
||||||
|
AI-powered tag suggestions for content
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"title": "string",
|
||||||
|
"content": "string",
|
||||||
|
"summary": "string",
|
||||||
|
"limit": 10
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"tagName": "fantasy",
|
||||||
|
"confidence": 0.85,
|
||||||
|
"reason": "Content contains magical elements"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 Collections Endpoints
|
||||||
|
|
||||||
|
#### GET /api/collections
|
||||||
|
Search and list collections with pagination (Typesense-powered)
|
||||||
|
Query parameters:
|
||||||
|
- `page` (integer): Page number
|
||||||
|
- `limit` (integer): Items per page
|
||||||
|
- `search` (string): Search query
|
||||||
|
- `tags` (string[]): Filter by tags
|
||||||
|
- `archived` (boolean): Include archived collections
|
||||||
|
|
||||||
|
#### GET /api/collections/{id}
|
||||||
|
Get collection details with story list
|
||||||
|
|
||||||
|
#### POST /api/collections
|
||||||
|
Create new collection
|
||||||
|
```json
|
||||||
|
Request (JSON or multipart/form-data):
|
||||||
|
{
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"tagNames": ["string"],
|
||||||
|
"storyIds": ["uuid"],
|
||||||
|
"coverImage": "file (optional for multipart)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### PUT /api/collections/{id}
|
||||||
|
Update collection metadata
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"name": "string",
|
||||||
|
"description": "string",
|
||||||
|
"tagNames": ["string"],
|
||||||
|
"rating": 1-5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DELETE /api/collections/{id}
|
||||||
|
Delete collection (stories remain in system)
|
||||||
|
|
||||||
|
#### PUT /api/collections/{id}/archive
|
||||||
|
Archive or unarchive collection
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"archived": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### POST /api/collections/{id}/cover
|
||||||
|
Upload collection cover image
|
||||||
|
|
||||||
|
#### DELETE /api/collections/{id}/cover
|
||||||
|
Remove collection cover image
|
||||||
|
|
||||||
|
#### GET /api/collections/{id}/stats
|
||||||
|
Get detailed collection statistics
|
||||||
|
```json
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"totalStories": 15,
|
||||||
|
"totalWordCount": 125000,
|
||||||
|
"estimatedReadingTime": 625,
|
||||||
|
"averageStoryRating": 4.2,
|
||||||
|
"tagFrequency": {
|
||||||
|
"fantasy": 12,
|
||||||
|
"adventure": 8
|
||||||
|
},
|
||||||
|
"authorDistribution": [
|
||||||
|
{"authorName": "string", "storyCount": 5}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5.1 Collection Story Management
|
||||||
|
|
||||||
|
#### POST /api/collections/{id}/stories
|
||||||
|
Add stories to collection with optional positioning
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"storyIds": ["uuid"],
|
||||||
|
"position": 3
|
||||||
|
}
|
||||||
|
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"added": 3,
|
||||||
|
"skipped": 1,
|
||||||
|
"totalStories": 15
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DELETE /api/collections/{id}/stories/{storyId}
|
||||||
|
Remove story from collection
|
||||||
|
|
||||||
|
#### PUT /api/collections/{id}/stories/order
|
||||||
|
Reorder stories in collection
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"storyOrders": [
|
||||||
|
{"storyId": "uuid", "position": 1},
|
||||||
|
{"storyId": "uuid", "position": 2}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### GET /api/collections/{id}/read/{storyId}
|
||||||
|
Get story content with collection navigation context
|
||||||
|
```json
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"story": { /* full story data */ },
|
||||||
|
"collection": {
|
||||||
|
"id": "uuid",
|
||||||
|
"name": "string",
|
||||||
|
"currentPosition": 3,
|
||||||
|
"totalStories": 10,
|
||||||
|
"previousStoryId": "uuid",
|
||||||
|
"nextStoryId": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5.2 Collection EPUB Export
|
||||||
|
|
||||||
|
#### GET /api/collections/{id}/epub
|
||||||
|
Export collection as EPUB file
|
||||||
|
|
||||||
|
#### POST /api/collections/{id}/epub
|
||||||
|
Export collection as EPUB with custom options
|
||||||
|
```json
|
||||||
|
Request:
|
||||||
|
{
|
||||||
|
"includeCoverImage": true,
|
||||||
|
"includeMetadata": true,
|
||||||
|
"includeTableOfContents": true
|
||||||
|
}
|
||||||
|
|
||||||
|
Response: EPUB file download
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.6 Series Endpoints
|
||||||
|
|
||||||
#### GET /api/series
|
#### GET /api/series
|
||||||
List all series with story counts
|
List all series with story counts
|
||||||
@@ -587,15 +1049,130 @@ APP_PASSWORD=application_password_here
|
|||||||
### 8.3 E2E Tests
|
### 8.3 E2E Tests
|
||||||
- Cypress or Playwright for critical user flows
|
- Cypress or Playwright for critical user flows
|
||||||
|
|
||||||
## 9. Phase 2 Roadmap
|
## 9. Advanced Features (✅ IMPLEMENTED)
|
||||||
|
|
||||||
### 9.1 URL Content Grabbing ✅ IMPLEMENTED
|
### 9.1 Collections Feature ✅ COMPLETED
|
||||||
|
|
||||||
|
Story Collections allow users to organize stories into ordered lists for better content management and reading workflows. Collections support custom ordering, metadata, and provide an enhanced reading experience for grouped content.
|
||||||
|
|
||||||
|
#### Core Capabilities
|
||||||
|
- Create and manage ordered lists of stories
|
||||||
|
- Stories can belong to multiple collections
|
||||||
|
- Drag-and-drop reordering with gap-based positioning
|
||||||
|
- Collection-level metadata and ratings
|
||||||
|
- Dedicated collection reading flow with navigation
|
||||||
|
- Batch operations on collection contents
|
||||||
|
- EPUB export for entire collections
|
||||||
|
- Archive functionality for collection management
|
||||||
|
|
||||||
|
#### Collection Data Model
|
||||||
|
Collections use a junction table approach with position-based ordering:
|
||||||
|
- **Collections Table**: Core collection metadata with archiving support
|
||||||
|
- **collection_stories**: Junction table with gap-based positioning (1000, 2000, 3000...)
|
||||||
|
- **collection_tags**: Many-to-many relationship with tags for categorization
|
||||||
|
|
||||||
|
#### Collection Reading Flow
|
||||||
|
Enhanced reading experience with collection context:
|
||||||
|
- Navigate between stories in collection order
|
||||||
|
- Show position within collection (Story 3 of 10)
|
||||||
|
- Previous/Next story navigation
|
||||||
|
- Collection-specific reading statistics
|
||||||
|
|
||||||
|
#### Collection EPUB Export
|
||||||
|
Export entire collections as single EPUB files:
|
||||||
|
- Maintains story order from collection
|
||||||
|
- Includes collection metadata as book metadata
|
||||||
|
- Generates table of contents with story titles
|
||||||
|
- Preserves individual story formatting and structure
|
||||||
|
|
||||||
|
### 9.2 Enhanced Tag System ✅ COMPLETED
|
||||||
|
|
||||||
|
Comprehensive enhancement of the tagging functionality including color tags, tag deletion, merging, aliases, and AI-powered suggestions.
|
||||||
|
|
||||||
|
#### Color Tags
|
||||||
|
- **Visual Organization**: Assign hex colors to tags for better visual distinction
|
||||||
|
- **Predefined Palette**: Theme-compatible color selection
|
||||||
|
- **Custom Colors**: Full color picker for advanced customization
|
||||||
|
- **Accessibility**: All colors ensure sufficient contrast ratios
|
||||||
|
|
||||||
|
#### Tag Aliases System
|
||||||
|
- **Automatic Resolution**: Users type "magictf" and get "magic tf" automatically
|
||||||
|
- **Transparent Integration**: Aliases work seamlessly in search and autocomplete
|
||||||
|
- **Merge Integration**: Automatically created when tags are merged
|
||||||
|
- **Hover Display**: Show all aliases when hovering over canonical tags
|
||||||
|
|
||||||
|
#### Tag Merging
|
||||||
|
Sophisticated tag merging with automatic aliasing:
|
||||||
|
1. Select multiple tags to merge
|
||||||
|
2. Choose canonical tag name
|
||||||
|
3. Preview merge impact (story counts)
|
||||||
|
4. All associations transfer to canonical tag
|
||||||
|
5. Source tags automatically become aliases
|
||||||
|
|
||||||
|
#### AI-Powered Tag Suggestions
|
||||||
|
- **Content Analysis**: Analyze story title, content, and summary
|
||||||
|
- **Confidence Scoring**: Each suggestion includes confidence level
|
||||||
|
- **Contextual Reasoning**: Explanations for why tags were suggested
|
||||||
|
- **Integration**: Available during story creation and editing
|
||||||
|
|
||||||
|
#### Tag Management Interface
|
||||||
|
Dedicated tag maintenance accessible through Settings:
|
||||||
|
- Searchable and filterable tag list
|
||||||
|
- Sortable by name, usage count, creation date
|
||||||
|
- Bulk selection for merge/delete operations
|
||||||
|
- Visual indicators for colors and alias counts
|
||||||
|
- Tag statistics and usage analytics
|
||||||
|
|
||||||
|
### 9.3 EPUB Import/Export System ✅ COMPLETED
|
||||||
|
|
||||||
|
Full-featured EPUB import and export system with advanced metadata handling and reading position preservation.
|
||||||
|
|
||||||
|
#### EPUB Import Features
|
||||||
|
- **Format Support**: EPUB 2.0 and 3.x formats, DRM-free
|
||||||
|
- **Validation**: Comprehensive file format and structure validation
|
||||||
|
- **Metadata Extraction**: Title, author, description, subjects/tags, cover images
|
||||||
|
- **Content Processing**: Multi-chapter combination with proper HTML sanitization
|
||||||
|
- **Duplicate Detection**: Smart detection of potentially duplicate content
|
||||||
|
- **Batch Import**: Support for importing multiple EPUB files
|
||||||
|
|
||||||
|
#### EPUB Export Features
|
||||||
|
- **Individual Stories**: Export single stories as properly formatted EPUB files
|
||||||
|
- **Collection Export**: Export entire collections as single EPUB with TOC
|
||||||
|
- **Metadata Preservation**: Complete metadata including tags, ratings, series info
|
||||||
|
- **Cover Images**: Automatic inclusion and optimization of cover artwork
|
||||||
|
- **Reading Positions**: EPUB CFI standard support for position preservation
|
||||||
|
|
||||||
|
#### Advanced Metadata Handling
|
||||||
|
Comprehensive extraction and preservation of:
|
||||||
|
- **Basic Metadata**: Title, author, description, language
|
||||||
|
- **Publication Data**: Publisher, publication date, identifiers (ISBN, etc.)
|
||||||
|
- **Categorization**: Subjects converted to tags automatically
|
||||||
|
- **Cover Processing**: Automatic extraction, validation, and optimization
|
||||||
|
- **Custom Metadata**: StoryCove-specific fields for ratings and series
|
||||||
|
|
||||||
|
#### Reading Position Integration
|
||||||
|
- **EPUB CFI Support**: Standard-compliant reading position tracking
|
||||||
|
- **Cross-Format Preservation**: Maintain positions when importing/exporting
|
||||||
|
- **Chapter Awareness**: Chapter-level position tracking with context
|
||||||
|
- **Percentage Calculation**: Accurate progress percentage based on content length
|
||||||
|
- **Context Preservation**: Before/after text context for position recovery
|
||||||
|
|
||||||
|
#### File Handling & Validation
|
||||||
|
- **Size Limits**: 50MB maximum file size with configurable limits
|
||||||
|
- **Security**: Comprehensive validation to prevent malicious content
|
||||||
|
- **Error Recovery**: Graceful handling of corrupted or invalid EPUB files
|
||||||
|
- **Progress Feedback**: Real-time progress indicators during import/export
|
||||||
|
- **Batch Operations**: Support for processing multiple files simultaneously
|
||||||
|
|
||||||
|
## 10. Future Roadmap
|
||||||
|
|
||||||
|
### 10.1 URL Content Grabbing ✅ IMPLEMENTED
|
||||||
- Configurable scrapers for specific sites
|
- Configurable scrapers for specific sites
|
||||||
- Site configuration stored in JSON files
|
- Site configuration stored in JSON files
|
||||||
- Content extraction rules per site (DeviantArt support added)
|
- Content extraction rules per site (DeviantArt support added)
|
||||||
- Adaptive content extraction for varying HTML structures
|
- Adaptive content extraction for varying HTML structures
|
||||||
|
|
||||||
### 9.2 Enhanced Image Processing & Optimization
|
### 10.2 Enhanced Image Processing & Optimization
|
||||||
- **Multi-size generation during upload**
|
- **Multi-size generation during upload**
|
||||||
- Cover images: thumbnail (200x300), medium (400x600), full (800x1200)
|
- Cover images: thumbnail (200x300), medium (400x600), full (800x1200)
|
||||||
- Avatar images: small (64x64), medium (200x200), full (400x400)
|
- Avatar images: small (64x64), medium (200x200), full (400x400)
|
||||||
|
|||||||
Reference in New Issue
Block a user