Compare commits
3 Commits
7ee6c5e276
...
f7f08fa45c
| Author | SHA1 | Date |
|---|---|---|
|
|
f7f08fa45c | 2 weeks ago |
|
|
7439d26744 | 2 weeks ago |
|
|
5159c32f58 | 2 weeks ago |
@ -0,0 +1,119 @@
|
|||||||
|
# ============================================================
|
||||||
|
# File: db/db.py
|
||||||
|
# Purpose:
|
||||||
|
# Raw SQLite engine for BookScraper.
|
||||||
|
# Provides ONLY low-level DB primitives.
|
||||||
|
# - Connection management (WAL mode)
|
||||||
|
# - init_db() schema creation
|
||||||
|
# - upsert_book() atomic write
|
||||||
|
# - raw fetch helpers (private)
|
||||||
|
#
|
||||||
|
# All business logic belongs in repository.py.
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
DB_PATH = os.environ.get("BOOKSCRAPER_DB", "/app/data/books.db")
|
||||||
|
|
||||||
|
# Ensure directory exists
|
||||||
|
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
# Per-process connection cache
|
||||||
|
_connection_cache = {}
|
||||||
|
_connection_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Connection handling
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def get_db():
|
||||||
|
pid = os.getpid()
|
||||||
|
|
||||||
|
if pid not in _connection_cache:
|
||||||
|
with _connection_lock:
|
||||||
|
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
enable_wal_mode(conn)
|
||||||
|
_connection_cache[pid] = conn
|
||||||
|
|
||||||
|
return _connection_cache[pid]
|
||||||
|
|
||||||
|
|
||||||
|
def enable_wal_mode(conn):
|
||||||
|
conn.execute("PRAGMA journal_mode=DELETE;")
|
||||||
|
conn.execute("PRAGMA synchronous=NORMAL;")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Schema creation
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def init_db():
|
||||||
|
conn = get_db()
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS books (
|
||||||
|
book_id TEXT PRIMARY KEY,
|
||||||
|
title TEXT,
|
||||||
|
author TEXT,
|
||||||
|
|
||||||
|
cover_url TEXT,
|
||||||
|
cover_path TEXT,
|
||||||
|
|
||||||
|
chapters_total INTEGER,
|
||||||
|
|
||||||
|
status TEXT,
|
||||||
|
downloaded INTEGER DEFAULT 0,
|
||||||
|
parsed INTEGER DEFAULT 0,
|
||||||
|
audio_done INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_update DATETIME
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# WRITE OPERATIONS
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def upsert_book(book_id, **fields):
|
||||||
|
"""
|
||||||
|
Raw upsert primitive. Repository layer should call this.
|
||||||
|
"""
|
||||||
|
conn = get_db()
|
||||||
|
|
||||||
|
keys = ["book_id"] + list(fields.keys())
|
||||||
|
values = [book_id] + list(fields.values())
|
||||||
|
placeholders = ",".join(["?"] * len(values))
|
||||||
|
|
||||||
|
updates = ", ".join([f"{k} = excluded.{k}" for k in fields.keys()])
|
||||||
|
|
||||||
|
sql = f"""
|
||||||
|
INSERT INTO books ({','.join(keys)})
|
||||||
|
VALUES ({placeholders})
|
||||||
|
ON CONFLICT(book_id)
|
||||||
|
DO UPDATE SET {updates},
|
||||||
|
last_update = CURRENT_TIMESTAMP;
|
||||||
|
"""
|
||||||
|
|
||||||
|
conn.execute(sql, values)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# RAW READ OPERATIONS (PRIVATE)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def _raw_get_book(book_id):
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute("SELECT * FROM books WHERE book_id = ?;", (book_id,)).fetchone()
|
||||||
|
return dict(row) if row else None
|
||||||
|
|
||||||
|
|
||||||
|
def _raw_get_all_books():
|
||||||
|
conn = get_db()
|
||||||
|
cur = conn.execute("SELECT * FROM books ORDER BY created_at DESC;")
|
||||||
|
return [dict(row) for row in cur.fetchall()]
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
# ============================================================
|
||||||
|
# File: db/repository.py
|
||||||
|
# Purpose:
|
||||||
|
# High-level BookScraper database interface.
|
||||||
|
# This is the ONLY module Celery tasks and Flask should use.
|
||||||
|
#
|
||||||
|
# Uses low-level primitives from db.db, but exposes
|
||||||
|
# domain-level operations:
|
||||||
|
# - fetch_book / fetch_all_books
|
||||||
|
# - create_or_update_book
|
||||||
|
# - set_status
|
||||||
|
# - incrementing counters
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
from db.db import (
|
||||||
|
upsert_book,
|
||||||
|
_raw_get_book,
|
||||||
|
_raw_get_all_books,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# FETCH OPERATIONS
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def fetch_book(book_id):
|
||||||
|
"""Return a single book dict or None."""
|
||||||
|
return _raw_get_book(book_id)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_all_books():
|
||||||
|
"""Return all books ordered newest → oldest."""
|
||||||
|
return _raw_get_all_books()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# BOOK CREATION / METADATA
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def create_or_update_book(
|
||||||
|
book_id,
|
||||||
|
title=None,
|
||||||
|
author=None,
|
||||||
|
chapters_total=None,
|
||||||
|
cover_url=None,
|
||||||
|
cover_path=None,
|
||||||
|
status=None,
|
||||||
|
):
|
||||||
|
fields = {}
|
||||||
|
|
||||||
|
if title is not None:
|
||||||
|
fields["title"] = title
|
||||||
|
if author is not None:
|
||||||
|
fields["author"] = author
|
||||||
|
if chapters_total is not None:
|
||||||
|
fields["chapters_total"] = chapters_total
|
||||||
|
if cover_url is not None:
|
||||||
|
fields["cover_url"] = cover_url
|
||||||
|
if cover_path is not None:
|
||||||
|
fields["cover_path"] = cover_path
|
||||||
|
if status is not None:
|
||||||
|
fields["status"] = status
|
||||||
|
|
||||||
|
if fields:
|
||||||
|
upsert_book(book_id, **fields)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# STATUS MANAGEMENT
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def set_status(book_id, status):
|
||||||
|
upsert_book(book_id, status=status)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# INCREMENTING COUNTERS (atomic)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
def inc_downloaded(book_id, amount=1):
|
||||||
|
book = _raw_get_book(book_id)
|
||||||
|
if not book:
|
||||||
|
return
|
||||||
|
cur = book.get("downloaded", 0) or 0
|
||||||
|
upsert_book(book_id, downloaded=cur + amount)
|
||||||
|
|
||||||
|
|
||||||
|
def inc_parsed(book_id, amount=1):
|
||||||
|
book = _raw_get_book(book_id)
|
||||||
|
if not book:
|
||||||
|
return
|
||||||
|
cur = book.get("parsed", 0) or 0
|
||||||
|
upsert_book(book_id, parsed=cur + amount)
|
||||||
|
|
||||||
|
|
||||||
|
def inc_audio_done(book_id, amount=1):
|
||||||
|
book = _raw_get_book(book_id)
|
||||||
|
if not book:
|
||||||
|
return
|
||||||
|
cur = book.get("audio_done", 0) or 0
|
||||||
|
upsert_book(book_id, audio_done=cur + amount)
|
||||||
Binary file not shown.
@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
main_dir="$( cd "$( dirname "$0" )" && pwd )"
|
||||||
|
|
||||||
|
shopt -s nocasematch
|
||||||
|
|
||||||
|
for subfolder in "$main_dir"/*; do
|
||||||
|
if [ -d "$subfolder" ]; then
|
||||||
|
audiofolder="$subfolder/Audio"
|
||||||
|
mkdir -p "$audiofolder"
|
||||||
|
|
||||||
|
for entry in "$subfolder"/*.txt; do
|
||||||
|
fn=$(basename "$entry")
|
||||||
|
[[ "${entry##*.}" =~ txt ]]
|
||||||
|
|
||||||
|
echo "$fn"
|
||||||
|
inputfile="$subfolder/$fn"
|
||||||
|
outputfile="$audiofolder/${fn%.*}.m4b"
|
||||||
|
|
||||||
|
now=$(date +"%T")
|
||||||
|
echo "Current time : $now"
|
||||||
|
echo "$inputfile ->"
|
||||||
|
echo "$outputfile"
|
||||||
|
|
||||||
|
if [ -f "$outputfile" ]; then
|
||||||
|
echo "$outputfile exists: skipping"
|
||||||
|
else
|
||||||
|
say --voice=Sinji \
|
||||||
|
--output-file="$outputfile" \
|
||||||
|
--input-file="$inputfile" \
|
||||||
|
--file-format=m4bf \
|
||||||
|
--quality=127 \
|
||||||
|
-r 200 \
|
||||||
|
--data-format=aac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue