19 |
--------------------------------------------------------------------------------
/app/static/default-book-cover-info.md:
--------------------------------------------------------------------------------
1 | # Default Book Cover Placeholder
2 |
3 | This is a placeholder for the default book cover image. In a real implementation, you would place a default book cover image here at:
4 |
5 | /Users/jeremiah/Documents/Python Projects/bibliotheca/app/static/default-book-cover.png
6 |
7 | For now, the timeline will gracefully handle missing cover images by showing colored rectangles instead.
8 |
9 | You can add a default book cover image by:
10 | 1. Creating or finding a suitable book cover placeholder image
11 | 2. Saving it as `default-book-cover.png` in the `app/static/` directory
12 | 3. The timeline will automatically use it for books without cover images
13 |
--------------------------------------------------------------------------------
/app/static/default-book-cover.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/templates/settings/partials/data_import_books.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Import Books
5 |
6 |
7 |
Import books from CSV/TSV files with field mapping and validation.
8 |
Use the dedicated import workflow page to upload and process your book data files.
29 |
--------------------------------------------------------------------------------
/scripts/migrations/migrate_db_schema.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Database Schema Migration Script for MyBibliotheca
4 |
5 | ⚠️ DEPRECATED: This manual migration script is no longer needed.
6 | Database migrations now run automatically when the application starts.
7 | See MIGRATION.md for details.
8 |
9 | This script adds the new security and privacy fields to the user table.
10 | """
11 |
12 | import sqlite3
13 | import os
14 | import sys
15 |
16 | def main():
17 | print("⚠️ WARNING: This migration script is deprecated.")
18 | print("Database migrations now run automatically when the application starts.")
19 | print("See MIGRATION.md for details.")
20 | print()
21 | print("The automatic migration system includes:")
22 | print(" - Database backup before migration")
23 | print(" - All security and privacy field additions")
24 | print(" - Multi-user system migration")
25 | print(" - Reading log updates")
26 | print()
27 | print("No manual migration is needed!")
28 | return True
29 |
30 | if __name__ == "__main__":
31 | main()
32 |
--------------------------------------------------------------------------------
/app/templates/settings/partials/privacy_form.html:
--------------------------------------------------------------------------------
1 | {% with msgs = get_flashed_messages(with_categories=True) %}
2 | {% if msgs %}
3 |
4 | {% for c,m in msgs %}
{{ m }}
{% endfor %}
5 |
6 | {% endif %}
7 | {% endwith %}
8 |
18 |
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 pickles
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | from dotenv import load_dotenv
2 |
3 | # Load environment variables from .env file
4 | load_dotenv()
5 |
6 | """Application entry point.
7 |
8 | Performs a schema preflight (additive column auto-upgrade with backup) before
9 | creating the Flask app so migrations happen deterministically at startup.
10 | """
11 |
12 | # Import triggers preflight side-effect (safe no-op if nothing to change)
13 | from app.startup import schema_preflight # noqa: F401
14 |
15 | from app import create_app
16 |
17 | # This app is intended to be run via Gunicorn only
18 | app = create_app()
19 | if __name__ == '__main__':
20 | import os
21 | import sys
22 |
23 | command = [
24 | "gunicorn",
25 | "-w", "1",
26 | "-b", "0.0.0.0:5054",
27 | "run:app"
28 | ]
29 |
30 | print(f"🚀 Launching Gunicorn with command: {' '.join(command)}")
31 | try:
32 | os.execvp(command[0], command)
33 | except FileNotFoundError:
34 | print("Error: 'gunicorn' command not found.", file=sys.stderr)
35 | print("Please install Gunicorn: pip install gunicorn", file=sys.stderr)
36 | sys.exit(1)
37 |
--------------------------------------------------------------------------------
/docs/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Pickles
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/scripts/adjust_cover_logging.py:
--------------------------------------------------------------------------------
1 | """Utility to temporarily downgrade normal cover service logs from ERROR to INFO.
2 | Run this once early in app startup (optional) if you want quieter logs while keeping failures at ERROR.
3 | """
4 | from app.services.cover_service import cover_service # noqa: F401
5 | from flask import current_app
6 |
7 | def downgrade_cover_logging():
8 | try:
9 | # Monkey patch logger call inside CoverService to use info for non-fatal paths
10 | import app.services.cover_service as cs
11 | orig = cs.cover_service.fetch_and_cache
12 | def wrapper(*a, **kw):
13 | res = orig(*a, **kw)
14 | # The original implementation logs at ERROR internally; optionally suppress or re-log
15 | # Here we just return result; future refactor could add a verbosity flag.
16 | return res
17 | cs.cover_service.fetch_and_cache = wrapper # type: ignore
18 | current_app.logger.info('[COVER][LOGGING] Downgraded cover service logging wrapper installed')
19 | except Exception as e:
20 | current_app.logger.error(f'[COVER][LOGGING] Failed to downgrade logging: {e}')
21 |
--------------------------------------------------------------------------------
/app/templates/settings/partials/profile_form.html:
--------------------------------------------------------------------------------
1 | {% with msgs = get_flashed_messages(with_categories=True) %}
2 | {% if msgs %}
3 |
4 | {% for c,m in msgs %}
{{ m }}
{% endfor %}
5 |
6 | {% endif %}
7 | {% endwith %}
8 |
22 |
--------------------------------------------------------------------------------
/app/utils/__init__.py:
--------------------------------------------------------------------------------
1 | # Utils package for Bibliotheca
2 |
3 | # Import functions from the book_utils module
4 | from .book_utils import (
5 | fetch_book_data,
6 | get_google_books_cover,
7 | fetch_author_data,
8 | generate_month_review_image,
9 | normalize_goodreads_value,
10 | search_author_by_name,
11 | search_book_by_title_author,
12 | search_multiple_books_by_title_author,
13 | search_google_books_by_title_author
14 | )
15 |
16 | # Import functions from the user_utils module
17 | from .user_utils import (
18 | calculate_reading_streak,
19 | get_reading_streak
20 | )
21 |
22 | # Unified metadata aggregation
23 | from .unified_metadata import (
24 | fetch_unified_by_isbn,
25 | fetch_unified_by_title,
26 | )
27 |
28 | __all__ = [
29 | 'fetch_book_data',
30 | 'get_google_books_cover',
31 | 'fetch_author_data',
32 | 'generate_month_review_image',
33 | 'normalize_goodreads_value',
34 | 'search_author_by_name',
35 | 'search_book_by_title_author',
36 | 'search_multiple_books_by_title_author',
37 | 'search_google_books_by_title_author',
38 | 'calculate_reading_streak',
39 | 'get_reading_streak',
40 | 'fetch_unified_by_isbn',
41 | 'fetch_unified_by_title',
42 | ]
43 |
--------------------------------------------------------------------------------
/scripts/setup_data_dir.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | REM Windows batch script to set up data directory for MyBibliotheca
3 | REM This ensures parity with Docker environment setup on Windows
4 |
5 | echo Setting up data directory for standalone execution on Windows...
6 | echo This ensures parity with Docker environment setup
7 |
8 | REM Create data directory if it doesn't exist
9 | if not exist "data" (
10 | mkdir data
11 | echo ✓ Data directory created: %CD%\data
12 | ) else (
13 | echo ✓ Data directory exists: %CD%\data
14 | )
15 |
16 | REM Create empty database file if it doesn't exist (matches Docker setup)
17 | if not exist "data\books.db" (
18 | type nul > "data\books.db"
19 | echo ✓ Database file created: %CD%\data\books.db
20 | ) else (
21 | echo ✓ Database file exists: %CD%\data\books.db
22 | )
23 |
24 | echo.
25 | echo Data directory setup complete for Windows!
26 | echo Data directory: %CD%\data
27 | echo Database path: %CD%\data\books.db
28 | echo Permissions: Using Windows default file permissions
29 | echo.
30 | echo You can now run MyBibliotheca using:
31 | echo python -m gunicorn -w 4 -b 0.0.0.0:5054 run:app
32 | echo.
33 | echo Or if you have gunicorn installed globally:
34 | echo gunicorn -w 4 -b 0.0.0.0:5054 run:app
35 |
36 | pause
37 |
--------------------------------------------------------------------------------
/app/templates/settings/partials/server_users.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | User Management (snapshot)
5 |
6 |
7 |
Showing up to 50 users. Open full user admin for more actions.
8 |
9 |
10 |
Username
Email
Role
Created
11 |
12 | {% for u in users %}
13 |
14 |
{{ u.username }}
15 |
{{ u.email }}
16 |
{% if u.is_admin %}Admin{% else %}User{% endif %}
17 |
{{ u.created_at.strftime('%Y-%m-%d') if u.created_at else '' }}
26 |
--------------------------------------------------------------------------------
/app/templates/partials/_accordion_styles.html:
--------------------------------------------------------------------------------
1 |
18 |
--------------------------------------------------------------------------------
/app/services.py:
--------------------------------------------------------------------------------
1 | """
2 | Legacy services module - now imports from the new services package.
3 |
4 | This module is kept for backward compatibility with existing imports.
5 | All service implementations have been moved to the services/ package.
6 | """
7 |
8 | # Import all services from the new organized structure
9 | from .services import (
10 | # Main service instances
11 | book_service,
12 | user_service,
13 |
14 | # Service classes for direct instantiation if needed
15 | KuzuServiceFacade,
16 | KuzuUserService,
17 | KuzuBookService, # Alias for KuzuServiceFacade
18 |
19 | # Utility functions
20 | run_async,
21 |
22 | # Stub services for compatibility
23 | reading_log_service,
24 | custom_field_service,
25 | import_mapping_service,
26 | direct_import_service,
27 | job_service
28 | )
29 |
30 | # Export everything for backward compatibility
31 | __all__ = [
32 | # Service instances (most commonly used)
33 | 'book_service',
34 | 'user_service',
35 | 'reading_log_service',
36 | 'custom_field_service',
37 | 'import_mapping_service',
38 | 'direct_import_service',
39 | 'job_service',
40 |
41 | # Service classes
42 | 'KuzuServiceFacade',
43 | 'KuzuUserService',
44 | 'KuzuBookService',
45 |
46 | # Utilities
47 | 'run_async'
48 | ]
49 |
--------------------------------------------------------------------------------
/app/templates/settings/partials/password_form.html:
--------------------------------------------------------------------------------
1 | {% with msgs = get_flashed_messages(with_categories=True) %}
2 | {% if msgs %}
3 |
4 | {% for c,m in msgs %}
{{ m }}
{% endfor %}
5 |
6 | {% endif %}
7 | {% endwith %}
8 |
15 |
--------------------------------------------------------------------------------
/app/template_filters/markdown_filters.py:
--------------------------------------------------------------------------------
1 | """
2 | Template filters for markdown rendering.
3 | """
4 | import logging
5 | import mistune
6 | from markupsafe import Markup, escape
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 | # Create a markdown instance with safe defaults
11 | # escape=True ensures user-provided HTML is escaped for security
12 | markdown = mistune.create_markdown(
13 | escape=True, # Escape HTML to prevent XSS
14 | plugins=['strikethrough', 'table', 'url']
15 | )
16 |
17 | def render_markdown(text):
18 | """
19 | Convert markdown text to HTML.
20 |
21 | Args:
22 | text: Markdown text to convert (should be a string)
23 |
24 | Returns:
25 | Safe HTML markup
26 | """
27 | if not text:
28 | return ''
29 |
30 | # Ensure text is a string
31 | if not isinstance(text, str):
32 | text = str(text)
33 |
34 | try:
35 | # Convert markdown to HTML (with HTML escaping enabled for security)
36 | html = markdown(text)
37 | # Return as safe markup so Jinja2 doesn't escape markdown-generated HTML
38 | return Markup(html)
39 | except Exception as e:
40 | # Mistune is very robust and rarely raises exceptions for valid strings
41 | # If it does fail (e.g., plugin issues), log and return escaped plain text
42 | # Note: Invalid markdown syntax doesn't raise exceptions - it renders as-is
43 | logger.warning(f"Markdown rendering failed: {type(e).__name__}: {e}")
44 | return Markup(f'
{escape(str(text))}
')
45 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | # Pytest configuration for Phase 2 testing
3 |
4 | # Test discovery (include both package tests directory and root-level test_*.py files)
5 | # Removed explicit testpaths previously limiting discovery; pytest will search current dir.
6 | python_files = test_*.py
7 | python_classes = Test*
8 | python_functions = test_*
9 |
10 | # Output options
11 | addopts =
12 | -v
13 | --tb=short
14 | --strict-markers
15 | --color=yes
16 |
17 | filterwarnings =
18 | ignore:ast.Str is deprecated and will be removed in Python 3.14:DeprecationWarning
19 | ignore:Attribute s is deprecated and will be removed in Python 3.14:DeprecationWarning
20 | ignore:Constant.__init__ got an unexpected keyword argument 's':DeprecationWarning
21 | ignore:Constant.__init__ missing 1 required positional argument:DeprecationWarning
22 | ignore:'before_first_request' is deprecated:DeprecationWarning
23 |
24 | # Test markers
25 | markers =
26 | unit: Unit tests for individual components
27 | integration: Integration tests between components
28 | facade: Tests specific to the KuzuServiceFacade
29 | compatibility: Backward compatibility tests
30 | performance: Performance-related tests
31 | error_handling: Error handling and edge case tests
32 | asyncio: Async tests using event loop
33 |
34 | # Minimum version requirements
35 | minversion = 6.0
36 |
37 | # (Optional) timeouts plugin removed; re-add if pytest-timeout is installed
38 |
39 | # Coverage settings (if pytest-cov is available)
40 | # addopts = --cov=app.services --cov-report=term-missing --cov-report=html
41 |
--------------------------------------------------------------------------------
/app/templates/partials/_quick_add_preview_summary.html:
--------------------------------------------------------------------------------
1 | {% if errors %}
2 |
3 |
We couldn't prepare the quick add run:
4 |
5 | {% for error in errors %}
6 |
{{ error }}
7 | {% endfor %}
8 |
9 |
10 | {% else %}
11 |
12 | You're about to add {{ days }} reading log{{ 's' if days != 1 else '' }} covering
13 | {{ start_date.strftime('%Y-%m-%d') if start_date else 'N/A' }}
14 | through
15 | {{ end_date.strftime('%Y-%m-%d') if end_date else 'N/A' }}.
16 |
17 |
18 |
Pages per day: {{ pages }}
19 |
Minutes per day: {{ minutes }}
20 |
Reading log book: {{ unassigned_title }}
21 |
22 | {% if sample_dates %}
23 |
24 |
Dates included
25 |
26 | {% for d in sample_dates %}
27 |
{{ d.strftime('%Y-%m-%d') }}
28 | {% endfor %}
29 |
30 | {% if total_dates > sample_dates|length %}
31 |
…plus {{ total_dates - sample_dates|length }} more day{{ 's' if (total_dates - sample_dates|length) != 1 else '' }}.
32 | {% endif %}
33 |
34 | {% endif %}
35 |
36 | Existing logs for the same day will be updated by adding the values above. New logs will be created when none exist.
37 |
38 | {% endif %}
39 |
--------------------------------------------------------------------------------
/prompts/book_extraction.mustache:
--------------------------------------------------------------------------------
1 | You are an expert librarian with excellent vision.
2 | Analyze the provided image and extract book information to help identify and catalog the book.
3 |
4 | Please examine the image carefully and extract the following information if visible:
5 |
6 | **Primary Information (Required):**
7 | - Title: The complete book title
8 | - Authors: All authors listed (separate multiple authors with semicolons)
9 | - ISBN: ISBN-10 or ISBN-13 if visible
10 |
11 | **Additional Information (If Available):**
12 | - Subtitle: Any subtitle, typically found below the title
13 | - Edition: Edition information (if specified)
14 | - Contributors: Editors, translators, illustrators, foreword by, etc. Return as an array of objects with "name" and "role" fields
15 | - Series: Book series name and number/position, if given. This may be stated as book series or something like "A story/tale"
16 | - Language: Publication language (if not English)
17 |
18 | **Additional Context:**
19 | - Special Features: Awards, annotations, special editions, etc.
20 |
21 | Please respond with a JSON object containing only the information you can confidently extract from the image. Use null for any fields where information is not visible or uncertain.
22 |
23 | **Example Response Format:**
24 | ```json
25 | {
26 | "title": "",
27 | "subtitle": "",
28 | "edition": "",
29 | "contributors": [
30 | {"name": "", "role": ""}, // repeat as many times and as many roles as you are able to detect
31 | ],
32 | "series": "",
33 | "genre": "",
34 | "language": ""
35 | }
36 | ```
37 |
38 | Important: Only include fields where you have high confidence in the accuracy. If text is unclear or partially obscured, use your best judgment but indicate uncertainty. Return "null" for any field you cannot determine from the image.
39 |
--------------------------------------------------------------------------------
/app/templates/import_reading_quick_add_confirm.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}Quick Add Reading Logs · Confirm{% endblock %}
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 |
10 | Confirm Quick Add
11 |
12 |
13 | {% include 'partials/_quick_add_preview_summary.html' %}
14 | {% if errors %}
15 |
11 | ✅ Current cover: {{ book.cover_url[:50] }}{% if book.cover_url|length > 50 %}...{% endif %}
12 | 💡 Leave this field empty to keep the current cover, or enter a new URL to replace it
13 |
14 | {% else %}
15 |
16 | Enter a URL to add a cover image for this book
17 |
46 | Monthly wrap-ups generate beautiful image collages of the books you've completed.
47 | Once you finish some books this month, come back to see your personalized reading summary!
48 |
34 | Note:
35 |
36 | • The directory will be created automatically if it doesn't exist.
37 | • Use relative paths (e.g., data/backups) or absolute paths (e.g., /mnt/backups).
38 | • For Docker deployments, ensure the path is within a mounted volume.
39 | • Existing backups remain in their current location until manually moved.
40 |
41 |
42 |
43 |
44 |
63 |
--------------------------------------------------------------------------------
/app/routes/misc_routes.py:
--------------------------------------------------------------------------------
1 | # Miscellaneous routes migrated from the original routes.py
2 | from flask import Blueprint, redirect, url_for, jsonify
3 | from flask_login import login_required
4 |
5 | misc_bp = Blueprint('misc', __name__)
6 |
7 | @misc_bp.route('/health')
8 | def health_check():
9 | """Health check endpoint for monitoring and testing."""
10 | return jsonify({'status': 'healthy'}), 200
11 |
12 | @misc_bp.route('/fetch_book/')
13 | def fetch_book(isbn):
14 | # Temporary compatibility redirect to book blueprint
15 | from app.routes.book_routes import fetch_book as book_fetch
16 | return book_fetch(isbn)
17 |
18 | @misc_bp.route('/reading_history', methods=['GET'])
19 | @login_required
20 | def reading_history_redirect():
21 | # Redirect to new stats reading history endpoint
22 | return redirect(url_for('stats.reading_history'))
23 |
24 | @misc_bp.route('/month_wrapup')
25 | @login_required
26 | def month_wrapup():
27 | # Redirect to stats page
28 | return redirect(url_for('stats.index'))
29 |
30 | @misc_bp.route('/community_activity')
31 | @login_required
32 | def community_activity():
33 | # Redirect to stats page
34 | return redirect(url_for('stats.index'))
35 |
36 | @misc_bp.route('/bulk_import', methods=['GET', 'POST'])
37 | @login_required
38 | def bulk_import():
39 | # Redirect to new import interface
40 | return redirect(url_for('import.import_books'))
41 |
42 | @misc_bp.route('/import-books', methods=['GET', 'POST'])
43 | @login_required
44 | def import_books():
45 | # Legacy import-books route
46 | return redirect(url_for('import.import_books'))
47 |
48 | @misc_bp.route('/import-books/execute', methods=['POST'])
49 | @login_required
50 | def import_books_execute():
51 | # Legacy import execute
52 | from app.routes.import_routes import import_books_execute as execute
53 | return execute()
54 |
55 | @misc_bp.route('/import-books/progress/')
56 | @login_required
57 | def import_books_progress(task_id):
58 | # Legacy import progress
59 | return redirect(url_for('import.import_books_progress', task_id=task_id))
60 |
61 | @misc_bp.route('/api/import/progress/')
62 | @login_required
63 | def api_import_progress(task_id):
64 | # Legacy API import progress
65 | from app.routes.import_routes import api_import_progress as api_prog
66 | return api_prog(task_id)
67 |
68 | @misc_bp.route('/api/import/errors/')
69 | @login_required
70 | def api_import_errors(task_id):
71 | # Legacy API import errors
72 | from app.routes.import_routes import api_import_errors as api_err
73 | return api_err(task_id)
74 |
75 | @misc_bp.route('/debug/import-jobs')
76 | @login_required
77 | def debug_import_jobs():
78 | # Legacy debug import jobs
79 | from app.routes.import_routes import debug_import_jobs as debug_jobs
80 | return debug_jobs()
81 |
82 | @misc_bp.route('/migrate-sqlite', methods=['GET', 'POST'])
83 | @login_required
84 | def migrate_sqlite():
85 | # Legacy migrate sqlite route
86 | from app.routes.import_routes import migrate_sqlite as migrate
87 | return migrate()
88 |
89 | @misc_bp.route('/migration-results')
90 | @login_required
91 | def migration_results():
92 | # Legacy migration results
93 | return redirect(url_for('import.migration_results'))
94 |
95 | @misc_bp.route('/detect-sqlite', methods=['POST'])
96 | @login_required
97 | def detect_sqlite():
98 | # Legacy detect sqlite endpoint
99 | from app.routes.import_routes import detect_sqlite as detect
100 | return detect()
101 |
102 | @misc_bp.route('/direct_import', methods=['GET', 'POST'])
103 | @login_required
104 | def direct_import():
105 | # Legacy direct import route
106 | from app.routes.import_routes import direct_import as direct_import_bp
107 | return direct_import_bp()
108 |
--------------------------------------------------------------------------------
/scripts/install_ocr.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # OCR Dependencies Installation Script for Bibliotheca
4 | # This script installs the required dependencies for OCR barcode scanning
5 |
6 | echo "🔧 Installing OCR dependencies for Bibliotheca..."
7 |
8 | # Check if we're in a virtual environment
9 | if [[ "$VIRTUAL_ENV" != "" ]]; then
10 | echo "✅ Virtual environment detected: $VIRTUAL_ENV"
11 | else
12 | echo "⚠️ Warning: No virtual environment detected. Consider using one."
13 | read -p "Continue anyway? (y/N): " -n 1 -r
14 | echo
15 | if [[ ! $REPLY =~ ^[Yy]$ ]]; then
16 | exit 1
17 | fi
18 | fi
19 |
20 | # Install Python packages
21 | echo "📦 Installing Python packages..."
22 | pip install opencv-python==4.8.1.78 pyzbar==0.1.9 pytesseract==0.3.10 numpy>=1.21.0
23 |
24 | # Check for system dependencies
25 | echo "🔍 Checking system dependencies..."
26 |
27 | # Check for Tesseract OCR
28 | if ! command -v tesseract &> /dev/null; then
29 | echo "⚠️ Tesseract OCR not found. Installing instructions:"
30 |
31 | if [[ "$OSTYPE" == "darwin"* ]]; then
32 | # macOS
33 | echo " For macOS, install using Homebrew:"
34 | echo " brew install tesseract"
35 |
36 | if command -v brew &> /dev/null; then
37 | read -p " Install Tesseract using Homebrew now? (y/N): " -n 1 -r
38 | echo
39 | if [[ $REPLY =~ ^[Yy]$ ]]; then
40 | brew install tesseract
41 | fi
42 | fi
43 |
44 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
45 | # Linux
46 | echo " For Ubuntu/Debian:"
47 | echo " sudo apt-get install tesseract-ocr"
48 | echo " For CentOS/RHEL:"
49 | echo " sudo yum install tesseract"
50 |
51 | else
52 | echo " Please install Tesseract OCR manually for your system"
53 | echo " Visit: https://github.com/tesseract-ocr/tesseract"
54 | fi
55 | else
56 | echo "✅ Tesseract OCR found: $(tesseract --version | head -1)"
57 | fi
58 |
59 | # Check for libzbar (required by pyzbar)
60 | echo "🔍 Checking for libzbar..."
61 |
62 | if [[ "$OSTYPE" == "darwin"* ]]; then
63 | # macOS
64 | if ! brew list zbar &> /dev/null; then
65 | echo "⚠️ libzbar not found. Installing with Homebrew..."
66 | if command -v brew &> /dev/null; then
67 | brew install zbar
68 | else
69 | echo " Please install Homebrew first: https://brew.sh/"
70 | echo " Then run: brew install zbar"
71 | fi
72 | else
73 | echo "✅ libzbar found"
74 | fi
75 |
76 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
77 | # Linux
78 | if ! ldconfig -p | grep libzbar &> /dev/null; then
79 | echo "⚠️ libzbar not found. Install with:"
80 | echo " Ubuntu/Debian: sudo apt-get install libzbar0"
81 | echo " CentOS/RHEL: sudo yum install zbar"
82 | else
83 | echo "✅ libzbar found"
84 | fi
85 | fi
86 |
87 | echo ""
88 | echo "🧪 Testing OCR functionality..."
89 | python3 -c "
90 | try:
91 | from app.ocr_scanner import is_ocr_available
92 | if is_ocr_available():
93 | print('✅ OCR functionality is ready!')
94 | else:
95 | print('❌ OCR dependencies not properly installed')
96 | exit(1)
97 | except ImportError as e:
98 | print(f'❌ Import error: {e}')
99 | exit(1)
100 | "
101 |
102 | if [ $? -eq 0 ]; then
103 | echo ""
104 | echo "🎉 Installation complete! OCR barcode scanning is now available."
105 | echo ""
106 | echo "Usage:"
107 | echo " 1. Go to Add Book page"
108 | echo " 2. Click 'Upload Image' button"
109 | echo " 3. Select an image containing a barcode or ISBN text"
110 | echo " 4. The ISBN will be automatically extracted and populated"
111 | else
112 | echo ""
113 | echo "❌ Installation failed. Please check the error messages above."
114 | exit 1
115 | fi
116 |
--------------------------------------------------------------------------------
/app/templates/macros/custom_field_inline.html:
--------------------------------------------------------------------------------
1 |
2 | {% set field_name = 'global_' + field.name if field.is_global else 'personal_' + field.name %}
3 | {% set current_value = global_metadata.get(field.name, '') if field.is_global else personal_metadata.get(field.name, '') %}
4 |
5 |
71 |
--------------------------------------------------------------------------------
/scripts/README.md:
--------------------------------------------------------------------------------
1 | # Default Import Templates
2 |
3 | This directory contains scripts and functionality for managing default import templates in MyBibliotheca.
4 |
5 | ## Overview
6 |
7 | Default import templates are pre-configured mapping templates that make it easy for users to import data from popular book tracking services like Goodreads and StoryGraph without having to manually map fields every time.
8 |
9 | ## Features
10 |
11 | ### Available Default Templates
12 |
13 | 1. **Goodreads Export Template**
14 | - Maps all standard Goodreads library export CSV fields
15 | - Handles ratings, reading status, dates, categories, and reviews
16 | - Automatically detects Goodreads CSV format
17 |
18 | 2. **StoryGraph Export Template**
19 | - Maps all standard StoryGraph library export CSV fields
20 | - Includes unique StoryGraph fields like moods, pace, and character analysis
21 | - Creates custom fields for StoryGraph-specific metadata
22 |
23 | ### System Templates vs User Templates
24 |
25 | - **System Templates**: Pre-installed default templates available to all users
26 | - Cannot be deleted by users
27 | - Marked with "System Default" badge
28 | - Automatically updated with application upgrades
29 |
30 | - **User Templates**: Custom templates created by individual users
31 | - Can be edited and deleted by the owner
32 | - Personal to each user account
33 | - Created when users save their own mapping configurations
34 |
35 | ## Usage
36 |
37 | ### Automatic Initialization
38 |
39 | Default templates are automatically created when the application starts if they don't already exist. This happens during the Flask app initialization process.
40 |
41 | ### Manual Creation
42 |
43 | You can also manually create or recreate the default templates using the provided script:
44 |
45 | ```bash
46 | python scripts/create_default_templates.py
47 | ```
48 |
49 | ### Template Detection
50 |
51 | When users upload a CSV file, the system automatically:
52 | 1. Analyzes the CSV headers
53 | 2. Attempts to match them against available templates
54 | 3. Suggests the best matching template (usually 70%+ similarity)
55 | 4. Pre-selects the suggested template in the import interface
56 |
57 | ## Technical Details
58 |
59 | ### Template Structure
60 |
61 | Each template contains:
62 | - **ID**: Unique identifier (e.g., "default_goodreads")
63 | - **User ID**: "__system__" for default templates
64 | - **Name**: Display name shown to users
65 | - **Description**: Helpful description of the template's purpose
66 | - **Source Type**: "goodreads", "storygraph", or "custom"
67 | - **Sample Headers**: List of expected CSV column names
68 | - **Field Mappings**: Dictionary mapping CSV columns to book fields
69 |
70 | ### Field Mapping Actions
71 |
72 | Templates can specify different actions for each CSV column:
73 | - `map_existing`: Map to a standard book field
74 | - `create_custom`: Create a new custom metadata field
75 | - `skip`: Ignore this column during import
76 |
77 | ### Custom Field Creation
78 |
79 | StoryGraph template includes custom fields for unique metadata:
80 | - **Moods**: Tags for reading atmosphere (e.g., "dark", "hopeful")
81 | - **Pace**: Reading speed perception (e.g., "fast", "slow")
82 | - **Character vs Plot**: Whether the book is character or plot driven
83 | - **Content Warnings**: Tags for sensitive content
84 |
85 | ## Benefits
86 |
87 | 1. **Faster Imports**: Users don't need to manually map common CSV formats
88 | 2. **Consistency**: Standardized field mappings across all users
89 | 3. **Onboarding**: New users can immediately import from popular services
90 | 4. **Rich Metadata**: Preserves unique metadata from different services
91 | 5. **Extensibility**: Easy to add new default templates for other services
92 |
93 | ## Future Enhancements
94 |
95 | - Add templates for other book tracking services (LibraryThing, Bookly, etc.)
96 | - Allow community sharing of custom templates
97 | - Automatic template updates and versioning
98 | - Template marketplace or repository
99 | - AI-powered template suggestions based on CSV structure
100 |
--------------------------------------------------------------------------------
/app/templates/locations/edit.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}Edit Location - MyBibliotheca{% endblock %}
3 |
4 | {% block content %}
5 |
6 |
7 |
8 |
9 |
10 |
📍 Edit Location
11 |
12 |
13 |
67 |
68 |
69 |
70 |
71 |
72 | {% endblock %}
73 |
--------------------------------------------------------------------------------
/scripts/setup_data_dir.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Cross-platform setup script to ensure data directory and database file exist with proper permissions.
4 | This ensures parity between Docker and standalone execution across Windows, macOS, and Linux.
5 | """
6 |
7 | import os
8 | import stat
9 | import platform
10 | from pathlib import Path
11 |
12 | def setup_data_directory():
13 | """Ensure data directory exists with proper permissions for standalone execution"""
14 |
15 | # Get the directory where this script is located (project root)
16 | project_root = Path(__file__).parent.absolute()
17 | data_dir = project_root / 'data'
18 | db_path = data_dir / 'books.db'
19 |
20 | system = platform.system()
21 | print(f"🔧 Setting up data directory for standalone execution on {system}...")
22 | print(" This ensures parity with Docker environment setup")
23 |
24 | # Create data directory if it doesn't exist
25 | try:
26 | if system == "Windows":
27 | # On Windows, just create the directory without specific mode
28 | data_dir.mkdir(exist_ok=True)
29 | else:
30 | # On Unix-like systems, set proper permissions
31 | data_dir.mkdir(mode=0o755, exist_ok=True)
32 | print(f"✅ Data directory created/verified: {data_dir}")
33 | except Exception as e:
34 | print(f"⚠️ Could not create data directory: {e}")
35 | return False
36 |
37 | # Create empty database file if it doesn't exist (matches Docker setup)
38 | try:
39 | if not db_path.exists():
40 | if system == "Windows":
41 | # On Windows, just create the file
42 | db_path.touch()
43 | else:
44 | # On Unix-like systems, set proper permissions
45 | db_path.touch(mode=0o664)
46 | print(f"✅ Database file created: {db_path}")
47 | else:
48 | print(f"✅ Database file exists: {db_path}")
49 | except Exception as e:
50 | print(f"⚠️ Could not create database file: {e}")
51 | return False
52 |
53 | # Set permissions (only on Unix-like systems)
54 | if system != "Windows":
55 | try:
56 | # Set directory permissions (755 = rwxr-xr-x)
57 | data_dir.chmod(0o755)
58 |
59 | # Set database file permissions (664 = rw-rw-r--)
60 | if db_path.exists():
61 | db_path.chmod(0o664)
62 |
63 | print("✅ Permissions set correctly (755 for directory, 664 for database)")
64 |
65 | except PermissionError as e:
66 | print(f"⚠️ Could not set permissions: {e}")
67 | print(" This is normal on some systems and won't affect functionality")
68 | except Exception as e:
69 | print(f"⚠️ Permission error: {e}")
70 | else:
71 | print("✅ Running on Windows - skipping Unix permission settings")
72 |
73 | # Verify the setup matches what config.py expects
74 | try:
75 | from config import Config
76 | config_db_path = Path(Config.DATABASE_PATH)
77 |
78 | if config_db_path.resolve() == db_path.resolve():
79 | print("✅ Database path matches config.py expectations")
80 | else:
81 | print(f"⚠️ Path mismatch - Config expects: {config_db_path}")
82 | print(f" But we created: {db_path}")
83 | except Exception as e:
84 | print(f"⚠️ Could not verify config path: {e}")
85 |
86 | print(f"\n🎉 Data directory setup complete for {system}!")
87 | print(f" Data directory: {data_dir}")
88 | print(f" Database path: {db_path}")
89 |
90 | if system != "Windows":
91 | print(f" Permissions: directory=755, database=664")
92 | else:
93 | print(f" Permissions: Using Windows default file permissions")
94 |
95 | print("\nYou can now run MyBibliotheca using:")
96 | if system == "Windows":
97 | print(" python -m gunicorn -w 4 -b 0.0.0.0:5054 run:app")
98 | else:
99 | print(" gunicorn -w 4 -b 0.0.0.0:5054 run:app")
100 |
101 | return True
102 |
103 | if __name__ == "__main__":
104 | setup_data_directory()
105 |
--------------------------------------------------------------------------------
/app/templates/admin/partials/backup_config.html:
--------------------------------------------------------------------------------
1 | {# Partial: Backup Configuration form for unified settings #}
2 |
3 |
4 |
Backup Configuration
5 |
6 |
7 |
Configure backup file storage location.
8 |
9 |
35 |
36 |
37 | Note:
38 |
39 | • The directory will be created automatically if it doesn't exist
40 | • Use relative paths (e.g., data/backups) or absolute paths (e.g., /mnt/backups)
41 | • For Docker deployments, ensure the path is within a mounted volume
42 | • Existing backups will remain in their current location until manually moved
43 |
44 |