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