Compare commits
No commits in common. 'f7f08fa45c531db3d393ebf62b0ffdfe170ddfa7' and '7ee6c5e27666f83cf02a0c0c4437da46482996f5' have entirely different histories.
f7f08fa45c
...
7ee6c5e276
@ -1,119 +0,0 @@
|
|||||||
# ============================================================
|
|
||||||
# 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()]
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
# ============================================================
|
|
||||||
# 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.
@ -1,38 +0,0 @@
|
|||||||
#!/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
|
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue